From ba2b09e3ecc63de4560e1425a4cabd81d68d3fdd Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Thu, 24 Sep 2026 18:24:22 +0000 Subject: [PATCH 01/49] feat: begin Rust support with governed input coverage Import and revise the Rust support roadmap against the current main baseline. Recognize Rust tool configuration as policy-sensitive and WGSL as governed source, preserving missing-analysis failures and module impact. Cargo workspace facts, sealed Rust tooling, semantic checks, dependency governance, structured test evidence, and installed-release parity remain open. Validation: formatting, design context, policy-unit, repository-unit, and development CLI documentation lookup pass. Quality-unit is running after restoring the pinned development tools; branch checkpoint verification remains pending. --- .code-polishy.json | 9 + docs/README.md | 4 + docs/catalog.json | 8 + docs/design/execution-observability.md | 4 + docs/design/rust-inputs.md | 33 +++ docs/plans/code-polishy-rust-support-plan.md | 269 +++++++++++++++++++ internal/policy/control_inputs.go | 2 + internal/policy/data.go | 2 +- internal/policy/load.go | 2 +- internal/policy/rust_inputs_test.go | 43 +++ internal/quality/wgsl_test.go | 51 ++++ internal/repository/repository.go | 2 +- internal/repository/rust_inputs_test.go | 96 +++++++ 13 files changed, 522 insertions(+), 3 deletions(-) create mode 100644 docs/design/rust-inputs.md create mode 100644 docs/plans/code-polishy-rust-support-plan.md create mode 100644 internal/policy/rust_inputs_test.go create mode 100644 internal/quality/wgsl_test.go create mode 100644 internal/repository/rust_inputs_test.go diff --git a/.code-polishy.json b/.code-polishy.json index 5be553d1..2386e957 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -23,6 +23,15 @@ }, "documentation": { "design": [ + { + "path": "docs/design/rust-inputs.md", + "sourcePaths": [ + "internal/policy/data.go", + "internal/policy/rust_inputs_test.go", + "internal/repository/rust_inputs_test.go", + "internal/quality/wgsl_test.go" + ] + }, { "path": "docs/design/provider-scope.md", "sourcePaths": [ diff --git a/docs/README.md b/docs/README.md index 97be9cba..0213e887 100644 --- a/docs/README.md +++ b/docs/README.md @@ -70,6 +70,10 @@ and aliases. Search is local, bounded, and deterministic. - [Source Comment Policy Design](design/source-comment-policy.md) explains the default-preserving switch and the rationale for repositories that select the strict no-comment boundary. +- [Rust and Shader Input Coverage](design/rust-inputs.md) defines Rust control + inputs, WGSL ownership, and the current analysis boundary. +- [Rust Support Plan](plans/code-polishy-rust-support-plan.md) tracks the beta + implementation and the remaining requirements for first-class Rust support. - [Source Provenance](source-provenance.md) records public design references, toolchain origins, and implementation boundaries. - [Release Checklist](release-checklist.md) defines immutable tags and the diff --git a/docs/catalog.json b/docs/catalog.json index 9f40f416..a690f531 100644 --- a/docs/catalog.json +++ b/docs/catalog.json @@ -294,6 +294,14 @@ "aliases": ["size-analysis-design"], "public": true }, + { + "id": "rust-inputs", + "path": "docs/design/rust-inputs.md", + "title": "Rust and Shader Input Coverage", + "summary": "Understand Rust control inputs, WGSL source ownership, selection, and unavailable analysis.", + "aliases": [], + "public": true + }, { "id": "security", "path": "docs/policies/security.md", diff --git a/docs/design/execution-observability.md b/docs/design/execution-observability.md index 165de060..ce2454de 100644 --- a/docs/design/execution-observability.md +++ b/docs/design/execution-observability.md @@ -39,6 +39,10 @@ within one command after configuration composition. Cross-process analyzer results are reused only through an existing digest-bound receipt mechanism; ordinary files are never trusted from a timestamp-only persistent cache. +[Rust and shader input coverage](rust-inputs.md) defines the Rust settings that +expand policy selection and WGSL's source classification. Language recognition +does not establish analyzer availability or Cargo workspace resolution. + Candidate test retries are outcome-bearing attempts, not baseline diagnostics. The first full attempt establishes all tests except the observed failures; an optional repository-declared retry command can establish those failures at a diff --git a/docs/design/rust-inputs.md b/docs/design/rust-inputs.md new file mode 100644 index 00000000..038624d0 --- /dev/null +++ b/docs/design/rust-inputs.md @@ -0,0 +1,33 @@ +# Rust and shader input coverage + +Rust compilation settings affect which code and diagnostics exist. Cargo +configuration can change source registries, rustflags, wrappers, linkers and +runners; rustup declarations change the selected toolchain; rustfmt and Clippy +settings change analysis. Their standard filenames are therefore sensitive +control inputs at every repository depth, alongside Cargo manifests and locks. +This includes both `.cargo/config` and `.cargo/config.toml`, `rust-toolchain` +and `rust-toolchain.toml`, `rustfmt.toml` and `.rustfmt.toml`, and `clippy.toml` +and `.clippy.toml`. + +The existing repository selection boundary expands changed or deleted control +inputs to full governed scope. This is deliberately conservative until Cargo +workspace and configuration resolution can prove a narrower affected closure. +Recognizing a filename does not parse, approve or execute its settings, inspect +configuration outside the repository, or establish reproducible Cargo execution. + +WGSL is executable shader source with its own built-in language identifier, +`wgsl`. It participates in module ownership, source-comment coverage and the +existing required analysis capabilities. A shader cannot be protected as data +or silently acquire Rust analysis coverage. Generated shader source retains +correctness and ownership obligations under the existing generated-source rules. +Recognition supplies no analyzer: absent structured evidence still produces +coverage findings, even when a generic Cargo command is configured. + +Ordinary shader changes retain focused file selection and the module dependency +closure. Projects express renderer-to-shader ownership through existing module +dependencies. Compiler-derived shader consumer edges and complete Rust receipt +inputs belong to later semantic integration; file recognition alone proves +neither shader validity nor GPU execution correctness. + +The [Rust support plan](../plans/code-polishy-rust-support-plan.md) records the +remaining discovery, analysis, dependency, test and installed-release work. diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md new file mode 100644 index 00000000..41cd1581 --- /dev/null +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -0,0 +1,269 @@ +# First-class Rust support in Code Polishy + +Date: 2026-09-24. Status: beta implementation in progress. +Implementation branch: `beta-rust-support`, based on `main` at +`d6d14503ec561e54e61c36beb218f7ce684f75fe`, locked to Code Polishy `0.27.11`. +Imported from Masterbrainer's `docs/plans/code-polishy-rust-support-plan.md` +at planning commit `be593fb`. This copy owns the Code Polishy implementation. + +## Outcome + +Rust receives the same product-level integration as Go and Python: automatic +discovery, policy-owned tooling, source facts, architecture, quality, dependency +governance, tests, reports, receipts, documentation, and verified releases. +Target repositories must receive complete Rust support through normal Code +Polishy setup and upgrade, without a separate pack-installation procedure or a +hand-maintained Cargo policy stack. Automatic installation of an authenticated, +release-owned Rust provider is compatible with this outcome; an optional pack +or a few Cargo commands alone are not. + +The Go policy engine remains the decision owner. A pinned Rust helper may supply +compiler/parser facts through a bounded child-process protocol. Do not embed +language-specific heuristics in unrelated engine modules or reimplement Rust +resolution with regular expressions. + +## Current baseline and architectural decisions + +The implementation base includes the accepted changes through `0.27.11`. +The earlier handoff inspected a divergent `0.25.0` checkout. Its unpublished +behavior-review changes are not part of this base. + +The current [first-party language-pack plan](installable-first-party-language-packs.md) +and [universal discovery plan](universal-language-pack-capabilities.md) are +proposals, not implemented contracts. Rust should reuse the existing ownership, +selection, runner, report, and receipt boundaries without making the complete +four-language extraction a prerequisite. Add only the Cargo-specific facts and +execution integration needed by Rust; coordinate any public provider-protocol +change as one coherent cutover. No second scheduling or policy engine. + +The current provider runtime resolves Node, and its discovery model is shaped +around JavaScript projects. Neither protocol version 3 nor a generic command +declaration establishes Cargo discovery, a Rust runtime, or a sandbox. +`runner.OSRunner` supplies process/environment controls; operating-system +filesystem and network containment still needs an explicit implementation and +host evidence before it can be advertised. + +Implementation uses the locked agent workflow and mapped design context. +Intermediate beta commits may be incomplete; successful source classification +does not establish analyzer coverage. Keep the locked stable release unchanged +until a separately authorized release/upgrade. Publication and Masterbrainer +adoption are later operations. + +## Baseline parity inventory + +This is a source audit at the recorded base, not execution evidence. The fixture +column defines acceptance work; unchecked capabilities remain unavailable. +Package paths below are under `internal/` in this repository. + +| Capability | Current Go/Python baseline and owner | Rust facts, enforcement, and limits | Required observable fixture | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | +| Discovery | `repository/go_modules.go` parses contained module/workspace identities; `repository/python_project.go` assigns projects and reports unsupported layouts | `.rs` is recognized, but Cargo has no built-in project model; add parsed manifests and versioned Cargo package/target facts with contained ownership | Virtual workspace, inherited package metadata, nested excluded package, escaping path, standalone source | +| Input selection | `policy/control_inputs.go` and `repository/repository.go` expand policy-sensitive changes; `repository/impact.go` follows declared reverse dependencies | Cargo manifests/locks already expand selection; Rust toolchain/analyzer configs and WGSL classification are missing; precise Cargo closure comes later | Editing or deleting config reselects affected work; shader change reaches its owning module and dependents | +| Formatting | `quality` owns Go formatting and Python conditional commands; selected writes protect generated/data inputs | Verified rustfmt, exact edition/config, and selected write authority; formatting a module must not rewrite its unselected children | Selected dirty source formatted, unchanged sibling/generated bytes preserved | +| Correctness | `quality` and `policymodule` compose native Go and Ruff/ty checks | Compiler/Clippy diagnostics with original paths and target/features; build-script and macro execution require controlled compilation | Valid crate and seeded type/lint defect, unavailable compiler, forbidden config suppression | +| Structure/comments | `quality`, `pythonfacts`, and `pack` supply parsed measurements/comment facts and core limits | Parser facts for functions, depth, parameters, comments and lint attributes; declare Rust limits rather than quietly inheriting TypeScript conventions | Excessive complexity, doc comment under strict policy, raw string containing comment-like text | +| Architecture | `architecture` and `architecture/sourcegraph` derive native imports and enforce owned edges/cycles | Cargo graph supplies inter-crate edges only; compiler/parser facts must account for intra-crate modules, cfg, re-exports and macros | Forbidden cross-module edge, valid alias, unresolved generated edge with explicit incomplete coverage | +| Dead code | Native Go tooling and Python Vulture/reachability contracts have language-specific limits | Compiler and entry-point evidence per configuration; public library APIs and macro expansion need stated limits | Unused private item, retained public API, feature-only use; absent evidence cannot pass | +| Supply chain | `supplychain` governs Go and Python/uv dependencies; unsupported Cargo manifests already report missing providers | Exact declarations, lock agreement, source identity, age, licenses and advisory assessments; candidate resolution is separate from compilation | Unpinned dependency, inherited dependency, lock drift, rejected Git provenance, stale assessment | +| Test evidence | `testing`, `testartifact`, and `testreceipt` own suite selection, artifacts and reuse | Add a supported structured Rust result adapter; existing generic command exits/artifact shapes alone do not prove nonempty test execution | Zero/all-skipped tests rejected, actual failed test, doctest result, cancellation, stale receipt | +| Portability/shaders | `portability` owns generic machine assumptions; no WGSL analyzer exists | Separate WGSL language and validator; host/target, FFI, unsafe and SDK evidence; GPU correctness stays project-owned | Invalid shader, missing SDK, unsupported cfg branch, changed shader invalidating dependent evidence | +| Distribution/reporting | `release`, `engine`, CLI, schemas and installed-release scripts own verified artifacts and reports | Deliver exact Rust tools/helper automatically on the existing five hosts; report coverage gaps and do not claim unsupported hosts | Public CLI through verified Linux/macOS/Windows installation, JSON/SARIF findings and unavailable-tool failure | + +## Implementation milestones + +1. **Input coverage foundation:** reserve WGSL as shader source; classify Cargo, + rustup, rustfmt and Clippy configuration as policy-sensitive; prove real + selection, ownership and missing-analysis failures. This does not resolve + Cargo projects, execute Rust tools, validate shaders, or establish Rust parity. +2. **Cargo facts and sealed discovery:** introduce a data-only manifest reader + and bounded Cargo metadata model alongside an exact governed toolchain. + Deliver the first usable public-CLI discovery path together; do not leave + an unused metadata abstraction waiting for future callers. +3. **One complete crate path:** for a contained crate with declared + target/features, wire formatting, compiler checks, core quality/architecture, + dependency policy and actual test evidence into existing commands. Unsupported + layouts remain explicit failures. This is a beta subset, not general support. +4. **Workspace and semantic coverage:** expand the supported configuration + matrix to the Phase 1–5 cases below, including WGSL and native boundaries. +5. **Installed-release parity:** prove every acceptance cell on the supported + hosts before advertising first-class support and adopting it in Masterbrainer. + +Milestones 2–4 cut across the phases below. Read-only manifest inventory can +precede tool execution, but resolved metadata cannot be admitted before the +tool/configuration boundary is ready. Reuse existing exact test suites for +changed modules; add a quick suite when a new module is introduced. + +## Phase 0: define the complete coverage contract + +- [x] Audit current Go/Python owners and Rust gaps against the recorded base; + establish the parity inventory above. +- [ ] Turn each acceptance cell into executable evidence, preserving explicit + unavailable/incomplete states and original-path diagnostics. +- [ ] Choose the supported Rust editions, compiler/MSRV relationship, target + triples, and feature configurations. Select exact versions at implementation + time; do not invent pins in this plan. Target editions 2015, 2018, 2021 and + 2024 subject to that toolchain's supported grammar. Code Polishy installation + hosts remain Linux/macOS on x64/arm64 and Windows x64; a host install does not + prove every Rust compilation target or feature combination. +- [ ] Define a first-class Rust conditional module activated by governed Rust + source/Cargo workspaces. Unsupported or missing required analysis must block + completion rather than fall back to a successful generic command. + +| Capability | Required built-in behavior | +| ------------ | ---------------------------------------------------------------------------------------------- | +| Discovery | Cargo workspace/package/target ownership, nested projects, standalone source coverage findings | +| Formatting | Policy-owned rustfmt, controlled configuration, selected writes only | +| Correctness | rustc/Clippy diagnostics with target/feature context and original locations | +| Structure | Complexity, size, depth, parameter/comment rules using parsed facts | +| Architecture | Resolved module/crate edges and actual dependency direction | +| Dead code | Honest compiler/reachability evidence with explicit public/macro limits | +| Supply chain | Exact declarations, lock resolution, age, vulnerabilities, sources, exceptions | +| Tests | Selection, affected closure, execution counts/outcomes, ownership and evidence | +| Portability | Native targets, conditional compilation, host requirements, FFI | +| Release | Verified Rust tools/helpers on every supported Code Polishy host | + +## Phase 1: discovery and Cargo model + +- [ ] Parse manifests as data, then use versioned Cargo metadata for resolved + facts. Model virtual workspaces, members/excludes/default members, inheritance, + renamed/path/git/registry dependencies, target-specific dependencies, features, + build dependencies, proc-macro crates, examples, benches, tests and doctests. + Use `--format-version 1`; package IDs and source IDs are opaque. Accept + additive metadata fields within the supported format, while validating the + fields actually consumed. `--no-deps` has no resolved dependency graph; + `resolve: null` cannot mean an empty or successfully checked graph. +- [ ] Own `.rs`, `.wgsl`, `Cargo.toml`, `Cargo.lock`, `rust-toolchain` and + `rust-toolchain.toml`, both `.cargo/config` and `.cargo/config.toml`, + `rustfmt.toml`/`.rustfmt.toml`, `clippy.toml`/`.clippy.toml`, and generated-output + provenance explicitly. WGSL is a distinct shader input, not Rust source or + ignored data. Cargo output directories must come from governed configuration + and metadata; do not globally ignore every directory named `target`. +- [ ] Reject path escapes and ambiguous ownership. Do not infer package names + from directory basenames. Record incomplete dependency metadata as unavailable. +- [ ] Keep discovery offline/read-only after controlled dependency preparation; + resolution must not silently refresh a lockfile or download dependencies. + Use frozen resolution after dependency preparation. The data-only bootstrap + path must still explain missing locks/dependencies without invoking a build. +- [ ] Propagate manifest, lockfile, feature, toolchain, shader and build-input + changes to affected modules and evidence invalidation. + +## Phase 2: sealed tools and execution boundaries + +- [ ] Add policy-owned, integrity-checked Cargo/rustc/rustfmt/Clippy/helper + installation and runtime resolution, with licenses, checksums and provenance. + Match compiler-coupled tools. Distinguish the target's minimum Rust version from + the exact approved execution toolchain. No implicit `stable` channel updates. +- [ ] Keep native SDK/linker requirements explicit per host. Missing Windows SDK, + Xcode, system libraries or target components cannot be reported as a pass. +- [ ] Separate non-executing source/manifest analysis from compilation. Cargo + checking can execute build scripts and procedural macros; `--offline`, + `--locked`, and `--frozen` do not make that execution safe by themselves. +- [ ] Contain authorized compilation with no credentials, bounded resources, + controlled network/filesystem access, dedicated caches and output directories, + and process-tree cancellation. Inventory generated `OUT_DIR` inputs and native + tool invocations. Do not run target code merely to discover its policy. +- [ ] Govern Cargo aliases, runners, wrappers, linkers, rustflags, environment, + source replacement, and local/user configs. Separate necessary build settings + from attempts to weaken analysis or execute unrelated commands. Account for + ancestor configuration outside the checkout and Cargo home, explicit config + files/includes, rustup overrides, and the actual invocation directory. A + workspace-root invocation does not read each member's Cargo configuration. + +## Phase 3: source semantics and architecture + +- [ ] Use parser/compiler-backed facts for modules, imports, re-exports, renamed + crates, `#[path]`, `include!`, macro-generated items, and `cfg`/feature branches. + Cargo's crate graph alone is insufficient for intra-crate module boundaries. +- [ ] Analyse each supported target/feature configuration honestly. Do not use + `--all-features` blindly when features are mutually exclusive, or treat a + Linux build as coverage of Windows/macOS branches. +- [ ] Feed resolved facts into existing ownership, cycle and dependency rules. + Missing macro expansion or unresolved dynamic/generated edges must be visible + coverage limitations, not fabricated source edges or silent success. +- [ ] Enforce quality rules via normalized facts and existing policy defaults. + Do not claim that Clippy alone supplies full dead-code or architecture proof. + Explain public-library entry points and feature-specific reachability. +- [ ] Inventory unsafe blocks/functions, FFI, and native dependencies with exact + ownership and rationale. Scoped graphics interop may require unsafe; neither + a blanket prohibition nor a global exemption is appropriate. +- [ ] Govern Rust lint attributes and `#[expect]` without blanket allows. Keep + diagnostic IDs stable; policy exceptions stay exact, justified and expiring. +- [ ] Validate WGSL with an exact compatible parser/validator, map shader assets + to consumers, and invalidate receipts when shaders change. GPU execution and + visual correctness remain project-specific evidence. + +## Phase 4: Rust supply-chain governance + +- [ ] Require exact registry declarations (`=x.y.z`), immutable full Git revisions, + contained path dependencies, and a checked-in lock for the governed workspace, + including library workspaces whose policy evidence depends on resolution. Plain `x.y.z` + permits compatible updates in Cargo and is not an exact pin. +- [ ] Review workspace inheritance, optional/build/dev dependencies, patches, + alternate registries, vendoring, yanked versions and native build inputs. +- [ ] Generate/review candidate resolution without running build scripts or + macros, then install approved dependencies through the existing workflow. +- [ ] Extend age/provenance/security-fix admission and signed Git evidence to + Cargo sources. Preserve distinctions among missing, stale and trusted facts. +- [ ] Integrate pinned Rust advisory tooling/data with the common finding and + exception model. A lockfile advisory scan is not a reachability analysis; + assess applicability through evidence before security-fix admission. +- [ ] Produce complete SBOM and release provenance, including bundled native + libraries and helper executables. Reuse artifact-security ownership rather + than adding an unrelated Rust scanner pipeline. + +## Phase 5: tests, reports and release integration + +- [ ] Map Rust suites to focused/recommended/full/supplemental profiles. Support + unit, integration, doctest, benchmark and native app workflows distinctly. +- [ ] Reject a required suite that selects zero tests, skips every test, or only + builds. Use structured, version-bound runner evidence; do not parse arbitrary + terminal prose as authoritative results. Choose a supported structured runner + deliberately if stable Cargo output cannot satisfy the evidence contract. +- [ ] Preserve temporary state, assertion strength, suite ownership, cancellation, + performance leases, live approvals, and receipt identity semantics. +- [ ] Bind compiler, helper, source/config closure, features, target, native SDK, + shaders and relevant environment into receipts. Do not reuse GPU performance + results across adapters, drivers, displays or execution environments. +- [ ] Extend schema, diagnostics, JSON/SARIF, doctor, installation, docs catalog, + adoption guidance, design context, CI bundles and release verification together. + Generated surfaces must be regenerated from their owners. +- [ ] Supply valid and seeded-invalid fixtures: workspace ownership, forbidden + edge, macro/cfg coverage failure, config suppression, unpinned dependency, + lock drift, rejected provenance, shader error, zero-test selection, timeout, + failed test and stale receipt. Include mixed Go/Python/Rust repositories and + real Windows/macOS/Linux installations. + +## Delivery and acceptance + +Release only after the upstream repository's authorized release workflow proves +the entire capability matrix through the public CLI and installed artifact. +Publish an exact version/digest through separately authorized publication. +Masterbrainer then adopts that release through its dependency workflow. + +No target should need a Rust language-pack installation, hand-maintained import +inventory, copied tool bootstrap, or duplicate lint/security gate to satisfy the +baseline. Required capabilities with known gaps cannot be described as complete. +Masterbrainer retains ownership of GPU baselines and real application workflows. + +## Implementation status + +The input coverage foundation is implemented: standard Rust settings trigger +full policy selection, and WGSL has distinct source classification and module +ownership. Temporary-repository fixtures cover changed/deleted settings, shader +dependency impact, data rejection, and missing structured analysis. Current +behavior is documented in [Rust and shader input coverage](../design/rust-inputs.md). + +The next milestone is Cargo facts and sealed discovery. Workspace resolution, +Rust tool installation, semantic analysis, supply-chain checks, structured Rust +test results and release parity remain open. The existing Rust coverage failures +remain until their implementations and evidence exist. This beta foundation is +not yet usable as first-class Rust support for Masterbrainer. + +Primary references: [Cargo metadata](https://doc.rust-lang.org/cargo/commands/cargo-metadata.html), +[Cargo dependency semantics](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html), +[build-script execution](https://doc.rust-lang.org/cargo/reference/build-scripts.html), +[Clippy configuration](https://doc.rust-lang.org/clippy/configuration.html). + +Additional references: [Cargo configuration](https://doc.rust-lang.org/cargo/reference/config.html), +[rustup overrides](https://rust-lang.github.io/rustup/overrides.html), +[rustfmt configuration](https://github.com/rust-lang/rustfmt#configuring-rustfmt). diff --git a/internal/policy/control_inputs.go b/internal/policy/control_inputs.go index c4e48791..d910ad45 100644 --- a/internal/policy/control_inputs.go +++ b/internal/policy/control_inputs.go @@ -12,6 +12,8 @@ var sensitiveControlPatterns = []string{ "**/package.json", "**/package-lock.json", "**/npm-shrinkwrap.json", "**/pnpm-lock.yaml", "**/pnpm-workspace.yaml", "**/pnpm-workspace.yml", "**/bun.lockb", "**/pyproject.toml", "**/requirements*.txt", "**/uv.lock", "**/Pipfile", "**/Cargo.toml", "**/pom.xml", "**/build.gradle", "**/build.gradle.kts", "**/Gemfile", "**/composer.json", "**/Package.swift", "**/Package.resolved", "**/pubspec.yaml", + "**/.cargo/config", "**/.cargo/config.toml", "**/rust-toolchain", "**/rust-toolchain.toml", + "**/rustfmt.toml", "**/.rustfmt.toml", "**/clippy.toml", "**/.clippy.toml", "**/*.lock", "**/*-lock.*", "**/eslint.config.*", "**/.eslintrc*", "**/knip.*", "**/.knip.*", "**/prettier.config.*", "**/.prettierrc*", "**/tsconfig*.json", "**/ruff.toml", "**/.ruff.toml", "**/ty.toml", "**/osv-scanner.toml", diff --git a/internal/policy/data.go b/internal/policy/data.go index dc1a891e..a1127867 100644 --- a/internal/policy/data.go +++ b/internal/policy/data.go @@ -92,7 +92,7 @@ func overlapsScope(pattern string, candidates []string) bool { func dataPatternIsExecutableSource(pattern string) bool { for _, extension := range []string{ - ".bash", ".c", ".cc", ".cjs", ".cpp", ".cs", ".cts", ".cxx", ".dart", ".go", ".h", ".hpp", ".java", ".js", ".jsx", ".kt", ".kts", ".mjs", ".mts", ".php", ".proto", ".py", ".pyi", ".rb", ".rs", ".sh", ".sql", ".swift", ".ts", ".tsx", ".vue", ".svelte", ".astro", + ".bash", ".c", ".cc", ".cjs", ".cpp", ".cs", ".cts", ".cxx", ".dart", ".go", ".h", ".hpp", ".java", ".js", ".jsx", ".kt", ".kts", ".mjs", ".mts", ".php", ".proto", ".py", ".pyi", ".rb", ".rs", ".sh", ".sql", ".swift", ".ts", ".tsx", ".vue", ".svelte", ".astro", ".wgsl", } { if strings.HasSuffix(strings.ToLower(pattern), extension) { return true diff --git a/internal/policy/load.go b/internal/policy/load.go index 9f971ca1..1719fa57 100644 --- a/internal/policy/load.go +++ b/internal/policy/load.go @@ -352,7 +352,7 @@ func validateScope(config *Config) error { } func validateLanguageRules(rules []LanguageRule) error { - reserved := []string{"go", "typescript", "python", "shell", "rust", "jvm", "ruby", "php", "swift", "native", "protobuf", "sql", "dart"} + reserved := []string{"go", "typescript", "python", "shell", "rust", "wgsl", "jvm", "ruby", "php", "swift", "native", "protobuf", "sql", "dart"} seen := map[string]bool{} for index, rule := range rules { label := fmt.Sprintf("scope.languages[%d]", index) diff --git a/internal/policy/rust_inputs_test.go b/internal/policy/rust_inputs_test.go new file mode 100644 index 00000000..2ebce2b1 --- /dev/null +++ b/internal/policy/rust_inputs_test.go @@ -0,0 +1,43 @@ +package policy + +import ( + "strings" + "testing" +) + +func TestRustConfigurationPathsAreSensitive(t *testing.T) { + t.Parallel() + for _, path := range []string{ + "Cargo.toml", "Cargo.lock", ".cargo/config", ".cargo/config.toml", + "rust-toolchain", "rust-toolchain.toml", "rustfmt.toml", ".rustfmt.toml", "clippy.toml", ".clippy.toml", + } { + for _, prefix := range []string{"", "crates/graphics/", "./crates/graphics/", `crates\graphics\`} { + if !IsSensitiveControlInput(prefix + path) { + t.Errorf("Rust control input %q was not recognized", prefix+path) + } + } + } + for _, path := range []string{"config.toml", "src/config.rs", "shaders/main.wgsl", "notes/rustfmt.toml.md", ".cargo/config.example", "cargo/config.toml"} { + if IsSensitiveControlInput(path) { + t.Errorf("ordinary path %q was classified as a Rust control input", path) + } + } +} + +func TestWGSLRulesCannotHideShaderSource(t *testing.T) { + t.Parallel() + for name, testCase := range map[string]struct { + scope string + message string + }{ + "reserved language": {scope: `{"languages":[{"name":"wgsl","paths":["shaders/**"]}]}`, message: "unique custom language identifier"}, + "shader data": {scope: `{"data":["shaders/**/*.wgsl"]}`, message: "must not match executable source"}, + } { + t.Run(name, func(t *testing.T) { + configured := strings.Replace(minimalConfig(), `"quality":{}`, `"quality":{},"scope":`+testCase.scope, 1) + if _, err := Load(writeConfig(t, configured), ""); err == nil || !strings.Contains(err.Error(), testCase.message) { + t.Fatalf("configuration error = %v, want %q", err, testCase.message) + } + }) + } +} diff --git a/internal/quality/wgsl_test.go b/internal/quality/wgsl_test.go new file mode 100644 index 00000000..abac0629 --- /dev/null +++ b/internal/quality/wgsl_test.go @@ -0,0 +1,51 @@ +package quality + +import ( + "slices" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" +) + +func TestWGSLRequiresAnalysisEvidence(t *testing.T) { + t.Parallel() + for _, ordinaryCommand := range []bool{false, true} { + repo := qualityRepository(t) + path := "shaders/main.wgsl" + repo.Config.Modules = []policy.Module{{Name: "shaders", Paths: []string{"shaders/**"}}} + repo.Config.ModuleByName = map[string]int{"shaders": 0} + if ordinaryCommand { + repo.Config.Checks = []policy.Command{{ + Name: "shader-check", Argv: []string{"cargo", "check"}, Modules: []string{"shaders"}, + Provides: []string{"format", "lint", "typecheck", "complexity", "dead-code", "architecture"}, RunOn: []string{"check", "gate"}, + }} + } + writeQualityFile(t, repo.Root, path, "@compute @workgroup_size(1) fn main() {}\n") + findings := CoverageFindings(repo, []string{path}) + for _, subject := range []string{"format:check", "lint:check", "typecheck:check", "complexity:check", "dead-code:check", "architecture:check", "typecheck:gate"} { + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return finding.Check == "policy.checkCoverage" && finding.Path == path && finding.Subject == subject + }) { + t.Fatalf("ordinary command %t: missing %s coverage failure: %+v", ordinaryCommand, subject, findings) + } + } + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return finding.Check == "policy.sourceCommentCoverage" && finding.Path == path && finding.Subject == "wgsl" + }) { + t.Fatalf("missing WGSL comment analysis passed: %+v", findings) + } + } +} + +func TestWGSLRequiresModuleOwnership(t *testing.T) { + t.Parallel() + repo := qualityRepository(t) + path := "shaders/main.wgsl" + writeQualityFile(t, repo.Root, path, "@compute @workgroup_size(1) fn main() {}\n") + findings := CoverageFindings(repo, []string{path}) + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return finding.Check == "policy.moduleCoverage" && finding.Path == path && finding.Subject == "unowned" + }) { + t.Fatalf("unowned shader passed coverage: %+v", findings) + } +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 4df804c0..c030e3ca 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -601,7 +601,7 @@ func builtInExtensionLanguage(path string) string { ".go": "go", ".ts": "typescript", ".tsx": "typescript", ".js": "typescript", ".jsx": "typescript", ".mjs": "typescript", ".cjs": "typescript", ".mts": "typescript", ".cts": "typescript", ".vue": "typescript", ".svelte": "typescript", ".astro": "typescript", - ".py": "python", ".pyi": "python", ".sh": "shell", ".bash": "shell", ".rs": "rust", + ".py": "python", ".pyi": "python", ".sh": "shell", ".bash": "shell", ".rs": "rust", ".wgsl": "wgsl", ".java": "jvm", ".kt": "jvm", ".kts": "jvm", ".rb": "ruby", ".php": "php", ".swift": "swift", ".c": "native", ".h": "native", ".cc": "native", ".cpp": "native", ".cxx": "native", ".hpp": "native", ".proto": "protobuf", ".sql": "sql", ".dart": "dart", diff --git a/internal/repository/rust_inputs_test.go b/internal/repository/rust_inputs_test.go new file mode 100644 index 00000000..6df3b7b1 --- /dev/null +++ b/internal/repository/rust_inputs_test.go @@ -0,0 +1,96 @@ +package repository + +import ( + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" +) + +func TestRustConfigurationChangesExpandSelection(t *testing.T) { + t.Parallel() + for _, path := range []string{ + "Cargo.toml", "Cargo.lock", ".cargo/config", "crates/graphics/.cargo/config.toml", + "rust-toolchain", "crates/graphics/rust-toolchain.toml", "rustfmt.toml", ".rustfmt.toml", + "clippy.toml", "crates/graphics/.clippy.toml", + } { + t.Run(path, func(t *testing.T) { + repo := newGitRepository(t) + writeFile(t, repo.Root, path, "old settings\n") + writeFile(t, repo.Root, "src/lib.rs", "pub fn value() -> u32 { 1 }\n") + writeFile(t, repo.Root, "shaders/main.wgsl", "@compute @workgroup_size(1) fn main() {}\n") + git(t, repo.Root, "add", ".") + git(t, repo.Root, "commit", "-m", "base") + writeFile(t, repo.Root, path, "new settings\n") + for _, mode := range []string{"changes", "staged"} { + git(t, repo.Root, "add", path) + selection, err := repo.Select(mode, nil) + if err != nil { + t.Fatal(err) + } + if !selection.All || !selection.PolicySensitive || !slices.Equal(selection.Candidate.AddedOrModified, []string{path}) || + !slices.Contains(selection.Files, "src/lib.rs") || !slices.Contains(selection.Files, "shaders/main.wgsl") { + t.Fatalf("%s selection = %+v", mode, selection) + } + } + if err := os.Remove(filepath.Join(repo.Root, filepath.FromSlash(path))); err != nil { + t.Fatal(err) + } + selection, err := repo.Select("changes", nil) + if err != nil { + t.Fatal(err) + } + if !selection.PolicySensitive || !slices.Equal(selection.Candidate.Deleted, []string{path}) || !slices.Contains(selection.Files, "src/lib.rs") { + t.Fatalf("deleted configuration selection = %+v", selection) + } + }) + } +} + +func TestWGSLSourceRetainsOwnershipAndChangedImpact(t *testing.T) { + t.Parallel() + repo := newGitRepository(t) + repo.Config.Modules = []policy.Module{ + {Name: "shaders", Paths: []string{"shaders/**"}}, + {Name: "renderer", Paths: []string{"src/**"}, DependsOn: []string{"shaders"}}, + } + writeFile(t, repo.Root, "src/lib.rs", "pub fn value() -> u32 { 1 }\n") + writeFile(t, repo.Root, "shaders/main.WGSL", "@compute @workgroup_size(1) fn main() {}\n") + git(t, repo.Root, "add", ".") + git(t, repo.Root, "commit", "-m", "base") + writeFile(t, repo.Root, "shaders/main.WGSL", "@compute @workgroup_size(2) fn main() {}\n") + selection, err := repo.Select("changes", nil) + if err != nil { + t.Fatal(err) + } + if selection.All || selection.PolicySensitive || !slices.Equal(selection.Files, []string{"shaders/main.WGSL"}) { + t.Fatalf("shader selection = %+v", selection) + } + path := selection.Files[0] + if repo.Language(path) != "wgsl" || !repo.IsExecutableSource(path) || repo.SourceCommentLanguage(path) != "wgsl" { + t.Fatal("shader was not classified as governed WGSL source") + } + impact := repo.CandidateImpact(selection.Candidate) + if !slices.Equal(impact.DirectModules, []string{"shaders"}) || !slices.Equal(impact.ImpactedModules, []string{"renderer", "shaders"}) { + t.Fatalf("shader impact = %+v", impact) + } +} + +func TestRustInputsCannotBeDeclaredData(t *testing.T) { + t.Parallel() + for _, path := range []string{"crates/app/.cargo/config.toml", "crates/app/.clippy.toml", "shaders/main.wgsl"} { + t.Run(path, func(t *testing.T) { + repo := Repository{Root: t.TempDir(), Config: policy.Config{Scope: policy.Scope{Data: []string{path}}}} + writeFile(t, repo.Root, path, "input\n") + if repo.IsData(path) { + t.Fatal("executable or control input was classified as data") + } + if _, err := repo.AllFiles(); err == nil || !strings.Contains(err.Error(), "scope.data must not classify") { + t.Fatalf("inventory error = %v", err) + } + }) + } +} From 7b815e8074aa0bb88950063355f69a4448ffe73e Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Thu, 24 Sep 2026 18:41:08 +0000 Subject: [PATCH 02/49] docs: align Rust input design title with catalog Verified with governed formatting and documentation-unit. The beta checkpoint will be rerun for this corrected candidate. --- docs/design/rust-inputs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/rust-inputs.md b/docs/design/rust-inputs.md index 038624d0..03c95943 100644 --- a/docs/design/rust-inputs.md +++ b/docs/design/rust-inputs.md @@ -1,4 +1,4 @@ -# Rust and shader input coverage +# Rust and Shader Input Coverage Rust compilation settings affect which code and diagnostics exist. Cargo configuration can change source registries, rustflags, wrappers, linkers and From 10bd023ac5021d8d403794d2c0a7d1e9b6763be4 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Thu, 24 Sep 2026 22:39:44 +0000 Subject: [PATCH 03/49] feat: add pinned Rust tooling and Cargo workspace discovery Add bounded Cargo manifest and metadata validation, governed discovery commands, and authenticated Rust 1.97.1 component installation. Verification: dependency review and offline supply-chain passed; focused source policy checks, repository-unit, rustfacts-unit, installer recovery contract, Cargo quality tests, and valid/invalid public CLI discovery passed. One existing quality.go file-length review warning remains. Progress milestone: compiler/formatter/source semantics, dependency policy, Rust test evidence, and installed-release packaging remain open. Full Rust coverage is not claimed. --- .code-polishy.json | 68 +++++- docs/catalog.json | 8 + docs/design/cargo-facts.md | 27 +++ docs/design/rust-inputs.md | 31 +++ docs/plans/code-polishy-rust-support-plan.md | 61 ++++-- go.mod | 1 + go.sum | 2 + internal/quality/cargo.go | 19 ++ internal/quality/cargo_manifest.go | 17 ++ internal/quality/cargo_manifest_test.go | 32 +++ internal/quality/cargo_test.go | 49 +++++ internal/quality/quality.go | 4 + internal/repository/cargo_manifest.go | 106 +++++++++ internal/repository/cargo_manifest_test.go | 112 ++++++++++ internal/repository/contained_file.go | 16 ++ internal/repository/repository.go | 9 + internal/repository/rust_tools.go | 10 + internal/repository/rust_tools_test.go | 31 +++ internal/rustfacts/configuration.go | 96 ++++++++ internal/rustfacts/configuration_test.go | 88 ++++++++ internal/rustfacts/discovery.go | 159 ++++++++++++++ internal/rustfacts/discovery_test.go | 134 ++++++++++++ internal/rustfacts/execution.go | 72 ++++++ internal/rustfacts/metadata.go | 218 +++++++++++++++++++ internal/rustfacts/metadata_test.go | 101 +++++++++ scripts/test-rust-runtime.sh | 90 ++++++++ tools/install-policy-tools.ps1 | 1 + tools/install-policy-tools.sh | 1 + tools/install-rust.ps1 | 86 ++++++++ tools/install-rust.sh | 84 +++++++ tools/rust-version.txt | 1 + tools/rust_checksums.txt | 25 +++ 32 files changed, 1742 insertions(+), 17 deletions(-) create mode 100644 docs/design/cargo-facts.md create mode 100644 internal/quality/cargo.go create mode 100644 internal/quality/cargo_manifest.go create mode 100644 internal/quality/cargo_manifest_test.go create mode 100644 internal/quality/cargo_test.go create mode 100644 internal/repository/cargo_manifest.go create mode 100644 internal/repository/cargo_manifest_test.go create mode 100644 internal/repository/rust_tools.go create mode 100644 internal/repository/rust_tools_test.go create mode 100644 internal/rustfacts/configuration.go create mode 100644 internal/rustfacts/configuration_test.go create mode 100644 internal/rustfacts/discovery.go create mode 100644 internal/rustfacts/discovery_test.go create mode 100644 internal/rustfacts/execution.go create mode 100644 internal/rustfacts/metadata.go create mode 100644 internal/rustfacts/metadata_test.go create mode 100755 scripts/test-rust-runtime.sh create mode 100644 tools/install-rust.ps1 create mode 100755 tools/install-rust.sh create mode 100644 tools/rust-version.txt create mode 100644 tools/rust_checksums.txt diff --git a/.code-polishy.json b/.code-polishy.json index 2386e957..9d3bbab2 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -23,13 +23,28 @@ }, "documentation": { "design": [ + { + "path": "docs/design/cargo-facts.md", + "module": "rustfacts" + }, { "path": "docs/design/rust-inputs.md", "sourcePaths": [ "internal/policy/data.go", "internal/policy/rust_inputs_test.go", + "internal/repository/cargo_manifest.go", + "internal/repository/cargo_manifest_test.go", + "internal/repository/rust_tools.go", + "internal/repository/rust_tools_test.go", "internal/repository/rust_inputs_test.go", - "internal/quality/wgsl_test.go" + "internal/quality/cargo_manifest.go", + "internal/quality/cargo_manifest_test.go", + "internal/quality/cargo.go", + "internal/quality/cargo_test.go", + "internal/quality/wgsl_test.go", + "tools/install-rust.sh", + "tools/install-rust.ps1", + "scripts/test-rust-runtime.sh" ] }, { @@ -280,6 +295,11 @@ "paths": ["internal/repository/**"], "dependsOn": ["policy", "pythonfacts"] }, + { + "name": "rustfacts", + "paths": ["internal/rustfacts/**"], + "dependsOn": ["policy", "repository", "runner"] + }, { "name": "runner", "paths": ["internal/runner/**"], @@ -351,7 +371,8 @@ "policy", "pythonfacts", "repository", - "runner" + "runner", + "rustfacts" ] }, { @@ -484,6 +505,7 @@ "policy", "spdx", "pythonfacts", + "rustfacts", "repository", "runner", "gaterun", @@ -568,6 +590,15 @@ "argv": ["go", "test", "-count=1", "./internal/repository/..."], "paths": ["internal/repository/**/*_test.go"] }, + { + "name": "rustfacts-unit", + "kind": "unit", + "scope": "module", + "cost": "quick", + "modules": ["rustfacts"], + "argv": ["go", "test", "-count=1", "./internal/rustfacts/..."], + "paths": ["internal/rustfacts/**/*_test.go"] + }, { "name": "runner-unit", "kind": "unit", @@ -793,6 +824,22 @@ "runOn": ["focused", "recommended", "full"], "modules": ["tooling"] }, + { + "name": "rust-runtime-contract", + "kind": "contract", + "scope": "module", + "cost": "quick", + "paths": [ + "scripts/test-rust-runtime.sh", + "tools/install-rust.*", + "tools/rust-version.txt", + "tools/rust_checksums.txt", + "tools/verify-sha256.sh" + ], + "argv": ["./scripts/test-rust-runtime.sh"], + "runOn": ["focused", "recommended", "full"], + "modules": ["tooling"] + }, { "name": "javascript-runtime-contract", "kind": "contract", @@ -1334,6 +1381,7 @@ "scripts/test-mutation-wrapper.sh", "scripts/test-install.sh", "scripts/test-release-preflight.sh", + "scripts/test-rust-runtime.sh", "scripts/test-javascript-runtime.sh", "scripts/test-javascript-bundle.sh", "scripts/test-javascript-runner.sh", @@ -1364,6 +1412,11 @@ "module": "repository", "focusedSuite": "repository-unit" }, + { + "paths": ["internal/rustfacts/**/*_test.go"], + "module": "rustfacts", + "focusedSuite": "rustfacts-unit" + }, { "paths": ["internal/runner/**/*_test.go"], "module": "runner", @@ -1489,6 +1542,11 @@ "module": "tooling", "focusedSuite": "release-preflight-contract" }, + { + "paths": ["scripts/test-rust-runtime.sh"], + "module": "tooling", + "focusedSuite": "rust-runtime-contract" + }, { "paths": ["scripts/test-javascript-runtime.sh"], "module": "tooling", @@ -1586,6 +1644,12 @@ "locator": "koalaman/shellcheck", "tagPrefix": "v" }, + { + "name": "rust", + "versionFile": "tools/rust-version.txt", + "source": "github-release", + "locator": "rust-lang/rust" + }, { "name": "staticcheck", "versionFile": "tools/staticcheck-version.txt", diff --git a/docs/catalog.json b/docs/catalog.json index a690f531..b7f8a539 100644 --- a/docs/catalog.json +++ b/docs/catalog.json @@ -302,6 +302,14 @@ "aliases": [], "public": true }, + { + "id": "cargo-facts", + "path": "docs/design/cargo-facts.md", + "title": "Cargo Project Facts", + "summary": "Understand governed Cargo discovery, workspace facts, and dependency-resolution boundaries.", + "aliases": [], + "public": true + }, { "id": "security", "path": "docs/policies/security.md", diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md new file mode 100644 index 00000000..ac3a4972 --- /dev/null +++ b/docs/design/cargo-facts.md @@ -0,0 +1,27 @@ +# Cargo Project Facts + +The `rustfacts` boundary owns Cargo command preparation and metadata validation; +the repository layer owns data-only manifest parsing and tool paths. Quality, +architecture and dependency policy consume facts without acquiring their own +Cargo resolvers. Commands use the common structured runner so limits, cancellation +and command evidence have one owner. + +Workspace discovery runs `cargo metadata --format-version 1 --no-deps --frozen`. +It can discover members, targets, inherited metadata, declared dependencies and +features before dependencies are materialized. A null `resolve` is deliberately +retained as absent dependency-resolution evidence. Discovery neither compiles +code nor proves that dependencies or any target configuration have been checked. +The selected manifest and Cargo's actual workspace membership determine scope; +directory naming is never a package identifier. + +Cargo runs from the repository root with a sealed environment, absolute pinned +compiler paths and repository-local Cargo home and target output under `.tools`. +Ambient rustup selection, wrappers and user Cargo home are not inherited. Ancestor +Cargo configuration outside the checkout must not silently enter analysis. +Repository configuration is parsed before execution; unsupported execution-altering +settings produce a diagnostic. This controls discovery inputs, not operating-system +access by future build scripts or procedural macros. + +[Rust input coverage](rust-inputs.md) defines control input selection and the +governed toolchain. The [implementation plan](../plans/code-polishy-rust-support-plan.md) +records the remaining Rust capabilities and acceptance work. diff --git a/docs/design/rust-inputs.md b/docs/design/rust-inputs.md index 03c95943..0d5fd2a6 100644 --- a/docs/design/rust-inputs.md +++ b/docs/design/rust-inputs.md @@ -15,6 +15,15 @@ workspace and configuration resolution can prove a narrower affected closure. Recognizing a filename does not parse, approve or execute its settings, inspect configuration outside the repository, or establish reproducible Cargo execution. +Cargo manifest inspection uses a pinned Go TOML parser and the existing bounded, +contained regular-file reader. It parses package and virtual-workspace headers +without invoking Cargo, importing project code or executing build scripts. +Unknown manifest fields remain available to Cargo rather than being rejected +by an incomplete schema. Malformed selected manifests produce original-path +diagnostics without discarding successfully parsed siblings or inspecting +unrelated projects during a focused check. This inventory is not a resolved +workspace, target, feature or dependency graph; those facts belong to Cargo. + WGSL is executable shader source with its own built-in language identifier, `wgsl`. It participates in module ownership, source-comment coverage and the existing required analysis capabilities. A shader cannot be protected as data @@ -31,3 +40,25 @@ neither shader validity nor GPU execution correctness. The [Rust support plan](../plans/code-polishy-rust-support-plan.md) records the remaining discovery, analysis, dependency, test and installed-release work. + +## Governed toolchain + +The initial compiler is Rust 1.97.1, released 2026-07-16. Cargo, rustc, rustdoc, +rustfmt and Clippy come from that same official release, alongside the host +standard library. The pin is independent of a project's declared minimum Rust +version. The beta targets editions 2015, 2018, 2021 and 2024 on the existing +five Code Polishy installation hosts; additional compilation targets require +separate component and SDK evidence. + +The [upstream 1.98 regression](https://github.com/rust-lang/rust/issues/161441) +does not reproduce on 1.97.1. The initial pin avoids both that affected release +and an under-30-day toolchain admission. Checksums come from the +[official 1.97.1 manifest](https://static.rust-lang.org/dist/channel-rust-1.97.1.toml). +Installers authenticate component archives, copy their payloads without executing +installation scripts, preserve bundled licenses, and stage a complete runtime +before replacing the previous installation. They do not install rustup or select +an ambient channel. Release publication must carry the runtime and its exact pin; +the developer installer alone does not establish installed-release coverage. + +[Cargo project facts](cargo-facts.md) defines workspace discovery and its execution +boundary separately from source classification. diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 41cd1581..4b29e20a 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -22,6 +22,14 @@ compiler/parser facts through a bounded child-process protocol. Do not embed language-specific heuristics in unrelated engine modules or reimplement Rust resolution with regular expressions. +Parity means the same ordinary user-facing commands and policy outcomes as Go, +with Rust-native compilation units, Cargo conventions and honest scope. Features, +targets, build scripts, procedural macros, tests and generated source need their +own semantics. Validate the selected supported configuration, not every possible +feature and target combination. Reuse existing execution controls and expose +their limits; a new Rust-only operating-system sandbox is not a prerequisite for +the capabilities already delivered through equivalent Go execution. + ## Current baseline and architectural decisions The implementation base includes the accepted changes through `0.27.11`. @@ -99,12 +107,12 @@ changed modules; add a quick suite when a new module is introduced. establish the parity inventory above. - [ ] Turn each acceptance cell into executable evidence, preserving explicit unavailable/incomplete states and original-path diagnostics. -- [ ] Choose the supported Rust editions, compiler/MSRV relationship, target - triples, and feature configurations. Select exact versions at implementation - time; do not invent pins in this plan. Target editions 2015, 2018, 2021 and - 2024 subject to that toolchain's supported grammar. Code Polishy installation - hosts remain Linux/macOS on x64/arm64 and Windows x64; a host install does not - prove every Rust compilation target or feature combination. +- [x] Choose the initial toolchain boundary: Rust 1.97.1 with its matching Cargo, + rustfmt and Clippy; editions 2015, 2018, 2021 and 2024; project minimum Rust + versions may not exceed the governed compiler. Compilation initially uses + the native host and default features. Additional feature/target selections + need explicit analysis evidence. Installation hosts remain Linux/macOS on + x64/arm64 and Windows x64; installation does not prove cross-compilation. - [ ] Define a first-class Rust conditional module activated by governed Rust source/Cargo workspaces. Unsupported or missing required analysis must block completion rather than fall back to a successful generic command. @@ -124,6 +132,12 @@ changed modules; add a quick suite when a new module is introduced. ## Phase 1: discovery and Cargo model +- [x] Discover workspace members, declared dependencies, inherited package + metadata, features and targets using bounded, versioned, frozen Cargo + metadata. Expose discovery failures through the normal check path and + include its commands in execution planning. Preserve null resolution as + absent dependency-graph evidence. Test real Cargo without executing build + scripts or materializing dependencies. - [ ] Parse manifests as data, then use versioned Cargo metadata for resolved facts. Model virtual workspaces, members/excludes/default members, inheritance, renamed/path/git/registry dependencies, target-specific dependencies, features, @@ -158,10 +172,13 @@ changed modules; add a quick suite when a new module is introduced. - [ ] Separate non-executing source/manifest analysis from compilation. Cargo checking can execute build scripts and procedural macros; `--offline`, `--locked`, and `--frozen` do not make that execution safe by themselves. -- [ ] Contain authorized compilation with no credentials, bounded resources, - controlled network/filesystem access, dedicated caches and output directories, - and process-tree cancellation. Inventory generated `OUT_DIR` inputs and native - tool invocations. Do not run target code merely to discover its policy. +- [ ] Run authorized compilation through the common governed runner, with a + controlled environment, bounded resources, dedicated caches/output and + process-tree cancellation. Govern dependency network access explicitly. + Inventory generated `OUT_DIR` inputs and native tool invocations. Document + build-script and procedural-macro execution and the actual containment + provided; do not describe environment controls as an OS sandbox. Do not run + target code merely to discover its policy. - [ ] Govern Cargo aliases, runners, wrappers, linkers, rustflags, environment, source replacement, and local/user configs. Separate necessary build settings from attempts to weaken analysis or execute unrelated commands. Account for @@ -253,11 +270,25 @@ ownership. Temporary-repository fixtures cover changed/deleted settings, shader dependency impact, data rejection, and missing structured analysis. Current behavior is documented in [Rust and shader input coverage](../design/rust-inputs.md). -The next milestone is Cargo facts and sealed discovery. Workspace resolution, -Rust tool installation, semantic analysis, supply-chain checks, structured Rust -test results and release parity remain open. The existing Rust coverage failures -remain until their implementations and evidence exist. This beta foundation is -not yet usable as first-class Rust support for Masterbrainer. +Cargo discovery now runs through the existing check path and command planning. +Bounded TOML inspection reports malformed manifests at their original locations; +Cargo supplies workspace membership, inherited metadata, declared dependencies, +features and targets. Discovery uses a sealed environment and does not execute +build scripts or resolve dependencies. Tests preserve valid sibling projects and +reject unsupported configuration, escaping paths, malformed metadata and missing +tools. [Cargo project facts](../design/cargo-facts.md) owns that boundary. + +Developer setup installs authenticated Rust 1.97.1 components without rustup or +installation scripts. Linux runtime execution and installer integrity/recovery +fixtures pass. macOS/Windows installers and checksums are present but host execution +and release packaging still require verification. + +The next milestone is one complete crate path: selected-file formatting, compiler +diagnostics, source facts, architecture, dependencies and nonempty test evidence. +Resolved dependency graphs, semantic analysis, supply-chain checks, structured Rust +test results and installed-release parity remain open. Existing missing-capability +findings remain until those implementations and evidence exist. This beta is not +yet usable as first-class Rust support for Masterbrainer. Primary references: [Cargo metadata](https://doc.rust-lang.org/cargo/commands/cargo-metadata.html), [Cargo dependency semantics](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html), diff --git a/go.mod b/go.mod index a24ee516..203f280c 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/CycloneDX/cyclonedx-go v0.11.0 github.com/dlclark/regexp2 v1.11.0 github.com/in-toto/attestation v1.2.0 + github.com/pelletier/go-toml/v2 v2.4.3 github.com/rhysd/actionlint v1.7.12 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/yuin/goldmark v1.8.5 diff --git a/go.sum b/go.sum index 8c1ebd8f..67d14a8c 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,8 @@ github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEj github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= +github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rhysd/actionlint v1.7.12 h1:vQ4GeJN86C0QH+gTUQcs8McmK62OLT3kmakPMtEWYnY= diff --git a/internal/quality/cargo.go b/internal/quality/cargo.go new file mode 100644 index 00000000..afac2ad6 --- /dev/null +++ b/internal/quality/cargo.go @@ -0,0 +1,19 @@ +package quality + +import ( + "context" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" + "github.com/riteofstring/code-polishy/internal/rustfacts" +) + +func cargoDiscoveryFindings(ctx context.Context, repo repository.Repository, files []string, commandRunner runner.Runner) []policy.Finding { + allFiles, err := repo.AllFiles() + if err != nil { + return []policy.Finding{{Check: "policy.rustProject", Path: "repository", Subject: "inventory", Message: err.Error()}} + } + plan := rustfacts.PlanDiscovery(repo, files, allFiles) + return rustfacts.Discover(ctx, repo, plan, commandRunner).Findings +} diff --git a/internal/quality/cargo_manifest.go b/internal/quality/cargo_manifest.go new file mode 100644 index 00000000..290bb8bf --- /dev/null +++ b/internal/quality/cargo_manifest.go @@ -0,0 +1,17 @@ +package quality + +import ( + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" +) + +func cargoManifestFindings(repo repository.Repository, files []string) []policy.Finding { + findings := []policy.Finding{} + for _, problem := range repo.InspectCargoManifests(files).Problems { + findings = append(findings, policy.Finding{ + Check: "policy.rustProject", Path: problem.Path, Line: problem.Line, + Subject: "manifest", Message: problem.Message, + }) + } + return findings +} diff --git a/internal/quality/cargo_manifest_test.go b/internal/quality/cargo_manifest_test.go new file mode 100644 index 00000000..9fc76148 --- /dev/null +++ b/internal/quality/cargo_manifest_test.go @@ -0,0 +1,32 @@ +package quality + +import ( + "slices" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" +) + +func TestCargoManifestCoverageReportsOriginalLocation(t *testing.T) { + t.Parallel() + repo := qualityRepository(t) + writeQualityFile(t, repo.Root, "crates/app/Cargo.toml", "[package]\nname = 'app'\nname = 'duplicate'\n") + findings := CoverageFindings(repo, []string{"crates/app/Cargo.toml"}) + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return finding.Check == "policy.rustProject" && finding.Path == "crates/app/Cargo.toml" && finding.Line == 3 && finding.Subject == "manifest" + }) { + t.Fatalf("missing Cargo manifest diagnostic: %+v", findings) + } +} + +func TestCargoManifestCoverageRetainsFocusedScope(t *testing.T) { + t.Parallel() + repo := qualityRepository(t) + writeQualityFile(t, repo.Root, "good/Cargo.toml", "[package]\nname = 'good'\nedition = '2024'\n") + writeQualityFile(t, repo.Root, "bad/Cargo.toml", "[package\n") + for _, finding := range CoverageFindings(repo, []string{"good/Cargo.toml"}) { + if finding.Check == "policy.rustProject" { + t.Fatalf("unselected Cargo project affected focused coverage: %+v", finding) + } + } +} diff --git a/internal/quality/cargo_test.go b/internal/quality/cargo_test.go new file mode 100644 index 00000000..dd946416 --- /dev/null +++ b/internal/quality/cargo_test.go @@ -0,0 +1,49 @@ +package quality + +import ( + "path/filepath" + "slices" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestCargoCheckDiscoversInvalidMemberFromSelectedRustSource(t *testing.T) { + t.Parallel() + repo := cargoQualityRepository(t) + writeQualityFile(t, repo.Root, "Cargo.toml", "[workspace]\nmembers=['app', 'missing']\n") + writeQualityFile(t, repo.Root, "app/Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\n") + writeQualityFile(t, repo.Root, "app/src/lib.rs", "pub fn app() {}\n") + findings := cargoDiscoveryFindings(t.Context(), repo, []string{"app/src/lib.rs"}, runner.OSRunner{}) + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return finding.Check == "policy.rustProject" && finding.Subject == "policy-cargo-metadata-app/Cargo.toml" + }) { + t.Fatalf("missing workspace failure from selected source: %+v", findings) + } +} + +func TestCargoCheckCommandsExposeActualDiscoveryPlan(t *testing.T) { + t.Parallel() + repo := cargoQualityRepository(t) + writeQualityFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\n") + writeQualityFile(t, repo.Root, "src/lib.rs", "pub fn app() {}\n") + commands := CheckCommands(repo, repository.Selection{Files: []string{"src/lib.rs"}}, "check") + for _, name := range []string{"policy-rust-version", "policy-cargo-version", "policy-cargo-metadata-Cargo.toml"} { + if !slices.ContainsFunc(commands, func(command policy.Command) bool { return command.Name == name && command.SealedEnvironment }) { + t.Errorf("Cargo command %s is missing from check planning: %+v", name, commands) + } + } +} + +func cargoQualityRepository(t *testing.T) repository.Repository { + t.Helper() + repo := qualityRepository(t) + var err error + repo.PolicyRoot, err = filepath.Abs("../..") + if err != nil { + t.Fatal(err) + } + return repo +} diff --git a/internal/quality/quality.go b/internal/quality/quality.go index 84a33556..805cc585 100644 --- a/internal/quality/quality.go +++ b/internal/quality/quality.go @@ -19,12 +19,14 @@ import ( "github.com/riteofstring/code-polishy/internal/policy" "github.com/riteofstring/code-polishy/internal/repository" "github.com/riteofstring/code-polishy/internal/runner" + "github.com/riteofstring/code-polishy/internal/rustfacts" ) func Check(ctx context.Context, repo repository.Repository, selection repository.Selection, commandRunner runner.Runner, profile string) []policy.Finding { repo = repo.WithAnalysisProfile(profile) findings := sourceChecks(repo, selection.Files) findings = append(findings, DataSyntaxFindings(ctx, repo, selection.Files)...) + findings = append(findings, cargoDiscoveryFindings(ctx, repo, selection.Files, commandRunner)...) findings = append(findings, goToolChecks(ctx, repo, selection.Files, commandRunner)...) findings = append(findings, shellChecks(ctx, repo, selection.Files, commandRunner)...) findings = append(findings, JavaScriptFormatFindings(ctx, repo, selection.Files)...) @@ -46,6 +48,7 @@ func CheckCommands(repo repository.Repository, selection repository.Selection, p if allFiles, err := repo.AllFiles(); err == nil { goCommands, _ := nativeGoToolCommands(repo, selection.Files, allFiles) commands = append(commands, goCommands...) + commands = append(commands, rustfacts.PlanDiscovery(repo, selection.Files, allFiles).Commands...) } shellCommands, _ := shellToolCommands(repo, selection.Files) commands = append(commands, shellCommands...) @@ -312,6 +315,7 @@ func commandFileArgument(cwd, path string) string { func CoverageFindings(repo repository.Repository, files []string) []policy.Finding { languagesByModule, findings := inventoryModuleLanguages(repo, files) + findings = append(findings, cargoManifestFindings(repo, files)...) findings = append(findings, repo.GeneratedJavaScriptOwnershipFindings(files)...) findings = append(findings, repo.InspectGeneration(files).Findings...) findings = append(findings, generatedStyleCoverageFindings(repo, files)...) diff --git a/internal/repository/cargo_manifest.go b/internal/repository/cargo_manifest.go new file mode 100644 index 00000000..1e4ab20a --- /dev/null +++ b/internal/repository/cargo_manifest.go @@ -0,0 +1,106 @@ +package repository + +import ( + "errors" + "os" + "path/filepath" + "strings" + + "github.com/pelletier/go-toml/v2" +) + +type CargoManifest struct { + Path string + Root string + Package *CargoPackageManifest + Workspace *CargoWorkspaceManifest +} + +type CargoPackageManifest struct { + Name string `toml:"name"` + Workspace string `toml:"workspace"` +} + +type CargoWorkspaceManifest struct { + Members []string `toml:"members"` + Exclude []string `toml:"exclude"` + DefaultMembers []string `toml:"default-members"` +} + +type CargoManifestProblem struct { + Path string + Line int + Message string +} + +type CargoManifestInventory struct { + Manifests []CargoManifest + Problems []CargoManifestProblem +} + +func (repo Repository) InspectCargoManifests(files []string) CargoManifestInventory { + inventory := CargoManifestInventory{} + paths := map[string]bool{} + for _, path := range files { + if filepath.Base(path) == "Cargo.toml" { + paths[path] = true + } + } + if len(paths) == 0 { + return inventory + } + root, err := os.OpenRoot(repo.Root) + if err != nil { + inventory.Problems = append(inventory.Problems, cargoManifestProblem("Cargo.toml", err)) + return inventory + } + defer root.Close() + for _, path := range sortedSet(paths) { + manifest, err := repo.readCargoManifest(root, path) + if err != nil { + inventory.Problems = append(inventory.Problems, cargoManifestProblem(path, err)) + continue + } + inventory.Manifests = append(inventory.Manifests, manifest) + } + return inventory +} + +func (repo Repository) readCargoManifest(root *os.Root, path string) (CargoManifest, error) { + info, err := repo.containedRegularFileInfo(root, path) + if err != nil { + return CargoManifest{}, err + } + data, err := readContainedFile(root, path, info, 8<<20) + if err != nil { + return CargoManifest{}, err + } + var document struct { + Package *CargoPackageManifest `toml:"package"` + Workspace *CargoWorkspaceManifest `toml:"workspace"` + } + if err := toml.Unmarshal(data, &document); err != nil { + return CargoManifest{}, err + } + if document.Package == nil && document.Workspace == nil { + return CargoManifest{}, errors.New("manifest must declare a package or workspace") + } + if document.Package != nil && strings.TrimSpace(document.Package.Name) == "" { + return CargoManifest{}, errors.New("package must declare its name") + } + if document.Package != nil && document.Package.Workspace != "" && document.Workspace != nil { + return CargoManifest{}, errors.New("a package cannot declare both package.workspace and a workspace table") + } + return CargoManifest{ + Path: path, Root: filepath.ToSlash(filepath.Dir(path)), Package: document.Package, Workspace: document.Workspace, + }, nil +} + +func cargoManifestProblem(path string, err error) CargoManifestProblem { + problem := CargoManifestProblem{Path: path, Message: "Cargo manifest cannot be read: " + err.Error()} + var decoding *toml.DecodeError + if errors.As(err, &decoding) { + problem.Line, _ = decoding.Position() + } + return problem +} diff --git a/internal/repository/cargo_manifest_test.go b/internal/repository/cargo_manifest_test.go new file mode 100644 index 00000000..66ae2c43 --- /dev/null +++ b/internal/repository/cargo_manifest_test.go @@ -0,0 +1,112 @@ +package repository + +import ( + "os" + "path/filepath" + "slices" + "strings" + "testing" +) + +func TestCargoManifestInventoryReadsPackagesAndVirtualWorkspaces(t *testing.T) { + t.Parallel() + repo := Repository{Root: t.TempDir()} + writeFile(t, repo.Root, "Cargo.toml", "[workspace]\nmembers = ['crates/*']\nexclude = ['crates/scratch']\ndefault-members = ['crates/app']\n[workspace.package]\nedition = '2024'\nversion = '1.0.0'\n") + writeFile(t, repo.Root, "crates/app/Cargo.toml", "[package]\nname = 'actual-name'\nworkspace = '../..'\nedition.workspace = true\nversion.workspace = true\n[package.metadata.editor]\nmessage = '''not a table: [package]'''\n") + inventory := repo.InspectCargoManifests([]string{"crates/app/Cargo.toml", "Cargo.toml", "Cargo.toml", "src/lib.rs"}) + if len(inventory.Problems) != 0 || len(inventory.Manifests) != 2 { + t.Fatalf("manifest inventory = %+v", inventory) + } + workspace, member := inventory.Manifests[0], inventory.Manifests[1] + if workspace.Package != nil || workspace.Workspace == nil || workspace.Root != "." || + !slices.Equal(workspace.Workspace.Members, []string{"crates/*"}) || + !slices.Equal(workspace.Workspace.Exclude, []string{"crates/scratch"}) || + !slices.Equal(workspace.Workspace.DefaultMembers, []string{"crates/app"}) { + t.Fatalf("virtual workspace = %+v", workspace) + } + if member.Package == nil || member.Package.Name != "actual-name" || member.Package.Workspace != "../.." || member.Root != "crates/app" { + t.Fatalf("member manifest = %+v", member) + } +} + +func TestCargoManifestInventoryReportsMalformedInputs(t *testing.T) { + t.Parallel() + for name, input := range map[string]string{ + "syntax": "[package\nname = 'app'\n", + "duplicate": "[package]\nname = 'app'\nname = 'other'\n", + "package shape": "package = 'app'\n", + "members shape": "[workspace]\nmembers = [1]\n", + "name shape": "[package]\nname = []\n", + "no package": "[profile.release]\nopt-level = 3\n", + "no name": "[package]\nversion = '1.0.0'\n", + "workspace conflict": "[package]\nname = 'app'\nworkspace = '..'\n[workspace]\n", + "invalid UTF-8": "[package]\nname = '\xff'\n", + } { + t.Run(name, func(t *testing.T) { + repo := Repository{Root: t.TempDir()} + writeFile(t, repo.Root, "Cargo.toml", input) + inventory := repo.InspectCargoManifests([]string{"Cargo.toml"}) + if len(inventory.Manifests) != 0 || len(inventory.Problems) != 1 || inventory.Problems[0].Path != "Cargo.toml" || inventory.Problems[0].Message == "" { + t.Fatalf("malformed manifest inventory = %+v", inventory) + } + }) + } +} + +func TestCargoManifestInventoryPreservesValidSiblingAndErrorLocation(t *testing.T) { + t.Parallel() + repo := Repository{Root: t.TempDir()} + writeFile(t, repo.Root, "good/Cargo.toml", "[package]\nname = 'good'\n") + writeFile(t, repo.Root, "bad/Cargo.toml", "[package]\nname = 'bad'\nname = 'duplicate'\n") + inventory := repo.InspectCargoManifests([]string{"bad/Cargo.toml", "good/Cargo.toml"}) + if len(inventory.Manifests) != 1 || inventory.Manifests[0].Package.Name != "good" || + len(inventory.Problems) != 1 || inventory.Problems[0].Path != "bad/Cargo.toml" || inventory.Problems[0].Line != 3 { + t.Fatalf("partial manifest inventory = %+v", inventory) + } +} + +func TestCargoManifestInventoryEnforcesContainedRegularInputs(t *testing.T) { + t.Parallel() + repo := Repository{Root: t.TempDir()} + for _, path := range []string{"../Cargo.toml", "missing/Cargo.toml"} { + inventory := repo.InspectCargoManifests([]string{path}) + if len(inventory.Manifests) != 0 || len(inventory.Problems) != 1 || inventory.Problems[0].Path != path { + t.Fatalf("unreadable input %q = %+v", path, inventory) + } + } +} + +func TestCargoManifestInventoryRejectsLinkedParents(t *testing.T) { + t.Parallel() + repo := Repository{Root: t.TempDir()} + outside := t.TempDir() + writeFile(t, outside, "Cargo.toml", "[package]\nname = 'outside'\n") + if err := os.Symlink(outside, filepath.Join(repo.Root, "linked")); err != nil { + t.Skipf("symlinks unavailable: %v", err) + } + inventory := repo.InspectCargoManifests([]string{"linked/Cargo.toml"}) + if len(inventory.Manifests) != 0 || len(inventory.Problems) != 1 || inventory.Problems[0].Path != "linked/Cargo.toml" { + t.Fatalf("linked parent inventory = %+v", inventory) + } +} + +func TestCargoManifestInventoryBoundsInputBeforeParsing(t *testing.T) { + t.Parallel() + repo := Repository{Root: t.TempDir()} + path := filepath.Join(repo.Root, "Cargo.toml") + file, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + if err := file.Truncate(8<<20 + 1); err != nil { + file.Close() + t.Fatal(err) + } + if err := file.Close(); err != nil { + t.Fatal(err) + } + inventory := repo.InspectCargoManifests([]string{"Cargo.toml"}) + if len(inventory.Problems) != 1 || !strings.Contains(inventory.Problems[0].Message, "input exceeds") { + t.Fatalf("oversized manifest = %+v", inventory) + } +} diff --git a/internal/repository/contained_file.go b/internal/repository/contained_file.go index a266d770..c63ad257 100644 --- a/internal/repository/contained_file.go +++ b/internal/repository/contained_file.go @@ -8,6 +8,22 @@ import ( "strings" ) +func (repo Repository) ReadContainedFile(path string, limit int) ([]byte, error) { + if limit <= 0 { + return nil, fmt.Errorf("input byte limit must be positive") + } + root, err := os.OpenRoot(repo.Root) + if err != nil { + return nil, err + } + defer root.Close() + info, err := repo.containedRegularFileInfo(root, path) + if err != nil { + return nil, err + } + return readContainedFile(root, path, info, limit) +} + func (repo Repository) containedRegularFileInfo(root *os.Root, path string) (os.FileInfo, error) { normalized, err := repo.NormalizePath(path) if err != nil || normalized != path || strings.ContainsAny(path, "\\\x00:") { diff --git a/internal/repository/repository.go b/internal/repository/repository.go index c030e3ca..edd31143 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -491,6 +491,15 @@ func (repo Repository) CommandEnvironment() GovernedEnvironment { {directory: shellcheckDirectory, tools: []GovernedTool{{Name: "shellcheck", Path: filepath.Join(shellcheckDirectory, executableName("shellcheck")), Version: repo.ToolPin("shellcheck")}}}, {directory: nodeDirectory, tools: []GovernedTool{{Name: "node", Path: filepath.Join(nodeDirectory, executableName("node")), Version: repo.ToolPin("node")}}}, {directory: bundleBin}, + {directory: filepath.Dir(repo.RustTool("cargo")), tools: []GovernedTool{ + {Name: "cargo", Path: repo.RustTool("cargo"), Version: repo.ToolPin("rust")}, + {Name: "rustc", Path: repo.RustTool("rustc"), Version: repo.ToolPin("rust")}, + {Name: "rustdoc", Path: repo.RustTool("rustdoc"), Version: repo.ToolPin("rust")}, + {Name: "rustfmt", Path: repo.RustTool("rustfmt"), Version: repo.ToolPin("rust")}, + {Name: "cargo-fmt", Path: repo.RustTool("cargo-fmt"), Version: repo.ToolPin("rust")}, + {Name: "cargo-clippy", Path: repo.RustTool("cargo-clippy"), Version: repo.ToolPin("rust")}, + {Name: "clippy-driver", Path: repo.RustTool("clippy-driver"), Version: repo.ToolPin("rust")}, + }}, } { if !isDirectory(optional.directory) { continue diff --git a/internal/repository/rust_tools.go b/internal/repository/rust_tools.go new file mode 100644 index 00000000..5e17462d --- /dev/null +++ b/internal/repository/rust_tools.go @@ -0,0 +1,10 @@ +package repository + +import ( + "path/filepath" + "runtime" +) + +func (repo Repository) RustTool(name string) string { + return filepath.Join(repo.PolicyRoot, ".tools", "rust", runtime.GOOS+"-"+releaseArchitecture(runtime.GOARCH), "bin", executableName(name)) +} diff --git a/internal/repository/rust_tools_test.go b/internal/repository/rust_tools_test.go new file mode 100644 index 00000000..45125da9 --- /dev/null +++ b/internal/repository/rust_tools_test.go @@ -0,0 +1,31 @@ +package repository + +import ( + "os" + "path/filepath" + "slices" + "testing" +) + +func TestRustToolsJoinGovernedEnvironmentWhenInstalled(t *testing.T) { + t.Parallel() + repo := Repository{PolicyRoot: t.TempDir()} + writeFile(t, repo.PolicyRoot, "tools/rust-version.txt", "1.97.1\n") + if slices.ContainsFunc(repo.CommandEnvironment().Tools, func(tool GovernedTool) bool { return tool.Name == "cargo" }) { + t.Fatal("absent Rust runtime entered the governed environment") + } + if err := os.MkdirAll(filepath.Dir(repo.RustTool("cargo")), 0o755); err != nil { + t.Fatal(err) + } + environment := repo.CommandEnvironment() + if !slices.Contains(environment.PathEntries, filepath.Dir(repo.RustTool("cargo"))) { + t.Fatalf("Rust executable directory missing from environment: %+v", environment) + } + for _, name := range []string{"cargo", "rustc", "rustdoc", "rustfmt", "cargo-fmt", "cargo-clippy", "clippy-driver"} { + if !slices.ContainsFunc(environment.Tools, func(tool GovernedTool) bool { + return tool.Name == name && tool.Path == repo.RustTool(name) && tool.Version == "1.97.1" + }) { + t.Errorf("missing governed tool %s: %+v", name, environment.Tools) + } + } +} diff --git a/internal/rustfacts/configuration.go b/internal/rustfacts/configuration.go new file mode 100644 index 00000000..67251ed4 --- /dev/null +++ b/internal/rustfacts/configuration.go @@ -0,0 +1,96 @@ +package rustfacts + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/pelletier/go-toml/v2" + "github.com/riteofstring/code-polishy/internal/repository" +) + +func configurationProblem(repo repository.Repository) error { + if err := executionDirectories(repo); err != nil { + return err + } + for parent := filepath.Dir(repo.Root); ; parent = filepath.Dir(parent) { + for _, name := range []string{"config", "config.toml"} { + path := filepath.Join(parent, ".cargo", name) + if _, err := os.Lstat(path); !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("cargo ancestor configuration %s is outside governed repository inputs", path) + } + } + if filepath.Dir(parent) == parent { + break + } + } + for _, name := range []string{".cargo/config", ".cargo/config.toml"} { + if _, err := os.Lstat(filepath.Join(repo.Root, filepath.FromSlash(name))); errors.Is(err, os.ErrNotExist) { + continue + } + data, err := repo.ReadContainedFile(name, 1<<20) + if err != nil { + return err + } + var settings map[string]any + if err := toml.Unmarshal(data, &settings); err != nil { + return fmt.Errorf("cargo configuration %s: %w", name, err) + } + if err := validateSettings(settings); err != nil { + return fmt.Errorf("cargo configuration %s: %w", name, err) + } + } + return nil +} + +func validateSettings(settings map[string]any) error { + keys := make([]string, 0, len(settings)) + for key := range settings { + keys = append(keys, key) + } + slices.Sort(keys) + for _, key := range keys { + switch key { + case "alias", "term": + continue + case "build", "net": + fields, ok := settings[key].(map[string]any) + if !ok { + return fmt.Errorf("%s must be a table", key) + } + for field := range fields { + if !slices.Contains([]string{"build.target", "build.target-dir", "build.jobs", "net.offline"}, key+"."+field) { + return fmt.Errorf("setting %s.%s needs governed Rust execution support", key, field) + } + } + default: + return fmt.Errorf("setting %s needs governed Rust execution support", key) + } + } + return nil +} + +func manifestPathProblem(repo repository.Repository, manifest repository.CargoManifest) error { + paths := []string{} + if manifest.Package != nil && manifest.Package.Workspace != "" { + paths = append(paths, manifest.Package.Workspace) + } + if manifest.Workspace != nil { + paths = append(paths, manifest.Workspace.Members...) + paths = append(paths, manifest.Workspace.Exclude...) + paths = append(paths, manifest.Workspace.DefaultMembers...) + } + for _, path := range paths { + if strings.ContainsAny(path, "\\\x00:") || filepath.IsAbs(path) { + return fmt.Errorf("cargo workspace path %q must be repository-relative", path) + } + joined := filepath.Join(manifest.Root, filepath.FromSlash(path), "Cargo.toml") + if _, err := repo.NormalizePath(filepath.ToSlash(joined)); err != nil { + return fmt.Errorf("cargo workspace path %q escapes the repository", path) + } + } + return nil +} diff --git a/internal/rustfacts/configuration_test.go b/internal/rustfacts/configuration_test.go new file mode 100644 index 00000000..383db96c --- /dev/null +++ b/internal/rustfacts/configuration_test.go @@ -0,0 +1,88 @@ +package rustfacts + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/repository" +) + +func TestCargoConfigurationRejectsExecutionOverridesBeforeDiscovery(t *testing.T) { + t.Parallel() + for name, configuration := range map[string]string{ + "compiler": "[build]\nrustc='unapproved'\n", "wrapper": "[build]\nrustc-wrapper='unapproved'\n", + "flags": "[build]\nrustflags=['--cap-lints=allow']\n", "environment": "[env]\nRUSTC='unapproved'\n", + "replacement": "[source.crates-io]\nreplace-with='local'\n", "include": "include='other.toml'\n", + "malformed": "[build\n", + } { + t.Run(name, func(t *testing.T) { + repo := repository.Repository{Root: t.TempDir()} + write(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\n") + write(t, repo.Root, ".cargo/config.toml", configuration) + plan := PlanDiscovery(repo, []string{"Cargo.toml"}, []string{"Cargo.toml", ".cargo/config.toml"}) + if len(plan.Commands) != 0 || len(plan.Findings) != 1 || plan.Findings[0].Subject != "configuration" { + t.Fatalf("unsafe configuration reached execution: %+v", plan) + } + }) + } +} + +func TestCargoConfigurationAcceptsNonExecutableSettings(t *testing.T) { + t.Parallel() + repo := repository.Repository{Root: t.TempDir()} + write(t, repo.Root, ".cargo/config.toml", "[alias]\nci='check --workspace'\n[build]\njobs=2\ntarget='x86_64-unknown-linux-gnu'\n[net]\noffline=true\n[term]\ncolor='never'\n") + if err := configurationProblem(repo); err != nil { + t.Fatal(err) + } +} + +func TestCargoConfigurationRejectsAncestorAndCargoHomeInputs(t *testing.T) { + t.Parallel() + for _, kind := range []string{"ancestor", "cargo-home"} { + t.Run(kind, func(t *testing.T) { + parent := t.TempDir() + repo := repository.Repository{Root: filepath.Join(parent, "repo")} + if err := os.Mkdir(repo.Root, 0o755); err != nil { + t.Fatal(err) + } + if kind == "ancestor" { + write(t, parent, ".cargo/config", "[env]\nRUSTC='external'\n") + } else { + write(t, repo.Root, ".tools/cargo-home/config.toml", "[env]\nRUSTC='external'\n") + } + if err := configurationProblem(repo); err == nil { + t.Fatalf("accepted %s configuration", kind) + } + }) + } +} + +func TestCargoConfigurationRejectsSymlinkedCache(t *testing.T) { + t.Parallel() + repo := repository.Repository{Root: t.TempDir()} + if err := os.Symlink(t.TempDir(), filepath.Join(repo.Root, ".tools")); err != nil { + t.Skipf("symlinks unavailable: %v", err) + } + if err := configurationProblem(repo); err == nil || !strings.Contains(err.Error(), "contained regular directory") { + t.Fatalf("linked execution directory accepted: %v", err) + } +} + +func TestCargoManifestWorkspacePathsStayWithinRepository(t *testing.T) { + t.Parallel() + repo := repository.Repository{Root: t.TempDir()} + for _, value := range []string{"../..", "../sibling", "."} { + manifest := repository.CargoManifest{Root: "crates/app", Package: &repository.CargoPackageManifest{Workspace: value}} + if err := manifestPathProblem(repo, manifest); err != nil { + t.Fatalf("contained workspace %q rejected: %v", value, err) + } + } + for _, value := range []string{"../../..", "/outside", "C:\\outside"} { + manifest := repository.CargoManifest{Root: "crates/app", Package: &repository.CargoPackageManifest{Workspace: value}} + if err := manifestPathProblem(repo, manifest); err == nil { + t.Fatalf("escaping workspace %q accepted", value) + } + } +} diff --git a/internal/rustfacts/discovery.go b/internal/rustfacts/discovery.go new file mode 100644 index 00000000..e61f77f7 --- /dev/null +++ b/internal/rustfacts/discovery.go @@ -0,0 +1,159 @@ +package rustfacts + +import ( + "context" + "fmt" + "path/filepath" + "slices" + "strings" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" +) + +type DiscoveryPlan struct { + Manifests []string + Commands []policy.Command + Findings []policy.Finding +} + +type Discovery struct { + Workspaces []Workspace + Findings []policy.Finding +} + +func PlanDiscovery(repo repository.Repository, selected, allFiles []string) DiscoveryPlan { + plan := DiscoveryPlan{} + manifests := selectedManifests(selected, allFiles) + if len(manifests) == 0 { + return plan + } + if err := configurationProblem(repo); err != nil { + plan.Findings = append(plan.Findings, projectFinding(manifests[0], "configuration", err.Error())) + return plan + } + inventory := repo.InspectCargoManifests(manifests) + for _, problem := range inventory.Problems { + finding := projectFinding(problem.Path, "manifest", problem.Message) + finding.Line = problem.Line + plan.Findings = append(plan.Findings, finding) + } + for _, manifest := range inventory.Manifests { + if err := manifestPathProblem(repo, manifest); err != nil { + plan.Findings = append(plan.Findings, projectFinding(manifest.Path, "manifest", err.Error())) + continue + } + plan.Manifests = append(plan.Manifests, manifest.Path) + } + if len(plan.Manifests) == 0 { + return plan + } + if err := ToolProblem(repo); err != nil { + plan.Findings = append(plan.Findings, policy.Finding{Check: "policy.tool", Path: manifests[0], Subject: "rust", Message: err.Error()}) + return plan + } + plan.Commands = append(plan.Commands, Command(repo, "policy-rust-version", "rustc", "--version"), Command(repo, "policy-cargo-version", "cargo", "--version")) + for _, manifest := range plan.Manifests { + plan.Commands = append(plan.Commands, Command(repo, "policy-cargo-metadata-"+manifest, "cargo", "metadata", "--format-version", "1", "--no-deps", "--frozen", "--manifest-path", manifest)) + } + return plan +} + +func Discover(ctx context.Context, repo repository.Repository, plan DiscoveryPlan, commandRunner runner.Runner) Discovery { + result := Discovery{Findings: slices.Clone(plan.Findings)} + if len(plan.Commands) == 0 { + return result + } + structured, ok := commandRunner.(runner.StructuredRunner) + if !ok { + result.Findings = append(result.Findings, projectFinding(plan.Manifests[0], "metadata", "the runner cannot capture bounded Cargo metadata")) + return result + } + seen := map[string]bool{} + for index, command := range plan.Commands { + manifest := plan.Manifests[max(0, index-2)] + _, output, err := structured.RunStructured(ctx, repo.Root, command) + if err != nil { + result.Findings = append(result.Findings, projectFinding(manifest, command.Name, fmt.Sprintf("Cargo discovery failed: %v: %s", err, diagnosticOutput(output.Stderr)))) + if index < 2 || ctx.Err() != nil { + break + } + continue + } + if index < 2 { + if !validToolVersion(output.Stdout, command, repo.ToolPin("rust")) { + result.Findings = append(result.Findings, projectFinding(plan.Manifests[0], command.Name, "Rust tool version does not match the governed release")) + break + } + continue + } + workspace, err := ParseWorkspace(repo, manifest, output.Stdout) + if err != nil { + result.Findings = append(result.Findings, projectFinding(manifest, "metadata", err.Error())) + continue + } + if !seen[workspace.Root] { + seen[workspace.Root] = true + result.Workspaces = append(result.Workspaces, workspace) + } + } + return result +} + +func selectedManifests(selected, allFiles []string) []string { + available := map[string]bool{} + for _, path := range allFiles { + if filepath.Base(path) == "Cargo.toml" { + available[path] = true + } + } + chosen := map[string]bool{} + for _, path := range selected { + if available[path] { + chosen[path] = true + continue + } + if filepath.Ext(path) != ".rs" || filepath.IsAbs(path) { + continue + } + if manifest := nearestManifest(path, available); manifest != "" { + chosen[manifest] = true + } + } + result := make([]string, 0, len(chosen)) + for path := range chosen { + result = append(result, path) + } + slices.Sort(result) + return result +} + +func nearestManifest(path string, available map[string]bool) string { + for parent := filepath.Dir(path); ; parent = filepath.Dir(parent) { + manifest := filepath.ToSlash(filepath.Join(parent, "Cargo.toml")) + if available[manifest] { + return manifest + } + if parent == "." || filepath.Dir(parent) == parent { + return "" + } + } +} + +func validToolVersion(output []byte, command policy.Command, pin string) bool { + fields := strings.Fields(string(output)) + name := strings.TrimSuffix(filepath.Base(command.Argv[0]), ".exe") + return len(fields) >= 2 && fields[0] == name && fields[1] == pin +} + +func diagnosticOutput(data []byte) string { + if len(data) > 4096 { + data = data[:4096] + } + return strings.TrimSpace(string(data)) +} + +func projectFinding(path, subject, message string) policy.Finding { + return policy.Finding{Check: "policy.rustProject", Path: path, Subject: subject, Message: message} +} diff --git a/internal/rustfacts/discovery_test.go b/internal/rustfacts/discovery_test.go new file mode 100644 index 00000000..c453d3bd --- /dev/null +++ b/internal/rustfacts/discovery_test.go @@ -0,0 +1,134 @@ +package rustfacts + +import ( + "context" + "errors" + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestCargoDiscoveryReadsRealWorkspaceWithoutExecutingBuildScript(t *testing.T) { + t.Parallel() + repo := rustRepository(t) + write(t, repo.Root, "Cargo.toml", "[workspace]\nmembers=['crates/*']\nexclude=['scratch']\ndefault-members=['crates/service']\nresolver='3'\n[workspace.package]\nversion='1.2.3'\nedition='2024'\nrust-version='1.85'\n") + write(t, repo.Root, "crates/service/Cargo.toml", "[package]\nname='actual-service'\nversion.workspace=true\nedition.workspace=true\nrust-version.workspace=true\n[features]\ndefault=['server']\nserver=[]\n") + write(t, repo.Root, "crates/service/src/lib.rs", "pub fn value() -> u32 { 42 }\n") + write(t, repo.Root, "crates/service/build.rs", "fn main() { std::fs::write(\"executed\", \"bad\").unwrap(); panic!(\"must not run\"); }\n") + write(t, repo.Root, "scratch/Cargo.toml", "[package\n") + files, err := repo.AllFiles() + if err != nil { + t.Fatal(err) + } + plan := PlanDiscovery(repo, []string{"crates/service/src/lib.rs"}, files) + if len(plan.Commands) != 3 || len(plan.Findings) != 0 { + t.Fatalf("discovery plan = %+v", plan) + } + result := Discover(t.Context(), repo, plan, runner.OSRunner{}) + if len(result.Findings) != 0 || len(result.Workspaces) != 1 { + t.Fatalf("discovery = %+v", result) + } + workspace := result.Workspaces[0] + if workspace.Root != "." || len(workspace.Packages) != 1 || len(workspace.DefaultMembers) != 1 { + t.Fatalf("workspace = %+v", workspace) + } + pkg := workspace.Packages[0] + if pkg.Name != "actual-service" || pkg.Version != "1.2.3" || pkg.Edition != "2024" || pkg.RustVersion != "1.85" || pkg.Manifest != "crates/service/Cargo.toml" { + t.Fatalf("package = %+v", pkg) + } + if !slices.ContainsFunc(pkg.Targets, func(target Target) bool { return slices.Contains(target.Kinds, "custom-build") }) { + t.Fatalf("build script target was not inventoried: %+v", pkg.Targets) + } + for _, path := range []string{"Cargo.lock", "crates/service/executed", "executed"} { + if _, err := os.Stat(filepath.Join(repo.Root, path)); !errors.Is(err, os.ErrNotExist) { + t.Errorf("discovery modified %s: %v", path, err) + } + } +} + +func TestCargoDiscoveryRejectsAmbientOverridesAndAdmitsMissingDependencies(t *testing.T) { + repo := rustRepository(t) + write(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='0.1.0'\nedition='2021'\n[dependencies]\nserde='=1.0.228'\n") + write(t, repo.Root, "src/lib.rs", "pub fn value() -> u32 { 42 }\n") + for _, key := range []string{"RUSTC", "RUSTC_WRAPPER", "RUSTC_WORKSPACE_WRAPPER", "RUSTUP_TOOLCHAIN", "CARGO_HOME", "CARGO_BUILD_RUSTC", "CARGO_TARGET_DIR"} { + t.Setenv(key, filepath.Join(t.TempDir(), "not-a-tool")) + } + plan := PlanDiscovery(repo, []string{"Cargo.toml"}, []string{"Cargo.toml", "src/lib.rs"}) + result := Discover(t.Context(), repo, plan, runner.OSRunner{}) + if len(result.Findings) != 0 || len(result.Workspaces) != 1 { + t.Fatalf("sealed metadata discovery = %+v", result) + } + dependencies := result.Workspaces[0].Packages[0].Dependencies + if len(dependencies) != 1 || dependencies[0].Name != "serde" || dependencies[0].Requirement != "=1.0.228" { + t.Fatalf("declared dependency facts = %+v", dependencies) + } + if _, err := os.Stat(filepath.Join(CargoHome(repo), "registry")); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("data-only discovery materialized the registry: %v", err) + } +} + +func TestCargoDiscoveryPreservesValidSiblingAfterManifestFailure(t *testing.T) { + t.Parallel() + repo := rustRepository(t) + write(t, repo.Root, "good/Cargo.toml", "[package]\nname='good'\nversion='1.0.0'\n") + write(t, repo.Root, "good/src/lib.rs", "pub fn good() {}\n") + write(t, repo.Root, "bad/Cargo.toml", "[package\n") + plan := PlanDiscovery(repo, []string{"bad/Cargo.toml", "good/Cargo.toml"}, []string{"bad/Cargo.toml", "good/Cargo.toml"}) + result := Discover(t.Context(), repo, plan, runner.OSRunner{}) + if len(result.Findings) != 1 || result.Findings[0].Path != "bad/Cargo.toml" || len(result.Workspaces) != 1 || result.Workspaces[0].Packages[0].Name != "good" { + t.Fatalf("partial discovery = %+v", result) + } +} + +func TestCargoDiscoveryRequiresPinnedToolsAndStructuredOutput(t *testing.T) { + t.Parallel() + repo := rustRepository(t) + write(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='0.1.0'\n") + write(t, repo.Root, "src/lib.rs", "pub fn app() {}\n") + plan := PlanDiscovery(repo, []string{"Cargo.toml"}, []string{"Cargo.toml"}) + result := Discover(t.Context(), repo, plan, noOutputRunner{}) + if len(result.Findings) != 1 || !strings.Contains(result.Findings[0].Message, "bounded Cargo metadata") { + t.Fatalf("unstructured discovery = %+v", result) + } + repo.PolicyRoot = t.TempDir() + plan = PlanDiscovery(repo, []string{"Cargo.toml"}, []string{"Cargo.toml"}) + if len(plan.Commands) != 0 || len(plan.Findings) != 1 || plan.Findings[0].Check != "policy.tool" { + t.Fatalf("missing tool plan = %+v", plan) + } +} + +type noOutputRunner struct{} + +func (noOutputRunner) Run(context.Context, string, policy.Command) error { + return errors.New("unexpected execution") +} + +func rustRepository(t *testing.T) repository.Repository { + t.Helper() + policyRoot, err := filepath.Abs("../..") + if err != nil { + t.Fatal(err) + } + repo := repository.Repository{Root: t.TempDir(), PolicyRoot: policyRoot} + if err := ToolProblem(repo); err != nil { + t.Fatalf("Rust test prerequisite: %v", err) + } + return repo +} + +func write(t *testing.T, root, path, data string) { + t.Helper() + destination := filepath.Join(root, filepath.FromSlash(path)) + if err := os.MkdirAll(filepath.Dir(destination), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(destination, []byte(data), 0o644); err != nil { + t.Fatal(err) + } +} diff --git a/internal/rustfacts/execution.go b/internal/rustfacts/execution.go new file mode 100644 index 00000000..ae8be118 --- /dev/null +++ b/internal/rustfacts/execution.go @@ -0,0 +1,72 @@ +package rustfacts + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" +) + +func CargoHome(repo repository.Repository) string { + return filepath.Join(repo.Root, ".tools", "cargo-home") +} + +func TargetDirectory(repo repository.Repository) string { + return filepath.Join(repo.Root, ".tools", "rust-target") +} + +func Command(repo repository.Repository, name, tool string, arguments ...string) policy.Command { + return policy.Command{ + Name: name, Argv: append([]string{repo.RustTool(tool)}, arguments...), Cwd: ".", + Managed: true, SealedEnvironment: true, TimeoutSeconds: 300, + Environment: []string{"SYSTEMROOT", "WINDIR"}, + EnvironmentOverrides: []string{ + "RUSTC=" + repo.RustTool("rustc"), "RUSTDOC=" + repo.RustTool("rustdoc"), + "CARGO_HOME=" + CargoHome(repo), "CARGO_TARGET_DIR=" + TargetDirectory(repo), + "CARGO_NET_OFFLINE=true", "CARGO_TERM_COLOR=never", "CARGO_INCREMENTAL=0", + }, + } +} + +func ToolProblem(repo repository.Repository) error { + pin := repo.ToolPin("rust") + if pin == "" { + return errors.New("pinned Rust version is unavailable") + } + for _, name := range []string{"rustc", "rustdoc", "cargo", "rustfmt", "cargo-fmt", "cargo-clippy", "clippy-driver"} { + info, err := os.Stat(repo.RustTool(name)) + if err != nil || !info.Mode().IsRegular() || runtime.GOOS != "windows" && info.Mode()&0o111 == 0 { + return fmt.Errorf("pinned Rust tool %s is unavailable; install the policy-owned Rust toolchain", name) + } + } + marker := filepath.Join(filepath.Dir(filepath.Dir(repo.RustTool("rustc"))), ".code-polishy-rust-release") + data, err := os.ReadFile(marker) + if err != nil || strings.TrimSpace(string(data)) != pin { + return errors.New("rust toolchain installation does not match tools/rust-version.txt") + } + return nil +} + +func executionDirectories(repo repository.Repository) error { + for _, relative := range []string{".tools", ".tools/cargo-home", ".tools/rust-target"} { + info, err := os.Lstat(filepath.Join(repo.Root, filepath.FromSlash(relative))) + if errors.Is(err, os.ErrNotExist) { + continue + } + if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("rust execution directory %q must be a contained regular directory", relative) + } + } + for _, name := range []string{"config", "config.toml"} { + path := filepath.Join(CargoHome(repo), name) + if _, err := os.Lstat(path); !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("governed Cargo home must not contain %s", path) + } + } + return nil +} diff --git a/internal/rustfacts/metadata.go b/internal/rustfacts/metadata.go new file mode 100644 index 00000000..8d81def8 --- /dev/null +++ b/internal/rustfacts/metadata.go @@ -0,0 +1,218 @@ +package rustfacts + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "path/filepath" + "slices" + "strings" + + "github.com/riteofstring/code-polishy/internal/repository" + "golang.org/x/mod/semver" +) + +type Workspace struct { + Root string + TargetDir string + Packages []Package + Members []string + DefaultMembers []string +} + +type Package struct { + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + Manifest string `json:"manifest_path"` + Edition string `json:"edition"` + RustVersion string `json:"rust_version"` + Source *string `json:"source"` + Targets []Target `json:"targets"` + Dependencies []Dependency `json:"dependencies"` + Features map[string][]string `json:"features"` +} + +type Target struct { + Name string `json:"name"` + Kinds []string `json:"kind"` + CrateTypes []string `json:"crate_types"` + Path string `json:"src_path"` + Edition string `json:"edition"` + RequiredFeatures []string `json:"required-features"` + Test bool `json:"test"` + Doctest bool `json:"doctest"` +} + +type Dependency struct { + Name string `json:"name"` + Requirement string `json:"req"` + Source *string `json:"source"` + Kind string `json:"kind"` + Rename string `json:"rename"` + Path string `json:"path"` + Target string `json:"target"` + Optional bool `json:"optional"` + DefaultFeatures bool `json:"uses_default_features"` + Features []string `json:"features"` +} + +type workspaceWire struct { + Version int `json:"version"` + Root string `json:"workspace_root"` + TargetDir string `json:"target_directory"` + Packages []Package `json:"packages"` + Members []string `json:"workspace_members"` + DefaultMembers []string `json:"workspace_default_members"` + Resolve json.RawMessage `json:"resolve"` +} + +func ParseWorkspace(repo repository.Repository, manifest string, data []byte) (Workspace, error) { + wire, err := decodeWorkspace(data) + if err != nil { + return Workspace{}, err + } + root, err := containedPath(repo, filepath.Join(wire.Root, "Cargo.toml")) + if err != nil { + return Workspace{}, fmt.Errorf("cargo workspace root: %w", err) + } + if wire.TargetDir != TargetDirectory(repo) { + return Workspace{}, errors.New("cargo metadata did not retain the governed target directory") + } + workspace := Workspace{Root: filepath.ToSlash(filepath.Dir(root)), TargetDir: wire.TargetDir, Packages: wire.Packages, Members: wire.Members, DefaultMembers: wire.DefaultMembers} + if err := validateWorkspace(repo, manifest, &workspace); err != nil { + return Workspace{}, err + } + return workspace, nil +} + +func decodeWorkspace(data []byte) (workspaceWire, error) { + if len(data) > 8<<20 { + return workspaceWire{}, errors.New("cargo metadata exceeds 8388608 bytes") + } + var wire workspaceWire + decoder := json.NewDecoder(bytes.NewReader(data)) + if err := decoder.Decode(&wire); err != nil { + return workspaceWire{}, fmt.Errorf("invalid Cargo metadata: %w", err) + } + var extra any + if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) { + return workspaceWire{}, errors.New("cargo metadata must contain one JSON value") + } + if wire.Version != 1 || len(wire.Packages) == 0 || len(wire.Members) == 0 || wire.DefaultMembers == nil { + return workspaceWire{}, errors.New("cargo metadata is missing version-1 workspace facts") + } + if !bytes.Equal(bytes.TrimSpace(wire.Resolve), []byte("null")) { + return workspaceWire{}, errors.New("workspace-only discovery must not claim a resolved dependency graph") + } + return wire, nil +} + +func validateWorkspace(repo repository.Repository, manifest string, workspace *Workspace) error { + ids := map[string]bool{} + manifests := map[string]bool{filepath.ToSlash(filepath.Join(workspace.Root, "Cargo.toml")): true} + for index := range workspace.Packages { + pkg := &workspace.Packages[index] + if ids[pkg.ID] || !completeWorkspacePackage(*pkg) { + return fmt.Errorf("cargo package %q has incomplete, duplicate or non-workspace identity", pkg.ID) + } + if err := validatePackage(repo, pkg); err != nil { + return fmt.Errorf("cargo package %q: %w", pkg.Name, err) + } + ids[pkg.ID] = true + manifests[pkg.Manifest] = true + } + if !manifests[manifest] || len(workspace.Members) != len(ids) { + return errors.New("cargo metadata does not describe the requested manifest and its workspace members") + } + if err := validateMembers(workspace.Members, ids); err != nil { + return err + } + return validateMembers(workspace.DefaultMembers, ids) +} + +func completeWorkspacePackage(pkg Package) bool { + return pkg.ID != "" && pkg.Name != "" && pkg.Version != "" && pkg.Source == nil && + len(pkg.Targets) > 0 && pkg.Features != nil && pkg.Dependencies != nil +} + +func validateMembers(members []string, ids map[string]bool) error { + seen := map[string]bool{} + for _, id := range members { + if !ids[id] || seen[id] { + return fmt.Errorf("cargo workspace membership repeats or omits package %q", id) + } + seen[id] = true + } + return nil +} + +func validatePackage(repo repository.Repository, pkg *Package) error { + manifest, err := containedPath(repo, pkg.Manifest) + if err != nil || filepath.Base(manifest) != "Cargo.toml" { + return fmt.Errorf("invalid manifest path %q", pkg.Manifest) + } + pkg.Manifest = manifest + if !supportedEdition(pkg.Edition) { + return fmt.Errorf("unsupported Rust edition %q", pkg.Edition) + } + if pkg.RustVersion != "" && (!semver.IsValid("v"+pkg.RustVersion) || semver.Compare("v"+pkg.RustVersion, "v"+repo.ToolPin("rust")) > 0) { + return fmt.Errorf("minimum Rust version %q is not supported by pinned Rust %s", pkg.RustVersion, repo.ToolPin("rust")) + } + for index := range pkg.Targets { + if err := validateTarget(repo, &pkg.Targets[index]); err != nil { + return err + } + } + return validateDependencies(repo, pkg.Dependencies) +} + +func validateTarget(repo repository.Repository, target *Target) error { + if target.Name == "" || len(target.Kinds) == 0 || len(target.CrateTypes) == 0 || !supportedEdition(target.Edition) { + return fmt.Errorf("target %q has incomplete or unsupported facts", target.Name) + } + path, err := containedPath(repo, target.Path) + if err != nil { + return fmt.Errorf("target %q: %w", target.Name, err) + } + target.Path = path + return nil +} + +func validateDependencies(repo repository.Repository, dependencies []Dependency) error { + for index := range dependencies { + dependency := &dependencies[index] + if dependency.Name == "" || dependency.Requirement == "" { + return errors.New("dependency declaration has no name or version requirement") + } + if dependency.Path != "" { + path, err := containedPath(repo, filepath.Join(dependency.Path, "Cargo.toml")) + if err != nil { + return fmt.Errorf("dependency %q: %w", dependency.Name, err) + } + dependency.Path = filepath.ToSlash(filepath.Dir(path)) + } + } + return nil +} + +func supportedEdition(edition string) bool { + return slices.Contains([]string{"2015", "2018", "2021", "2024"}, edition) +} + +func containedPath(repo repository.Repository, absolute string) (string, error) { + if !filepath.IsAbs(absolute) { + return "", errors.New("cargo reported a non-absolute input path") + } + relative, err := filepath.Rel(repo.Root, absolute) + if err != nil || relative == "." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) || relative == ".." { + return "", fmt.Errorf("cargo input %q escapes the repository", absolute) + } + path := filepath.ToSlash(relative) + if _, err := repo.ReadContainedFile(path, 8<<20); err != nil { + return "", fmt.Errorf("cargo input %q: %w", path, err) + } + return path, nil +} diff --git a/internal/rustfacts/metadata_test.go b/internal/rustfacts/metadata_test.go new file mode 100644 index 00000000..d483cbba --- /dev/null +++ b/internal/rustfacts/metadata_test.go @@ -0,0 +1,101 @@ +package rustfacts + +import ( + "bytes" + "encoding/json" + "path/filepath" + "testing" + + "github.com/riteofstring/code-polishy/internal/repository" +) + +func TestCargoMetadataAcceptsOpaqueIDsAndAdditionalFields(t *testing.T) { + t.Parallel() + repo, metadata := metadataFixture(t) + data, err := json.Marshal(metadata) + if err != nil { + t.Fatal(err) + } + var expanded map[string]any + if err := json.Unmarshal(data, &expanded); err != nil { + t.Fatal(err) + } + expanded["future_field"] = map[string]any{"version": 8} + data, err = json.Marshal(expanded) + if err != nil { + t.Fatal(err) + } + workspace, err := ParseWorkspace(repo, "Cargo.toml", data) + if err != nil || workspace.Packages[0].ID != "opaque::member#identity" || workspace.Packages[0].Targets[0].Path != "src/lib.rs" { + t.Fatalf("metadata = %+v, %v", workspace, err) + } +} + +func TestCargoMetadataRejectsIncompleteOrEscapingFacts(t *testing.T) { + t.Parallel() + for name, mutate := range map[string]func(*workspaceWire){ + "version": func(wire *workspaceWire) { wire.Version = 2 }, + "false resolved graph": func(wire *workspaceWire) { wire.Resolve = json.RawMessage(`{"nodes":[]}`) }, + "missing package": func(wire *workspaceWire) { wire.Packages = nil }, + "duplicate package": func(wire *workspaceWire) { wire.Packages = append(wire.Packages, wire.Packages[0]) }, + "unknown member": func(wire *workspaceWire) { wire.Members[0] = "unknown" }, + "default member": func(wire *workspaceWire) { wire.DefaultMembers = []string{"unknown"} }, + "escape": func(wire *workspaceWire) { wire.Packages[0].Manifest = filepath.Join(t.TempDir(), "Cargo.toml") }, + "relative manifest": func(wire *workspaceWire) { wire.Packages[0].Manifest = "Cargo.toml" }, + "missing target": func(wire *workspaceWire) { wire.Packages[0].Targets[0].Path += ".missing" }, + "edition": func(wire *workspaceWire) { wire.Packages[0].Edition = "future" }, + "MSRV": func(wire *workspaceWire) { wire.Packages[0].RustVersion = "1.99" }, + "unknown output": func(wire *workspaceWire) { wire.TargetDir = t.TempDir() }, + } { + t.Run(name, func(t *testing.T) { + repo, metadata := metadataFixture(t) + mutate(&metadata) + data, err := json.Marshal(metadata) + if err != nil { + t.Fatal(err) + } + if _, err := ParseWorkspace(repo, "Cargo.toml", data); err == nil { + t.Fatal("invalid metadata was accepted") + } + }) + } +} + +func TestCargoMetadataRejectsTrailingAndOversizedOutput(t *testing.T) { + t.Parallel() + repo, metadata := metadataFixture(t) + data, err := json.Marshal(metadata) + if err != nil { + t.Fatal(err) + } + if _, err := ParseWorkspace(repo, "Cargo.toml", append(data, []byte(" {}")...)); err == nil { + t.Fatal("trailing metadata value was accepted") + } + missing := bytes.Replace(data, []byte(`,"resolve":null`), nil, 1) + if bytes.Equal(missing, data) { + t.Fatal("metadata fixture did not contain the resolution field") + } + if _, err := ParseWorkspace(repo, "Cargo.toml", missing); err == nil { + t.Fatal("missing resolution field was accepted") + } + if _, err := ParseWorkspace(repo, "Cargo.toml", make([]byte, 8<<20+1)); err == nil { + t.Fatal("oversized metadata was accepted") + } +} + +func metadataFixture(t *testing.T) (repository.Repository, workspaceWire) { + t.Helper() + repo := repository.Repository{Root: t.TempDir(), PolicyRoot: t.TempDir()} + write(t, repo.PolicyRoot, "tools/rust-version.txt", "1.97.1\n") + write(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\n") + write(t, repo.Root, "src/lib.rs", "pub fn app() {}\n") + id := "opaque::member#identity" + return repo, workspaceWire{ + Version: 1, Root: repo.Root, TargetDir: TargetDirectory(repo), Resolve: json.RawMessage("null"), + Members: []string{id}, DefaultMembers: []string{id}, Packages: []Package{{ + ID: id, Name: "app", Version: "1.0.0", Manifest: filepath.Join(repo.Root, "Cargo.toml"), Edition: "2024", + Dependencies: []Dependency{}, Features: map[string][]string{}, + Targets: []Target{{Name: "app", Kinds: []string{"lib"}, CrateTypes: []string{"lib"}, Path: filepath.Join(repo.Root, "src/lib.rs"), Edition: "2024", Test: true, Doctest: true}}, + }}, + } +} diff --git a/scripts/test-rust-runtime.sh b/scripts/test-rust-runtime.sh new file mode 100755 index 00000000..c428d510 --- /dev/null +++ b/scripts/test-rust-runtime.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +set -euo pipefail + +policy_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +fixture="$(mktemp -d "${TMPDIR:-/tmp}/code-polishy-rust-runtime.XXXXXX")" +cleanup() { rm -rf "${fixture}"; } +trap cleanup EXIT +mkdir -p "${fixture}/tools" "${fixture}/fake-bin" "${fixture}/archives" +cp "${policy_root}/tools/install-rust.sh" "${policy_root}/tools/verify-sha256.sh" "${fixture}/tools/" +printf '1.2.3\n' >"${fixture}/tools/rust-version.txt" +case "$(uname -s)" in + Linux) os_tag="linux"; archive_os="unknown-linux-gnu" ;; + Darwin) os_tag="darwin"; archive_os="apple-darwin" ;; + *) echo 'Rust installer contract requires Linux or macOS.' >&2; exit 1 ;; +esac +case "$(uname -m)" in + x86_64|amd64) arch_tag="x64"; archive_arch="x86_64" ;; + aarch64|arm64) arch_tag="arm64"; archive_arch="aarch64" ;; + *) echo 'Unsupported Rust installer contract architecture.' >&2; exit 1 ;; +esac +triple="${archive_arch}-${archive_os}" +for component in rustc rust-std cargo rustfmt clippy; do + stem="${component}-1.2.3-${triple}" + payload="${component}" + case "${component}" in + rust-std) payload="rust-std-${triple}" ;; + rustfmt|clippy) payload="${component}-preview" ;; + esac + root="${fixture}/archives/${stem}" + mkdir -p "${root}/${payload}" + printf 'fixture license\n' >"${root}/LICENSE-MIT" + if [[ "${component}" == rust-std ]]; then + mkdir -p "${root}/${payload}/lib/rustlib/${triple}/lib" + printf 'standard library\n' >"${root}/${payload}/lib/rustlib/${triple}/lib/libstd.rlib" + else + mkdir "${root}/${payload}/bin" + case "${component}" in + rustc) binaries=(rustc rustdoc) ;; + cargo) binaries=(cargo) ;; + rustfmt) binaries=(rustfmt cargo-fmt) ;; + clippy) binaries=(clippy-driver cargo-clippy) ;; + esac + for binary in "${binaries[@]}"; do + printf '#!/bin/sh\nprintf "%s 1.2.3 (fixture)\\n"\n' "${binary}" >"${root}/${payload}/bin/${binary}" + chmod +x "${root}/${payload}/bin/${binary}" + done + fi + tar -cJf "${fixture}/archives/${stem}.tar.xz" -C "${fixture}/archives" "${stem}" + if command -v shasum >/dev/null 2>&1; then + digest="$(shasum -a 256 "${fixture}/archives/${stem}.tar.xz" | awk '{print $1}')" + else + digest="$(sha256sum "${fixture}/archives/${stem}.tar.xz" | awk '{print $1}')" + fi + printf '%s %s\n' "${stem}.tar.xz" "${digest}" >>"${fixture}/tools/rust_checksums.txt" +done +cat >"${fixture}/fake-bin/curl" <<'CURL' +#!/usr/bin/env bash +set -euo pipefail +while (($#)); do + case "$1" in + https:*) asset="${1##*/}" ;; + -o) shift; output="$1" ;; + esac + shift +done +cp "${RUST_FIXTURE_ARCHIVES}/${asset}" "${output}" +CURL +chmod +x "${fixture}/fake-bin/curl" +export RUST_FIXTURE_ARCHIVES="${fixture}/archives" +export PATH="${fixture}/fake-bin:${PATH}" +"${fixture}/tools/install-rust.sh" +runtime="${fixture}/.tools/rust/${os_tag}-${arch_tag}" +for binary in rustc rustdoc cargo rustfmt cargo-fmt cargo-clippy clippy-driver; do + [[ "$("${runtime}/bin/${binary}" --version)" == "${binary} 1.2.3 (fixture)" ]] +done +[[ "$(<"${runtime}/share/licenses/rustc/LICENSE-MIT")" == 'fixture license' ]] +[[ "$(<"${runtime}/lib/rustlib/${triple}/lib/libstd.rlib")" == 'standard library' ]] +mv "${fixture}/archives" "${fixture}/saved-archives" +"${fixture}/tools/install-rust.sh" +mv "${fixture}/saved-archives" "${fixture}/archives" +printf 'old-release\n' >"${runtime}/.code-polishy-rust-release" +printf 'corrupt archive\n' >"${fixture}/archives/cargo-1.2.3-${triple}.tar.xz" +if "${fixture}/tools/install-rust.sh" >"${fixture}/failure" 2>&1; then + echo 'Rust installer accepted a corrupt component.' >&2 + exit 1 +fi +[[ "$(<"${fixture}/failure")" == *'Checksum mismatch'* ]] +[[ "$(<"${runtime}/.code-polishy-rust-release")" == old-release ]] +[[ "$("${runtime}/bin/cargo" --version)" == 'cargo 1.2.3 (fixture)' ]] +echo 'Rust runtime installation, reuse, integrity failure and recovery passed.' diff --git a/tools/install-policy-tools.ps1 b/tools/install-policy-tools.ps1 index 1d13a277..8ed3f354 100644 --- a/tools/install-policy-tools.ps1 +++ b/tools/install-policy-tools.ps1 @@ -7,6 +7,7 @@ $PolicyRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path if ($env:PROCESSOR_ARCHITECTURE -ne 'AMD64') { throw 'Code Polishy Windows releases currently require x64.' } +& (Join-Path $PSScriptRoot 'install-rust.ps1') function Read-Pin([string]$Relative) { return (Get-Content -Raw (Join-Path $PolicyRoot $Relative)).Trim() diff --git a/tools/install-policy-tools.sh b/tools/install-policy-tools.sh index b0bc728a..070e71eb 100755 --- a/tools/install-policy-tools.sh +++ b/tools/install-policy-tools.sh @@ -3,6 +3,7 @@ set -euo pipefail policy_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" "${policy_root}/tools/install-go.sh" +"${policy_root}/tools/install-rust.sh" "${policy_root}/tools/install-go-tools.sh" "${policy_root}/tools/install-javascript-runtime.sh" "${policy_root}/tools/install-javascript-bundle.sh" diff --git a/tools/install-rust.ps1 b/tools/install-rust.ps1 new file mode 100644 index 00000000..8e76f67f --- /dev/null +++ b/tools/install-rust.ps1 @@ -0,0 +1,86 @@ +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +$PolicyRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +if ($env:PROCESSOR_ARCHITECTURE -ne 'AMD64') { + throw 'Code Polishy Windows Rust tooling currently requires x64.' +} +$RustVersion = (Get-Content -Raw (Join-Path $PolicyRoot 'tools/rust-version.txt')).Trim() +if ($RustVersion -notmatch '^[0-9]+\.[0-9]+\.[0-9]+$') { + throw 'tools/rust-version.txt must pin an exact Rust release.' +} +$Triple = 'x86_64-pc-windows-msvc' +$Destination = Join-Path $PolicyRoot '.tools/rust/windows-x64' + +function Test-RustRuntime([string]$Directory) { + foreach ($Tool in @('rustc', 'rustdoc', 'cargo', 'rustfmt', 'cargo-fmt', 'clippy-driver', 'cargo-clippy')) { + if (-not (Test-Path -LiteralPath (Join-Path $Directory "bin/$Tool.exe") -PathType Leaf)) { return $false } + } + $Marker = Join-Path $Directory '.code-polishy-rust-release' + if (-not (Test-Path -LiteralPath $Marker -PathType Leaf)) { return $false } + if ((Get-Content -Raw -LiteralPath $Marker).Trim() -ne $RustVersion) { return $false } + if (-not (Test-Path -LiteralPath (Join-Path $Directory "lib/rustlib/$Triple/lib") -PathType Container)) { return $false } + $Reported = & (Join-Path $Directory 'bin/rustc.exe') --version + return $LASTEXITCODE -eq 0 -and $Reported -match "^rustc $([regex]::Escape($RustVersion)) " +} + +if (Test-RustRuntime $Destination) { + Write-Host "Rust $RustVersion is already installed at $Destination." + exit 0 +} +$Checksums = @{} +Get-Content (Join-Path $PolicyRoot 'tools/rust_checksums.txt') | ForEach-Object { + $Parts = $_.Trim() -split '\s+' + if ($Parts.Count -ne 2 -or $Checksums.ContainsKey($Parts[0]) -or $Parts[1] -notmatch '^[0-9a-f]{64}$') { + throw 'Malformed Rust checksum inventory.' + } + $Checksums[$Parts[0]] = $Parts[1] +} +$Scratch = Join-Path $PolicyRoot ('.tools/rust/.installation.' + [guid]::NewGuid().ToString('N')) +$Staging = Join-Path $Scratch 'runtime' +$Previous = Join-Path $Scratch 'previous' +New-Item -ItemType Directory -Path $Staging -Force | Out-Null +try { + foreach ($Component in @('rustc', 'rust-std', 'cargo', 'rustfmt', 'clippy')) { + $Stem = "$Component-$RustVersion-$Triple" + $Asset = "$Stem.tar.xz" + if (-not $Checksums.ContainsKey($Asset)) { throw "No pinned checksum for $Asset." } + $Archive = Join-Path $Scratch $Asset + Invoke-WebRequest -Uri "https://static.rust-lang.org/dist/$Asset" -OutFile $Archive + if ((Get-FileHash -Algorithm SHA256 -LiteralPath $Archive).Hash.ToLowerInvariant() -ne $Checksums[$Asset]) { + throw "Checksum mismatch for $Asset." + } + & tar.exe -xJf $Archive -C $Scratch + if ($LASTEXITCODE -ne 0) { throw "Cannot extract $Asset." } + $Payload = $Component + if ($Component -eq 'rust-std') { $Payload = "rust-std-$Triple" } + if ($Component -in @('rustfmt', 'clippy')) { $Payload = "$Component-preview" } + $Extracted = Join-Path $Scratch $Stem + foreach ($Directory in @('bin', 'lib', 'libexec', 'share', 'etc')) { + $Source = Join-Path $Extracted "$Payload/$Directory" + if (Test-Path -LiteralPath $Source -PathType Container) { + $Target = Join-Path $Staging $Directory + New-Item -ItemType Directory -Path $Target -Force | Out-Null + Get-ChildItem -LiteralPath $Source -Force | Copy-Item -Destination $Target -Recurse -Force + } + } + $LicenseDirectory = Join-Path $Staging "share/licenses/$Component" + New-Item -ItemType Directory -Path $LicenseDirectory -Force | Out-Null + foreach ($License in @('LICENSE-APACHE', 'LICENSE-MIT', 'COPYRIGHT')) { + $Source = Join-Path $Extracted $License + if (Test-Path -LiteralPath $Source -PathType Leaf) { + Copy-Item -LiteralPath $Source -Destination (Join-Path $LicenseDirectory $License) + } + } + } + Set-Content -LiteralPath (Join-Path $Staging '.code-polishy-rust-release') -Value $RustVersion + if (-not (Test-RustRuntime $Staging)) { throw 'The staged Rust toolchain is incomplete or has the wrong version.' } + if (Test-Path -LiteralPath $Destination) { Move-Item -LiteralPath $Destination -Destination $Previous } + Move-Item -LiteralPath $Staging -Destination $Destination + Write-Host "Installed Rust $RustVersion at $Destination." +} finally { + if ((Test-Path -LiteralPath $Previous) -and -not (Test-Path -LiteralPath $Destination)) { + Move-Item -LiteralPath $Previous -Destination $Destination + } + Remove-Item -LiteralPath $Scratch -Recurse -Force +} diff --git a/tools/install-rust.sh b/tools/install-rust.sh new file mode 100755 index 00000000..de591e21 --- /dev/null +++ b/tools/install-rust.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -euo pipefail + +policy_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +rust_version="$(tr -d '[:space:]' <"${policy_root}/tools/rust-version.txt")" +if [[ ! "${rust_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo 'tools/rust-version.txt must pin an exact Rust release.' >&2 + exit 1 +fi +case "$(uname -s)" in + Linux) os_tag="linux"; archive_os="unknown-linux-gnu" ;; + Darwin) os_tag="darwin"; archive_os="apple-darwin" ;; + *) echo "Unsupported Rust installation host: $(uname -s)" >&2; exit 1 ;; +esac +case "$(uname -m)" in + x86_64|amd64) arch_tag="x64"; archive_arch="x86_64" ;; + aarch64|arm64) arch_tag="arm64"; archive_arch="aarch64" ;; + *) echo "Unsupported Rust installation architecture: $(uname -m)" >&2; exit 1 ;; +esac +triple="${archive_arch}-${archive_os}" +destination="${policy_root}/.tools/rust/${os_tag}-${arch_tag}" + +verified_runtime() { + local directory="$1" tool + for tool in rustc rustdoc cargo rustfmt cargo-fmt clippy-driver cargo-clippy; do + [[ -x "${directory}/bin/${tool}" ]] || return 1 + done + [[ "$("${directory}/bin/rustc" --version | awk '{print $2}')" == "${rust_version}" ]] && + [[ -d "${directory}/lib/rustlib/${triple}/lib" ]] && + [[ -f "${directory}/.code-polishy-rust-release" ]] && + [[ "$(tr -d '[:space:]' <"${directory}/.code-polishy-rust-release")" == "${rust_version}" ]] +} + +if verified_runtime "${destination}"; then + echo "Rust ${rust_version} is already installed at ${destination}." + exit 0 +fi +mkdir -p "${policy_root}/.tools/rust" +scratch="$(mktemp -d "${policy_root}/.tools/rust/.installation.XXXXXX")" +staging="${scratch}/runtime" +mkdir "${staging}" +cleanup() { + if [[ -d "${scratch}/previous" && ! -e "${destination}" ]]; then + mv "${scratch}/previous" "${destination}" + fi + rm -rf "${scratch}" +} +trap cleanup EXIT + +for component in rustc rust-std cargo rustfmt clippy; do + archive="${component}-${rust_version}-${triple}.tar.xz" + extracted="${scratch}/${archive%.tar.xz}" + curl --fail --silent --show-error --location --retry 3 "https://static.rust-lang.org/dist/${archive}" -o "${scratch}/${archive}" + "${policy_root}/tools/verify-sha256.sh" "${policy_root}/tools/rust_checksums.txt" "${archive}" "${scratch}/${archive}" + tar -xJf "${scratch}/${archive}" -C "${scratch}" + payload="${component}" + case "${component}" in + rust-std) payload="rust-std-${triple}" ;; + rustfmt|clippy) payload="${component}-preview" ;; + esac + for directory in bin lib libexec share etc; do + if [[ -d "${extracted}/${payload}/${directory}" ]]; then + mkdir -p "${staging}/${directory}" + cp -R "${extracted}/${payload}/${directory}/." "${staging}/${directory}/" + fi + done + license_directory="${staging}/share/licenses/${component}" + mkdir -p "${license_directory}" + for license in LICENSE-APACHE LICENSE-MIT COPYRIGHT; do + if [[ -f "${extracted}/${license}" ]]; then + cp "${extracted}/${license}" "${license_directory}/${license}" + fi + done +done +printf '%s\n' "${rust_version}" >"${staging}/.code-polishy-rust-release" +if ! verified_runtime "${staging}"; then + echo 'The staged Rust toolchain is incomplete or has the wrong version.' >&2 + exit 1 +fi +if [[ -e "${destination}" || -L "${destination}" ]]; then + mv "${destination}" "${scratch}/previous" +fi +mv "${staging}" "${destination}" +echo "Installed Rust ${rust_version} at ${destination}." diff --git a/tools/rust-version.txt b/tools/rust-version.txt new file mode 100644 index 00000000..4d00f49d --- /dev/null +++ b/tools/rust-version.txt @@ -0,0 +1 @@ +1.97.1 diff --git a/tools/rust_checksums.txt b/tools/rust_checksums.txt new file mode 100644 index 00000000..72a71ecc --- /dev/null +++ b/tools/rust_checksums.txt @@ -0,0 +1,25 @@ +cargo-1.97.1-aarch64-apple-darwin.tar.xz 2d84a74e9558192a7de674aca6aa3ab7464bed2df97e0377156ddb7e09a0fd7a +cargo-1.97.1-aarch64-unknown-linux-gnu.tar.xz 8f70bcaccea5ba4db187c3fd4d64e24592b4e16af513497201f5909d61691dbe +cargo-1.97.1-x86_64-apple-darwin.tar.xz 1bd1029b579d0563ca851ebd095914871535bfd1978a123eeaa03107e89b0e03 +cargo-1.97.1-x86_64-pc-windows-msvc.tar.xz 1180ac0cd30ee98af682528c10505f5cba118f122aec9b7ca18ae605b1db38a0 +cargo-1.97.1-x86_64-unknown-linux-gnu.tar.xz e1be5f5ff7f7f80ca506fb65770b759edbdc6d303781ed71c5de8ec8a8394779 +clippy-1.97.1-aarch64-apple-darwin.tar.xz 5e44c0ac5ca9b6f14a3c9031a61f583348b902f908f46e95717aef1dbd2807db +clippy-1.97.1-aarch64-unknown-linux-gnu.tar.xz d8bac7b0ba5ca9bb868ccb9e367a1d52f4837f3ebf4892eaf64cda37ce362bb5 +clippy-1.97.1-x86_64-apple-darwin.tar.xz 6dad187a2210db93c63cdf21a376db8b7fe4f5e64d6ef4d404a74d166e59ad74 +clippy-1.97.1-x86_64-pc-windows-msvc.tar.xz 3be927ecfbbba535bee2f4d23cd08c639278c8746c04bd70fb21ea58667b054b +clippy-1.97.1-x86_64-unknown-linux-gnu.tar.xz 3441df8fb54db985f8c8a3e8356b8874a3f92cc8cca8565cfe36f1dc15935e72 +rust-std-1.97.1-aarch64-apple-darwin.tar.xz a4895f5c6995e83cab8687e46b14324592398049def71ce75ca308c981cf200d +rust-std-1.97.1-aarch64-unknown-linux-gnu.tar.xz 46aed8e63186350004d8ec6afca798811e6530b514352e5a8a26f3dc4939b3be +rust-std-1.97.1-x86_64-apple-darwin.tar.xz 0fa78653023be5bdfeb419edc82e3b1346ccaa23eaa036491cce084101c741dd +rust-std-1.97.1-x86_64-pc-windows-msvc.tar.xz 05f356609926e663a81e9697077214236514b2f9ff7a36e63b0070f43f073f66 +rust-std-1.97.1-x86_64-unknown-linux-gnu.tar.xz 1c1e704ae80126b7de34f72ea2825f7fd01736dec20732faed47374b95282fba +rustc-1.97.1-aarch64-apple-darwin.tar.xz 6076cad38ccabaa24325f26a74080a363a2633a9cd34c473a8977255d8a593cb +rustc-1.97.1-aarch64-unknown-linux-gnu.tar.xz b344b81f0cd4c2246c7da8b197fe7a339d7dd02bb15cb69b2524115d9c75224c +rustc-1.97.1-x86_64-apple-darwin.tar.xz 3c38289f319bf02fa1c8149ce3e00f261e4efd14813a99f7f7ae4f180c7d1173 +rustc-1.97.1-x86_64-pc-windows-msvc.tar.xz 0119e2788f3391a891b2e0fe611e82b433670eeae76c45995b081d0ac7715c6d +rustc-1.97.1-x86_64-unknown-linux-gnu.tar.xz 9819d0a32d56bd339585319c80260e332779f5541fd66838ab7e016d6c814819 +rustfmt-1.97.1-aarch64-apple-darwin.tar.xz 358bbba5d0c7c37116ec15f67cfd3ac4da5d3c319cddb49389c26d3a0c65747a +rustfmt-1.97.1-aarch64-unknown-linux-gnu.tar.xz 3dbde15d30794924195ae446f3d2ceb542a131306d22ae7912c7634d414622a8 +rustfmt-1.97.1-x86_64-apple-darwin.tar.xz 457c35a619207d35da2a3804940e620ad7cdc8e0808b17f2f6c2202f9e3f3d91 +rustfmt-1.97.1-x86_64-pc-windows-msvc.tar.xz 1718eea97fc34543c71a5849bc72952f88bc6c789c9110626bc00b2791642e81 +rustfmt-1.97.1-x86_64-unknown-linux-gnu.tar.xz 907fe97d6afbde1eca1b34c992c76e1406d422e2e6f137813d382acec7eb4d14 From 45e6176af220cc5c19f2127aa8cd6abde34af942 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Thu, 24 Sep 2026 22:57:44 +0000 Subject: [PATCH 04/49] feat: integrate selected Rust formatting Use pinned rustfmt with Cargo editions, contained project configuration and selected-file writes. Preserve child modules, generated inputs and concurrent edits. Align mixed Go/Rust command planning with execution. Verified: rustfacts-unit, focused Cargo/Rust quality tests, scoped policy checks and public CLI formatting. Compiler, source semantics, supply chain, test evidence and release integration remain in progress; no final checkpoint or supplemental suites run for this progress commit. --- .code-polishy.json | 2 + docs/design/cargo-facts.md | 17 ++++ internal/quality/cargo.go | 3 +- internal/quality/cargo_test.go | 39 ++++++++ internal/quality/quality.go | 7 +- internal/quality/rust_format.go | 92 +++++++++++++++++ internal/quality/rust_format_test.go | 143 +++++++++++++++++++++++++++ internal/repository/providers.go | 2 + internal/rustfacts/format.go | 142 ++++++++++++++++++++++++++ internal/rustfacts/format_test.go | 32 ++++++ tools/rustfmt.toml | 1 + 11 files changed, 477 insertions(+), 3 deletions(-) create mode 100644 internal/quality/rust_format.go create mode 100644 internal/quality/rust_format_test.go create mode 100644 internal/rustfacts/format.go create mode 100644 internal/rustfacts/format_test.go create mode 100644 tools/rustfmt.toml diff --git a/.code-polishy.json b/.code-polishy.json index 9d3bbab2..533d216c 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -41,6 +41,8 @@ "internal/quality/cargo_manifest_test.go", "internal/quality/cargo.go", "internal/quality/cargo_test.go", + "internal/quality/rust_format.go", + "internal/quality/rust_format_test.go", "internal/quality/wgsl_test.go", "tools/install-rust.sh", "tools/install-rust.ps1", diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index ac3a4972..25fc35d2 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -22,6 +22,23 @@ Repository configuration is parsed before execution; unsupported execution-alter settings produce a diagnostic. This controls discovery inputs, not operating-system access by future build scripts or procedural macros. +## Selected formatting + +The pinned rustfmt receives one selected source through stdin, with the edition +reported by Cargo and an explicit contained configuration file. It never receives +a source filename that would authorize recursive formatting of child modules. +Configuration discovery stops at the checkout boundary; absent project settings +use the release-owned default. Stable project style settings are honored, but +formatting exclusions, disabled formatting, edition conflicts and unknown or +unstable settings fail visibly. + +Formatting excludes generated/data/link inputs and writes through the existing +contained regular-file boundary. The current bytes are compared with the original +input immediately before writing, preserving concurrent caller edits without a +local-state digest. Read-only formatting, like Go's gofmt check, is an internal +source operation; Cargo discovery and compilation use the check execution plan. +The public format operation routes all child processes through its supplied runner. + [Rust input coverage](rust-inputs.md) defines control input selection and the governed toolchain. The [implementation plan](../plans/code-polishy-rust-support-plan.md) records the remaining Rust capabilities and acceptance work. diff --git a/internal/quality/cargo.go b/internal/quality/cargo.go index afac2ad6..1ca5cc59 100644 --- a/internal/quality/cargo.go +++ b/internal/quality/cargo.go @@ -15,5 +15,6 @@ func cargoDiscoveryFindings(ctx context.Context, repo repository.Repository, fil return []policy.Finding{{Check: "policy.rustProject", Path: "repository", Subject: "inventory", Message: err.Error()}} } plan := rustfacts.PlanDiscovery(repo, files, allFiles) - return rustfacts.Discover(ctx, repo, plan, commandRunner).Findings + discovery := rustfacts.Discover(ctx, repo, plan, commandRunner) + return append(discovery.Findings, rustFormatFindings(ctx, repo, files, discovery.Workspaces)...) } diff --git a/internal/quality/cargo_test.go b/internal/quality/cargo_test.go index dd946416..ca0df9c2 100644 --- a/internal/quality/cargo_test.go +++ b/internal/quality/cargo_test.go @@ -1,7 +1,9 @@ package quality import ( + "context" "path/filepath" + "reflect" "slices" "testing" @@ -47,3 +49,40 @@ func cargoQualityRepository(t *testing.T) repository.Repository { } return repo } + +func TestCargoAndGoCheckPlanMatchesObservedCommandOrder(t *testing.T) { + t.Parallel() + repo := cargoQualityRepository(t) + writeQualityFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\n") + writeQualityFile(t, repo.Root, "src/lib.rs", "pub fn app() {}\n") + writeQualityFile(t, repo.Root, "go.mod", "module example.test/mixed\n\ngo 1.26.6\n") + writeQualityFile(t, repo.Root, "main.go", "package main\n\nfunc main() {}\n") + selection := repository.Selection{Files: []string{"main.go", "src/lib.rs"}} + planned := CheckCommands(repo, selection, "check") + boundary := &cargoRecordingRunner{OSRunner: runner.OSRunner{}} + if findings := Check(t.Context(), repo, selection, boundary, "check"); len(findings) != 0 { + t.Fatalf("mixed language check: %+v", findings) + } + want := []string{} + for _, command := range planned { + want = append(want, command.Name) + } + if !reflect.DeepEqual(boundary.names, want) || len(want) < 5 { + t.Fatalf("executed commands = %v, planned = %v", boundary.names, want) + } +} + +type cargoRecordingRunner struct { + runner.OSRunner + names []string +} + +func (boundary *cargoRecordingRunner) Run(ctx context.Context, root string, command policy.Command) error { + boundary.names = append(boundary.names, command.Name) + return boundary.OSRunner.Run(ctx, root, command) +} + +func (boundary *cargoRecordingRunner) RunStructured(ctx context.Context, root string, command policy.Command) (runner.Result, runner.Output, error) { + boundary.names = append(boundary.names, command.Name) + return boundary.OSRunner.RunStructured(ctx, root, command) +} diff --git a/internal/quality/quality.go b/internal/quality/quality.go index 805cc585..bdf64efc 100644 --- a/internal/quality/quality.go +++ b/internal/quality/quality.go @@ -46,9 +46,9 @@ func CheckCommands(repo repository.Repository, selection repository.Selection, p repo = repo.WithAnalysisProfile(profile) commands := []policy.Command{} if allFiles, err := repo.AllFiles(); err == nil { + commands = append(commands, rustfacts.PlanDiscovery(repo, selection.Files, allFiles).Commands...) goCommands, _ := nativeGoToolCommands(repo, selection.Files, allFiles) commands = append(commands, goCommands...) - commands = append(commands, rustfacts.PlanDiscovery(repo, selection.Files, allFiles).Commands...) } shellCommands, _ := shellToolCommands(repo, selection.Files) commands = append(commands, shellCommands...) @@ -84,6 +84,7 @@ func Format(ctx context.Context, repo repository.Repository, selection repositor } } findings = append(findings, JavaScriptFormatWrite(ctx, repo, selection.Files)...) + findings = append(findings, rustFormatWrite(ctx, repo, selection.Files, commandRunner)...) findings = append(findings, RunCommands(ctx, repo, selection, commandRunner, "format")...) return findings } @@ -506,7 +507,7 @@ func ToolFindings(repo repository.Repository, files []string) []policy.Finding { if shellLint { findings = append(findings, shellToolFindings(repo)...) } - return findings + return append(findings, rustToolFindings(repo, files)...) } func goToolFindings(repo repository.Repository, capabilities map[string]bool) []policy.Finding { @@ -921,6 +922,8 @@ func builtInCapability(language, capability string) bool { switch language { case "go", "typescript", "python": return slices.Contains(builtInCapabilities, capability) + case "rust": + return capability == "format" case "shell": return slices.Contains([]string{"format", "lint", "typecheck"}, capability) } diff --git a/internal/quality/rust_format.go b/internal/quality/rust_format.go new file mode 100644 index 00000000..d18475b3 --- /dev/null +++ b/internal/quality/rust_format.go @@ -0,0 +1,92 @@ +package quality + +import ( + "bytes" + "context" + "errors" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" + "github.com/riteofstring/code-polishy/internal/rustfacts" +) + +func rustFormatWrite(ctx context.Context, repo repository.Repository, selected []string, commandRunner runner.Runner) []policy.Finding { + files := editableFiles(repo, languageFiles(repo, repo.NativeAnalysisFiles(selected, "format", ""), "rust")) + if len(files) == 0 { + return nil + } + allFiles, err := repo.AllFiles() + if err != nil { + return []policy.Finding{rustFormatFinding("repository", err.Error())} + } + plan := rustfacts.PlanDiscovery(repo, files, allFiles) + discovery := rustfacts.Discover(ctx, repo, plan, commandRunner) + findings := append([]policy.Finding{}, discovery.Findings...) + for _, path := range files { + findings = append(findings, rustFormatFile(ctx, repo, path, discovery.Workspaces, commandRunner, true)...) + } + return findings +} + +func rustFormatFindings(ctx context.Context, repo repository.Repository, selected []string, workspaces []rustfacts.Workspace) []policy.Finding { + files := editableFiles(repo, languageFiles(repo, repo.NativeAnalysisFiles(selected, "format", ""), "rust")) + findings := []policy.Finding{} + for _, path := range files { + findings = append(findings, rustFormatFile(ctx, repo, path, workspaces, runner.OSRunner{}, false)...) + } + return findings +} + +func rustFormatFile(ctx context.Context, repo repository.Repository, path string, workspaces []rustfacts.Workspace, commandRunner runner.Runner, write bool) []policy.Finding { + edition, err := rustfacts.SourceEdition(workspaces, path) + if err != nil { + return []policy.Finding{rustFormatFinding(path, err.Error())} + } + input, err := repo.ReadContainedFile(path, 8<<20) + if err != nil { + return []policy.Finding{rustFormatFinding(path, err.Error())} + } + formatted, err := rustfacts.FormatSource(ctx, repo, path, edition, input, commandRunner) + if err != nil { + return []policy.Finding{rustFormatFinding(path, err.Error())} + } + if bytes.Equal(input, formatted) { + return nil + } + if !write { + return []policy.Finding{rustFormatFinding(path, "file is not rustfmt formatted")} + } + if err := writeRustFormat(repo, path, input, formatted); err != nil { + return []policy.Finding{rustFormatFinding(path, err.Error())} + } + return nil +} + +func writeRustFormat(repo repository.Repository, path string, input, formatted []byte) error { + current, err := repo.ReadContainedFile(path, 8<<20) + if err != nil { + return err + } + if !bytes.Equal(current, input) { + return errors.New("rust source changed during formatting; no output was written") + } + return repo.WriteRegularFile(path, formatted) +} + +func rustFormatFinding(path, message string) policy.Finding { + return policy.Finding{Check: "quality.rustfmt", Path: path, Subject: "format", Message: message} +} + +func rustToolFindings(repo repository.Repository, files []string) []policy.Finding { + for _, path := range files { + if repo.Language(path) != "rust" || !repo.NativeAnalysis(path, "format") { + continue + } + if err := rustfacts.ToolProblem(repo); err != nil { + return []policy.Finding{toolFinding("rust", err.Error())} + } + return nil + } + return nil +} diff --git a/internal/quality/rust_format_test.go b/internal/quality/rust_format_test.go new file mode 100644 index 00000000..1b779263 --- /dev/null +++ b/internal/quality/rust_format_test.go @@ -0,0 +1,143 @@ +package quality + +import ( + "context" + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestRustFormatWritesOnlySelectedHandwrittenSource(t *testing.T) { + t.Parallel() + repo := rustFormatRepository(t) + dirty := "pub fn value()->u32{42}\n" + writeQualityFile(t, repo.Root, "src/lib.rs", "mod child;\n"+dirty) + writeQualityFile(t, repo.Root, "src/child.rs", dirty) + writeQualityFile(t, repo.Root, "src/schema.generated.rs", dirty) + repo.Config.Scope.Generated = []string{"**/*.generated.rs"} + selection := repository.Selection{Files: []string{"src/lib.rs", "src/schema.generated.rs"}} + findings := Format(t.Context(), repo, selection, runner.OSRunner{}) + if len(findings) != 0 { + t.Fatalf("format findings: %+v", findings) + } + want := "mod child;\npub fn value() -> u32 {\n 42\n}\n" + if got := rustFormatFileText(t, repo, "src/lib.rs"); got != want { + t.Fatalf("formatted source = %q, want %q", got, want) + } + for _, path := range []string{"src/child.rs", "src/schema.generated.rs"} { + if got := rustFormatFileText(t, repo, path); got != dirty { + t.Errorf("unselected or generated source %s changed: %q", path, got) + } + } +} + +func TestRustFormatUsesCargoEditionAndProjectStyle(t *testing.T) { + t.Parallel() + repo := rustFormatRepository(t) + writeQualityFile(t, repo.Root, "rustfmt.toml", "hard_tabs=true\nstyle_edition='2021'\n") + writeQualityFile(t, repo.Root, "src/lib.rs", "pub async fn answer()->u32{42}\n") + findings := rustFormatWrite(t.Context(), repo, []string{"src/lib.rs"}, runner.OSRunner{}) + if len(findings) != 0 { + t.Fatalf("format findings: %+v", findings) + } + if got := rustFormatFileText(t, repo, "src/lib.rs"); got != "pub async fn answer() -> u32 {\n\t42\n}\n" { + t.Fatalf("edition/style formatting = %q", got) + } +} + +func TestRustFormatCheckReportsWithoutWriting(t *testing.T) { + t.Parallel() + repo := rustFormatRepository(t) + dirty := "pub fn value()->u32{42}\n" + writeQualityFile(t, repo.Root, "src/lib.rs", dirty) + findings := cargoDiscoveryFindings(t.Context(), repo, []string{"src/lib.rs"}, runner.OSRunner{}) + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return finding.Check == "quality.rustfmt" && finding.Path == "src/lib.rs" && strings.Contains(finding.Message, "not rustfmt formatted") + }) { + t.Fatalf("missing formatting finding: %+v", findings) + } + if got := rustFormatFileText(t, repo, "src/lib.rs"); got != dirty { + t.Fatalf("read-only check wrote %q", got) + } +} + +func TestRustFormatRejectsInvalidSourceAndWeakenedConfiguration(t *testing.T) { + t.Parallel() + for name, configuration := range map[string]string{ + "edition mismatch": "edition='2015'\n", "disabled": "disable_all_formatting=true\n", + "ignored": "ignore=['src/lib.rs']\n", "unknown": "imaginary_option=true\n", "malformed": "bad=[\n", + } { + t.Run(name, func(t *testing.T) { + repo := rustFormatRepository(t) + dirty := "pub fn value()->u32{42}\n" + writeQualityFile(t, repo.Root, "src/lib.rs", dirty) + writeQualityFile(t, repo.Root, ".rustfmt.toml", configuration) + if findings := rustFormatWrite(t.Context(), repo, []string{"src/lib.rs"}, runner.OSRunner{}); len(findings) == 0 { + t.Fatal("invalid configuration was accepted") + } + if got := rustFormatFileText(t, repo, "src/lib.rs"); got != dirty { + t.Fatalf("rejected formatting changed source: %q", got) + } + }) + } + repo := rustFormatRepository(t) + invalid := "pub fn unfinished(\n" + writeQualityFile(t, repo.Root, "src/lib.rs", invalid) + if findings := rustFormatWrite(t.Context(), repo, []string{"src/lib.rs"}, runner.OSRunner{}); len(findings) == 0 { + t.Fatal("invalid Rust source was accepted") + } + if got := rustFormatFileText(t, repo, "src/lib.rs"); got != invalid { + t.Fatalf("invalid source was overwritten: %q", got) + } +} + +func TestRustFormatPreservesConcurrentSourceEdit(t *testing.T) { + t.Parallel() + repo := rustFormatRepository(t) + writeQualityFile(t, repo.Root, "src/lib.rs", "pub fn value()->u32{42}\n") + concurrent := "pub fn value() -> u32 { 99 }\n" + boundary := rustEditingRunner{OSRunner: runner.OSRunner{}, edit: func() { writeQualityFile(t, repo.Root, "src/lib.rs", concurrent) }} + findings := rustFormatWrite(t.Context(), repo, []string{"src/lib.rs"}, boundary) + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return strings.Contains(finding.Message, "changed during formatting") + }) { + t.Fatalf("concurrent edit was not reported: %+v", findings) + } + if got := rustFormatFileText(t, repo, "src/lib.rs"); got != concurrent { + t.Fatalf("concurrent source edit was overwritten: %q", got) + } +} + +type rustEditingRunner struct { + runner.OSRunner + edit func() +} + +func (boundary rustEditingRunner) RunStructured(ctx context.Context, root string, command policy.Command) (runner.Result, runner.Output, error) { + if strings.HasPrefix(command.Name, "policy-rustfmt-") { + boundary.edit() + } + return boundary.OSRunner.RunStructured(ctx, root, command) +} + +func rustFormatRepository(t *testing.T) repository.Repository { + t.Helper() + repo := cargoQualityRepository(t) + writeQualityFile(t, repo.Root, "Cargo.toml", "[package]\nname='format-fixture'\nversion='1.0.0'\nedition='2024'\n") + return repo +} + +func rustFormatFileText(t *testing.T, repo repository.Repository, path string) string { + t.Helper() + data, err := os.ReadFile(filepath.Join(repo.Root, filepath.FromSlash(path))) + if err != nil { + t.Fatal(err) + } + return string(data) +} diff --git a/internal/repository/providers.go b/internal/repository/providers.go index c659f9d8..dc40987e 100644 --- a/internal/repository/providers.go +++ b/internal/repository/providers.go @@ -126,6 +126,8 @@ func (repo Repository) NativeCapability(path, capability string) bool { switch language { case "go", "python": return slices.Contains([]string{"format", "lint", "typecheck", "complexity", "dead-code", "architecture"}, capability) + case "rust": + return capability == "format" case "shell": return slices.Contains([]string{"format", "lint", "typecheck"}, capability) case "typescript": diff --git a/internal/rustfacts/format.go b/internal/rustfacts/format.go new file mode 100644 index 00000000..8bb19cf5 --- /dev/null +++ b/internal/rustfacts/format.go @@ -0,0 +1,142 @@ +package rustfacts + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "unicode/utf8" + + "github.com/pelletier/go-toml/v2" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func FormatSource(ctx context.Context, repo repository.Repository, path, edition string, source []byte, commandRunner runner.Runner) ([]byte, error) { + structured, ok := commandRunner.(runner.StructuredRunner) + if !ok { + return nil, errors.New("the formatter runner cannot capture bounded rustfmt output") + } + configuration, style, err := formatConfiguration(repo, path, edition) + if err != nil { + return nil, err + } + command := Command(repo, "policy-rustfmt-"+path, "rustfmt", "--edition", edition, "--style-edition", style, "--emit", "stdout", "--config-path", configuration) + command.Stdin = source + _, output, err := structured.RunStructured(ctx, repo.Root, command) + if err != nil { + return nil, fmt.Errorf("rustfmt failed for %s: %v %s", path, err, diagnosticOutput(output.Stderr)) + } + if len(output.Stderr) > 0 { + return nil, fmt.Errorf("rustfmt reported a configuration or formatting problem for %s: %s", path, diagnosticOutput(output.Stderr)) + } + if !utf8.Valid(output.Stdout) { + return nil, errors.New("rustfmt returned non-UTF-8 source") + } + return output.Stdout, nil +} + +func formatConfiguration(repo repository.Repository, path, edition string) (string, string, error) { + if !supportedEdition(edition) { + return "", "", fmt.Errorf("unsupported Rust formatting edition %q", edition) + } + for parent := filepath.Dir(path); ; parent = filepath.Dir(parent) { + for _, name := range []string{".rustfmt.toml", "rustfmt.toml"} { + candidate := filepath.ToSlash(filepath.Join(parent, name)) + if _, err := os.Lstat(filepath.Join(repo.Root, filepath.FromSlash(candidate))); errors.Is(err, os.ErrNotExist) { + continue + } + data, err := repo.ReadContainedFile(candidate, 1<<20) + if err != nil { + return "", "", err + } + style, err := formatStyle(data, edition) + if err != nil { + return "", "", fmt.Errorf("rustfmt configuration %s: %w", candidate, err) + } + return filepath.Join(repo.Root, filepath.FromSlash(candidate)), style, nil + } + if parent == "." || filepath.Dir(parent) == parent { + break + } + } + return filepath.Join(repo.PolicyRoot, "tools", "rustfmt.toml"), edition, nil +} + +func formatStyle(data []byte, edition string) (string, error) { + var settings map[string]any + if err := toml.Unmarshal(data, &settings); err != nil { + return "", err + } + for _, name := range []string{"disable_all_formatting", "ignore", "skip_children", "skip_macro_invocations", "unstable_features"} { + if _, present := settings[name]; present { + return "", fmt.Errorf("setting %s cannot weaken governed source formatting", name) + } + } + if declared, exists := settings["edition"]; exists && declared != edition { + return "", fmt.Errorf("formatting edition must match Cargo target edition %s", edition) + } + if style, exists := settings["style_edition"]; exists { + text, ok := style.(string) + if !ok || !supportedEdition(text) { + return "", errors.New("unsupported Rust style edition") + } + return text, nil + } + return edition, nil +} + +func SourceEdition(workspaces []Workspace, path string) (string, error) { + if edition, err := targetEdition(workspaces, path); edition != "" || err != nil { + return edition, err + } + selected := sourcePackage(workspaces, path) + if selected == nil { + return "", errors.New("selected Rust source has no discovered Cargo package or target") + } + for _, target := range selected.Targets { + if target.Edition != selected.Edition { + return "", errors.New("source edition is ambiguous across Cargo targets; resolved module ownership is required") + } + } + return selected.Edition, nil +} + +func sourcePackage(workspaces []Workspace, path string) *Package { + selected := (*Package)(nil) + selectedSize := -1 + for _, workspace := range workspaces { + for index := range workspace.Packages { + pkg := &workspace.Packages[index] + root := filepath.ToSlash(filepath.Dir(pkg.Manifest)) + size := len(root) + if root == "." { + size = 0 + } + if (root == "." || strings.HasPrefix(path, root+"/")) && size > selectedSize { + selected, selectedSize = pkg, size + } + } + } + return selected +} + +func targetEdition(workspaces []Workspace, path string) (string, error) { + edition := "" + for _, workspace := range workspaces { + for _, pkg := range workspace.Packages { + for _, target := range pkg.Targets { + if target.Path != path { + continue + } + if edition != "" && edition != target.Edition { + return "", errors.New("selected source belongs to Cargo targets with different Rust editions") + } + edition = target.Edition + } + } + } + return edition, nil +} diff --git a/internal/rustfacts/format_test.go b/internal/rustfacts/format_test.go new file mode 100644 index 00000000..79103426 --- /dev/null +++ b/internal/rustfacts/format_test.go @@ -0,0 +1,32 @@ +package rustfacts + +import "testing" + +func TestRustSourceEditionRetainsTargetAndNestedPackageIdentity(t *testing.T) { + t.Parallel() + workspaces := []Workspace{{Packages: []Package{ + {Manifest: "Cargo.toml", Edition: "2018", Targets: []Target{{Path: "src/lib.rs", Edition: "2018"}}}, + {Manifest: "x/Cargo.toml", Edition: "2024", Targets: []Target{{Path: "shared/main.rs", Edition: "2024"}}}, + }}} + for path, want := range map[string]string{"src/lib.rs": "2018", "src/child.rs": "2018", "x/src/child.rs": "2024", "shared/main.rs": "2024"} { + got, err := SourceEdition(workspaces, path) + if err != nil || got != want { + t.Fatalf("%s edition = %q, %v, want %q", path, got, err, want) + } + } +} + +func TestRustSourceEditionRejectsAmbiguousOrAbsentOwnership(t *testing.T) { + t.Parallel() + for name, workspaces := range map[string][]Workspace{ + "absent": {}, + "mixed targets": {{Packages: []Package{{Manifest: "Cargo.toml", Edition: "2024", Targets: []Target{{Path: "src/bin/tool.rs", Edition: "2018"}}}}}}, + "shared target": {{Packages: []Package{{Manifest: "Cargo.toml", Edition: "2024", Targets: []Target{{Path: "src/lib.rs", Edition: "2024"}, {Path: "src/lib.rs", Edition: "2018"}}}}}}, + } { + t.Run(name, func(t *testing.T) { + if edition, err := SourceEdition(workspaces, "src/lib.rs"); err == nil { + t.Fatalf("ambiguous source received edition %q", edition) + } + }) + } +} diff --git a/tools/rustfmt.toml b/tools/rustfmt.toml new file mode 100644 index 00000000..12d797cc --- /dev/null +++ b/tools/rustfmt.toml @@ -0,0 +1 @@ +hard_tabs = false From 7087425cee9bb055054e9a683d9ba64c49b46e0f Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Thu, 24 Sep 2026 23:16:55 +0000 Subject: [PATCH 05/49] feat: integrate governed Rust compiler diagnostics Run matching pinned Cargo and Clippy through the common command plan with frozen native-host compilation. Require reviewed lockfiles, retain original diagnostic locations and compiler target context, validate nonempty structured outcomes, and inventory build-script facts. Keep unsupported Clippy configuration and non-native targets explicit. Verified: rustfacts-unit, repository-unit, focused Cargo/Rust quality tests, scoped policy checks, and public CLI valid/seeded-type-error crates. Real fixtures also cover default features, lint suppression, lock drift, missing linkers and cancellation. Source semantics, dependency governance, Rust test evidence, WGSL and release integration remain unfinished; no final checkpoint or supplemental suites run for this progress commit. --- docs/design/cargo-facts.md | 33 +++ docs/plans/code-polishy-rust-support-plan.md | 17 ++ internal/quality/cargo.go | 4 +- internal/quality/cargo_test.go | 1 + internal/quality/quality.go | 6 +- internal/repository/providers.go | 2 +- internal/rustfacts/compilation.go | 147 +++++++++++ internal/rustfacts/compilation_test.go | 258 +++++++++++++++++++ internal/rustfacts/compiler_diagnostics.go | 76 ++++++ internal/rustfacts/compiler_output.go | 200 ++++++++++++++ internal/rustfacts/compiler_output_test.go | 97 +++++++ internal/rustfacts/configuration.go | 31 +++ internal/rustfacts/configuration_test.go | 11 + tools/clippy.toml | 1 + 14 files changed, 880 insertions(+), 4 deletions(-) create mode 100644 internal/rustfacts/compilation.go create mode 100644 internal/rustfacts/compilation_test.go create mode 100644 internal/rustfacts/compiler_diagnostics.go create mode 100644 internal/rustfacts/compiler_output.go create mode 100644 internal/rustfacts/compiler_output_test.go create mode 100644 tools/clippy.toml diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index 25fc35d2..58f46215 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -39,6 +39,39 @@ local-state digest. Read-only formatting, like Go's gofmt check, is an internal source operation; Cargo discovery and compilation use the check execution plan. The public format operation routes all child processes through its supplied runner. +## Compiler checks + +Compilation uses the matching pinned cargo-clippy executable directly, with an +absolute Cargo path. This bypasses Cargo's ambient external-subcommand lookup. +The common runner captures Cargo's JSON events with bounded output and process +tree cancellation. Cargo must retain its reviewed lock and available dependency +cache through `--frozen`; a missing lock is a preparation failure, never an +invitation for the check to resolve dependencies. + +Checks select the manifest's ordinary Cargo packages, default features and all +enabled targets, using the governed native host explicitly. They do not enable +every feature or establish coverage of other target configurations. Compiler +warnings, Clippy's default lint group and unused-code lints are forbidden at the +invocation boundary. Parser-backed attribute governance and complete dead-code +coverage remain separate requirements. Project Clippy settings currently fail +with an explicit unsupported-configuration diagnostic; they cannot silently +override the release-owned defaults. + +The compiler receives the host SDK's executable and library search environment, +but no ambient Cargo/rustup flags, wrappers or Rust compiler selection. Build +scripts and procedural macros may execute during checking. Their output +directories, emitted configuration and native linkage are compiler evidence, +not a claim of filesystem or network sandboxing. Native SDK failures remain +ordinary failed checks; the Rust distribution does not supply every native SDK. + +Structured success requires a final Cargo outcome and a compiled target for +every selected package. An empty stream, unrecognized output, partial compilation +or missing target is not a pass. Findings retain package/target/compiler context +and contained original source locations, including available macro call sites. +Diagnostics outside available source remain attached to the selected manifest +with the reported source identified; they are not discarded or assigned an +invented line. Workspace metadata, not parsed package-ID strings, owns identity. + [Rust input coverage](rust-inputs.md) defines control input selection and the governed toolchain. The [implementation plan](../plans/code-polishy-rust-support-plan.md) records the remaining Rust capabilities and acceptance work. diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 4b29e20a..85eeb41b 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -101,6 +101,23 @@ precede tool execution, but resolved metadata cannot be admitted before the tool/configuration boundary is ready. Reuse existing exact test suites for changed modules; add a quick suite when a new module is introduced. +### Implemented beta slices + +- Pinned toolchain installation and Cargo workspace discovery are connected to + the normal check path and command plan. Discovery does not execute build scripts + or materialize dependencies. +- Selected-file rustfmt uses Cargo editions and bounded project configuration; + generated files, unselected child modules and concurrent edits remain protected. +- Frozen native-host compiler/Clippy checks preserve original-path diagnostics, + default feature selection and gated-target behavior. Real fixtures cover type + errors, lint defects, attempted suppression, absent/drifting locks, unavailable + linkers, cancellation and build-script configuration output. Public CLI checks + retain the remaining coverage failures instead of declaring full Rust support. +- Parser-backed source rules, architecture, Cargo supply-chain governance, + structured Rust test evidence, WGSL and installed-release integration remain + unfinished. Project Clippy overrides and non-native targets currently fail + explicitly rather than weakening the supported compiler configuration. + ## Phase 0: define the complete coverage contract - [x] Audit current Go/Python owners and Rust gaps against the recorded base; diff --git a/internal/quality/cargo.go b/internal/quality/cargo.go index 1ca5cc59..118a55d0 100644 --- a/internal/quality/cargo.go +++ b/internal/quality/cargo.go @@ -16,5 +16,7 @@ func cargoDiscoveryFindings(ctx context.Context, repo repository.Repository, fil } plan := rustfacts.PlanDiscovery(repo, files, allFiles) discovery := rustfacts.Discover(ctx, repo, plan, commandRunner) - return append(discovery.Findings, rustFormatFindings(ctx, repo, files, discovery.Workspaces)...) + findings := append(discovery.Findings, rustFormatFindings(ctx, repo, files, discovery.Workspaces)...) + compilation := rustfacts.PlanCompilation(repo, plan, allFiles) + return append(findings, rustfacts.Compile(ctx, repo, compilation, discovery, commandRunner)...) } diff --git a/internal/quality/cargo_test.go b/internal/quality/cargo_test.go index ca0df9c2..a14edfce 100644 --- a/internal/quality/cargo_test.go +++ b/internal/quality/cargo_test.go @@ -54,6 +54,7 @@ func TestCargoAndGoCheckPlanMatchesObservedCommandOrder(t *testing.T) { t.Parallel() repo := cargoQualityRepository(t) writeQualityFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\n") + writeQualityFile(t, repo.Root, "Cargo.lock", "version=4\n[[package]]\nname='app'\nversion='1.0.0'\n") writeQualityFile(t, repo.Root, "src/lib.rs", "pub fn app() {}\n") writeQualityFile(t, repo.Root, "go.mod", "module example.test/mixed\n\ngo 1.26.6\n") writeQualityFile(t, repo.Root, "main.go", "package main\n\nfunc main() {}\n") diff --git a/internal/quality/quality.go b/internal/quality/quality.go index bdf64efc..d463533b 100644 --- a/internal/quality/quality.go +++ b/internal/quality/quality.go @@ -46,7 +46,9 @@ func CheckCommands(repo repository.Repository, selection repository.Selection, p repo = repo.WithAnalysisProfile(profile) commands := []policy.Command{} if allFiles, err := repo.AllFiles(); err == nil { - commands = append(commands, rustfacts.PlanDiscovery(repo, selection.Files, allFiles).Commands...) + discovery := rustfacts.PlanDiscovery(repo, selection.Files, allFiles) + commands = append(commands, discovery.Commands...) + commands = append(commands, rustfacts.PlanCompilation(repo, discovery, allFiles).Commands...) goCommands, _ := nativeGoToolCommands(repo, selection.Files, allFiles) commands = append(commands, goCommands...) } @@ -923,7 +925,7 @@ func builtInCapability(language, capability string) bool { case "go", "typescript", "python": return slices.Contains(builtInCapabilities, capability) case "rust": - return capability == "format" + return slices.Contains([]string{"format", "lint", "typecheck"}, capability) case "shell": return slices.Contains([]string{"format", "lint", "typecheck"}, capability) } diff --git a/internal/repository/providers.go b/internal/repository/providers.go index dc40987e..cb9a84d4 100644 --- a/internal/repository/providers.go +++ b/internal/repository/providers.go @@ -127,7 +127,7 @@ func (repo Repository) NativeCapability(path, capability string) bool { case "go", "python": return slices.Contains([]string{"format", "lint", "typecheck", "complexity", "dead-code", "architecture"}, capability) case "rust": - return capability == "format" + return slices.Contains([]string{"format", "lint", "typecheck"}, capability) case "shell": return slices.Contains([]string{"format", "lint", "typecheck"}, capability) case "typescript": diff --git a/internal/rustfacts/compilation.go b/internal/rustfacts/compilation.go new file mode 100644 index 00000000..7ce53ea2 --- /dev/null +++ b/internal/rustfacts/compilation.go @@ -0,0 +1,147 @@ +package rustfacts + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "slices" + "strings" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" +) + +type CompilationPlan struct { + Manifests []string + Commands []policy.Command + Findings []policy.Finding +} + +func PlanCompilation(repo repository.Repository, discovery DiscoveryPlan, allFiles []string) CompilationPlan { + plan := CompilationPlan{} + if len(discovery.Commands) == 0 { + return plan + } + for _, manifest := range discovery.Manifests { + files := compilationSources(repo, manifest, allFiles) + native, err := repo.NativeUnit(files, "lint", "typecheck") + if err != nil { + plan.Findings = append(plan.Findings, projectFinding(manifest, "compilation-unit", err.Error())) + continue + } + if !native { + continue + } + if err := compilationConfiguration(repo, manifest); err != nil { + plan.Findings = append(plan.Findings, projectFinding(manifest, "configuration", err.Error())) + continue + } + plan.Manifests = append(plan.Manifests, manifest) + plan.Commands = append(plan.Commands, compilationCommand(repo, manifest)) + } + return plan +} + +func compilationSources(repo repository.Repository, manifest string, allFiles []string) []string { + root := filepath.ToSlash(filepath.Dir(manifest)) + files := []string{} + for _, path := range allFiles { + if repo.Language(path) == "rust" && (root == "." || strings.HasPrefix(path, root+"/")) { + files = append(files, path) + } + } + return files +} + +func compilationCommand(repo repository.Repository, manifest string) policy.Command { + command := Command(repo, "policy-clippy-"+manifest, "cargo-clippy", "clippy", "--frozen", "--all-targets", "--no-deps", "--message-format=json", "--target", HostTarget(), "--manifest-path", manifest, "--", "-F", "warnings", "-F", "clippy::all", "-F", "unused") + command.Environment = append(command.Environment, "PATH", "LIB", "INCLUDE", "LIBPATH", "SDKROOT", "DEVELOPER_DIR") + command.EnvironmentOverrides = append(command.EnvironmentOverrides, "CARGO="+repo.RustTool("cargo"), "CLIPPY_CONF_DIR="+filepath.Join(repo.PolicyRoot, "tools")) + return command +} + +func HostTarget() string { + architecture := map[string]string{"amd64": "x86_64", "arm64": "aarch64"}[runtime.GOARCH] + platform := map[string]string{"linux": "unknown-linux-gnu", "darwin": "apple-darwin", "windows": "pc-windows-msvc"}[runtime.GOOS] + if architecture == "" || platform == "" || runtime.GOOS == "windows" && runtime.GOARCH != "amd64" { + return "" + } + return architecture + "-" + platform +} + +func Compile(ctx context.Context, repo repository.Repository, plan CompilationPlan, discovery Discovery, commandRunner runner.Runner) []policy.Finding { + findings := slices.Clone(plan.Findings) + for index, command := range plan.Commands { + manifest := plan.Manifests[index] + workspace := workspaceForManifest(discovery.Workspaces, manifest) + if workspace == nil { + findings = append(findings, projectFinding(manifest, "compilation", "compilation requires valid Cargo workspace discovery")) + continue + } + findings = append(findings, compileManifest(ctx, repo, manifest, *workspace, command, commandRunner)...) + } + return findings +} + +func workspaceForManifest(workspaces []Workspace, manifest string) *Workspace { + for index := range workspaces { + workspace := &workspaces[index] + if filepath.ToSlash(filepath.Join(workspace.Root, "Cargo.toml")) == manifest || slices.ContainsFunc(workspace.Packages, func(pkg Package) bool { return pkg.Manifest == manifest }) { + return workspace + } + } + return nil +} + +func compileManifest(ctx context.Context, repo repository.Repository, manifest string, workspace Workspace, command policy.Command, commandRunner runner.Runner) []policy.Finding { + lock := filepath.ToSlash(filepath.Join(workspace.Root, "Cargo.lock")) + if _, err := repo.ReadContainedFile(lock, 8<<20); err != nil { + return []policy.Finding{projectFinding(lock, "lockfile", fmt.Sprintf("governed compilation requires a contained Cargo.lock; prepare and review dependency resolution before checking: %v", err))} + } + structured, ok := commandRunner.(runner.StructuredRunner) + if !ok { + return []policy.Finding{projectFinding(manifest, "compilation", "the runner cannot capture bounded Cargo compiler output")} + } + result, output, runErr := structured.RunStructured(ctx, repo.Root, command) + report, parseErr := ParseCompilation(repo, manifest, workspace, output.Stdout) + findings := report.Findings + if parseErr != nil { + findings = append(findings, projectFinding(manifest, "compiler-output", fmt.Sprintf("invalid Cargo compiler evidence: %v; %s", parseErr, diagnosticOutput(output.Stderr)))) + } + if runErr != nil && !compilerDiagnosticExit(ctx, result, report, parseErr, runErr) { + findings = append(findings, projectFinding(manifest, "compilation", fmt.Sprintf("Cargo compilation failed: %v; %s", runErr, diagnosticOutput(output.Stderr)))) + } + if parseErr == nil && runErr == nil && !report.Success { + findings = append(findings, projectFinding(manifest, "compilation", "Cargo reported an unsuccessful build without a failing process exit")) + } + return findings +} + +func compilerDiagnosticExit(ctx context.Context, result runner.Result, report Compilation, parseErr, runErr error) bool { + return runErr != nil && parseErr == nil && !report.Success && len(report.Findings) > 0 && + result.ExitStatus == 101 && runner.FailureCategoryFor(ctx, result, runErr) == runner.FailureCommandExit +} + +func compilationConfiguration(repo repository.Repository, manifest string) error { + if HostTarget() == "" { + return errors.New("governed Rust compilation is unavailable on this host") + } + if err := compilationTargetConfiguration(repo); err != nil { + return err + } + for parent := filepath.Dir(manifest); ; parent = filepath.Dir(parent) { + for _, name := range []string{"clippy.toml", ".clippy.toml"} { + path := filepath.Join(repo.Root, parent, name) + if _, err := os.Lstat(path); !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("project Clippy configuration %s needs governed settings support; policy-owned Clippy defaults are required", filepath.ToSlash(filepath.Join(parent, name))) + } + } + if parent == "." || filepath.Dir(parent) == parent { + return nil + } + } +} diff --git a/internal/rustfacts/compilation_test.go b/internal/rustfacts/compilation_test.go new file mode 100644 index 00000000..0aa642f5 --- /dev/null +++ b/internal/rustfacts/compilation_test.go @@ -0,0 +1,258 @@ +package rustfacts + +import ( + "bytes" + "context" + "errors" + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestRustCompilationChecksRealCrateAndKeepsLockUnchanged(t *testing.T) { + t.Parallel() + repo := compilationRepository(t, "pub fn answer() -> u32 { 42 }\n") + before := readRustFile(t, repo, "Cargo.lock") + plan, discovery := compilationDiscovery(t, repo, "src/lib.rs") + if findings := Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}); len(findings) != 0 { + t.Fatalf("valid crate failed: %+v", findings) + } + if after := readRustFile(t, repo, "Cargo.lock"); !bytes.Equal(before, after) { + t.Fatal("compiler changed the reviewed lockfile") + } + if _, err := os.Stat(filepath.Join(TargetDirectory(repo), HostTarget(), "debug", "deps")); err != nil { + t.Fatalf("compilation did not use its governed native target: %v", err) + } +} + +func TestRustCompilationReportsOriginalTypeAndLintLocations(t *testing.T) { + t.Parallel() + for name, test := range map[string]struct{ source, check, subject string }{ + "type": {"pub fn answer() -> u32 {\n \"wrong\"\n}\n", "quality.rustCompiler", "E0308"}, + "clippy": {"pub fn answer(value: bool) -> bool {\n if value { true } else { false }\n}\n", "quality.clippy", "clippy::needless_bool"}, + "private dead code": {"fn unused() {}\npub fn answer() {}\n", "quality.rustCompiler", "dead_code"}, + } { + t.Run(name, func(t *testing.T) { + repo := compilationRepository(t, test.source) + plan, discovery := compilationDiscovery(t, repo, "src/lib.rs") + findings := Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}) + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return finding.Check == test.check && finding.Subject == test.subject && finding.Path == "src/lib.rs" && finding.Line > 0 && finding.Column > 0 && finding.Fields["target"] == "app" + }) { + t.Fatalf("missing original compiler diagnostic: %+v", findings) + } + }) + } +} + +func TestRustCompilationRejectsLintAttributeSuppression(t *testing.T) { + t.Parallel() + for name, source := range map[string]string{ + "allow clippy": "#[allow(clippy::needless_bool)]\npub fn answer(value: bool) -> bool { if value { true } else { false } }\n", + "expect clippy": "#[expect(clippy::needless_bool)]\npub fn answer(value: bool) -> bool { if value { true } else { false } }\n", + "allow unused": "#[allow(dead_code)]\nfn unused() {}\npub fn answer() {}\n", + } { + t.Run(name, func(t *testing.T) { + repo := compilationRepository(t, source) + plan, discovery := compilationDiscovery(t, repo, "src/lib.rs") + findings := Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}) + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return finding.Check == "quality.rustCompiler" && strings.Contains(finding.Message, "incompatible with previous forbid") && finding.Path == "src/lib.rs" + }) { + t.Fatalf("suppression did not conflict with governed lint policy: %+v", findings) + } + }) + } +} + +func TestRustCompilationUsesWorkspaceRelativeSourcePaths(t *testing.T) { + t.Parallel() + repo := rustRepository(t) + write(t, repo.Root, "nested/Cargo.toml", "[workspace]\nmembers=['crates/service']\nresolver='3'\n") + write(t, repo.Root, "nested/Cargo.lock", "version = 4\n[[package]]\nname = 'service'\nversion = '1.0.0'\n") + write(t, repo.Root, "nested/crates/service/Cargo.toml", "[package]\nname='service'\nversion='1.0.0'\nedition='2024'\n") + write(t, repo.Root, "nested/crates/service/src/lib.rs", "pub fn answer() -> u32 { \"bad\" }\n") + write(t, repo.Root, "src/lib.rs", "this is not the selected Rust source\n") + plan, discovery := compilationDiscovery(t, repo, "nested/crates/service/src/lib.rs") + findings := Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}) + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return finding.Subject == "E0308" && finding.Path == "nested/crates/service/src/lib.rs" && finding.Line == 1 + }) { + t.Fatalf("nested workspace source was lost or misattributed: %+v", findings) + } +} + +func TestRustCompilationRequiresLockWithoutExecuting(t *testing.T) { + t.Parallel() + repo := rustRepository(t) + write(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\n") + write(t, repo.Root, "src/lib.rs", "pub fn app() {}\n") + plan, discovery := compilationDiscovery(t, repo, "src/lib.rs") + findings := Compile(t.Context(), repo, plan, discovery, noOutputRunner{}) + if len(findings) != 1 || findings[0].Path != "Cargo.lock" || findings[0].Subject != "lockfile" { + t.Fatalf("missing lock diagnostic: %+v", findings) + } + if _, err := os.Stat(filepath.Join(repo.Root, "Cargo.lock")); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("check generated a lockfile: %v", err) + } +} + +func TestRustCompilationRejectsLockDriftWithoutRepair(t *testing.T) { + t.Parallel() + repo := compilationRepository(t, "pub fn answer() {}\n") + write(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.1'\nedition='2024'\n") + before := readRustFile(t, repo, "Cargo.lock") + plan, discovery := compilationDiscovery(t, repo, "src/lib.rs") + findings := Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}) + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { return strings.Contains(finding.Message, "lock file") }) { + t.Fatalf("lock drift did not fail explicitly: %+v", findings) + } + if !bytes.Equal(before, readRustFile(t, repo, "Cargo.lock")) { + t.Fatal("check repaired the lockfile") + } +} + +func TestRustCompilationInventoriesGeneratedBuildScriptFacts(t *testing.T) { + t.Parallel() + repo := compilationRepository(t, "pub fn answer() -> u32 { 42 }\n") + write(t, repo.Root, "build.rs", "fn main() { println!(\"cargo::rustc-cfg=generated_build\"); println!(\"cargo::rustc-check-cfg=cfg(generated_build)\"); println!(\"cargo::rustc-env=GENERATED_VALUE=42\"); }\n") + plan, discovery := compilationDiscovery(t, repo, "src/lib.rs") + _, output, err := (runner.OSRunner{}).RunStructured(t.Context(), repo.Root, plan.Commands[0]) + if err != nil { + t.Fatalf("build script compilation: %v %s", err, output.Stderr) + } + report, err := ParseCompilation(repo, "Cargo.toml", discovery.Workspaces[0], output.Stdout) + if err != nil || !report.Success || len(report.BuildScripts) != 1 { + t.Fatalf("generated build evidence: %+v, %v", report, err) + } + build := report.BuildScripts[0] + if !slices.Contains(build.Cfgs, "generated_build") || !slices.Contains(build.Environment, [2]string{"GENERATED_VALUE", "42"}) || !withinDirectory(TargetDirectory(repo), build.OutputDir) { + t.Fatalf("incomplete generated build facts: %+v", build) + } +} + +func TestRustCompilationIgnoresAmbientCompilerOverrides(t *testing.T) { + repo := compilationRepository(t, "pub fn answer() -> u32 { 42 }\n") + for _, key := range []string{"CARGO", "RUSTC", "RUSTC_WRAPPER", "RUSTC_WORKSPACE_WRAPPER", "CLIPPY_ARGS", "CLIPPY_CONF_DIR", "RUSTFLAGS", "CARGO_ENCODED_RUSTFLAGS", "RUSTUP_TOOLCHAIN"} { + t.Setenv(key, "unapproved-compiler-override") + } + plan, discovery := compilationDiscovery(t, repo, "src/lib.rs") + if findings := Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}); len(findings) != 0 { + t.Fatalf("ambient override affected governed compilation: %+v", findings) + } +} + +func TestRustCompilationCancellationIsNotDiagnosticSuccess(t *testing.T) { + t.Parallel() + repo := compilationRepository(t, "pub fn answer() -> u32 { 42 }\n") + plan, discovery := compilationDiscovery(t, repo, "src/lib.rs") + ctx, cancel := context.WithCancel(t.Context()) + cancel() + findings := Compile(ctx, repo, plan, discovery, runner.OSRunner{}) + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { return strings.Contains(finding.Message, "canceled") }) { + t.Fatalf("canceled compilation was not a failure: %+v", findings) + } +} + +func TestRustCompilationKeepsDefaultFeaturesAndSkipsGatedTargets(t *testing.T) { + t.Parallel() + repo := compilationRepository(t, "#[cfg(all(feature = \"first\", feature = \"second\"))]\ncompile_error!(\"mutually exclusive features\");\npub fn answer() -> u32 { 42 }\n") + manifest := string(readRustFile(t, repo, "Cargo.toml")) + write(t, repo.Root, "Cargo.toml", manifest+"[features]\ndefault=['first']\nfirst=[]\nsecond=[]\n[[bin]]\nname='gated'\npath='gated.rs'\nrequired-features=['second']\n") + write(t, repo.Root, "gated.rs", "compile_error!(\"unselected target\");\nfn main() {}\n") + plan, discovery := compilationDiscovery(t, repo, "src/lib.rs") + _, output, err := (runner.OSRunner{}).RunStructured(t.Context(), repo.Root, plan.Commands[0]) + if err != nil { + t.Fatalf("default configuration failed: %v %s", err, output.Stderr) + } + report, err := ParseCompilation(repo, "Cargo.toml", discovery.Workspaces[0], output.Stdout) + if err != nil || !report.Success || len(report.Artifacts) == 0 { + t.Fatalf("default feature evidence: %+v, %v", report, err) + } + for _, artifact := range report.Artifacts { + if !slices.Contains(artifact.Features, "first") || slices.Contains(artifact.Features, "second") || artifact.Target.Name == "gated" { + t.Errorf("compiled an unselected feature/target: %+v", artifact) + } + } +} + +func TestRustCompilationReportsUnavailableNativeLinker(t *testing.T) { + t.Parallel() + repo := compilationRepository(t, "pub fn answer() {}\n") + write(t, repo.Root, "build.rs", "fn main() {}\n") + plan, discovery := compilationDiscovery(t, repo, "src/lib.rs") + linkerKey := "CARGO_TARGET_" + strings.ToUpper(strings.ReplaceAll(HostTarget(), "-", "_")) + "_LINKER=" + plan.Commands[0].EnvironmentOverrides = append(plan.Commands[0].EnvironmentOverrides, linkerKey+filepath.Join(t.TempDir(), "missing-linker")) + findings := Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}) + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return strings.Contains(finding.Message, "linker") || strings.Contains(finding.Message, "link.exe") + }) { + t.Fatalf("missing SDK/linker did not fail explicitly: %+v", findings) + } +} + +func TestRustCompilationRejectsSettingsAndSplitProviderUnits(t *testing.T) { + t.Parallel() + for name, configure := range map[string]func(repository.Repository) repository.Repository{ + "project clippy settings": func(repo repository.Repository) repository.Repository { + write(t, repo.Root, "clippy.toml", "too-many-arguments-threshold=1000\n") + return repo + }, + "foreign target": func(repo repository.Repository) repository.Repository { + write(t, repo.Root, ".cargo/config.toml", "[build]\ntarget='wasm32-unknown-unknown'\n") + return repo + }, + "partial provider": func(repo repository.Repository) repository.Repository { + repo.Config.Checks = []policy.Command{{Name: "custom-lint", Provides: []string{"lint"}, Paths: []string{"src/lib.rs"}, RunOn: []string{"check"}, Adapter: &policy.PackAdapter{PackName: "custom", Capability: "lint"}}} + return repo + }, + } { + t.Run(name, func(t *testing.T) { + repo := configure(compilationRepository(t, "pub fn app() {}\n")) + files := []string{"Cargo.toml", "src/lib.rs"} + plan := PlanCompilation(repo, PlanDiscovery(repo, files, files), files) + if len(plan.Commands) != 0 || len(plan.Findings) != 1 { + t.Fatalf("unsupported compilation scope reached execution: %+v", plan) + } + }) + } +} + +func compilationRepository(t *testing.T, source string) repository.Repository { + t.Helper() + repo := rustRepository(t) + write(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\nedition='2024'\n") + write(t, repo.Root, "Cargo.lock", "version = 4\n[[package]]\nname = 'app'\nversion = '1.0.0'\n") + write(t, repo.Root, "src/lib.rs", source) + return repo +} + +func compilationDiscovery(t *testing.T, repo repository.Repository, selected string) (CompilationPlan, Discovery) { + t.Helper() + files, err := repo.AllFiles() + if err != nil { + t.Fatal(err) + } + discoveryPlan := PlanDiscovery(repo, []string{selected}, files) + discovery := Discover(t.Context(), repo, discoveryPlan, runner.OSRunner{}) + plan := PlanCompilation(repo, discoveryPlan, files) + if len(discovery.Findings) != 0 || len(plan.Findings) != 0 || len(plan.Commands) != 1 { + t.Fatalf("compilation preparation: %+v, %+v", discovery, plan) + } + return plan, discovery +} + +func readRustFile(t *testing.T, repo repository.Repository, path string) []byte { + t.Helper() + data, err := os.ReadFile(filepath.Join(repo.Root, filepath.FromSlash(path))) + if err != nil { + t.Fatal(err) + } + return data +} diff --git a/internal/rustfacts/compiler_diagnostics.go b/internal/rustfacts/compiler_diagnostics.go new file mode 100644 index 00000000..b245fa72 --- /dev/null +++ b/internal/rustfacts/compiler_diagnostics.go @@ -0,0 +1,76 @@ +package rustfacts + +import ( + "errors" + "path/filepath" + "strings" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" +) + +func compilerFindings(repo repository.Repository, manifest string, workspace Workspace, event compilerEvent) ([]policy.Finding, error) { + if err := validateCompilerTarget(repo, workspace, event); err != nil { + return nil, err + } + diagnostic := event.Message + if diagnostic.Message == "" || diagnostic.Level == "" { + return nil, errors.New("cargo compiler diagnostic omitted its message or severity") + } + switch diagnostic.Level { + case "note", "help", "failure-note": + return nil, nil + case "error", "warning", "error: internal compiler error": + default: + return nil, errors.New("cargo compiler diagnostic has an unsupported severity") + } + code, check := "rustc", "quality.rustCompiler" + if diagnostic.Code != nil && diagnostic.Code.Code != "" { + code = diagnostic.Code.Code + } + if strings.HasPrefix(code, "clippy::") { + check = "quality.clippy" + } + finding := policy.Finding{ + Check: check, Path: manifest, Subject: code, Message: diagnostic.Message, + Fields: map[string]string{"packageId": event.PackageID, "target": event.Target.Name, "targetKinds": strings.Join(event.Target.Kinds, ","), "edition": event.Target.Edition, "hostTarget": HostTarget(), "rustVersion": repo.ToolPin("rust"), "featureSelection": "default"}, + } + return []policy.Finding{locateCompilerFinding(repo, workspace, finding, diagnostic.Spans)}, nil +} + +func locateCompilerFinding(repo repository.Repository, workspace Workspace, finding policy.Finding, spans []compilerSpan) policy.Finding { + for _, span := range spans { + if !span.Primary { + continue + } + location, ok := compilerLocation(repo, workspace, span) + if !ok { + finding.Fields["reportedSource"] = span.File + continue + } + if finding.Line == 0 { + finding.Path, finding.Line, finding.Column = location.Path, location.Line, location.Column + finding.EndLine, finding.EndColumn = location.EndLine, location.EndColumn + } else { + finding.Related = append(finding.Related, location) + } + } + return finding +} + +func compilerLocation(repo repository.Repository, workspace Workspace, span compilerSpan) (policy.FindingLocation, bool) { + for depth := 0; depth < 64; depth++ { + path := span.File + if !filepath.IsAbs(path) { + path = filepath.Join(repo.Root, filepath.FromSlash(workspace.Root), path) + } + if relative, err := containedPath(repo, path); err == nil && span.Line > 0 && span.Column > 0 { + return policy.FindingLocation{Path: relative, Line: span.Line, Column: span.Column, EndLine: span.EndLine, EndColumn: span.EndColumn}, true + } + if span.Expansion == nil { + break + } + span = span.Expansion.Span + } + return policy.FindingLocation{}, false +} diff --git a/internal/rustfacts/compiler_output.go b/internal/rustfacts/compiler_output.go new file mode 100644 index 00000000..f259b301 --- /dev/null +++ b/internal/rustfacts/compiler_output.go @@ -0,0 +1,200 @@ +package rustfacts + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "path/filepath" + "slices" + "strings" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" +) + +type Compilation struct { + Success bool + Findings []policy.Finding + Artifacts []CompiledTarget + BuildScripts []BuildScript +} + +type CompiledTarget struct { + PackageID string + Target Target + Features []string + Fresh bool +} + +type BuildScript struct { + PackageID string + OutputDir string + Cfgs []string + Environment [][2]string + LinkedLibs []string + LinkedPaths []string +} + +type compilerEvent struct { + Reason string `json:"reason"` + PackageID string `json:"package_id"` + Target Target `json:"target"` + Message compilerDiagnostic `json:"message"` + Features []string `json:"features"` + Fresh *bool `json:"fresh"` + Success *bool `json:"success"` + OutputDir string `json:"out_dir"` + Cfgs []string `json:"cfgs"` + Environment [][2]string `json:"env"` + LinkedLibs []string `json:"linked_libs"` + LinkedPaths []string `json:"linked_paths"` +} + +type compilerDiagnostic struct { + Message string `json:"message"` + Level string `json:"level"` + Code *compilerCode `json:"code"` + Spans []compilerSpan `json:"spans"` +} + +type compilerCode struct { + Code string `json:"code"` +} + +type compilerSpan struct { + File string `json:"file_name"` + Line int `json:"line_start"` + Column int `json:"column_start"` + EndLine int `json:"line_end"` + EndColumn int `json:"column_end"` + Primary bool `json:"is_primary"` + Expansion *compilerExpansion `json:"expansion"` +} + +type compilerExpansion struct { + Span compilerSpan `json:"span"` +} + +func ParseCompilation(repo repository.Repository, manifest string, workspace Workspace, data []byte) (Compilation, error) { + report := Compilation{} + if len(data) > 8<<20 { + return report, errors.New("cargo compiler output exceeds 8388608 bytes") + } + finished := false + for _, line := range bytes.Split(data, []byte{'\n'}) { + if len(bytes.TrimSpace(line)) == 0 { + continue + } + var event compilerEvent + if err := json.Unmarshal(line, &event); err != nil { + return report, fmt.Errorf("expected one Cargo JSON event per line: %w", err) + } + if finished { + return report, errors.New("cargo emitted events after build-finished") + } + if err := consumeCompilerEvent(repo, manifest, workspace, event, &report); err != nil { + return report, err + } + finished = event.Reason == "build-finished" + } + if !finished { + return report, errors.New("cargo emitted no build-finished event") + } + if report.Success { + return report, validateCompiledPackages(manifest, workspace, report.Artifacts) + } + return report, nil +} + +func consumeCompilerEvent(repo repository.Repository, manifest string, workspace Workspace, event compilerEvent, report *Compilation) error { + switch event.Reason { + case "compiler-message": + findings, err := compilerFindings(repo, manifest, workspace, event) + report.Findings = append(report.Findings, findings...) + return err + case "compiler-artifact": + if err := validateCompilerTarget(repo, workspace, event); err != nil { + return err + } + if event.Fresh == nil || event.Features == nil { + return errors.New("cargo artifact omitted freshness or feature evidence") + } + report.Artifacts = append(report.Artifacts, CompiledTarget{PackageID: event.PackageID, Target: event.Target, Features: event.Features, Fresh: *event.Fresh}) + case "build-script-executed": + build, err := compilerBuildScript(repo, event) + if err != nil { + return err + } + report.BuildScripts = append(report.BuildScripts, build) + case "build-finished": + if event.Success == nil { + return errors.New("cargo build-finished event omitted its outcome") + } + report.Success = *event.Success + default: + return fmt.Errorf("unsupported Cargo compiler event %q", event.Reason) + } + return nil +} + +func compilerBuildScript(repo repository.Repository, event compilerEvent) (BuildScript, error) { + if event.PackageID == "" || !withinDirectory(TargetDirectory(repo), event.OutputDir) || event.Cfgs == nil || event.Environment == nil { + return BuildScript{}, errors.New("cargo build-script evidence omitted identity, configuration or governed output directory") + } + return BuildScript{PackageID: event.PackageID, OutputDir: event.OutputDir, Cfgs: event.Cfgs, Environment: event.Environment, LinkedLibs: event.LinkedLibs, LinkedPaths: event.LinkedPaths}, nil +} + +func validateCompilerTarget(repo repository.Repository, workspace Workspace, event compilerEvent) error { + if event.PackageID == "" || event.Target.Name == "" || len(event.Target.Kinds) == 0 || !filepath.IsAbs(event.Target.Path) || !supportedEdition(event.Target.Edition) { + return errors.New("cargo compiler event omitted package or target identity") + } + for _, pkg := range workspace.Packages { + if pkg.ID != event.PackageID { + continue + } + if matchesDiscoveredTarget(repo, pkg.Targets, event.Target) { + return nil + } + return fmt.Errorf("compiler target %q does not match discovered package %q", event.Target.Name, pkg.Name) + } + return nil +} + +func matchesDiscoveredTarget(repo repository.Repository, targets []Target, reported Target) bool { + return slices.ContainsFunc(targets, func(target Target) bool { + absolute := filepath.Join(repo.Root, filepath.FromSlash(target.Path)) + return reported.Name == target.Name && filepath.Clean(reported.Path) == absolute && slices.Equal(reported.Kinds, target.Kinds) && reported.Edition == target.Edition + }) +} + +func validateCompiledPackages(manifest string, workspace Workspace, artifacts []CompiledTarget) error { + selected := workspace.DefaultMembers + if manifest != filepath.ToSlash(filepath.Join(workspace.Root, "Cargo.toml")) { + selected = nil + for _, pkg := range workspace.Packages { + if pkg.Manifest == manifest { + selected = []string{pkg.ID} + } + } + } + if len(selected) == 0 { + return errors.New("cargo compilation selected no workspace packages") + } + for _, id := range selected { + if !slices.ContainsFunc(artifacts, func(artifact CompiledTarget) bool { + return artifact.PackageID == id && !slices.Contains(artifact.Target.Kinds, "custom-build") + }) { + return fmt.Errorf("cargo emitted no compiled target for selected package %q", id) + } + } + return nil +} + +func withinDirectory(root, path string) bool { + if !filepath.IsAbs(path) { + return false + } + relative, err := filepath.Rel(root, path) + return err == nil && relative != "." && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator)) +} diff --git a/internal/rustfacts/compiler_output_test.go b/internal/rustfacts/compiler_output_test.go new file mode 100644 index 00000000..894685eb --- /dev/null +++ b/internal/rustfacts/compiler_output_test.go @@ -0,0 +1,97 @@ +package rustfacts + +import ( + "encoding/json" + "path/filepath" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/repository" +) + +func TestCargoCompilerOutputRequiresCompleteTargetEvidence(t *testing.T) { + t.Parallel() + repo, workspace, artifact := compilerOutputFixture(t) + for name, output := range map[string]string{ + "empty": "", "no finish": artifact, "no targets": `{"reason":"build-finished","success":true}`, + "missing outcome": artifact + "\n" + `{"reason":"build-finished"}`, + "duplicate outcome": artifact + "\n" + `{"reason":"build-finished","success":true}` + "\n" + `{"reason":"build-finished","success":true}`, + "arbitrary prose": "Finished successfully\n", "unknown event": `{"reason":"ignored-check"}`, + "foreign package": strings.ReplaceAll(artifact, "opaque::member#identity", "unknown-package") + "\n" + `{"reason":"build-finished","success":true}`, + "wrong target": strings.ReplaceAll(artifact, `"name":"app"`, `"name":"unselected"`) + "\n" + `{"reason":"build-finished","success":true}`, + "oversized": strings.Repeat(" ", 8<<20+1), + } { + t.Run(name, func(t *testing.T) { + if report, err := ParseCompilation(repo, "Cargo.toml", workspace, []byte(output)); err == nil { + t.Fatalf("incomplete evidence passed: %+v", report) + } + }) + } + valid := artifact + "\n" + `{"reason":"build-finished","success":true,"future_field":3}` + report, err := ParseCompilation(repo, "Cargo.toml", workspace, []byte(valid)) + if err != nil || !report.Success || len(report.Artifacts) != 1 || !report.Artifacts[0].Fresh { + t.Fatalf("valid cached target evidence rejected: %+v, %v", report, err) + } +} + +func TestCargoCompilerDiagnosticsRetainMacroAndUnavailableLocations(t *testing.T) { + t.Parallel() + repo, workspace, _ := compilerOutputFixture(t) + target := workspace.Packages[0].Targets[0] + target.Path = filepath.Join(repo.Root, target.Path) + for name, span := range map[string]compilerSpan{ + "macro": {File: "/unavailable/macro.rs", Primary: true, Expansion: &compilerExpansion{Span: compilerSpan{File: "src/lib.rs", Line: 1, Column: 8}}}, + "external": {File: "/unavailable/dependency.rs", Line: 5, Column: 3, Primary: true}, + } { + t.Run(name, func(t *testing.T) { + event := compilerEvent{Reason: "compiler-message", PackageID: workspace.Packages[0].ID, Target: target, Message: compilerDiagnostic{Message: "seeded type error", Level: "error", Code: &compilerCode{Code: "E0308"}, Spans: []compilerSpan{span}}} + findings, err := compilerFindings(repo, "Cargo.toml", workspace, event) + if err != nil || len(findings) != 1 { + t.Fatalf("diagnostic was discarded: %+v, %v", findings, err) + } + finding := findings[0] + if name == "macro" && (finding.Path != "src/lib.rs" || finding.Line != 1 || finding.Column != 8) { + t.Fatalf("macro invocation was not used: %+v", finding) + } + if name == "external" && (finding.Path != "Cargo.toml" || finding.Line != 0 || finding.Fields["reportedSource"] != span.File) { + t.Fatalf("unavailable location was invented: %+v", finding) + } + }) + } +} + +func TestCargoCompilerOutputRetainsDiagnosticsBeforeTruncation(t *testing.T) { + t.Parallel() + repo, workspace, _ := compilerOutputFixture(t) + target := workspace.Packages[0].Targets[0] + target.Path = filepath.Join(repo.Root, target.Path) + event := compilerEvent{Reason: "compiler-message", PackageID: workspace.Packages[0].ID, Target: target, Message: compilerDiagnostic{Message: "actual error", Level: "error", Code: &compilerCode{Code: "E0308"}}} + data, err := json.Marshal(event) + if err != nil { + t.Fatal(err) + } + report, err := ParseCompilation(repo, "Cargo.toml", workspace, append(data, []byte("\n{\"reason\":")...)) + if err == nil || len(report.Findings) != 1 || report.Findings[0].Subject != "E0308" || report.Success { + t.Fatalf("partial compiler output: %+v, %v", report, err) + } +} + +func compilerOutputFixture(t *testing.T) (repository.Repository, Workspace, string) { + t.Helper() + repo, wire := metadataFixture(t) + data, err := json.Marshal(wire) + if err != nil { + t.Fatal(err) + } + workspace, err := ParseWorkspace(repo, "Cargo.toml", data) + if err != nil { + t.Fatal(err) + } + fresh := true + event := compilerEvent{Reason: "compiler-artifact", PackageID: workspace.Packages[0].ID, Target: wire.Packages[0].Targets[0], Features: []string{}, Fresh: &fresh} + data, err = json.Marshal(event) + if err != nil { + t.Fatal(err) + } + return repo, workspace, string(data) +} diff --git a/internal/rustfacts/configuration.go b/internal/rustfacts/configuration.go index 67251ed4..348d82d0 100644 --- a/internal/rustfacts/configuration.go +++ b/internal/rustfacts/configuration.go @@ -73,6 +73,37 @@ func validateSettings(settings map[string]any) error { return nil } +func compilationTargetConfiguration(repo repository.Repository) error { + for _, name := range []string{".cargo/config", ".cargo/config.toml"} { + if _, err := os.Lstat(filepath.Join(repo.Root, filepath.FromSlash(name))); errors.Is(err, os.ErrNotExist) { + continue + } + data, err := repo.ReadContainedFile(name, 1<<20) + if err != nil { + return err + } + var settings struct { + Build struct { + Target any `toml:"target"` + } `toml:"build"` + } + if err := toml.Unmarshal(data, &settings); err != nil { + return err + } + if target := settings.Build.Target; target != nil && !nativeTargetSetting(target) { + return fmt.Errorf("cargo configuration %s requests target %v; governed compilation currently supports only native target %s", name, target, HostTarget()) + } + } + return nil +} + +func nativeTargetSetting(value any) bool { + if list, ok := value.([]any); ok { + return len(list) == 1 && list[0] == HostTarget() + } + return value == HostTarget() +} + func manifestPathProblem(repo repository.Repository, manifest repository.CargoManifest) error { paths := []string{} if manifest.Package != nil && manifest.Package.Workspace != "" { diff --git a/internal/rustfacts/configuration_test.go b/internal/rustfacts/configuration_test.go index 383db96c..4770e5e6 100644 --- a/internal/rustfacts/configuration_test.go +++ b/internal/rustfacts/configuration_test.go @@ -38,6 +38,17 @@ func TestCargoConfigurationAcceptsNonExecutableSettings(t *testing.T) { } } +func TestCargoCompilationAdmitsOnlyMatchingNativeTarget(t *testing.T) { + t.Parallel() + for _, target := range []string{"'" + HostTarget() + "'", "['" + HostTarget() + "']"} { + repo := repository.Repository{Root: t.TempDir()} + write(t, repo.Root, ".cargo/config.toml", "[build]\ntarget="+target+"\n") + if err := compilationTargetConfiguration(repo); err != nil { + t.Fatalf("native target declaration was rejected: %v", err) + } + } +} + func TestCargoConfigurationRejectsAncestorAndCargoHomeInputs(t *testing.T) { t.Parallel() for _, kind := range []string{"ancestor", "cargo-home"} { diff --git a/tools/clippy.toml b/tools/clippy.toml new file mode 100644 index 00000000..5f304987 --- /dev/null +++ b/tools/clippy.toml @@ -0,0 +1 @@ +avoid-breaking-exported-api = true From 2df4bc198b90247d12b4176f6d16d5e6d306b2a1 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Thu, 24 Sep 2026 23:42:54 +0000 Subject: [PATCH 06/49] feat: govern Cargo dependency inputs Parse Cargo dependency declarations across workspace and target-specific tables, enforce exact registry and Git pins, and contain local dependency paths. Validate versioned lock package identities and registry checksums without running target code. Preserve Git/registry source distinctions and the shared optional signed-assessment policy. Verified: repository-unit, supplychain-unit, rustfacts-unit, focused quality/workflow tests and scoped policy checks. Public CLI offline supply-chain rejects an unpinned Cargo dependency without running child commands. Resolved lock agreement, dependency-review comparison, age, security, licenses and remaining Rust language/release capabilities are unfinished. No final checkpoint or supplemental suites run for this progress commit. --- .code-polishy.json | 9 + docs/design/rust-inputs.md | 38 ++++ docs/plans/code-polishy-rust-support-plan.md | 8 + internal/repository/cargo_dependencies.go | 162 ++++++++++++++++++ .../repository/cargo_dependencies_test.go | 53 ++++++ internal/repository/cargo_lock.go | 88 ++++++++++ internal/repository/cargo_lock_test.go | 55 ++++++ internal/repository/cargo_manifest.go | 44 +++-- internal/supplychain/cargo.go | 118 +++++++++++++ internal/supplychain/cargo_lock.go | 48 ++++++ internal/supplychain/cargo_lock_test.go | 58 +++++++ internal/supplychain/cargo_test.go | 117 +++++++++++++ internal/supplychain/supplychain.go | 12 +- internal/supplychain/supplychain_test.go | 5 +- 14 files changed, 797 insertions(+), 18 deletions(-) create mode 100644 internal/repository/cargo_dependencies.go create mode 100644 internal/repository/cargo_dependencies_test.go create mode 100644 internal/repository/cargo_lock.go create mode 100644 internal/repository/cargo_lock_test.go create mode 100644 internal/supplychain/cargo.go create mode 100644 internal/supplychain/cargo_lock.go create mode 100644 internal/supplychain/cargo_lock_test.go create mode 100644 internal/supplychain/cargo_test.go diff --git a/.code-polishy.json b/.code-polishy.json index 533d216c..b8d2845f 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -34,6 +34,10 @@ "internal/policy/rust_inputs_test.go", "internal/repository/cargo_manifest.go", "internal/repository/cargo_manifest_test.go", + "internal/repository/cargo_dependencies.go", + "internal/repository/cargo_dependencies_test.go", + "internal/repository/cargo_lock.go", + "internal/repository/cargo_lock_test.go", "internal/repository/rust_tools.go", "internal/repository/rust_tools_test.go", "internal/repository/rust_inputs_test.go", @@ -44,6 +48,11 @@ "internal/quality/rust_format.go", "internal/quality/rust_format_test.go", "internal/quality/wgsl_test.go", + "internal/supplychain/cargo.go", + "internal/supplychain/cargo_test.go", + "internal/supplychain/cargo_lock.go", + "internal/supplychain/cargo_lock_test.go", + "internal/supplychain/supplychain.go", "tools/install-rust.sh", "tools/install-rust.ps1", "scripts/test-rust-runtime.sh" diff --git a/docs/design/rust-inputs.md b/docs/design/rust-inputs.md index 0d5fd2a6..4d90471f 100644 --- a/docs/design/rust-inputs.md +++ b/docs/design/rust-inputs.md @@ -24,6 +24,44 @@ diagnostics without discarding successfully parsed siblings or inspecting unrelated projects during a focused check. This inventory is not a resolved workspace, target, feature or dependency graph; those facts belong to Cargo. +## Dependency declarations + +The same data-only reader preserves registry versions, renamed packages, local +paths, Git selectors and workspace inheritance across ordinary, optional, build, +development and target-specific dependency tables. Workspace definitions and +patch/replace tables are inventoried too. Source identity comes from declaration +fields, not directory names or a guessed Cargo package ID. Inherited declarations +retain the inheritance marker until Cargo establishes their effective workspace; +the reader never fills in a guessed version. + +Native pin policy requires exact `=major.minor.patch` registry requirements, +including prereleases when selected. Cargo ignores build metadata in requirements, +so it cannot provide an exact artifact pin. Version constraints accompanying a +path or Git dependency must also be exact because they govern registry resolution +when the crate is published. Local dependency manifests must be contained regular +inputs; Git declarations need a full immutable revision and the existing validated +Git repository identity. Pin checks do not authenticate Git contents or invent a +publication date. Cargo follows the shared [optional Git assessment +policy](git-evidence-policy.md); signed evidence is required only when selected. + +Source substitutions, unsupported dependency fields and alternate registries need +explicit source governance. Static manifest checks neither install dependencies +nor run build scripts. They do not establish lock agreement, vulnerability, license +or release-age coverage; those remain separate dependency-policy operations. + +Lock inventory supports Cargo's version-3 and version-4 records. A package +identity retains its name, complete version and exact source; different versions +or sources are not collapsed. Registry records must carry their archive checksum, +which is authoritative artifact identity rather than a local change detector. +Dependency-edge strings remain opaque until Cargo supplies resolved graph facts. +The version-bound source decoder recognizes the pinned Cargo release's public +crates.io identity and immutable Git source encoding. Unknown sources fail +explicitly and never become public registry queries. Git packages sharing a name +and version with a registry crate remain separate coordinates. Parsing this +inventory alone does not prove that it agrees with the selected manifests. + +## Shader inputs + WGSL is executable shader source with its own built-in language identifier, `wgsl`. It participates in module ownership, source-comment coverage and the existing required analysis capabilities. A shader cannot be protected as data diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 85eeb41b..f397186b 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -118,6 +118,14 @@ changed modules; add a quick suite when a new module is introduced. unfinished. Project Clippy overrides and non-native targets currently fail explicitly rather than weakening the supported compiler configuration. +Cargo dependency-input governance now parses ordinary, build, development, +target-specific and workspace declaration tables, checks exact registry and Git +pins, contains local paths, and validates versioned lock identities/checksums. +It keeps Git and registry coordinates separate and retains explicit failures for +unsupported source substitutions. This is data-only preparation: resolved lock +agreement, dependency-review comparison, age, vulnerability and license integration +remain in progress. + ## Phase 0: define the complete coverage contract - [x] Audit current Go/Python owners and Rust gaps against the recorded base; diff --git a/internal/repository/cargo_dependencies.go b/internal/repository/cargo_dependencies.go new file mode 100644 index 00000000..b8c9bebb --- /dev/null +++ b/internal/repository/cargo_dependencies.go @@ -0,0 +1,162 @@ +package repository + +import ( + "fmt" + "maps" + "slices" +) + +type CargoDependencyDeclaration struct { + Table string + Alias string + Package string + Version string + Path string + Git string + Revision string + Branch string + Tag string + Registry string + Workspace bool + Optional bool + Fields []string +} + +type cargoDependencyTables struct { + Dependencies map[string]any `toml:"dependencies"` + DevDependencies map[string]any `toml:"dev-dependencies"` + BuildDependencies map[string]any `toml:"build-dependencies"` +} + +type cargoWorkspaceDocument struct { + CargoWorkspaceManifest + Dependencies map[string]any `toml:"dependencies"` +} + +type cargoManifestDocument struct { + Package *CargoPackageManifest `toml:"package"` + Workspace *cargoWorkspaceDocument `toml:"workspace"` + cargoDependencyTables + Target map[string]cargoDependencyTables `toml:"target"` + Patch map[string]map[string]any `toml:"patch"` + Replace map[string]any `toml:"replace"` +} + +func cargoDependencyDeclarations(document cargoManifestDocument) ([]CargoDependencyDeclaration, error) { + declarations, err := cargoTableDeclarations("", document.cargoDependencyTables) + if err != nil { + return nil, err + } + for _, target := range slices.Sorted(maps.Keys(document.Target)) { + entries, err := cargoTableDeclarations("target."+target+".", document.Target[target]) + if err != nil { + return nil, err + } + declarations = append(declarations, entries...) + } + if document.Workspace != nil { + entries, err := cargoDeclarationTable("workspace.dependencies", document.Workspace.Dependencies) + if err != nil { + return nil, err + } + declarations = append(declarations, entries...) + } + for _, registry := range slices.Sorted(maps.Keys(document.Patch)) { + entries, err := cargoDeclarationTable("patch."+registry, document.Patch[registry]) + if err != nil { + return nil, err + } + declarations = append(declarations, entries...) + } + replaced, err := cargoDeclarationTable("replace", document.Replace) + return append(declarations, replaced...), err +} + +func cargoTableDeclarations(prefix string, tables cargoDependencyTables) ([]CargoDependencyDeclaration, error) { + declarations := []CargoDependencyDeclaration{} + for _, table := range []struct { + name string + entries map[string]any + }{{"dependencies", tables.Dependencies}, {"dev-dependencies", tables.DevDependencies}, {"build-dependencies", tables.BuildDependencies}} { + entries, err := cargoDeclarationTable(prefix+table.name, table.entries) + if err != nil { + return nil, err + } + declarations = append(declarations, entries...) + } + return declarations, nil +} + +func cargoDeclarationTable(table string, entries map[string]any) ([]CargoDependencyDeclaration, error) { + declarations := []CargoDependencyDeclaration{} + for _, alias := range slices.Sorted(maps.Keys(entries)) { + declaration, err := cargoDeclaration(table, alias, entries[alias]) + if err != nil { + return nil, fmt.Errorf("cargo dependency %s.%s: %w", table, alias, err) + } + declarations = append(declarations, declaration) + } + return declarations, nil +} + +func cargoDeclaration(table, alias string, value any) (CargoDependencyDeclaration, error) { + declaration := CargoDependencyDeclaration{Table: table, Alias: alias, Package: alias} + if version, ok := value.(string); ok { + declaration.Version, declaration.Fields = version, []string{"version"} + return declaration, nil + } + fields, ok := value.(map[string]any) + if !ok { + return declaration, fmt.Errorf("declaration must be a version string or table") + } + declaration.Fields = slices.Sorted(maps.Keys(fields)) + text := map[string]*string{ + "package": &declaration.Package, "version": &declaration.Version, "path": &declaration.Path, + "git": &declaration.Git, "rev": &declaration.Revision, "branch": &declaration.Branch, + "tag": &declaration.Tag, "registry": &declaration.Registry, + } + for _, name := range declaration.Fields { + if destination := text[name]; destination != nil { + value, ok := fields[name].(string) + if !ok { + return declaration, fmt.Errorf("%s must be a string", name) + } + *destination = value + continue + } + if err := cargoDependencyOption(&declaration, name, fields[name]); err != nil { + return declaration, err + } + } + return declaration, nil +} + +func cargoDependencyOption(declaration *CargoDependencyDeclaration, name string, value any) error { + switch name { + case "workspace", "optional", "default-features": + enabled, ok := value.(bool) + if !ok { + return fmt.Errorf("%s must be a boolean", name) + } + if name == "workspace" { + declaration.Workspace = enabled + if !enabled { + return fmt.Errorf("workspace dependency inheritance must be true") + } + } + if name == "optional" { + declaration.Optional = enabled + } + case "features": + features, ok := value.([]any) + if !ok { + return fmt.Errorf("dependency features must be an array of strings") + } + for _, feature := range features { + if _, ok := feature.(string); !ok { + return fmt.Errorf("dependency features must be strings") + } + } + } + return nil +} diff --git a/internal/repository/cargo_dependencies_test.go b/internal/repository/cargo_dependencies_test.go new file mode 100644 index 00000000..f97ffa4b --- /dev/null +++ b/internal/repository/cargo_dependencies_test.go @@ -0,0 +1,53 @@ +package repository + +import ( + "slices" + "testing" +) + +func TestCargoManifestReadsEveryDependencyTable(t *testing.T) { + t.Parallel() + repo := Repository{Root: t.TempDir()} + writeFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\n[dependencies]\njson={package='serde_json', version='=1.0.140', optional=true, features=['raw_value']}\n[dev-dependencies]\nasserts='=1.0.0'\n[build-dependencies]\ncc.workspace=true\n[target.'cfg(windows)'.dependencies]\nwin='=0.1.0'\n[target.'cfg(unix)'.dev-dependencies]\nunix='=0.2.0'\n[target.'cfg(unix)'.build-dependencies]\nnative={path='native'}\n[workspace.dependencies]\ncc='=1.2.0'\n[patch.crates-io]\npatched={git='https://example.test/repo', rev='fixed'}\n[replace]\n'old:1.0.0'={path='fork'}\n") + inventory := repo.InspectCargoManifests([]string{"Cargo.toml"}) + if len(inventory.Problems) != 0 || len(inventory.Manifests) != 1 { + t.Fatalf("dependency manifest failed: %+v", inventory) + } + declarations := inventory.Manifests[0].Dependencies + if len(declarations) != 9 { + t.Fatalf("dependency tables were omitted: %+v", declarations) + } + for _, table := range []string{"dependencies", "dev-dependencies", "build-dependencies", "target.cfg(windows).dependencies", "target.cfg(unix).dev-dependencies", "target.cfg(unix).build-dependencies", "workspace.dependencies", "patch.crates-io", "replace"} { + if !slices.ContainsFunc(declarations, func(declaration CargoDependencyDeclaration) bool { return declaration.Table == table }) { + t.Errorf("missing %s dependencies", table) + } + } + if !slices.ContainsFunc(declarations, func(declaration CargoDependencyDeclaration) bool { + return declaration.Alias == "json" && declaration.Package == "serde_json" && declaration.Version == "=1.0.140" && declaration.Optional + }) { + t.Fatalf("renamed/optional dependency lost its identity: %+v", declarations) + } + if !slices.ContainsFunc(declarations, func(declaration CargoDependencyDeclaration) bool { + return declaration.Table == "build-dependencies" && declaration.Alias == "cc" && declaration.Workspace && declaration.Version == "" + }) { + t.Fatalf("workspace inheritance was replaced with a fabricated version: %+v", declarations) + } +} + +func TestCargoManifestRejectsMalformedDependencyShapes(t *testing.T) { + t.Parallel() + for name, value := range map[string]string{ + "number": "3", "array": "['=1.0.0']", "version": "{version=1}", "path": "{path=['lib']}", + "features": "{version='=1.0.0', features=[1]}", "optional": "{version='=1.0.0', optional='true'}", + "inheritance": "{workspace=false}", "default features": "{version='=1.0.0', default-features=1}", + } { + t.Run(name, func(t *testing.T) { + repo := Repository{Root: t.TempDir()} + writeFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\n[dependencies]\ncrate="+value+"\n") + inventory := repo.InspectCargoManifests([]string{"Cargo.toml"}) + if len(inventory.Manifests) != 0 || len(inventory.Problems) != 1 { + t.Fatalf("invalid dependency became a usable manifest: %+v", inventory) + } + }) + } +} diff --git a/internal/repository/cargo_lock.go b/internal/repository/cargo_lock.go new file mode 100644 index 00000000..35616eb3 --- /dev/null +++ b/internal/repository/cargo_lock.go @@ -0,0 +1,88 @@ +package repository + +import ( + "encoding/hex" + "errors" + "fmt" + "strings" + + "github.com/pelletier/go-toml/v2" + "golang.org/x/mod/semver" +) + +const CargoCratesIOSource = "registry+https://github.com/rust-lang/crates.io-index" + +type CargoLock struct { + Path string + Version int + Packages []CargoLockedPackage +} + +type CargoLockedPackage struct { + Name string `toml:"name"` + Version string `toml:"version"` + Source string `toml:"source"` + Checksum string `toml:"checksum"` + Dependencies []string `toml:"dependencies"` +} + +func (repo Repository) ReadCargoLock(path string) (CargoLock, error) { + data, err := repo.ReadContainedFile(path, 8<<20) + if err != nil { + return CargoLock{}, err + } + return ParseCargoLock(path, data) +} + +func ParseCargoLock(path string, data []byte) (CargoLock, error) { + if len(data) > 8<<20 { + return CargoLock{}, errors.New("cargo lockfile exceeds 8388608 bytes") + } + var document struct { + Version int `toml:"version"` + Packages []CargoLockedPackage `toml:"package"` + } + if err := toml.Unmarshal(data, &document); err != nil { + return CargoLock{}, fmt.Errorf("parse Cargo lockfile: %w", err) + } + if document.Version != 3 && document.Version != 4 { + return CargoLock{}, fmt.Errorf("unsupported Cargo lockfile version %d; version 3 or 4 is required", document.Version) + } + if len(document.Packages) == 0 { + return CargoLock{}, errors.New("cargo lockfile contains no package identities") + } + seen := map[[3]string]bool{} + for _, pkg := range document.Packages { + if err := validateCargoLockedPackage(pkg); err != nil { + return CargoLock{}, fmt.Errorf("locked Cargo package %q: %w", pkg.Name, err) + } + identity := [3]string{pkg.Name, pkg.Version, pkg.Source} + if seen[identity] { + return CargoLock{}, fmt.Errorf("cargo lockfile repeats package %q version %s from the same source", pkg.Name, pkg.Version) + } + seen[identity] = true + } + return CargoLock{Path: path, Version: document.Version, Packages: document.Packages}, nil +} + +func validateCargoLockedPackage(pkg CargoLockedPackage) error { + core, _, _ := strings.Cut(pkg.Version, "+") + core, _, _ = strings.Cut(core, "-") + if strings.TrimSpace(pkg.Name) == "" || !semver.IsValid("v"+pkg.Version) || strings.Count(core, ".") != 2 { + return errors.New("package must have a name and complete semantic version") + } + if strings.HasPrefix(pkg.Source, "registry+") { + checksum, err := hex.DecodeString(pkg.Checksum) + if err != nil || len(checksum) != 32 { + return errors.New("registry package must carry its complete SHA-256 archive checksum") + } + } else if pkg.Checksum != "" { + return errors.New("non-registry package cannot claim a registry archive checksum") + } + for _, dependency := range pkg.Dependencies { + if strings.TrimSpace(dependency) == "" { + return errors.New("locked dependency identity cannot be empty") + } + } + return nil +} diff --git a/internal/repository/cargo_lock_test.go b/internal/repository/cargo_lock_test.go new file mode 100644 index 00000000..85573ae1 --- /dev/null +++ b/internal/repository/cargo_lock_test.go @@ -0,0 +1,55 @@ +package repository + +import ( + "strings" + "testing" +) + +func TestCargoLockPreservesPackageSourcesVersionsAndOpaqueEdges(t *testing.T) { + t.Parallel() + data := "version = 4\n[[package]]\nname='app'\nversion='1.0.0'\ndependencies=['renamed 2.1.0 (" + CargoCratesIOSource + ")']\n[[package]]\nname='renamed'\nversion='2.1.0'\nsource='" + CargoCratesIOSource + "'\nchecksum='" + strings.Repeat("a", 64) + "'\n" + lock, err := ParseCargoLock("Cargo.lock", []byte(data)) + if err != nil || lock.Version != 4 || len(lock.Packages) != 2 { + t.Fatalf("lockfile inventory: %+v, %v", lock, err) + } + if lock.Packages[1].Source != CargoCratesIOSource || lock.Packages[1].Version != "2.1.0" || lock.Packages[0].Dependencies[0] != "renamed 2.1.0 ("+CargoCratesIOSource+")" { + t.Fatalf("source or dependency identity was rewritten: %+v", lock.Packages) + } + if _, err := ParseCargoLock("Cargo.lock", []byte(strings.Replace(data, "version = 4", "version = 3", 1))); err != nil { + t.Fatalf("supported version-3 Cargo lockfile rejected: %v", err) + } +} + +func TestCargoLockRejectsIncompleteAndAmbiguousInventories(t *testing.T) { + t.Parallel() + root := "version=4\n[[package]]\nname='app'\nversion='1.0.0'\n" + for name, data := range map[string]string{ + "empty": "version=4\n", "unsupported version": strings.Replace(root, "version=4", "version=5", 1), + "missing name": strings.Replace(root, "name='app'", "", 1), "short version": strings.Replace(root, "1.0.0", "1.0", 1), + "duplicate": root + "[[package]]\nname='app'\nversion='1.0.0'\n", + "missing checksum": root + "source='" + CargoCratesIOSource + "'\n", + "short checksum": root + "source='" + CargoCratesIOSource + "'\nchecksum='abc123'\n", + "local checksum": root + "checksum='" + strings.Repeat("a", 64) + "'\n", + "empty edge": root + "dependencies=['']\n", "malformed edge": root + "dependencies=[1]\n", + "oversized": strings.Repeat(" ", 8<<20+1), + } { + t.Run(name, func(t *testing.T) { + if lock, err := ParseCargoLock("Cargo.lock", []byte(data)); err == nil { + t.Fatalf("invalid lockfile passed: %+v", lock) + } + }) + } +} + +func TestCargoLockAllowsDifferentVersionsAndSourcesOfOnePackage(t *testing.T) { + t.Parallel() + data := "version=4\n" + for _, version := range []string{"1.0.0", "2.0.0", "2.0.0+vendor"} { + data += "[[package]]\nname='same'\nversion='" + version + "'\n" + } + data += "[[package]]\nname='same'\nversion='2.0.0'\nsource='git+https://example.test/repo?branch=main#" + strings.Repeat("a", 40) + "'\n" + lock, err := ParseCargoLock("Cargo.lock", []byte(data)) + if err != nil || len(lock.Packages) != 4 { + t.Fatalf("distinct immutable package identities were collapsed: %+v, %v", lock, err) + } +} diff --git a/internal/repository/cargo_manifest.go b/internal/repository/cargo_manifest.go index 1e4ab20a..fdf55fc5 100644 --- a/internal/repository/cargo_manifest.go +++ b/internal/repository/cargo_manifest.go @@ -10,10 +10,11 @@ import ( ) type CargoManifest struct { - Path string - Root string - Package *CargoPackageManifest - Workspace *CargoWorkspaceManifest + Path string + Root string + Package *CargoPackageManifest + Workspace *CargoWorkspaceManifest + Dependencies []CargoDependencyDeclaration } type CargoPackageManifest struct { @@ -75,25 +76,42 @@ func (repo Repository) readCargoManifest(root *os.Root, path string) (CargoManif if err != nil { return CargoManifest{}, err } - var document struct { - Package *CargoPackageManifest `toml:"package"` - Workspace *CargoWorkspaceManifest `toml:"workspace"` + return ParseCargoManifest(path, data) +} + +func ParseCargoManifest(path string, data []byte) (CargoManifest, error) { + if len(data) > 8<<20 { + return CargoManifest{}, errors.New("cargo manifest exceeds 8388608 bytes") } + var document cargoManifestDocument if err := toml.Unmarshal(data, &document); err != nil { return CargoManifest{}, err } + if err := validateCargoManifestDocument(document); err != nil { + return CargoManifest{}, err + } + dependencies, err := cargoDependencyDeclarations(document) + if err != nil { + return CargoManifest{}, err + } + manifest := CargoManifest{Path: path, Root: filepath.ToSlash(filepath.Dir(path)), Package: document.Package, Dependencies: dependencies} + if document.Workspace != nil { + manifest.Workspace = &document.Workspace.CargoWorkspaceManifest + } + return manifest, nil +} + +func validateCargoManifestDocument(document cargoManifestDocument) error { if document.Package == nil && document.Workspace == nil { - return CargoManifest{}, errors.New("manifest must declare a package or workspace") + return errors.New("manifest must declare a package or workspace") } if document.Package != nil && strings.TrimSpace(document.Package.Name) == "" { - return CargoManifest{}, errors.New("package must declare its name") + return errors.New("package must declare its name") } if document.Package != nil && document.Package.Workspace != "" && document.Workspace != nil { - return CargoManifest{}, errors.New("a package cannot declare both package.workspace and a workspace table") + return errors.New("a package cannot declare both package.workspace and a workspace table") } - return CargoManifest{ - Path: path, Root: filepath.ToSlash(filepath.Dir(path)), Package: document.Package, Workspace: document.Workspace, - }, nil + return nil } func cargoManifestProblem(path string, err error) CargoManifestProblem { diff --git a/internal/supplychain/cargo.go b/internal/supplychain/cargo.go new file mode 100644 index 00000000..060576e4 --- /dev/null +++ b/internal/supplychain/cargo.go @@ -0,0 +1,118 @@ +package supplychain + +import ( + "fmt" + "path/filepath" + "slices" + "strings" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "golang.org/x/mod/semver" +) + +func checkCargoManifest(repo repository.Repository, path string) []policy.Finding { + inventory := repo.InspectCargoManifests([]string{path}) + findings := []policy.Finding{} + for _, problem := range inventory.Problems { + findings = append(findings, policy.Finding{Check: "supplyChain.cargoManifest", Path: problem.Path, Line: problem.Line, Subject: "manifest", Message: problem.Message}) + } + for _, manifest := range inventory.Manifests { + for _, dependency := range manifest.Dependencies { + findings = append(findings, cargoDeclarationFindings(repo, manifest, dependency)...) + } + } + return findings +} + +func cargoDeclarationFindings(repo repository.Repository, manifest repository.CargoManifest, dependency repository.CargoDependencyDeclaration) []policy.Finding { + if dependency.Table == "replace" || strings.HasPrefix(dependency.Table, "patch.") { + return []policy.Finding{cargoDependencyFinding(manifest.Path, dependency, "supplyChain.cargoSource", "Cargo patch/replace declarations require governed override and resolved-source evidence")} + } + if err := cargoDeclarationFields(dependency); err != nil { + return []policy.Finding{cargoDependencyFinding(manifest.Path, dependency, "supplyChain.cargoManifest", err.Error())} + } + if dependency.Workspace { + return nil + } + findings := []policy.Finding{} + if dependency.Version != "" || dependency.Path == "" && dependency.Git == "" { + if !exactCargoRequirement(dependency.Version) { + findings = append(findings, cargoDependencyFinding(manifest.Path, dependency, "supplyChain.exactVersion", "Cargo dependencies must use an exact =major.minor.patch requirement without build metadata; bare versions and ranges permit updates")) + } + } + if err := cargoDeclarationSource(repo, manifest, dependency); err != nil { + findings = append(findings, cargoDependencyFinding(manifest.Path, dependency, "supplyChain.cargoSource", err.Error())) + } + return findings +} + +func cargoDeclarationFields(dependency repository.CargoDependencyDeclaration) error { + allowed := []string{"package", "version", "path", "git", "rev", "branch", "tag", "registry", "workspace", "optional", "features", "default-features"} + if dependency.Workspace { + allowed = []string{"workspace", "optional", "features"} + } + for _, field := range dependency.Fields { + if !slices.Contains(allowed, field) { + return fmt.Errorf("dependency field %s requires supported Cargo source governance", field) + } + } + if dependency.Table == "workspace.dependencies" && (dependency.Workspace || dependency.Optional) { + return fmt.Errorf("workspace dependency definitions cannot be inherited or optional") + } + if dependency.Package == "" { + return fmt.Errorf("dependency package identity cannot be empty") + } + return nil +} + +func exactCargoRequirement(requirement string) bool { + value, exact := strings.CutPrefix(strings.TrimSpace(requirement), "=") + value = strings.TrimSpace(value) + return exact && !strings.Contains(value, "+") && exactVersion.MatchString(value) && semver.IsValid("v"+value) +} + +func cargoDeclarationSource(repo repository.Repository, manifest repository.CargoManifest, dependency repository.CargoDependencyDeclaration) error { + if dependency.Path != "" && dependency.Git != "" { + return fmt.Errorf("a Cargo dependency cannot combine path and Git sources") + } + if dependency.Registry != "" && dependency.Registry != "crates-io" { + return fmt.Errorf("alternate Cargo registry %q requires authenticated provenance, age and security evidence", dependency.Registry) + } + if dependency.Git != "" { + return cargoGitDependency(dependency) + } + if dependency.Revision != "" || dependency.Branch != "" || dependency.Tag != "" { + return fmt.Errorf("cargo revision, branch and tag selectors require a Git source") + } + if dependency.Path != "" { + return cargoLocalDependency(repo, manifest, dependency.Path) + } + return nil +} + +func cargoGitDependency(dependency repository.CargoDependencyDeclaration) error { + if !fullCommit.MatchString(dependency.Revision) || dependency.Branch != "" || dependency.Tag != "" { + return fmt.Errorf("cargo Git dependencies must select a full immutable revision, without branch or tag selectors") + } + if strings.ContainsAny(dependency.Git, "?#") { + return fmt.Errorf("cargo Git URLs must not embed revision selectors or query parameters") + } + _, err := repository.ParsePythonGitSource(dependency.Git, dependency.Revision, "") + return err +} + +func cargoLocalDependency(repo repository.Repository, manifest repository.CargoManifest, directory string) error { + if filepath.IsAbs(directory) || strings.ContainsAny(directory, "\\\x00:") { + return fmt.Errorf("cargo path dependencies must use contained repository-relative paths") + } + path := filepath.ToSlash(filepath.Join(manifest.Root, directory, "Cargo.toml")) + if _, err := repo.ReadContainedFile(path, 8<<20); err != nil { + return fmt.Errorf("cargo path dependency %q is not a contained readable manifest: %w", directory, err) + } + return nil +} + +func cargoDependencyFinding(path string, dependency repository.CargoDependencyDeclaration, check, message string) policy.Finding { + return policy.Finding{Check: check, Path: path, Subject: dependency.Table + ":" + dependency.Alias, Fields: map[string]string{"package": dependency.Package, "table": dependency.Table}, Message: message} +} diff --git a/internal/supplychain/cargo_lock.go b/internal/supplychain/cargo_lock.go new file mode 100644 index 00000000..d44a3abf --- /dev/null +++ b/internal/supplychain/cargo_lock.go @@ -0,0 +1,48 @@ +package supplychain + +import ( + "strings" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" +) + +func checkCargoLock(repo repository.Repository, path string) []policy.Finding { + lock, err := repo.ReadCargoLock(path) + if err != nil { + return []policy.Finding{{Check: "supplyChain.cargoLock", Path: path, Subject: "lockfile", Message: err.Error()}} + } + _, findings := resolvedCargoPackages(lock) + return findings +} + +func resolvedCargoPackages(lock repository.CargoLock) ([]resolvedPackage, []policy.Finding) { + packages := []resolvedPackage{} + findings := []policy.Finding{} + for _, pkg := range lock.Packages { + if pkg.Source == "" { + continue + } + item := resolvedPackage{Ecosystem: "crates.io", Name: pkg.Name, Version: pkg.Version, Scope: lock.Path} + switch { + case pkg.Source == repository.CargoCratesIOSource: + item.Source = resolvedSource{Kind: "registry", Registry: "https://crates.io"} + case strings.HasPrefix(pkg.Source, "git+"): + source, err := repository.ParsePythonGitInventorySource(pkg.Source, "", "") + if err != nil { + findings = append(findings, cargoLockSourceFinding(lock.Path, pkg, err.Error())) + continue + } + item.Ecosystem, item.Source = "git", resolvedSource{Kind: "git", Git: source} + default: + findings = append(findings, cargoLockSourceFinding(lock.Path, pkg, "Cargo lock source requires authenticated registry/source governance; it is not a public crates.io coordinate")) + continue + } + packages = append(packages, item) + } + return packages, findings +} + +func cargoLockSourceFinding(path string, pkg repository.CargoLockedPackage, message string) policy.Finding { + return policy.Finding{Check: "supplyChain.cargoSource", Path: path, Subject: pkg.Name + "@" + pkg.Version, Message: message} +} diff --git a/internal/supplychain/cargo_lock_test.go b/internal/supplychain/cargo_lock_test.go new file mode 100644 index 00000000..33a7db7b --- /dev/null +++ b/internal/supplychain/cargo_lock_test.go @@ -0,0 +1,58 @@ +package supplychain + +import ( + "slices" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" +) + +func TestCargoLockPolicySeparatesRegistryAndGitCoordinates(t *testing.T) { + t.Parallel() + lock := repository.CargoLock{Path: "Cargo.lock", Version: 4, Packages: []repository.CargoLockedPackage{ + {Name: "app", Version: "1.0.0"}, + {Name: "same-name", Version: "1.0.0", Source: repository.CargoCratesIOSource}, + {Name: "same-name", Version: "1.0.0", Source: "git+https://github.com/example/repo?branch=main#" + strings.Repeat("a", 40)}, + }} + packages, findings := resolvedCargoPackages(lock) + if len(findings) != 0 || len(packages) != 2 { + t.Fatalf("resolved source inventory: %+v, %+v", packages, findings) + } + if packages[0].Ecosystem != "crates.io" || packages[0].Source.Kind != "registry" || packages[1].Ecosystem != "git" || packages[1].Source.Git.Commit != strings.Repeat("a", 40) { + t.Fatalf("Git source received a registry identity: %+v", packages) + } +} + +func TestCargoLockPolicyRejectsUnknownSourcesWithoutDroppingValidPackages(t *testing.T) { + t.Parallel() + lock := repository.CargoLock{Path: "Cargo.lock", Packages: []repository.CargoLockedPackage{ + {Name: "public", Version: "1.0.0", Source: repository.CargoCratesIOSource}, + {Name: "private", Version: "1.0.0", Source: "registry+https://private.example/index"}, + {Name: "mutable-git", Version: "1.0.0", Source: "git+https://example.test/repo?branch=main#short"}, + }} + packages, findings := resolvedCargoPackages(lock) + if len(packages) != 1 || packages[0].Name != "public" || len(findings) != 2 { + t.Fatalf("partial source inventory: %+v, %+v", packages, findings) + } +} + +func TestCargoStaticChecksLockChecksumsAndSourceCoverage(t *testing.T) { + t.Parallel() + repo := supplyRepository(t) + for name, source := range map[string]string{ + "missing checksum": repository.CargoCratesIOSource, + "unknown source": "unrecognized+https://example.test/source", + } { + t.Run(name, func(t *testing.T) { + writeSupplyFile(t, repo.Root, "Cargo.lock", "version=4\n[[package]]\nname='crate'\nversion='1.0.0'\nsource='"+source+"'\n") + findings := Static(t.Context(), repo, []string{"Cargo.lock"}) + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return finding.Path == "Cargo.lock" && (finding.Check == "supplyChain.cargoLock" || finding.Check == "supplyChain.cargoSource") + }) { + t.Fatalf("invalid locked source passed static checks: %+v", findings) + } + }) + } +} diff --git a/internal/supplychain/cargo_test.go b/internal/supplychain/cargo_test.go new file mode 100644 index 00000000..f6000613 --- /dev/null +++ b/internal/supplychain/cargo_test.go @@ -0,0 +1,117 @@ +package supplychain + +import ( + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" +) + +func TestCargoStaticPolicyRejectsCompatibleRangesAcrossDependencyKinds(t *testing.T) { + t.Parallel() + repo := supplyRepository(t) + writeSupplyFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\n[dependencies]\nnormal='1.0.0'\n[dev-dependencies]\ntesting='^1.0.0'\n[build-dependencies]\nbuild='~1.0.0'\n[target.'cfg(windows)'.dependencies]\nplatform='*'\n[workspace.dependencies]\ninherited='>=1.0.0'\n") + findings := Static(t.Context(), repo, []string{"Cargo.toml"}) + if countFindings(findings, "supplyChain.exactVersion") != 5 { + t.Fatalf("Cargo version ranges escaped static policy: %+v", findings) + } + for _, dependency := range []string{"normal", "testing", "build", "platform", "inherited"} { + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return finding.Path == "Cargo.toml" && finding.Fields["package"] == dependency && finding.Check == "supplyChain.exactVersion" + }) { + t.Errorf("dependency %s was not attributed to its manifest", dependency) + } + } +} + +func TestCargoStaticPolicyAcceptsExactRenamedAndWorkspaceDeclarations(t *testing.T) { + t.Parallel() + repo := supplyRepository(t) + writeSupplyFile(t, repo.Root, "Cargo.toml", "[workspace]\nmembers=['app']\n[workspace.dependencies]\njson={package='serde_json', version='=1.0.140', features=['raw_value']}\n") + writeSupplyFile(t, repo.Root, "app/Cargo.toml", "[package]\nname='app'\n[dependencies]\njson={workspace=true, optional=true, features=['preserve_order']}\n[build-dependencies]\ncc='= 1.2.0'\n[dev-dependencies]\npreview='=1.0.0-alpha.3'\n") + findings := DependencyReviewStatic(t.Context(), repo, []string{"Cargo.toml", "app/Cargo.toml"}) + if len(findings) != 0 { + t.Fatalf("exact Cargo source declarations rejected: %+v", findings) + } + if _, err := os.Stat(filepath.Join(repo.Root, ".tools")); !os.IsNotExist(err) { + t.Fatalf("static dependency review ran tooling or installed dependencies: %v", err) + } +} + +func TestCargoStaticPolicyRetainsContainedPathDependencies(t *testing.T) { + t.Parallel() + repo := supplyRepository(t) + writeSupplyFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\n[dependencies]\nlocal={path='crates/local'}\n") + writeSupplyFile(t, repo.Root, "crates/local/Cargo.toml", "[package]\nname='local'\n") + if findings := checkCargoManifest(repo, "Cargo.toml"); len(findings) != 0 { + t.Fatalf("contained path dependency rejected: %+v", findings) + } + writeSupplyFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\n[dependencies]\nlocal={path='../outside'}\n") + if findings := checkCargoManifest(repo, "Cargo.toml"); !supplyChecks(findings)["supplyChain.cargoSource"] { + t.Fatalf("escaping path dependency accepted: %+v", findings) + } +} + +func TestCargoStaticPolicyRejectsLinkedPathDependencies(t *testing.T) { + t.Parallel() + repo := supplyRepository(t) + outside := t.TempDir() + writeSupplyFile(t, outside, "Cargo.toml", "[package]\nname='outside'\n") + if err := os.Symlink(outside, filepath.Join(repo.Root, "linked")); err != nil { + t.Skipf("symlinks unavailable: %v", err) + } + writeSupplyFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\n[dependencies]\nlocal={path='linked'}\n") + if findings := checkCargoManifest(repo, "Cargo.toml"); !supplyChecks(findings)["supplyChain.cargoSource"] { + t.Fatalf("linked path dependency accepted: %+v", findings) + } +} + +func TestCargoStaticPolicyRejectsIgnoredAndAmbiguousSourceSelectors(t *testing.T) { + t.Parallel() + for name, declaration := range map[string]string{ + "short exact": "'=1.0'", "metadata": "'=1.0.0+ignored'", "invalid prerelease": "'=1.0.0-01'", + "Git branch": "{git='https://example.test/repo', branch='main'}", "Git short revision": "{git='https://example.test/repo', rev='abc123'}", + "dual source": "{git='https://example.test/repo', path='local'}", "alternate registry": "{version='=1.0.0', registry='private'}", + "inheritance override": "{workspace=true, version='=1.0.0'}", "unknown source": "{version='=1.0.0', registry-index='https://example.test/index'}", + "revision without Git": "{version='=1.0.0', rev='fixed'}", + } { + t.Run(name, func(t *testing.T) { + repo := supplyRepository(t) + writeSupplyFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\n[dependencies]\ncrate="+declaration+"\n") + if findings := checkCargoManifest(repo, "Cargo.toml"); len(findings) == 0 { + t.Fatal("unproven dependency declaration passed") + } + }) + } +} + +func TestCargoGitPinsDoNotUnconditionallyRequireSignedAssessments(t *testing.T) { + t.Parallel() + repo := supplyRepository(t) + writeSupplyFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\n[dependencies]\ncrate={git='https://github.com/example/repo', rev='"+strings.Repeat("a", 40)+"'}\n") + if findings := checkCargoManifest(repo, "Cargo.toml"); len(findings) != 0 { + t.Fatalf("immutable source pin required a separate attestation service: %+v", findings) + } +} + +func TestCargoNativePinsDoNotClaimUnimplementedSupplyCoverage(t *testing.T) { + t.Parallel() + repo := supplyRepository(t) + repo.Config.Scope.Generated = []string{"Cargo.toml"} + writeSupplyFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\n[dependencies]\ncrate='1.0.0'\n") + if findings := Static(t.Context(), repo, []string{"Cargo.toml"}); !supplyChecks(findings)["supplyChain.exactVersion"] { + t.Fatalf("generated classification hid an unpinned dependency: %+v", findings) + } + findings := CoverageFindings(repo, []string{"Cargo.toml"}) + for _, missing := range []string{"lock-sync", "release-age", "security"} { + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { return finding.Subject == "repository:"+missing }) { + t.Errorf("unimplemented %s received native coverage: %+v", missing, findings) + } + } + if slices.ContainsFunc(findings, func(finding policy.Finding) bool { return finding.Subject == "repository:dependency-policy" }) { + t.Fatal("implemented Cargo pin policy still required a custom provider") + } +} diff --git a/internal/supplychain/supplychain.go b/internal/supplychain/supplychain.go index 312a668a..241d9143 100644 --- a/internal/supplychain/supplychain.go +++ b/internal/supplychain/supplychain.go @@ -82,6 +82,10 @@ func staticPathFindings(repo repository.Repository, path string) []policy.Findin return checkGoModule(repo, path) case name == "go.work": return checkGoWorkspace(repo, path) + case name == "Cargo.toml": + return checkCargoManifest(repo, path) + case name == "Cargo.lock": + return checkCargoLock(repo, path) case name == "pyproject.toml": return checkPythonProject(repo, path) case name == "osv-scanner.toml": @@ -89,7 +93,7 @@ func staticPathFindings(repo repository.Repository, path string) []policy.Findin Check: "supplyChain.auditIgnore", Path: path, Subject: "osv-scanner configuration", Message: "repository OSV configuration is forbidden because scanner ignores bypass governed vulnerability assessments", }} - case strings.HasPrefix(path, ".github/workflows/") && (strings.HasSuffix(path, ".yml") || strings.HasSuffix(path, ".yaml")): + case strings.HasPrefix(path, ".github/workflows/") && slices.Contains([]string{".yml", ".yaml"}, filepath.Ext(path)): return checkWorkflowPins(repo, path) case isDockerfile(path): return checkContainerPins(repo, path) @@ -109,6 +113,8 @@ func CoverageFindings(repo repository.Repository, files []string) []policy.Findi findings = append(findings, manifestCapabilityFindings(repo, files, path, ecosystem, []string{"lock-sync", "release-age"})...) case "python-uv": findings = append(findings, manifestCapabilityFindings(repo, files, path, ecosystem, []string{"lock-sync"})...) + case "cargo": + findings = append(findings, manifestCapabilityFindings(repo, files, path, ecosystem, []string{"lock-sync", "release-age", "security"})...) case "unsupported": capabilities := []string{"dependency-policy", "lock-sync", "release-age", "security"} findings = append(findings, manifestCapabilityFindings(repo, files, path, ecosystem, capabilities)...) @@ -151,7 +157,9 @@ func dependencyEcosystem(repo repository.Repository, path string) string { return "unsupported" case "go.mod", "go.work": return "go" - case "Cargo.toml", "Pipfile", "pom.xml", "build.gradle", "build.gradle.kts", "Gemfile", "composer.json", "Package.swift", "pubspec.yaml": + case "Cargo.toml": + return "cargo" + case "Pipfile", "pom.xml", "build.gradle", "build.gradle.kts", "Gemfile", "composer.json", "Package.swift", "pubspec.yaml": return "unsupported" default: if strings.HasPrefix(name, "requirements") && strings.HasSuffix(name, ".txt") { diff --git a/internal/supplychain/supplychain_test.go b/internal/supplychain/supplychain_test.go index 39b2ecbe..bf88eb2e 100644 --- a/internal/supplychain/supplychain_test.go +++ b/internal/supplychain/supplychain_test.go @@ -729,7 +729,6 @@ func TestUnsupportedManifestsRequireCompleteProvidersAtTheirLanguageModule(t *te source string }{ {name: "python requirements", manifest: "requirements-prod.txt", source: "src/main.py"}, - {name: "cargo", manifest: "Cargo.toml", source: "src/lib.rs"}, {name: "maven", manifest: "pom.xml", source: "src/Main.java"}, {name: "gradle", manifest: "build.gradle.kts", source: "src/Main.java"}, {name: "bundler", manifest: "Gemfile", source: "src/main.rb"}, @@ -769,8 +768,8 @@ func TestUnsupportedManifestsRequireCompleteProvidersAtTheirLanguageModule(t *te func TestGeneratedScopeCannotHideDependencyManifest(t *testing.T) { t.Parallel() repo := supplyRepository(t) - repo.Config.Scope.Generated = []string{"Cargo.toml"} - findings := CoverageFindings(repo, []string{"Cargo.toml"}) + repo.Config.Scope.Generated = []string{"Gemfile"} + findings := CoverageFindings(repo, []string{"Gemfile"}) if countFindings(findings, "policy.supplyChainCoverage") != 4 || supplyChecks(findings)["policy.securityMonitoring"] { t.Fatalf("generated scope must not suppress dependency coverage: %+v", findings) } From 901d8fecc8ac0dfb3ce968164f306f7e0b1c5e43 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Thu, 24 Sep 2026 23:53:53 +0000 Subject: [PATCH 07/49] fix: scope Rust minimum versions to checked packages --- docs/design/cargo-facts.md | 5 ++++ docs/plans/code-polishy-rust-support-plan.md | 6 +++-- internal/rustfacts/compilation_test.go | 24 ++++++++++++++++++++ internal/rustfacts/metadata.go | 4 ++-- internal/rustfacts/metadata_test.go | 2 +- 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index 58f46215..1bc9ddc4 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -14,6 +14,11 @@ code nor proves that dependencies or any target configuration have been checked. The selected manifest and Cargo's actual workspace membership determine scope; directory naming is never a package identifier. +Discovery retains each package's minimum Rust version as a fact. Cargo enforces +compiler compatibility when the selected package/feature/target configuration is +checked. An unselected member with a newer minimum version must not prevent +discovery, selected formatting or a compatible default-member build. + Cargo runs from the repository root with a sealed environment, absolute pinned compiler paths and repository-local Cargo home and target output under `.tools`. Ambient rustup selection, wrappers and user Cargo home are not inherited. Ancestor diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index f397186b..9dc632e2 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -133,8 +133,10 @@ remain in progress. - [ ] Turn each acceptance cell into executable evidence, preserving explicit unavailable/incomplete states and original-path diagnostics. - [x] Choose the initial toolchain boundary: Rust 1.97.1 with its matching Cargo, - rustfmt and Clippy; editions 2015, 2018, 2021 and 2024; project minimum Rust - versions may not exceed the governed compiler. Compilation initially uses + rustfmt and Clippy; editions 2015, 2018, 2021 and 2024; minimum Rust versions + in the compiled configuration may not exceed the governed compiler. + Unselected workspace members retain their declared requirements without + blocking compatible selected builds. Compilation initially uses the native host and default features. Additional feature/target selections need explicit analysis evidence. Installation hosts remain Linux/macOS on x64/arm64 and Windows x64; installation does not prove cross-compilation. diff --git a/internal/rustfacts/compilation_test.go b/internal/rustfacts/compilation_test.go index 0aa642f5..953e5b1c 100644 --- a/internal/rustfacts/compilation_test.go +++ b/internal/rustfacts/compilation_test.go @@ -197,6 +197,30 @@ func TestRustCompilationReportsUnavailableNativeLinker(t *testing.T) { } } +func TestRustCompilationScopesMinimumVersionToSelectedPackages(t *testing.T) { + t.Parallel() + for _, selected := range []string{"Cargo.toml", "current/src/lib.rs"} { + t.Run(selected, func(t *testing.T) { + repo := rustRepository(t) + write(t, repo.Root, "Cargo.toml", "[workspace]\nmembers=['current', 'future']\ndefault-members=['current']\nresolver='3'\n") + write(t, repo.Root, "Cargo.lock", "version=4\n[[package]]\nname='current'\nversion='1.0.0'\n[[package]]\nname='future'\nversion='1.0.0'\n") + write(t, repo.Root, "current/Cargo.toml", "[package]\nname='current'\nversion='1.0.0'\nedition='2024'\nrust-version='1.85'\n") + write(t, repo.Root, "current/src/lib.rs", "pub fn current() {}\n") + write(t, repo.Root, "future/Cargo.toml", "[package]\nname='future'\nversion='1.0.0'\nedition='2024'\nrust-version='1.99'\n") + write(t, repo.Root, "future/src/lib.rs", "pub fn future() {}\n") + plan, discovery := compilationDiscovery(t, repo, selected) + if findings := Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}); len(findings) != 0 { + t.Fatalf("unselected workspace MSRV blocked the selected build: %+v", findings) + } + plan, discovery = compilationDiscovery(t, repo, "future/src/lib.rs") + findings := Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}) + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { return strings.Contains(finding.Message, "rustc 1.99") }) { + t.Fatalf("selected incompatible package did not fail: %+v", findings) + } + }) + } +} + func TestRustCompilationRejectsSettingsAndSplitProviderUnits(t *testing.T) { t.Parallel() for name, configure := range map[string]func(repository.Repository) repository.Repository{ diff --git a/internal/rustfacts/metadata.go b/internal/rustfacts/metadata.go index 8d81def8..3c00192b 100644 --- a/internal/rustfacts/metadata.go +++ b/internal/rustfacts/metadata.go @@ -158,8 +158,8 @@ func validatePackage(repo repository.Repository, pkg *Package) error { if !supportedEdition(pkg.Edition) { return fmt.Errorf("unsupported Rust edition %q", pkg.Edition) } - if pkg.RustVersion != "" && (!semver.IsValid("v"+pkg.RustVersion) || semver.Compare("v"+pkg.RustVersion, "v"+repo.ToolPin("rust")) > 0) { - return fmt.Errorf("minimum Rust version %q is not supported by pinned Rust %s", pkg.RustVersion, repo.ToolPin("rust")) + if pkg.RustVersion != "" && !semver.IsValid("v"+pkg.RustVersion) { + return fmt.Errorf("invalid minimum Rust version %q", pkg.RustVersion) } for index := range pkg.Targets { if err := validateTarget(repo, &pkg.Targets[index]); err != nil { diff --git a/internal/rustfacts/metadata_test.go b/internal/rustfacts/metadata_test.go index d483cbba..cf43b1eb 100644 --- a/internal/rustfacts/metadata_test.go +++ b/internal/rustfacts/metadata_test.go @@ -44,7 +44,7 @@ func TestCargoMetadataRejectsIncompleteOrEscapingFacts(t *testing.T) { "relative manifest": func(wire *workspaceWire) { wire.Packages[0].Manifest = "Cargo.toml" }, "missing target": func(wire *workspaceWire) { wire.Packages[0].Targets[0].Path += ".missing" }, "edition": func(wire *workspaceWire) { wire.Packages[0].Edition = "future" }, - "MSRV": func(wire *workspaceWire) { wire.Packages[0].RustVersion = "1.99" }, + "MSRV": func(wire *workspaceWire) { wire.Packages[0].RustVersion = "not-a-version" }, "unknown output": func(wire *workspaceWire) { wire.TargetDir = t.TempDir() }, } { t.Run(name, func(t *testing.T) { From 30dbbfb6aaf729fce5c2719eb6e48b9317badd07 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 00:10:26 +0000 Subject: [PATCH 08/49] feat: verify locked Cargo dependency resolution Use sealed structured Cargo metadata in online and offline supply-chain plans. Preserve workspace, renamed dependency, target, feature and full lock identities without compiling project code or repairing locks. Verification: rustfacts-unit, supplychain-unit and engine-unit passed; scoped policy checks passed with the existing engine file-length warning. Public CLI fixtures exercised valid and stale locks. Rust support remains in progress; registry policy, review diffs, source semantics, tests and distribution remain outstanding. --- .code-polishy.json | 5 + docs/design/cargo-facts.md | 24 +++ docs/design/rust-inputs.md | 5 + docs/plans/code-polishy-rust-support-plan.md | 20 ++- internal/engine/cargo_supplychain_test.go | 67 ++++++++ internal/engine/engine.go | 1 + internal/engine/merge_gate.go | 1 + internal/rustfacts/metadata.go | 24 ++- internal/rustfacts/resolution.go | 95 +++++++++++ internal/rustfacts/resolution_test.go | 152 ++++++++++++++++++ internal/rustfacts/resolved_graph.go | 99 ++++++++++++ internal/rustfacts/resolved_metadata.go | 148 +++++++++++++++++ internal/rustfacts/resolved_metadata_test.go | 115 +++++++++++++ internal/supplychain/cargo_resolution.go | 27 ++++ internal/supplychain/cargo_resolution_test.go | 88 ++++++++++ internal/supplychain/cargo_test.go | 8 +- internal/supplychain/online_profile.go | 6 +- internal/supplychain/supplychain.go | 2 +- 18 files changed, 873 insertions(+), 14 deletions(-) create mode 100644 internal/engine/cargo_supplychain_test.go create mode 100644 internal/rustfacts/resolution.go create mode 100644 internal/rustfacts/resolution_test.go create mode 100644 internal/rustfacts/resolved_graph.go create mode 100644 internal/rustfacts/resolved_metadata.go create mode 100644 internal/rustfacts/resolved_metadata_test.go create mode 100644 internal/supplychain/cargo_resolution.go create mode 100644 internal/supplychain/cargo_resolution_test.go diff --git a/.code-polishy.json b/.code-polishy.json index b8d2845f..afa1709e 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -52,7 +52,11 @@ "internal/supplychain/cargo_test.go", "internal/supplychain/cargo_lock.go", "internal/supplychain/cargo_lock_test.go", + "internal/supplychain/cargo_resolution.go", + "internal/supplychain/cargo_resolution_test.go", + "internal/supplychain/online_profile.go", "internal/supplychain/supplychain.go", + "internal/engine/cargo_supplychain_test.go", "tools/install-rust.sh", "tools/install-rust.ps1", "scripts/test-rust-runtime.sh" @@ -411,6 +415,7 @@ "policy", "repository", "runner", + "rustfacts", "spdx", "workflow" ] diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index 1bc9ddc4..b934679b 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -27,6 +27,30 @@ Repository configuration is parsed before execution; unsupported execution-alter settings produce a diagnostic. This controls discovery inputs, not operating-system access by future build scripts or procedural macros. +## Locked resolution + +Dependency verification uses full version-1 Cargo metadata separately from +workspace-only discovery. Online supply-chain checks and pre-install review use +`--locked`: Cargo may fetch the already locked sources into its governed home, +but cannot update the lock or execute build scripts and procedural macros. +Offline verification uses `--frozen` and requires that cache to be prepared. +Both modes require a contained, parsed workspace lock before resolution runs. +Neither repairs a missing or stale lockfile. + +Resolved facts preserve opaque package IDs, source identities, renamed edges, +dependency kinds, target predicates and enabled features. Every graph node and +edge must refer to a real package, and resolved package coordinates must occur +in the reviewed lock inventory. Cargo owns resolution; this validation does not +reimplement its resolver. Remote manifests and source targets must remain in the +governed Cargo home. Local path packages retain contained checkout paths. + +Metadata uses default features and retains target-specific edges for all +platforms. It is not evidence that every feature or target was compiled. The lock +may also include inactive optional dependencies absent from this resolved graph; +dependency age and advisory inventories must not drop those locked packages. +Commands belong to the existing supply-chain execution plan, including offline +gates, so receipts and cancellation have the same owner as other languages. + ## Selected formatting The pinned rustfmt receives one selected source through stdin, with the edition diff --git a/docs/design/rust-inputs.md b/docs/design/rust-inputs.md index 4d90471f..efe4180c 100644 --- a/docs/design/rust-inputs.md +++ b/docs/design/rust-inputs.md @@ -59,6 +59,11 @@ crates.io identity and immutable Git source encoding. Unknown sources fail explicitly and never become public registry queries. Git packages sharing a name and version with a registry crate remain separate coordinates. Parsing this inventory alone does not prove that it agrees with the selected manifests. +Native lock agreement is established separately through [governed Cargo +resolution](cargo-facts.md#locked-resolution), using the actual workspace lock +and the existing supply-chain runner. Pre-install review may fetch locked source +data; offline verification may not. Age, security and license coverage still +require their respective evidence, not merely successful Cargo resolution. ## Shader inputs diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 9dc632e2..535590a0 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -122,9 +122,17 @@ Cargo dependency-input governance now parses ordinary, build, development, target-specific and workspace declaration tables, checks exact registry and Git pins, contains local paths, and validates versioned lock identities/checksums. It keeps Git and registry coordinates separate and retains explicit failures for -unsupported source substitutions. This is data-only preparation: resolved lock -agreement, dependency-review comparison, age, vulnerability and license integration -remain in progress. +unsupported source substitutions. These declaration and lock readers are +data-only. Dependency-review comparison, age, vulnerability and license +integration remain in progress. + +Native Cargo lock verification now uses bounded, complete resolved metadata through +the ordinary supply-chain runner. Online verification may fetch already locked +sources with `--locked`; offline verification requires prepared sources with +`--frozen`. Neither mode updates the lock or compiles project code. Facts retain +renamed edges, dependency kinds, target predicates and default-feature selection; +inactive optional packages remain in the full lock inventory for supply-chain +policy even when absent from the active graph. ## Phase 0: define the complete coverage contract @@ -182,8 +190,10 @@ remain in progress. - [ ] Reject path escapes and ambiguous ownership. Do not infer package names from directory basenames. Record incomplete dependency metadata as unavailable. - [ ] Keep discovery offline/read-only after controlled dependency preparation; - resolution must not silently refresh a lockfile or download dependencies. - Use frozen resolution after dependency preparation. The data-only bootstrap + normal checks use frozen resolution and never refresh a lockfile or fetch + dependencies. Online supply-chain review may fetch already locked source + data without compiling it; offline verification requires prepared sources. + The data-only bootstrap path must still explain missing locks/dependencies without invoking a build. - [ ] Propagate manifest, lockfile, feature, toolchain, shader and build-input changes to affected modules and evidence invalidation. diff --git a/internal/engine/cargo_supplychain_test.go b/internal/engine/cargo_supplychain_test.go new file mode 100644 index 00000000..3f5df8e5 --- /dev/null +++ b/internal/engine/cargo_supplychain_test.go @@ -0,0 +1,67 @@ +package engine + +import ( + "slices" + "testing" + + "github.com/riteofstring/code-polishy/internal/gaterun" + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" + testpolicy "github.com/riteofstring/code-polishy/internal/testing" +) + +func TestCargoSupplyChainRunsTheGatePlanInBothNetworkModes(t *testing.T) { + t.Parallel() + for _, offline := range []bool{true, false} { + t.Run(map[bool]string{true: "offline", false: "online"}[offline], func(t *testing.T) { + engine := cargoSupplyEngine(t) + level := testpolicy.MergeLevelFull + if offline { + level = testpolicy.MergeLevelRecommended + } + plan, err := engine.appendMergeGateSupplyChainCommands(MergeGateExecutionPlan{Level: level}) + if err != nil { + t.Fatal(err) + } + if len(plan.Commands) != 4 || plan.Commands[3].Category != gaterun.SupplyChain { + t.Fatalf("Cargo gate plan = %+v", plan.Commands) + } + boundary := &mergeGatePlannedRunner{root: engine.Repository.Root, delegate: runner.OSRunner{}, expected: plan.Commands} + engine.Runner = boundary + report, err := engine.SupplyChain(t.Context(), offline) + if err != nil || boundary.next != len(plan.Commands) { + t.Fatalf("Cargo execution differs from the gate plan: next=%d, report=%+v, error=%v", boundary.next, report, err) + } + for _, finding := range report.Findings { + if slices.Contains([]string{"supplyChain.cargoLock", "policy.rustProject", "policy.tool"}, finding.Check) { + t.Errorf("valid Cargo workspace failed supply-chain verification: %+v", finding) + } + } + }) + } +} + +func TestCargoSupplyChainRejectsDriftInThePublicEngine(t *testing.T) { + t.Parallel() + engine := cargoSupplyEngine(t) + writeEngineFile(t, engine.Repository.Root, "Cargo.lock", "version=4\n[[package]]\nname='app'\nversion='0.9.0'\n", 0o600) + report, err := engine.SupplyChain(t.Context(), true) + if err != nil { + t.Fatal(err) + } + if !slices.ContainsFunc(report.Findings, func(finding policy.Finding) bool { + return finding.Check == "supplyChain.cargoLock" && finding.Path == "Cargo.lock" + }) { + t.Fatalf("stale Cargo lock passed public supply-chain verification: %+v", report.Findings) + } +} + +func cargoSupplyEngine(t *testing.T) *Engine { + t.Helper() + root := t.TempDir() + writeEngineFile(t, root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\nedition='2021'\n", 0o600) + writeEngineFile(t, root, "Cargo.lock", "version=4\n[[package]]\nname='app'\nversion='1.0.0'\n", 0o600) + writeEngineFile(t, root, "src/lib.rs", "pub fn app() {}\n", 0o600) + return &Engine{Repository: repository.Repository{Root: root, PolicyRoot: enginePolicyRoot(t)}, Runner: runner.OSRunner{}} +} diff --git a/internal/engine/engine.go b/internal/engine/engine.go index d591c2df..7e219cab 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -643,6 +643,7 @@ func (engine *Engine) supplyChain(ctx context.Context, offline, includeInstalled gitEvidence := []supplychain.GitEvidenceReceipt{} if offline { mode = "offline" + findings = append(findings, supplychain.CheckCargoResolution(ctx, engine.Repository, files, true, engine.Runner)...) findings = append(findings, quality.RunCommands(ctx, engine.Repository, selection, engine.Runner, "supply-chain")...) } if !offline { diff --git a/internal/engine/merge_gate.go b/internal/engine/merge_gate.go index 7c0598c1..b97298c1 100644 --- a/internal/engine/merge_gate.go +++ b/internal/engine/merge_gate.go @@ -340,6 +340,7 @@ func (engine *Engine) appendMergeGateSupplyChainCommands(plan MergeGateExecution return MergeGateExecutionPlan{}, err } if plan.Level != testpolicy.MergeLevelFull { + plan.Commands = append(plan.Commands, mergeGateCheckCommands(gaterun.SupplyChain, supplychain.CargoResolutionCommands(engine.Repository, supplySelection.Files, true))...) plan.Commands = append(plan.Commands, mergeGateCheckCommands(gaterun.SupplyChain, quality.CommandsForProfiles(engine.Repository, supplySelection, "supply-chain"))...) return plan, nil } diff --git a/internal/rustfacts/metadata.go b/internal/rustfacts/metadata.go index 3c00192b..5e6bba68 100644 --- a/internal/rustfacts/metadata.go +++ b/internal/rustfacts/metadata.go @@ -74,6 +74,10 @@ func ParseWorkspace(repo repository.Repository, manifest string, data []byte) (W if err != nil { return Workspace{}, err } + return workspaceFromWire(repo, manifest, wire) +} + +func workspaceFromWire(repo repository.Repository, manifest string, wire workspaceWire) (Workspace, error) { root, err := containedPath(repo, filepath.Join(wire.Root, "Cargo.toml")) if err != nil { return Workspace{}, fmt.Errorf("cargo workspace root: %w", err) @@ -89,6 +93,17 @@ func ParseWorkspace(repo repository.Repository, manifest string, data []byte) (W } func decodeWorkspace(data []byte) (workspaceWire, error) { + wire, err := decodeMetadata(data) + if err != nil { + return workspaceWire{}, err + } + if !bytes.Equal(bytes.TrimSpace(wire.Resolve), []byte("null")) { + return workspaceWire{}, errors.New("workspace-only discovery must not claim a resolved dependency graph") + } + return wire, nil +} + +func decodeMetadata(data []byte) (workspaceWire, error) { if len(data) > 8<<20 { return workspaceWire{}, errors.New("cargo metadata exceeds 8388608 bytes") } @@ -104,9 +119,6 @@ func decodeWorkspace(data []byte) (workspaceWire, error) { if wire.Version != 1 || len(wire.Packages) == 0 || len(wire.Members) == 0 || wire.DefaultMembers == nil { return workspaceWire{}, errors.New("cargo metadata is missing version-1 workspace facts") } - if !bytes.Equal(bytes.TrimSpace(wire.Resolve), []byte("null")) { - return workspaceWire{}, errors.New("workspace-only discovery must not claim a resolved dependency graph") - } return wire, nil } @@ -134,7 +146,11 @@ func validateWorkspace(repo repository.Repository, manifest string, workspace *W } func completeWorkspacePackage(pkg Package) bool { - return pkg.ID != "" && pkg.Name != "" && pkg.Version != "" && pkg.Source == nil && + return pkg.Source == nil && completePackage(pkg) +} + +func completePackage(pkg Package) bool { + return pkg.ID != "" && pkg.Name != "" && pkg.Version != "" && len(pkg.Targets) > 0 && pkg.Features != nil && pkg.Dependencies != nil } diff --git a/internal/rustfacts/resolution.go b/internal/rustfacts/resolution.go new file mode 100644 index 00000000..0729a2a3 --- /dev/null +++ b/internal/rustfacts/resolution.go @@ -0,0 +1,95 @@ +package rustfacts + +import ( + "context" + "fmt" + "path/filepath" + "slices" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" +) + +type ResolutionPlan struct { + Discovery DiscoveryPlan + Commands []policy.Command +} + +type ResolutionResult struct { + Resolutions []Resolution + Findings []policy.Finding +} + +func PlanResolution(repo repository.Repository, files []string, offline bool) ResolutionPlan { + discovery := PlanDiscovery(repo, files, files) + plan := ResolutionPlan{Discovery: discovery, Commands: slices.Clone(discovery.Commands)} + if len(plan.Commands) == 0 { + return plan + } + for _, manifest := range discovery.Manifests { + plan.Commands = append(plan.Commands, resolutionCommand(repo, manifest, offline)) + } + return plan +} + +func resolutionCommand(repo repository.Repository, manifest string, offline bool) policy.Command { + mode := "--frozen" + if !offline { + mode = "--locked" + } + command := Command(repo, "policy-cargo-resolution-"+manifest, "cargo", "metadata", "--format-version", "1", mode, "--manifest-path", manifest) + if !offline { + index := slices.Index(command.EnvironmentOverrides, "CARGO_NET_OFFLINE=true") + command.EnvironmentOverrides[index] = "CARGO_NET_OFFLINE=false" + } + return command +} + +func Resolve(ctx context.Context, repo repository.Repository, plan ResolutionPlan, commandRunner runner.Runner) ResolutionResult { + discovery := Discover(ctx, repo, plan.Discovery, commandRunner) + result := ResolutionResult{Findings: discovery.Findings} + for index, command := range plan.Commands[len(plan.Discovery.Commands):] { + manifest := plan.Discovery.Manifests[index] + workspace := workspaceForManifest(discovery.Workspaces, manifest) + if workspace == nil { + result.Findings = append(result.Findings, resolutionFinding(manifest, "Cargo lock verification requires valid workspace discovery")) + continue + } + resolution, findings := resolveManifest(ctx, repo, manifest, *workspace, command, commandRunner) + result.Findings = append(result.Findings, findings...) + if len(findings) == 0 { + result.Resolutions = append(result.Resolutions, resolution) + } + } + return result +} + +func resolveManifest(ctx context.Context, repo repository.Repository, manifest string, workspace Workspace, command policy.Command, commandRunner runner.Runner) (Resolution, []policy.Finding) { + path := filepath.ToSlash(filepath.Join(workspace.Root, "Cargo.lock")) + lock, err := repo.ReadCargoLock(path) + if err != nil { + return Resolution{}, []policy.Finding{resolutionFinding(path, fmt.Sprintf("Cargo lock verification requires a reviewed, contained lockfile: %v", err))} + } + structured, ok := commandRunner.(runner.StructuredRunner) + if !ok { + return Resolution{}, []policy.Finding{resolutionFinding(manifest, "the runner cannot capture bounded Cargo resolution metadata")} + } + _, output, err := structured.RunStructured(ctx, repo.Root, command) + if err != nil { + return Resolution{}, []policy.Finding{resolutionFinding(path, fmt.Sprintf("Cargo could not verify locked resolution: %v; %s", err, diagnosticOutput(output.Stderr)))} + } + resolution, err := ParseResolution(repo, manifest, output.Stdout) + if err == nil { + err = validateResolutionLock(resolution, lock) + } + if err != nil { + return Resolution{}, []policy.Finding{resolutionFinding(path, fmt.Sprintf("invalid Cargo resolution evidence: %v", err))} + } + resolution.Lock = lock + return resolution, nil +} + +func resolutionFinding(path, message string) policy.Finding { + return policy.Finding{Check: "supplyChain.cargoLock", Path: path, Subject: "resolved-graph", Message: message} +} diff --git a/internal/rustfacts/resolution_test.go b/internal/rustfacts/resolution_test.go new file mode 100644 index 00000000..91bd783d --- /dev/null +++ b/internal/rustfacts/resolution_test.go @@ -0,0 +1,152 @@ +package rustfacts + +import ( + "context" + "os" + "path/filepath" + "reflect" + "slices" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestCargoResolutionPreservesWorkspaceEdgesWithoutExecutingCode(t *testing.T) { + t.Parallel() + for _, offline := range []bool{true, false} { + t.Run(map[bool]string{true: "offline", false: "online"}[offline], func(t *testing.T) { + repo, originalLock := resolutionRepository(t) + plan := PlanResolution(repo, []string{"Cargo.toml"}, offline) + boundary := &resolutionRunner{} + result := Resolve(t.Context(), repo, plan, boundary) + if len(result.Findings) != 0 || len(result.Resolutions) != 1 { + t.Fatalf("resolution = %+v", result) + } + if !reflect.DeepEqual(boundary.commands, plan.Commands) { + t.Fatal("resolution did not execute its exact structured command plan") + } + assertResolutionWorkspace(t, result.Resolutions[0]) + assertResolutionInputs(t, repo, originalLock) + }) + } +} + +func TestCargoResolutionRejectsStaleLocksWithoutRepair(t *testing.T) { + t.Parallel() + repo, originalLock := resolutionRepository(t) + write(t, repo.Root, "utility/Cargo.toml", "[package]\nname='utility'\nversion='2.0.0'\nedition='2021'\n") + result := Resolve(t.Context(), repo, PlanResolution(repo, []string{"Cargo.toml"}, true), runner.OSRunner{}) + if len(result.Resolutions) != 0 || len(result.Findings) != 1 || !strings.Contains(result.Findings[0].Message, "lock file") { + t.Fatalf("stale lock resolution = %+v", result) + } + assertResolutionInputs(t, repo, originalLock) +} + +func TestCargoResolutionRequiresLockAndOfflineSources(t *testing.T) { + t.Parallel() + for _, lock := range []string{"", "version=4\n[[package]]\nname='app'\nversion='1.0.0'\n"} { + t.Run(map[bool]string{true: "missing lock", false: "missing sources"}[lock == ""], func(t *testing.T) { + repo := rustRepository(t) + write(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\nedition='2021'\n[dependencies]\nserde='=1.0.228'\n") + write(t, repo.Root, "src/lib.rs", "pub fn app() {}\n") + if lock != "" { + write(t, repo.Root, "Cargo.lock", lock) + } + result := Resolve(t.Context(), repo, PlanResolution(repo, []string{"Cargo.toml"}, true), runner.OSRunner{}) + if len(result.Resolutions) != 0 || len(result.Findings) != 1 || result.Findings[0].Path != "Cargo.lock" { + t.Fatalf("unprepared resolution = %+v", result) + } + if _, err := os.Stat(filepath.Join(CargoHome(repo), "registry", "src")); !os.IsNotExist(err) { + t.Fatalf("offline resolution materialized registry sources: %v", err) + } + }) + } +} + +func TestCargoResolutionPreservesValidIndependentProjects(t *testing.T) { + t.Parallel() + repo := rustRepository(t) + write(t, repo.Root, "bad/Cargo.toml", "[package\n") + write(t, repo.Root, "good/Cargo.toml", "[package]\nname='good'\nversion='1.0.0'\nedition='2021'\n") + write(t, repo.Root, "good/Cargo.lock", "version=4\n[[package]]\nname='good'\nversion='1.0.0'\n") + write(t, repo.Root, "good/src/lib.rs", "pub fn good() {}\n") + result := Resolve(t.Context(), repo, PlanResolution(repo, []string{"bad/Cargo.toml", "good/Cargo.toml"}, true), runner.OSRunner{}) + if len(result.Findings) != 1 || len(result.Resolutions) != 1 || result.Resolutions[0].Lock.Path != "good/Cargo.lock" { + t.Fatalf("independent resolution = %+v", result) + } +} + +func resolutionRepository(t *testing.T) (repository.Repository, string) { + t.Helper() + repo := rustRepository(t) + write(t, repo.Root, "Cargo.toml", "[workspace]\nmembers=['app','utility','windows-support']\nexclude=['optional']\ndefault-members=['app']\nresolver='3'\n") + write(t, repo.Root, "app/Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\nedition='2021'\n[features]\ndefault=[]\nextra=['dep:optional']\n[dependencies]\nrenamed={package='utility',path='../utility'}\noptional={path='../optional',optional=true}\n[build-dependencies]\nrenamed={package='utility',path='../utility'}\n[target.'cfg(windows)'.dependencies]\nwindows-support={path='../windows-support'}\n") + write(t, repo.Root, "app/src/lib.rs", "compile_error!(\"metadata must not compile project code\");\n") + write(t, repo.Root, "app/build.rs", "fn main() { std::fs::write(\"executed\", \"bad\").unwrap(); panic!(\"must not run\"); }\n") + for _, name := range []string{"utility", "windows-support", "optional"} { + write(t, repo.Root, name+"/Cargo.toml", "[package]\nname='"+name+"'\nversion='1.0.0'\nedition='2021'\n") + write(t, repo.Root, name+"/src/lib.rs", "pub fn value() -> u32 { 42 }\n") + } + lock := "version=4\n[[package]]\nname='app'\nversion='1.0.0'\ndependencies=['optional','utility','windows-support']\n" + for _, name := range []string{"optional", "utility", "windows-support"} { + lock += "[[package]]\nname='" + name + "'\nversion='1.0.0'\n" + } + write(t, repo.Root, "Cargo.lock", lock) + return repo, lock +} + +func assertResolutionWorkspace(t *testing.T, resolution Resolution) { + t.Helper() + if resolution.Graph.Root != nil || len(resolution.Packages) != 3 || len(resolution.Lock.Packages) != 4 { + t.Fatalf("inactive optional dependency confused lock and active graph coverage: %+v", resolution) + } + packageIndex := slices.IndexFunc(resolution.Packages, func(pkg Package) bool { return pkg.Name == "app" }) + nodeIndex := slices.IndexFunc(resolution.Graph.Nodes, func(node ResolvedNode) bool { return node.ID == resolution.Packages[packageIndex].ID }) + node := resolution.Graph.Nodes[nodeIndex] + if len(node.Deps) != 2 || !slices.Equal(node.Features, []string{"default"}) { + t.Fatalf("resolved application = %+v", node) + } + for _, edge := range node.Deps { + switch edge.Name { + case "renamed": + if len(edge.Kinds) != 2 || !slices.Contains(edge.Kinds, ResolvedDependencyKind{Kind: "build"}) { + t.Fatalf("renamed build dependency = %+v", edge) + } + case "windows_support": + if !slices.Equal(edge.Kinds, []ResolvedDependencyKind{{Target: "cfg(windows)"}}) { + t.Fatalf("platform dependency = %+v", edge) + } + default: + t.Fatalf("unexpected edge = %+v", edge) + } + } +} + +func assertResolutionInputs(t *testing.T, repo repository.Repository, originalLock string) { + t.Helper() + lock, err := os.ReadFile(filepath.Join(repo.Root, "Cargo.lock")) + if err != nil || string(lock) != originalLock { + t.Fatalf("resolution changed the reviewed lock: %s, %v", lock, err) + } + for _, path := range []string{"app/executed", "executed", ".tools/rust-target"} { + if _, err := os.Stat(filepath.Join(repo.Root, filepath.FromSlash(path))); !os.IsNotExist(err) { + t.Errorf("resolution compiled or executed project code at %s: %v", path, err) + } + } +} + +type resolutionRunner struct { + commands []policy.Command +} + +func (boundary *resolutionRunner) Run(context.Context, string, policy.Command) error { + panic("Cargo resolution must use bounded structured output") +} + +func (boundary *resolutionRunner) RunStructured(ctx context.Context, root string, command policy.Command) (runner.Result, runner.Output, error) { + boundary.commands = append(boundary.commands, command) + return (runner.OSRunner{}).RunStructured(ctx, root, command) +} diff --git a/internal/rustfacts/resolved_graph.go b/internal/rustfacts/resolved_graph.go new file mode 100644 index 00000000..049a0059 --- /dev/null +++ b/internal/rustfacts/resolved_graph.go @@ -0,0 +1,99 @@ +package rustfacts + +import ( + "errors" + "fmt" + "slices" +) + +func validateResolvedGraph(graph ResolvedGraph, packages []Package, manifest string) error { + ids := map[string]Package{} + root := "" + for _, pkg := range packages { + ids[pkg.ID] = pkg + if pkg.Manifest == manifest { + root = pkg.ID + } + } + if !resolvedRootMatches(graph.Root, root) { + return errors.New("cargo resolve root does not identify the requested manifest") + } + seen := map[string]bool{} + for _, node := range graph.Nodes { + pkg, ok := ids[node.ID] + if !ok || seen[node.ID] { + return fmt.Errorf("cargo resolve graph has an unknown or duplicate node %q", node.ID) + } + seen[node.ID] = true + if err := validateResolvedNode(node, pkg, ids); err != nil { + return fmt.Errorf("cargo resolve node %q: %w", node.ID, err) + } + } + return nil +} + +func resolvedRootMatches(root *string, expected string) bool { + if root == nil { + return expected == "" + } + return *root != "" && *root == expected +} + +func validateResolvedNode(node ResolvedNode, pkg Package, ids map[string]Package) error { + if node.Dependencies == nil || node.Deps == nil || node.Features == nil { + return errors.New("dependency identities, renamed edges and enabled features are required") + } + features := map[string]bool{} + for _, feature := range node.Features { + if _, ok := pkg.Features[feature]; !ok || features[feature] { + return fmt.Errorf("enabled feature %q is unknown or repeated", feature) + } + features[feature] = true + } + dependencies, err := validateResolvedEdges(node.Deps, ids) + if err != nil { + return err + } + declared := slices.Clone(node.Dependencies) + slices.Sort(declared) + if !slices.Equal(declared, dependencies) { + return errors.New("dependency identities and renamed edges disagree") + } + return nil +} + +func validateResolvedEdges(edges []ResolvedDependency, ids map[string]Package) ([]string, error) { + seen := map[[2]string]bool{} + dependencies := map[string]bool{} + for _, edge := range edges { + key := [2]string{edge.Name, edge.Package} + if _, ok := ids[edge.Package]; !ok || edge.Name == "" || seen[key] { + return nil, errors.New("resolved dependency edge has an unknown, empty or repeated identity") + } + seen[key] = true + dependencies[edge.Package] = true + if err := validateResolvedKinds(edge.Kinds); err != nil { + return nil, err + } + } + result := make([]string, 0, len(dependencies)) + for id := range dependencies { + result = append(result, id) + } + slices.Sort(result) + return result, nil +} + +func validateResolvedKinds(kinds []ResolvedDependencyKind) error { + if len(kinds) == 0 { + return errors.New("resolved dependency edge has no dependency kind") + } + seen := map[ResolvedDependencyKind]bool{} + for _, kind := range kinds { + if !slices.Contains([]string{"", "dev", "build"}, kind.Kind) || seen[kind] { + return errors.New("resolved dependency edge has an unsupported or repeated kind") + } + seen[kind] = true + } + return nil +} diff --git a/internal/rustfacts/resolved_metadata.go b/internal/rustfacts/resolved_metadata.go new file mode 100644 index 00000000..ebf1a944 --- /dev/null +++ b/internal/rustfacts/resolved_metadata.go @@ -0,0 +1,148 @@ +package rustfacts + +import ( + "encoding/json" + "errors" + "fmt" + "path/filepath" + "slices" + "strings" + + "github.com/riteofstring/code-polishy/internal/repository" +) + +type Resolution struct { + Manifest string + Workspace Workspace + Packages []Package + Graph ResolvedGraph + Lock repository.CargoLock +} + +type ResolvedGraph struct { + Root *string `json:"root"` + Nodes []ResolvedNode `json:"nodes"` +} + +type ResolvedNode struct { + ID string `json:"id"` + Dependencies []string `json:"dependencies"` + Deps []ResolvedDependency `json:"deps"` + Features []string `json:"features"` +} + +type ResolvedDependency struct { + Name string `json:"name"` + Package string `json:"pkg"` + Kinds []ResolvedDependencyKind `json:"dep_kinds"` +} + +type ResolvedDependencyKind struct { + Kind string `json:"kind"` + Target string `json:"target"` +} + +func ParseResolution(repo repository.Repository, manifest string, data []byte) (Resolution, error) { + wire, err := decodeMetadata(data) + if err != nil { + return Resolution{}, err + } + workspaceWire := wire + workspaceWire.Packages = slices.DeleteFunc(slices.Clone(wire.Packages), func(pkg Package) bool { return !slices.Contains(wire.Members, pkg.ID) }) + workspace, err := workspaceFromWire(repo, manifest, workspaceWire) + if err != nil { + return Resolution{}, err + } + packages, err := resolvedPackages(repo, wire.Packages, workspace) + if err != nil { + return Resolution{}, err + } + graph, err := parseResolvedGraph(wire.Resolve, packages, manifest) + if err != nil { + return Resolution{}, err + } + return Resolution{Manifest: manifest, Workspace: workspace, Packages: packages, Graph: graph}, nil +} + +func resolvedPackages(repo repository.Repository, packages []Package, workspace Workspace) ([]Package, error) { + members := map[string]Package{} + for _, pkg := range workspace.Packages { + members[pkg.ID] = pkg + } + seen := map[string]bool{} + for index := range packages { + pkg := &packages[index] + if seen[pkg.ID] || !completePackage(*pkg) { + return nil, fmt.Errorf("resolved Cargo package %q has incomplete or duplicate identity", pkg.ID) + } + seen[pkg.ID] = true + if member, ok := members[pkg.ID]; ok { + *pkg = member + continue + } + if err := validatePackage(repo, pkg); err != nil { + return nil, fmt.Errorf("resolved Cargo package %q: %w", pkg.ID, err) + } + if err := resolvedSourcePaths(*pkg); err != nil { + return nil, err + } + } + return packages, nil +} + +func resolvedSourcePaths(pkg Package) error { + if pkg.Source == nil { + return nil + } + if *pkg.Source == "" || !strings.HasPrefix(pkg.Manifest, ".tools/cargo-home/") { + return fmt.Errorf("resolved remote Cargo package %q is outside the governed Cargo home or has no source identity", pkg.ID) + } + for _, target := range pkg.Targets { + if !strings.HasPrefix(target.Path, ".tools/cargo-home/") { + return fmt.Errorf("resolved remote Cargo target %q is outside the governed Cargo home", target.Path) + } + } + return nil +} + +func validateResolutionLock(resolution Resolution, lock repository.CargoLock) error { + identities := map[[3]string]bool{} + for _, pkg := range lock.Packages { + identities[[3]string{pkg.Name, pkg.Version, pkg.Source}] = true + } + for _, pkg := range resolution.Packages { + source := "" + if pkg.Source != nil { + source = *pkg.Source + } + if !identities[[3]string{pkg.Name, pkg.Version, source}] { + return fmt.Errorf("resolved Cargo package %q is absent from the reviewed lock inventory", pkg.ID) + } + } + expected := filepath.ToSlash(filepath.Join(resolution.Workspace.Root, "Cargo.lock")) + if lock.Path != expected { + return errors.New("cargo resolution does not describe the reviewed workspace lock") + } + return nil +} + +func parseResolvedGraph(data []byte, packages []Package, manifest string) (ResolvedGraph, error) { + var wire struct { + Root json.RawMessage `json:"root"` + Nodes []ResolvedNode `json:"nodes"` + } + if err := json.Unmarshal(data, &wire); err != nil { + return ResolvedGraph{}, fmt.Errorf("invalid Cargo resolve graph: %w", err) + } + if len(wire.Root) == 0 || len(wire.Nodes) != len(packages) { + return ResolvedGraph{}, errors.New("cargo resolve graph must describe every resolved package and the current root") + } + graph := ResolvedGraph{Nodes: wire.Nodes} + if err := json.Unmarshal(wire.Root, &graph.Root); err != nil { + return ResolvedGraph{}, fmt.Errorf("invalid Cargo resolve root: %w", err) + } + if err := validateResolvedGraph(graph, packages, manifest); err != nil { + return ResolvedGraph{}, err + } + return graph, nil +} diff --git a/internal/rustfacts/resolved_metadata_test.go b/internal/rustfacts/resolved_metadata_test.go new file mode 100644 index 00000000..6ee2a8e5 --- /dev/null +++ b/internal/rustfacts/resolved_metadata_test.go @@ -0,0 +1,115 @@ +package rustfacts + +import ( + "encoding/json" + "path/filepath" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/repository" +) + +func TestResolvedCargoMetadataRetainsSourceAndRenamedIdentity(t *testing.T) { + t.Parallel() + repo, wire, graph := resolvedMetadataFixture(t) + resolution, err := ParseResolution(repo, "Cargo.toml", resolvedMetadataJSON(t, wire, graph)) + if err != nil { + t.Fatal(err) + } + dependency := resolution.Packages[1] + edge := resolution.Graph.Nodes[0].Deps[0] + if dependency.ID != "opaque::dependency!id" || dependency.Source == nil || *dependency.Source != repository.CargoCratesIOSource || edge.Package != dependency.ID || edge.Name != "renamed" { + t.Fatalf("resolved identity = %+v, %+v", dependency, edge) + } + lock := repository.CargoLock{Path: "Cargo.lock", Packages: []repository.CargoLockedPackage{ + {Name: "app", Version: "1.0.0"}, {Name: "dep", Version: "2.0.0", Source: repository.CargoCratesIOSource}, + }} + if err := validateResolutionLock(resolution, lock); err != nil { + t.Fatal(err) + } + lock.Packages[1].Source = "git+https://example.test/dep#" + strings.Repeat("a", 40) + if err := validateResolutionLock(resolution, lock); err == nil { + t.Fatal("a same-version package from a different source satisfied locked identity") + } +} + +func TestResolvedCargoMetadataRejectsIncompleteGraphEvidence(t *testing.T) { + t.Parallel() + for name, mutate := range map[string]func(*workspaceWire, *ResolvedGraph){ + "no nodes": func(_ *workspaceWire, graph *ResolvedGraph) { graph.Nodes = nil }, + "unknown node": func(_ *workspaceWire, graph *ResolvedGraph) { graph.Nodes[0].ID = "foreign" }, + "duplicate node": func(_ *workspaceWire, graph *ResolvedGraph) { graph.Nodes[1] = graph.Nodes[0] }, + "unknown edge": func(_ *workspaceWire, graph *ResolvedGraph) { graph.Nodes[0].Deps[0].Package = "foreign" }, + "missing renamed edges": func(_ *workspaceWire, graph *ResolvedGraph) { graph.Nodes[0].Deps = nil }, + "disagreeing edges": func(_ *workspaceWire, graph *ResolvedGraph) { graph.Nodes[0].Dependencies = []string{} }, + "no dependency kinds": func(_ *workspaceWire, graph *ResolvedGraph) { graph.Nodes[0].Deps[0].Kinds = nil }, + "unsupported kind": func(_ *workspaceWire, graph *ResolvedGraph) { graph.Nodes[0].Deps[0].Kinds[0].Kind = "unknown" }, + "unknown feature": func(_ *workspaceWire, graph *ResolvedGraph) { graph.Nodes[0].Features = []string{"unknown"} }, + "missing features": func(_ *workspaceWire, graph *ResolvedGraph) { graph.Nodes[0].Features = nil }, + "incorrect virtual root": func(_ *workspaceWire, graph *ResolvedGraph) { graph.Root = nil }, + "duplicate package": func(wire *workspaceWire, _ *ResolvedGraph) { wire.Packages = append(wire.Packages, wire.Packages[1]) }, + "registry pretending to be local": func(wire *workspaceWire, _ *ResolvedGraph) { wire.Packages[1].Manifest = wire.Packages[0].Manifest }, + "registry target in checkout": func(wire *workspaceWire, _ *ResolvedGraph) { + wire.Packages[1].Targets[0].Path = wire.Packages[0].Targets[0].Path + }, + "empty source": func(wire *workspaceWire, _ *ResolvedGraph) { *wire.Packages[1].Source = "" }, + "empty root": func(_ *workspaceWire, graph *ResolvedGraph) { *graph.Root = "" }, + } { + t.Run(name, func(t *testing.T) { + repo, wire, graph := resolvedMetadataFixture(t) + mutate(&wire, &graph) + if _, err := ParseResolution(repo, "Cargo.toml", resolvedMetadataJSON(t, wire, graph)); err == nil { + t.Fatal("incomplete resolution was accepted") + } + }) + } +} + +func TestResolvedCargoMetadataRequiresExplicitResolution(t *testing.T) { + t.Parallel() + for _, graph := range []string{"null", `{}`, `{"nodes":[]}`, `{"root":"","nodes":[]}`} { + repo, wire, _ := resolvedMetadataFixture(t) + wire.Resolve = json.RawMessage(graph) + data, err := json.Marshal(wire) + if err != nil { + t.Fatal(err) + } + if _, err := ParseResolution(repo, "Cargo.toml", data); err == nil { + t.Fatalf("missing resolution accepted: %s", graph) + } + } +} + +func resolvedMetadataFixture(t *testing.T) (repository.Repository, workspaceWire, ResolvedGraph) { + t.Helper() + repo, wire := metadataFixture(t) + directory := ".tools/cargo-home/registry/src/source-id/dep-2.0.0" + write(t, repo.Root, directory+"/Cargo.toml", "[package]\nname='dep'\nversion='2.0.0'\n") + write(t, repo.Root, directory+"/src/lib.rs", "pub fn value() {}\n") + source := repository.CargoCratesIOSource + dependency := Package{ID: "opaque::dependency!id", Name: "dep", Version: "2.0.0", Source: &source, + Manifest: filepath.Join(repo.Root, directory, "Cargo.toml"), Edition: "2021", Dependencies: []Dependency{}, Features: map[string][]string{}, + Targets: []Target{{Name: "dep", Kinds: []string{"lib"}, CrateTypes: []string{"lib"}, Path: filepath.Join(repo.Root, directory, "src/lib.rs"), Edition: "2021"}}, + } + wire.Packages = append(wire.Packages, dependency) + id := wire.Packages[0].ID + graph := ResolvedGraph{Root: &id, Nodes: []ResolvedNode{ + {ID: id, Dependencies: []string{dependency.ID}, Features: []string{}, Deps: []ResolvedDependency{{Name: "renamed", Package: dependency.ID, Kinds: []ResolvedDependencyKind{{}}}}}, + {ID: dependency.ID, Dependencies: []string{}, Features: []string{}, Deps: []ResolvedDependency{}}, + }} + return repo, wire, graph +} + +func resolvedMetadataJSON(t *testing.T, wire workspaceWire, graph ResolvedGraph) []byte { + t.Helper() + data, err := json.Marshal(graph) + if err != nil { + t.Fatal(err) + } + wire.Resolve = data + data, err = json.Marshal(wire) + if err != nil { + t.Fatal(err) + } + return data +} diff --git a/internal/supplychain/cargo_resolution.go b/internal/supplychain/cargo_resolution.go new file mode 100644 index 00000000..6ce498b9 --- /dev/null +++ b/internal/supplychain/cargo_resolution.go @@ -0,0 +1,27 @@ +package supplychain + +import ( + "context" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" + "github.com/riteofstring/code-polishy/internal/rustfacts" +) + +func CargoResolutionCommands(repo repository.Repository, files []string, offline bool) []policy.Command { + return rustfacts.PlanResolution(repo, files, offline).Commands +} + +func CheckCargoResolution(ctx context.Context, repo repository.Repository, files []string, offline bool, commandRunner runner.Runner) []policy.Finding { + plan := rustfacts.PlanResolution(repo, files, offline) + return rustfacts.Resolve(ctx, repo, plan, commandRunner).Findings +} + +func onlineCargoFindings(ctx context.Context, repo repository.Repository, files []string, commands []policy.Command, commandRunner runner.Runner, findings []policy.Finding, index int) ([]policy.Finding, int) { + plan := rustfacts.PlanResolution(repo, files, false) + count := len(plan.Commands) + plan.Commands = commands[index : index+count] + result := rustfacts.Resolve(ctx, repo, plan, commandRunner) + return append(findings, result.Findings...), index + count +} diff --git a/internal/supplychain/cargo_resolution_test.go b/internal/supplychain/cargo_resolution_test.go new file mode 100644 index 00000000..28606ed3 --- /dev/null +++ b/internal/supplychain/cargo_resolution_test.go @@ -0,0 +1,88 @@ +package supplychain + +import ( + "context" + "path/filepath" + "reflect" + "slices" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestCargoSupplyChainResolvesThroughItsExactCommandPlan(t *testing.T) { + t.Parallel() + repo := cargoSupplyRepository(t) + commands, err := OnlineCommands(repo, []string{"Cargo.toml", "Cargo.lock"}) + if err != nil { + t.Fatal(err) + } + boundary := &cargoSupplyRunner{} + findings := OnlineWithCommands(t.Context(), repo, []string{"Cargo.toml", "Cargo.lock"}, commands, boundary) + if len(findings) != 0 || len(boundary.commands) != 4 || !reflect.DeepEqual(commands, boundary.commands) { + t.Fatalf("Cargo online supply-chain = %+v, commands = %+v", findings, boundary.commands) + } + command := commands[len(commands)-1] + if !slices.Contains(command.Argv, "--locked") || !command.SealedEnvironment || !slices.Contains(command.EnvironmentOverrides, "CARGO_NET_OFFLINE=false") { + t.Fatalf("online lock verification = %+v", command) + } + boundary.commands = nil + findings = CheckCargoResolution(t.Context(), repo, []string{"Cargo.toml", "Cargo.lock"}, true, boundary) + if len(findings) != 0 || len(boundary.commands) != 4 || !slices.Contains(boundary.commands[3].Argv, "--frozen") { + t.Fatalf("Cargo offline supply-chain = %+v, commands = %+v", findings, boundary.commands) + } +} + +func TestCargoSupplyChainRejectsAlteredExecutionEnvironments(t *testing.T) { + t.Parallel() + for name, mutate := range map[string]func(*policy.Command){ + "unsealed": func(command *policy.Command) { command.SealedEnvironment = false }, + "compiler override": func(command *policy.Command) { + command.EnvironmentOverrides = append(command.EnvironmentOverrides, "RUSTC=untrusted") + }, + } { + t.Run(name, func(t *testing.T) { + repo := cargoSupplyRepository(t) + commands, err := OnlineCommands(repo, []string{"Cargo.toml"}) + if err != nil { + t.Fatal(err) + } + mutate(&commands[len(commands)-1]) + boundary := &cargoSupplyRunner{} + findings := OnlineWithCommands(t.Context(), repo, []string{"Cargo.toml"}, commands, boundary) + if len(boundary.commands) != 0 || len(findings) != 1 || !strings.Contains(findings[0].Message, "differs from policy-derived") { + t.Fatalf("altered resolution plan accepted: %+v", findings) + } + }) + } +} + +func cargoSupplyRepository(t *testing.T) repository.Repository { + t.Helper() + repo := supplyRepository(t) + root, err := filepath.Abs("../..") + if err != nil { + t.Fatal(err) + } + repo.PolicyRoot = root + writeSupplyFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\nedition='2021'\n") + writeSupplyFile(t, repo.Root, "Cargo.lock", "version=4\n[[package]]\nname='app'\nversion='1.0.0'\n") + writeSupplyFile(t, repo.Root, "src/lib.rs", "pub fn app() {}\n") + return repo +} + +type cargoSupplyRunner struct { + commands []policy.Command +} + +func (boundary *cargoSupplyRunner) Run(context.Context, string, policy.Command) error { + panic("Cargo must use the structured execution boundary") +} + +func (boundary *cargoSupplyRunner) RunStructured(ctx context.Context, root string, command policy.Command) (runner.Result, runner.Output, error) { + boundary.commands = append(boundary.commands, command) + return (runner.OSRunner{}).RunStructured(ctx, root, command) +} diff --git a/internal/supplychain/cargo_test.go b/internal/supplychain/cargo_test.go index f6000613..e1f71fdc 100644 --- a/internal/supplychain/cargo_test.go +++ b/internal/supplychain/cargo_test.go @@ -106,12 +106,14 @@ func TestCargoNativePinsDoNotClaimUnimplementedSupplyCoverage(t *testing.T) { t.Fatalf("generated classification hid an unpinned dependency: %+v", findings) } findings := CoverageFindings(repo, []string{"Cargo.toml"}) - for _, missing := range []string{"lock-sync", "release-age", "security"} { + for _, missing := range []string{"release-age", "security"} { if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { return finding.Subject == "repository:"+missing }) { t.Errorf("unimplemented %s received native coverage: %+v", missing, findings) } } - if slices.ContainsFunc(findings, func(finding policy.Finding) bool { return finding.Subject == "repository:dependency-policy" }) { - t.Fatal("implemented Cargo pin policy still required a custom provider") + if slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return slices.Contains([]string{"repository:dependency-policy", "repository:lock-sync"}, finding.Subject) + }) { + t.Fatal("implemented Cargo pin or lock policy still required a custom provider") } } diff --git a/internal/supplychain/online_profile.go b/internal/supplychain/online_profile.go index 594bd922..00e2a2b8 100644 --- a/internal/supplychain/online_profile.go +++ b/internal/supplychain/online_profile.go @@ -27,6 +27,7 @@ func OnlineCommands(repo repository.Repository, files []string) ([]policy.Comman for _, module := range repo.GoModules(files) { commands = append(commands, goOnlineCommands(repo, module)...) } + commands = append(commands, CargoResolutionCommands(repo, files, false)...) auditCommands, err := pnpmAuditCommands(repo, files) if err != nil { return nil, err @@ -52,6 +53,7 @@ func OnlineWithCommands(ctx context.Context, repo repository.Repository, files [ } findings := releaseArtifactAgeFindings(ctx, repo, registryClient(), time.Now().UTC()) findings, commandIndex := onlineGoModuleFindings(ctx, repo, files, commands, commandRunner, findings) + findings, commandIndex = onlineCargoFindings(ctx, repo, files, commands, commandRunner, findings, commandIndex) findings, commandIndex = onlineNodeFindings(ctx, repo, files, commands, commandRunner, findings, commandIndex) findings = append(findings, onlinePythonFindings(ctx, repo)...) if activePolicyModule(repo.Config, "osv") { @@ -135,13 +137,15 @@ func sameOnlineCommand(left, right policy.Command) bool { func sameOnlineCommandIdentity(left, right policy.Command) bool { return left.Name == right.Name && left.Cwd == right.Cwd && left.TimeoutSeconds == right.TimeoutSeconds && - left.Managed == right.Managed && left.PassFiles == right.PassFiles && left.ReportProtocol == right.ReportProtocol + left.Managed == right.Managed && left.PassFiles == right.PassFiles && left.ReportProtocol == right.ReportProtocol && + left.SealedEnvironment == right.SealedEnvironment } func sameOnlineCommandCollections(left, right policy.Command) bool { return slices.Equal(left.Argv, right.Argv) && slices.Equal(left.Provides, right.Provides) && slices.Equal(left.Paths, right.Paths) && slices.Equal(left.Modules, right.Modules) && slices.Equal(left.RunOn, right.RunOn) && slices.Equal(left.Environment, right.Environment) && + slices.Equal(left.EnvironmentOverrides, right.EnvironmentOverrides) && slices.Equal(left.ExclusiveResources, right.ExclusiveResources) && slices.Equal(left.PassFilePaths, right.PassFilePaths) } diff --git a/internal/supplychain/supplychain.go b/internal/supplychain/supplychain.go index 241d9143..b29a6e3a 100644 --- a/internal/supplychain/supplychain.go +++ b/internal/supplychain/supplychain.go @@ -114,7 +114,7 @@ func CoverageFindings(repo repository.Repository, files []string) []policy.Findi case "python-uv": findings = append(findings, manifestCapabilityFindings(repo, files, path, ecosystem, []string{"lock-sync"})...) case "cargo": - findings = append(findings, manifestCapabilityFindings(repo, files, path, ecosystem, []string{"lock-sync", "release-age", "security"})...) + findings = append(findings, manifestCapabilityFindings(repo, files, path, ecosystem, []string{"release-age", "security"})...) case "unsupported": capabilities := []string{"dependency-policy", "lock-sync", "release-age", "security"} findings = append(findings, manifestCapabilityFindings(repo, files, path, ecosystem, capabilities)...) From b9fa9d953af2c5fb2ceaa78c0b4f6f8978f63e16 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 00:27:35 +0000 Subject: [PATCH 09/49] feat: enforce crates.io minimum release age Inspect the complete Cargo lock inventory, reuse authoritative workspace locks, and batch exact version metadata through a cancellation-aware one-request-per-second client. Preserve optional Git warnings, fail required unsupported Cargo attestations explicitly, and never query private sources at crates.io. Verification: policy-unit and supplychain-unit passed; focused Cargo engine tests and scoped policy checks passed. Public offline CLI fixture retains its remaining security-coverage finding. Dependency-review diffs, preferred-age advisories, signed Cargo Git inventory, advisory/license policy, source semantics, structured Rust tests and distribution remain in progress. --- .code-polishy.json | 7 + docs/design/rust-inputs.md | 27 +++ docs/plans/code-polishy-rust-support-plan.md | 11 +- internal/policy/cargo_release_age_test.go | 30 ++++ internal/policy/supplychain_config.go | 3 + internal/supplychain/cargo_registry.go | 37 ++++ internal/supplychain/cargo_registry_test.go | 98 ++++++++++ internal/supplychain/cargo_release_age.go | 133 ++++++++++++++ .../supplychain/cargo_release_age_test.go | 168 ++++++++++++++++++ internal/supplychain/cargo_resolution.go | 8 +- internal/supplychain/cargo_test.go | 4 +- internal/supplychain/release_age.go | 5 + internal/supplychain/supplychain.go | 2 +- schema/code-polishy-supply-chain.schema.json | 4 +- 14 files changed, 530 insertions(+), 7 deletions(-) create mode 100644 internal/policy/cargo_release_age_test.go create mode 100644 internal/supplychain/cargo_registry.go create mode 100644 internal/supplychain/cargo_registry_test.go create mode 100644 internal/supplychain/cargo_release_age.go create mode 100644 internal/supplychain/cargo_release_age_test.go diff --git a/.code-polishy.json b/.code-polishy.json index afa1709e..70d80826 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -32,6 +32,8 @@ "sourcePaths": [ "internal/policy/data.go", "internal/policy/rust_inputs_test.go", + "internal/policy/supplychain_config.go", + "internal/policy/cargo_release_age_test.go", "internal/repository/cargo_manifest.go", "internal/repository/cargo_manifest_test.go", "internal/repository/cargo_dependencies.go", @@ -54,6 +56,11 @@ "internal/supplychain/cargo_lock_test.go", "internal/supplychain/cargo_resolution.go", "internal/supplychain/cargo_resolution_test.go", + "internal/supplychain/cargo_release_age.go", + "internal/supplychain/cargo_release_age_test.go", + "internal/supplychain/cargo_registry.go", + "internal/supplychain/cargo_registry_test.go", + "internal/supplychain/release_age.go", "internal/supplychain/online_profile.go", "internal/supplychain/supplychain.go", "internal/engine/cargo_supplychain_test.go", diff --git a/docs/design/rust-inputs.md b/docs/design/rust-inputs.md index efe4180c..575e8760 100644 --- a/docs/design/rust-inputs.md +++ b/docs/design/rust-inputs.md @@ -65,6 +65,33 @@ and the existing supply-chain runner. Pre-install review may fetch locked source data; offline verification may not. Age, security and license coverage still require their respective evidence, not merely successful Cargo resolution. +## Registry release age + +Online age checks cover every public crates.io package in the complete Cargo +lock, including inactive optional and platform-specific dependencies. Requests +include the authoritative workspace locks returned by Cargo even when a selected +file list only names member manifests. Existing resolved lock facts are reused +within the operation; independently inventoried locks are read through the same +contained boundary. Requests batch exact versions by crate across lock scopes +and use crates.io's publication +timestamps. The sparse index does not provide those timestamps; downloading the +entire registry database is disproportionate for a project-sized inventory. +The [crates.io API](https://crates.io/api/openapi.json) supplies this evidence. + +The client follows the registry's [data-access policy](https://crates.io/data-access): +identified requests, at most one request per second within the process, bounded +responses and no metadata redirects. Waiting callers honor cancellation without +reserving future request slots. Missing timestamps, unexpected identities, +malformed responses and request failures remain missing evidence, never age passes. +The existing exact release-age assessment policy supports the `crates.io` +ecosystem and the actual Cargo.lock scope. It cannot fill missing observations. + +Git, local paths and alternate registries never become public crates.io queries. +Optional Git assessment policy retains its visible coverage warning; required +Cargo Git evidence currently fails explicitly until the signed inventory protocol +supports Cargo locks. Registry age evidence does not establish vulnerability or +license coverage and is not collected by the offline profile. + ## Shader inputs WGSL is executable shader source with its own built-in language identifier, diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 535590a0..11e34715 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -123,8 +123,8 @@ target-specific and workspace declaration tables, checks exact registry and Git pins, contains local paths, and validates versioned lock identities/checksums. It keeps Git and registry coordinates separate and retains explicit failures for unsupported source substitutions. These declaration and lock readers are -data-only. Dependency-review comparison, age, vulnerability and license -integration remain in progress. +data-only. Dependency-review comparison, complete Git evidence, vulnerability and +license integration remain in progress. Native Cargo lock verification now uses bounded, complete resolved metadata through the ordinary supply-chain runner. Online verification may fetch already locked @@ -134,6 +134,13 @@ renamed edges, dependency kinds, target predicates and default-feature selection inactive optional packages remain in the full lock inventory for supply-chain policy even when absent from the active graph. +Public crates.io minimum-age checks cover complete lock inventories, batch exact +versions by crate, and follow the registry's identified one-request-per-second +API limit. Missing metadata cannot pass or be filled by an assessment. Existing +exact release-age assessments now support `crates.io` and Cargo.lock scopes; +Git/private sources are not sent to the public registry. New-dependency preferred-age +advisories and signed Cargo Git inventory support remain unfinished. + ## Phase 0: define the complete coverage contract - [x] Audit current Go/Python owners and Rust gaps against the recorded base; diff --git a/internal/policy/cargo_release_age_test.go b/internal/policy/cargo_release_age_test.go new file mode 100644 index 00000000..5065e637 --- /dev/null +++ b/internal/policy/cargo_release_age_test.go @@ -0,0 +1,30 @@ +package policy + +import ( + "strings" + "testing" + "time" +) + +func TestCargoReleaseAgeAssessmentUsesExactRegistryVersions(t *testing.T) { + t.Parallel() + now := time.Now().UTC() + supply := `"supplyChain":{"releaseAgeAssessments":[{ + "id":"cargo-fix","ecosystem":"crates.io","package":"example","version":"1.2.3","scope":"Cargo.lock", + "category":"security-fix","evidence":"https://example.test/advisory","reason":"reachable security fix","owner":"security", + "reviewed":"` + now.Format("2006-01-02") + `","expires":"` + now.AddDate(0, 0, 5).Format("2006-01-02") + `" + }]}` + configured := strings.Replace(minimalConfig(), `"supplyChain":{}`, supply, 1) + for _, version := range []string{"1.2.3", "1.2.3+native", "1.2.3-beta.1"} { + candidate := strings.Replace(configured, `"version":"1.2.3"`, `"version":"`+version+`"`, 1) + if _, err := Load(writeConfig(t, candidate), ""); err != nil { + t.Fatalf("exact Cargo version %s rejected: %v", version, err) + } + } + for _, version := range []string{"=1.2.3", "^1.2.3", "1.2", "1.2.3-01"} { + candidate := strings.Replace(configured, `"version":"1.2.3"`, `"version":"`+version+`"`, 1) + if _, err := Load(writeConfig(t, candidate), ""); err == nil { + t.Fatalf("inexact Cargo assessment version %s accepted", version) + } + } +} diff --git a/internal/policy/supplychain_config.go b/internal/policy/supplychain_config.go index 8cf2c447..310f32b2 100644 --- a/internal/policy/supplychain_config.go +++ b/internal/policy/supplychain_config.go @@ -9,6 +9,7 @@ import ( "time" "github.com/riteofstring/code-polishy/internal/spdx" + "golang.org/x/mod/semver" ) var ( @@ -285,6 +286,8 @@ func exactReleaseAssessmentVersion(ecosystem, version string) bool { return goReleaseVersionPattern.MatchString(version) case "npm", "pnpm": return nodeReleaseVersionPattern.MatchString(version) + case "crates.io": + return nodeReleaseVersionPattern.MatchString(version) && semver.IsValid("v"+version) case "pypi": return pypiReleaseVersionPattern.MatchString(version) default: diff --git a/internal/supplychain/cargo_registry.go b/internal/supplychain/cargo_registry.go new file mode 100644 index 00000000..1787d676 --- /dev/null +++ b/internal/supplychain/cargo_registry.go @@ -0,0 +1,37 @@ +package supplychain + +import ( + "net/http" + "time" +) + +var cargoMetadataRequests = cargoRequestLimit{gate: make(chan struct{}, 1)} + +type cargoRequestLimit struct { + gate chan struct{} + next time.Time +} + +func (limit *cargoRequestLimit) do(client *http.Client, request *http.Request) (*http.Response, error) { + ctx := request.Context() + select { + case limit.gate <- struct{}{}: + case <-ctx.Done(): + return nil, ctx.Err() + } + defer func() { <-limit.gate }() + timer := time.NewTimer(time.Until(limit.next)) + defer timer.Stop() + select { + case <-timer.C: + case <-ctx.Done(): + return nil, ctx.Err() + } + if err := ctx.Err(); err != nil { + return nil, err + } + limit.next = time.Now().Add(time.Second) + bounded := *client + bounded.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } + return bounded.Do(request) +} diff --git a/internal/supplychain/cargo_registry_test.go b/internal/supplychain/cargo_registry_test.go new file mode 100644 index 00000000..a02ae9b0 --- /dev/null +++ b/internal/supplychain/cargo_registry_test.go @@ -0,0 +1,98 @@ +package supplychain + +import ( + "context" + "errors" + "io" + "net/http" + "strings" + "testing" + "time" +) + +func TestCargoRegistryRequestsRemainSpacedAfterCancellation(t *testing.T) { + t.Parallel() + limit := cargoRequestLimit{gate: make(chan struct{}, 1)} + transport := &cargoRequestTransport{} + client := &http.Client{Transport: transport} + request, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "https://crates.io/api/v1/crates/example/versions", nil) + if err != nil { + t.Fatal(err) + } + response, err := limit.do(client, request) + if err != nil { + t.Fatal(err) + } + response.Body.Close() + ctx, cancel := context.WithCancel(t.Context()) + cancel() + if _, err := limit.do(client, request.Clone(ctx)); !errors.Is(err, context.Canceled) { + t.Fatalf("canceled request = %v", err) + } + response, err = limit.do(client, request) + if err != nil { + t.Fatal(err) + } + response.Body.Close() + if len(transport.starts) != 2 || transport.starts[1].Sub(transport.starts[0]) < 990*time.Millisecond { + t.Fatalf("crate requests exceeded one per second or cancellation sent a request: %v", transport.starts) + } +} + +func TestCargoRegistryCancellationReleasesQueuedCallers(t *testing.T) { + t.Parallel() + for _, phase := range []string{"queue", "rate wait"} { + t.Run(phase, func(t *testing.T) { + limit := cargoRequestLimit{gate: make(chan struct{}, 1), next: time.Now().Add(time.Hour)} + if phase == "queue" { + limit.gate <- struct{}{} + } + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond) + defer cancel() + request, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://crates.io/api/v1/crates/example/versions", nil) + if err != nil { + t.Fatal(err) + } + transport := &cargoRequestTransport{} + if _, err := limit.do(&http.Client{Transport: transport}, request); !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("blocked caller ignored cancellation: %v", err) + } + if len(transport.starts) != 0 { + t.Fatal("canceled queued caller reached the registry") + } + }) + } +} + +func TestCargoRegistryDoesNotFollowMetadataRedirects(t *testing.T) { + t.Parallel() + limit := cargoRequestLimit{gate: make(chan struct{}, 1)} + transport := &cargoRequestTransport{redirect: true} + request, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "https://crates.io/api/v1/crates/example/versions", nil) + if err != nil { + t.Fatal(err) + } + response, err := limit.do(&http.Client{Transport: transport}, request) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusFound || len(transport.starts) != 1 { + t.Fatalf("foreign metadata redirect was followed: %d, %v", response.StatusCode, transport.starts) + } +} + +type cargoRequestTransport struct { + starts []time.Time + redirect bool +} + +func (transport *cargoRequestTransport) RoundTrip(*http.Request) (*http.Response, error) { + transport.starts = append(transport.starts, time.Now()) + response := &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{}`)), Header: http.Header{}} + if transport.redirect { + response.StatusCode = http.StatusFound + response.Header.Set("Location", "https://foreign.example.test/metadata") + } + return response, nil +} diff --git a/internal/supplychain/cargo_release_age.go b/internal/supplychain/cargo_release_age.go new file mode 100644 index 00000000..619a8685 --- /dev/null +++ b/internal/supplychain/cargo_release_age.go @@ -0,0 +1,133 @@ +package supplychain + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "maps" + "net/http" + "net/url" + "path/filepath" + "slices" + "strings" + "time" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" +) + +func cargoReleaseAgeFindings(ctx context.Context, repo repository.Repository, files []string, resolved []repository.CargoLock) []policy.Finding { + locks, findings := cargoAgeLocks(repo, files, resolved) + packages := []resolvedPackage{} + for _, lock := range locks { + locked, problems := resolvedCargoPackages(lock) + findings = append(findings, problems...) + if gitLockPackages(locked) { + findings = append(findings, cargoGitAgeFindings(repo, lock.Path, locked)...) + } + packages = append(packages, registryReleasePackages(locked)...) + } + return append(findings, releaseAgeFindings(ctx, repo, packages, time.Now().UTC())...) +} + +func cargoAgeLocks(repo repository.Repository, files []string, resolved []repository.CargoLock) ([]repository.CargoLock, []policy.Finding) { + locks := map[string]repository.CargoLock{} + for _, lock := range resolved { + locks[lock.Path] = lock + } + findings := []policy.Finding{} + for _, path := range files { + if _, exists := locks[path]; exists || filepath.Base(path) != "Cargo.lock" { + continue + } + lock, err := repo.ReadCargoLock(path) + if err != nil { + findings = append(findings, policy.Finding{Check: "supplyChain.releaseAge", Path: path, Subject: "cargo:locked-inventory", Message: err.Error()}) + continue + } + locks[path] = lock + } + ordered := make([]repository.CargoLock, 0, len(locks)) + for _, path := range slices.Sorted(maps.Keys(locks)) { + ordered = append(ordered, locks[path]) + } + return ordered, findings +} + +func cargoGitAgeFindings(repo repository.Repository, path string, packages []resolvedPackage) []policy.Finding { + if repo.Config.SupplyChain.GitEvidence.Required { + return []policy.Finding{{Check: "supplyChain.gitEvidence", Path: path, Subject: "cargo-source", Message: "Required Cargo Git assessments need Cargo lock inventory support; a Python lock attestation cannot establish evidence for this Cargo lockfile"}} + } + findings, _ := verifyGitLockEvidence(repo, path, packages, time.Now().UTC()) + return findings +} + +func lookupCargoReleaseGroup(ctx context.Context, client *http.Client, group releaseGroup) (map[string]time.Time, error) { + query := url.Values{} + versions := []string{} + for _, item := range group.Items { + versions = append(versions, item.Version) + } + if len(versions) == 0 { + return nil, fmt.Errorf("crate release lookup requires exact requested versions") + } + slices.Sort(versions) + for _, version := range slices.Compact(versions) { + query.Add("nums[]", version) + } + endpoint := "https://crates.io/api/v1/crates/" + url.PathEscape(group.Name) + "/versions?" + query.Encode() + if len(endpoint) > 8192 { + return nil, fmt.Errorf("crate release metadata request exceeds 8192 bytes") + } + request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + request.Header.Set("User-Agent", "Code-Polishy (https://github.com/riteofstring/code-polishy)") + response, err := cargoMetadataRequests.do(client, request) + if err != nil { + return nil, err + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return nil, fmt.Errorf("crate release metadata request failed with HTTP %d", response.StatusCode) + } + return decodeCargoReleaseTimes(response.Body, group.Name) +} + +func decodeCargoReleaseTimes(reader io.Reader, name string) (map[string]time.Time, error) { + data, err := io.ReadAll(io.LimitReader(reader, 8<<20+1)) + if err != nil { + return nil, err + } + if len(data) > 8<<20 { + return nil, fmt.Errorf("crate release metadata exceeds 8388608 bytes") + } + var payload struct { + Versions []struct { + Name string `json:"crate"` + Version string `json:"num"` + Released time.Time `json:"created_at"` + } `json:"versions"` + } + decoder := json.NewDecoder(bytes.NewReader(data)) + if err := decoder.Decode(&payload); err != nil { + return nil, err + } + if err := requireOneJSONValue(decoder); err != nil { + return nil, err + } + times := map[string]time.Time{} + for _, version := range payload.Versions { + if !strings.EqualFold(version.Name, name) || version.Version == "" || version.Released.IsZero() { + return nil, fmt.Errorf("crate release metadata omitted exact package identity or publication time") + } + if _, exists := times[version.Version]; exists { + return nil, fmt.Errorf("crate release metadata repeats version %q", version.Version) + } + times[version.Version] = version.Released + } + return times, nil +} diff --git a/internal/supplychain/cargo_release_age_test.go b/internal/supplychain/cargo_release_age_test.go new file mode 100644 index 00000000..d7af74e2 --- /dev/null +++ b/internal/supplychain/cargo_release_age_test.go @@ -0,0 +1,168 @@ +package supplychain + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "slices" + "strings" + "testing" + "time" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" +) + +func TestCargoReleaseAgeCoversTheWholeLockAndRetainsExactAssessments(t *testing.T) { + repo := supplyRepository(t) + repo.Config.SupplyChain.MinimumReleaseAgeDays = 30 + now := time.Now().UTC() + transport := &cargoAgeTransport{released: map[string]time.Time{"1.0.0": now.AddDate(0, 0, -60), "2.0.0": now.AddDate(0, 0, -5)}} + installCargoAgeTransport(t, transport) + lock := "version=4\n" + for _, version := range []string{"1.0.0", "2.0.0"} { + lock += fmt.Sprintf("[[package]]\nname='crate-name'\nversion=%q\nsource=%q\nchecksum=%q\n", version, "registry+https://github.com/rust-lang/crates.io-index", strings.Repeat("a", 64)) + } + writeSupplyFile(t, repo.Root, "Cargo.lock", lock) + writeSupplyFile(t, repo.Root, "nested/Cargo.lock", lock) + findings := cargoReleaseAgeFindings(t.Context(), repo, []string{"Cargo.lock", "nested/Cargo.lock"}, nil) + if len(findings) != 2 || len(transport.requests) != 1 || !slices.Equal(transport.requests[0].URL.Query()["nums[]"], []string{"1.0.0", "2.0.0"}) { + t.Fatalf("locked release coverage = %+v, requests = %+v", findings, transport.requests) + } + for _, finding := range findings { + if finding.ReleaseAge == nil || finding.ReleaseAge.Ecosystem != "crates.io" || finding.ReleaseAge.Version != "2.0.0" { + t.Fatalf("young crate lost exact release identity: %+v", finding) + } + } + assessment := policy.ReleaseAgeAssessment{ID: "security-fix", Ecosystem: "crates.io", Package: "crate-name", Version: "2.0.0", Scope: "Cargo.lock", Expires: policy.Date{Time: now.AddDate(0, 0, 5)}} + kept, accepted := policy.ApplyReleaseAgeAssessments(findings, []policy.ReleaseAgeAssessment{assessment}, now, true) + if len(accepted) != 1 || len(kept) != 1 || kept[0].Path != "nested/Cargo.lock" { + t.Fatalf("exact scope assessment leaked across Cargo locks: kept=%+v, accepted=%+v", kept, accepted) + } +} + +func TestCargoReleaseAgeDoesNotSendGitOrPrivateSourcesToTheRegistry(t *testing.T) { + repo := supplyRepository(t) + transport := &cargoAgeTransport{released: map[string]time.Time{"1.0.0": time.Now().UTC().AddDate(0, 0, -90)}} + installCargoAgeTransport(t, transport) + lock := fmt.Sprintf("version=4\n[[package]]\nname='crate-name'\nversion='1.0.0'\nsource=%q\nchecksum=%q\n", "registry+https://github.com/rust-lang/crates.io-index", strings.Repeat("a", 64)) + lock += fmt.Sprintf("[[package]]\nname='private-git'\nversion='1.0.0'\nsource='git+https://private.example.test/repo?rev=%s#%s'\n", strings.Repeat("b", 40), strings.Repeat("b", 40)) + lock += fmt.Sprintf("[[package]]\nname='private-registry'\nversion='1.0.0'\nsource='registry+https://private.example.test/index'\nchecksum=%q\n", strings.Repeat("c", 64)) + writeSupplyFile(t, repo.Root, "Cargo.lock", lock) + findings := cargoReleaseAgeFindings(t.Context(), repo, []string{"Cargo.lock"}, nil) + if len(transport.requests) != 1 || len(findings) != 2 || !supplyChecks(findings)["supplyChain.cargoSource"] { + t.Fatalf("mixed-source release checks = %+v, requests = %+v", findings, transport.requests) + } + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return finding.Check == "supplyChain.gitSourceCoverage" && finding.Severity == policy.FindingWarning + }) { + t.Fatalf("optional Git policy changed: %+v", findings) + } + repo.Config.SupplyChain.GitEvidence.Required = true + findings = cargoReleaseAgeFindings(t.Context(), repo, []string{"Cargo.lock"}, nil) + if !supplyChecks(findings)["supplyChain.gitEvidence"] { + t.Fatalf("required Git evidence was silently accepted: %+v", findings) + } +} + +func TestCargoReleaseAgeMissingMetadataCannotBeWaived(t *testing.T) { + repo := supplyRepository(t) + transport := &cargoAgeTransport{released: map[string]time.Time{}} + installCargoAgeTransport(t, transport) + packages := []resolvedPackage{{Ecosystem: "crates.io", Name: "crate-name", Version: "1.0.0", Scope: "Cargo.lock"}} + findings := releaseAgeFindings(t.Context(), repo, packages, time.Now().UTC()) + if len(findings) != 1 || findings[0].ReleaseAge != nil || !strings.Contains(findings[0].Message, "omitted the exact timestamp") { + t.Fatalf("missing metadata claimed age coverage: %+v", findings) + } + assessment := policy.ReleaseAgeAssessment{ID: "incomplete", Ecosystem: "crates.io", Package: "crate-name", Version: "1.0.0", Scope: "Cargo.lock", Expires: policy.Date{Time: time.Now().UTC().AddDate(0, 0, 1)}} + kept, accepted := policy.ApplyReleaseAgeAssessments(findings, []policy.ReleaseAgeAssessment{assessment}, time.Now().UTC(), true) + if len(accepted) != 0 || len(kept) != 1 { + t.Fatalf("an assessment filled missing registry evidence: %+v, %+v", kept, accepted) + } +} + +func TestCargoReleaseAgeRunsInTheOnlineSupplyChainProfile(t *testing.T) { + repo := supplyRepository(t) + transport := &cargoAgeTransport{released: map[string]time.Time{"1.0.0": time.Now().UTC().AddDate(0, 0, -1)}} + installCargoAgeTransport(t, transport) + lock := fmt.Sprintf("version=4\n[[package]]\nname='crate-name'\nversion='1.0.0'\nsource=%q\nchecksum=%q\n", "registry+https://github.com/rust-lang/crates.io-index", strings.Repeat("a", 64)) + writeSupplyFile(t, repo.Root, "Cargo.lock", lock) + findings := Online(t.Context(), repo, []string{"Cargo.lock"}, &cargoSupplyRunner{}) + if len(findings) != 1 || findings[0].ReleaseAge == nil || len(transport.requests) != 1 { + t.Fatalf("online profile omitted Cargo age policy: %+v, requests=%+v", findings, transport.requests) + } +} + +func TestCargoReleaseAgeIncludesWorkspaceLocksBeyondTheSelectedFiles(t *testing.T) { + repo := supplyRepository(t) + transport := &cargoAgeTransport{released: map[string]time.Time{"1.0.0": time.Now().UTC().AddDate(0, 0, -1)}} + installCargoAgeTransport(t, transport) + lock := repository.CargoLock{Path: "workspace/Cargo.lock", Packages: []repository.CargoLockedPackage{ + {Name: "crate-name", Version: "1.0.0", Source: repository.CargoCratesIOSource}, + }} + findings := cargoReleaseAgeFindings(t.Context(), repo, []string{"workspace/app/Cargo.toml"}, []repository.CargoLock{lock}) + if len(findings) != 1 || findings[0].Path != lock.Path || findings[0].ReleaseAge == nil || len(transport.requests) != 1 { + t.Fatalf("workspace lock outside selected files escaped age checks: %+v", findings) + } +} + +func TestCargoReleaseMetadataRejectsWrongAndIncompleteIdentity(t *testing.T) { + t.Parallel() + for _, body := range []string{ + `{"versions":[{"crate":"different","num":"1.0.0","created_at":"2026-01-01T00:00:00Z"}]}`, + `{"versions":[{"crate":"crate-name","num":"1.0.0"}]}`, + `{"versions":[{"crate":"crate-name","num":"1.0.0","created_at":"invalid"}]}`, + `{"versions":[{"crate":"crate-name","num":"1.0.0","created_at":"2026-01-01T00:00:00Z"},{"crate":"crate-name","num":"1.0.0","created_at":"2026-01-01T00:00:00Z"}]}`, + `{"versions":[]} {}`, + `{"versions":[]}` + strings.Repeat(" ", 8<<20), + } { + if _, err := decodeCargoReleaseTimes(strings.NewReader(body), "crate-name"); err == nil { + t.Fatal("invalid registry evidence passed") + } + } +} + +func installCargoAgeTransport(t *testing.T, transport http.RoundTripper) { + t.Helper() + previous := http.DefaultTransport + http.DefaultTransport = transport + t.Cleanup(func() { http.DefaultTransport = previous }) +} + +type cargoAgeTransport struct { + released map[string]time.Time + requests []*http.Request +} + +func (transport *cargoAgeTransport) RoundTrip(request *http.Request) (*http.Response, error) { + if request.URL.Scheme != "https" || request.URL.Host != "crates.io" || request.URL.Path != "/api/v1/crates/crate-name/versions" { + return nil, fmt.Errorf("unexpected public request: %s", request.URL.Redacted()) + } + if request.UserAgent() != "Code-Polishy (https://github.com/riteofstring/code-polishy)" { + return nil, fmt.Errorf("crate request did not identify Code Polishy") + } + transport.requests = append(transport.requests, request) + versions := []map[string]any{} + for _, version := range request.URL.Query()["nums[]"] { + if released, exists := transport.released[version]; exists { + versions = append(versions, map[string]any{"crate": "crate-name", "num": version, "created_at": released}) + } + } + data, err := json.Marshal(map[string]any{"versions": versions}) + if err != nil { + return nil, err + } + return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(string(data))), Header: http.Header{}}, nil +} + +func TestCargoReleaseLookupHonorsCanceledRequests(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(t.Context()) + cancel() + _, err := lookupCargoReleaseGroup(ctx, &http.Client{Transport: &cargoAgeTransport{}}, releaseGroup{Kind: "crates.io", Name: "crate-name", Items: []resolvedPackage{{Version: "1.0.0"}}}) + if err == nil { + t.Fatal("canceled registry lookup succeeded") + } +} diff --git a/internal/supplychain/cargo_resolution.go b/internal/supplychain/cargo_resolution.go index 6ce498b9..a93bda4d 100644 --- a/internal/supplychain/cargo_resolution.go +++ b/internal/supplychain/cargo_resolution.go @@ -23,5 +23,11 @@ func onlineCargoFindings(ctx context.Context, repo repository.Repository, files count := len(plan.Commands) plan.Commands = commands[index : index+count] result := rustfacts.Resolve(ctx, repo, plan, commandRunner) - return append(findings, result.Findings...), index + count + findings = append(findings, result.Findings...) + locks := []repository.CargoLock{} + for _, resolution := range result.Resolutions { + locks = append(locks, resolution.Lock) + } + findings = append(findings, cargoReleaseAgeFindings(ctx, repo, files, locks)...) + return findings, index + count } diff --git a/internal/supplychain/cargo_test.go b/internal/supplychain/cargo_test.go index e1f71fdc..9cc1ebc6 100644 --- a/internal/supplychain/cargo_test.go +++ b/internal/supplychain/cargo_test.go @@ -106,13 +106,13 @@ func TestCargoNativePinsDoNotClaimUnimplementedSupplyCoverage(t *testing.T) { t.Fatalf("generated classification hid an unpinned dependency: %+v", findings) } findings := CoverageFindings(repo, []string{"Cargo.toml"}) - for _, missing := range []string{"release-age", "security"} { + for _, missing := range []string{"security"} { if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { return finding.Subject == "repository:"+missing }) { t.Errorf("unimplemented %s received native coverage: %+v", missing, findings) } } if slices.ContainsFunc(findings, func(finding policy.Finding) bool { - return slices.Contains([]string{"repository:dependency-policy", "repository:lock-sync"}, finding.Subject) + return slices.Contains([]string{"repository:dependency-policy", "repository:lock-sync", "repository:release-age"}, finding.Subject) }) { t.Fatal("implemented Cargo pin or lock policy still required a custom provider") } diff --git a/internal/supplychain/release_age.go b/internal/supplychain/release_age.go index 54161818..9ef3965f 100644 --- a/internal/supplychain/release_age.go +++ b/internal/supplychain/release_age.go @@ -188,6 +188,9 @@ func groupReleasePackages(packages []resolvedPackage) []releaseGroup { if item.Ecosystem == "pypi" { kind = "pypi" } + if item.Ecosystem == "crates.io" { + kind = "crates.io" + } key := kind + "\x00" + strings.ToLower(item.Name) if groups[key] == nil { groups[key] = &releaseGroup{Kind: kind, Name: item.Name} @@ -213,6 +216,8 @@ func lookupReleaseGroup(ctx context.Context, client *http.Client, repo repositor endpoint = strings.TrimSuffix(repo.Config.SupplyChain.NPMRegistryURL, "/") + "/" + url.PathEscape(group.Name) case "pypi": endpoint = "https://pypi.org/pypi/" + url.PathEscape(group.Name) + "/json" + case "crates.io": + return lookupCargoReleaseGroup(ctx, client, group) default: return nil, fmt.Errorf("unsupported release metadata ecosystem %q", group.Kind) } diff --git a/internal/supplychain/supplychain.go b/internal/supplychain/supplychain.go index b29a6e3a..0119474b 100644 --- a/internal/supplychain/supplychain.go +++ b/internal/supplychain/supplychain.go @@ -114,7 +114,7 @@ func CoverageFindings(repo repository.Repository, files []string) []policy.Findi case "python-uv": findings = append(findings, manifestCapabilityFindings(repo, files, path, ecosystem, []string{"lock-sync"})...) case "cargo": - findings = append(findings, manifestCapabilityFindings(repo, files, path, ecosystem, []string{"release-age", "security"})...) + findings = append(findings, manifestCapabilityFindings(repo, files, path, ecosystem, []string{"security"})...) case "unsupported": capabilities := []string{"dependency-policy", "lock-sync", "release-age", "security"} findings = append(findings, manifestCapabilityFindings(repo, files, path, ecosystem, capabilities)...) diff --git a/schema/code-polishy-supply-chain.schema.json b/schema/code-polishy-supply-chain.schema.json index e469a271..cbddf729 100644 --- a/schema/code-polishy-supply-chain.schema.json +++ b/schema/code-polishy-supply-chain.schema.json @@ -208,7 +208,9 @@ "required": ["id", "ecosystem", "package", "version"], "properties": { "id": { "$ref": "code-polishy.schema.json#/$defs/identifier" }, - "ecosystem": { "enum": ["artifact", "go", "npm", "pnpm", "pypi"] }, + "ecosystem": { + "enum": ["artifact", "go", "npm", "pnpm", "pypi", "crates.io"] + }, "package": { "$ref": "code-polishy.schema.json#/$defs/nonemptyString" }, "version": { "$ref": "code-polishy.schema.json#/$defs/nonemptyString" }, "scope": { "$ref": "code-polishy.schema.json#/$defs/concretePath" }, From aa42356c54bd2c06056505447bc641f6114c55a2 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 00:54:11 +0000 Subject: [PATCH 10/49] feat: compare Cargo dependency declarations and locks --- .code-polishy.json | 9 +- docs/design/historical-review-policy.md | 30 ++++ docs/design/rust-inputs.md | 9 +- docs/plans/code-polishy-rust-support-plan.md | 17 +- internal/repository/cargo_git.go | 50 ++++++ internal/repository/cargo_git_test.go | 65 +++++++ internal/repository/cargo_lock.go | 2 +- internal/repository/cargo_lock_test.go | 13 ++ internal/supplychain/cargo_lock.go | 2 +- internal/supplychain/cargo_review.go | 108 ++++++++++++ internal/supplychain/cargo_review_lock.go | 55 ++++++ internal/supplychain/cargo_review_test.go | 169 +++++++++++++++++++ internal/supplychain/review.go | 15 +- internal/supplychain/review_inventory.go | 2 +- 14 files changed, 534 insertions(+), 12 deletions(-) create mode 100644 internal/repository/cargo_git.go create mode 100644 internal/repository/cargo_git_test.go create mode 100644 internal/supplychain/cargo_review.go create mode 100644 internal/supplychain/cargo_review_lock.go create mode 100644 internal/supplychain/cargo_review_test.go diff --git a/.code-polishy.json b/.code-polishy.json index 70d80826..b55668e9 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -38,6 +38,8 @@ "internal/repository/cargo_manifest_test.go", "internal/repository/cargo_dependencies.go", "internal/repository/cargo_dependencies_test.go", + "internal/repository/cargo_git.go", + "internal/repository/cargo_git_test.go", "internal/repository/cargo_lock.go", "internal/repository/cargo_lock_test.go", "internal/repository/rust_tools.go", @@ -208,7 +210,12 @@ "sourcePaths": [ "internal/policy/review_snapshot.go", "internal/engine/behavior_review_decision.go", - "internal/engine/merge_gate.go" + "internal/engine/merge_gate.go", + "internal/supplychain/review.go", + "internal/supplychain/review_inventory.go", + "internal/supplychain/cargo_review.go", + "internal/supplychain/cargo_review_lock.go", + "internal/supplychain/cargo_review_test.go" ] }, { diff --git a/docs/design/historical-review-policy.md b/docs/design/historical-review-policy.md index 106a9950..dd375063 100644 --- a/docs/design/historical-review-policy.md +++ b/docs/design/historical-review-policy.md @@ -25,3 +25,33 @@ base revision. An absent configuration selects first-adoption behavior; malformed or unsupported historical evidence fails comparison. Required base features and their suites survive removal from the candidate, so policy edits cannot silently remove the review obligations being evaluated. + +## Cargo dependency comparison + +Dependency review reads Cargo declarations and lock records through the shared +bounded inventory reader, including immutable historical Git blobs. It never +runs Cargo, installs dependencies or executes historical project code. Current +pin/source policy remains separate: a historical branch or private registry can +be compared and removed without being admitted in the candidate. Unsafe and +ambiguous history fails comparison without copying credential-bearing values +into errors. + +Cargo bare versions are compatible ranges, so the review represents `1.2.3` as +`^1.2.3`; only an exact `=1.2.3` declaration becomes an exact version. Renamed +packages retain their declared package identity. Runtime, optional, development +and build usage remain distinct. Workspace definitions and override declarations +have separate review roles. An inherited dependency is a `cargo-workspace` +reference, including an explicit workspace path when present; it is not assigned +a guessed registry version or package identity. Cargo owns effective workspace +resolution. + +Lock rows have the `resolved` role rather than being called transitive merely +because their direct declaration is inherited or elsewhere. These rows retain +each lock scope even when another manifest already declares that version. Git +coordinates preserve the observed commit separately from an optional declared +ref. Private registry rows retain their exact registry identity, including the +sparse protocol, and cannot select public crates.io age queries. Historical +non-HTTPS registries can be compared for removal without becoming authenticated +or admitted sources. Exact new public runtime and optional declarations +use the shared preferred-age advisory; unused workspace definitions and unresolved +inheritance are not described as new runtime dependencies. diff --git a/docs/design/rust-inputs.md b/docs/design/rust-inputs.md index 575e8760..d8f4369f 100644 --- a/docs/design/rust-inputs.md +++ b/docs/design/rust-inputs.md @@ -51,7 +51,8 @@ or release-age coverage; those remain separate dependency-policy operations. Lock inventory supports Cargo's version-3 and version-4 records. A package identity retains its name, complete version and exact source; different versions -or sources are not collapsed. Registry records must carry their archive checksum, +or sources are not collapsed. Git-index and sparse registry records must carry +their archive checksum, which is authoritative artifact identity rather than a local change detector. Dependency-edge strings remain opaque until Cargo supplies resolved graph facts. The version-bound source decoder recognizes the pinned Cargo release's public @@ -92,6 +93,12 @@ Cargo Git evidence currently fails explicitly until the signed inventory protoco supports Cargo locks. Registry age evidence does not establish vulnerability or license coverage and is not collected by the offline profile. +The [historical dependency comparison policy](historical-review-policy.md#cargo-dependency-comparison) +keeps declaration changes separate from effective Cargo resolution. Git lock +entries may record a commit without a declared ref; that commit is preserved +without inventing a branch or treating the commit as a declaration from the +manifest. Historical moving refs remain reviewable but do not become exact pins. + ## Shader inputs WGSL is executable shader source with its own built-in language identifier, diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 11e34715..a0d53200 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -123,8 +123,8 @@ target-specific and workspace declaration tables, checks exact registry and Git pins, contains local paths, and validates versioned lock identities/checksums. It keeps Git and registry coordinates separate and retains explicit failures for unsupported source substitutions. These declaration and lock readers are -data-only. Dependency-review comparison, complete Git evidence, vulnerability and -license integration remain in progress. +data-only. Complete Git evidence, vulnerability and license integration remain +in progress. Native Cargo lock verification now uses bounded, complete resolved metadata through the ordinary supply-chain runner. Online verification may fetch already locked @@ -138,8 +138,17 @@ Public crates.io minimum-age checks cover complete lock inventories, batch exact versions by crate, and follow the registry's identified one-request-per-second API limit. Missing metadata cannot pass or be filled by an assessment. Existing exact release-age assessments now support `crates.io` and Cargo.lock scopes; -Git/private sources are not sent to the public registry. New-dependency preferred-age -advisories and signed Cargo Git inventory support remain unfinished. +Git/private sources are not sent to the public registry. Exact new direct runtime +and optional declarations receive preferred-age advice; workspace definitions do +not receive direct-dependency advice without a proven consumer. + +Cargo dependency review compares declarations and complete external lock +inventories without invoking Cargo. Rows preserve direct, workspace-definition, +inherited, override and resolved roles; renamed packages and source changes retain +their identity across lock scopes. Bare Cargo version requirements remain ranges. +Historical branch, tag, alternate-registry and override declarations can be +reviewed for removal without admitting them into current dependency policy. +Credential-bearing, malformed or escaping inputs fail before comparison. ## Phase 0: define the complete coverage contract diff --git a/internal/repository/cargo_git.go b/internal/repository/cargo_git.go new file mode 100644 index 00000000..bb203a9d --- /dev/null +++ b/internal/repository/cargo_git.go @@ -0,0 +1,50 @@ +package repository + +import ( + "fmt" + "net/url" +) + +func ParseCargoGitDeclaration(declaration CargoDependencyDeclaration) (PythonGitSource, error) { + source, err := parsePythonGitRepositoryURL(declaration.Git, false) + if err != nil { + return PythonGitSource{}, err + } + for _, selector := range []struct{ kind, value string }{ + {"rev", declaration.Revision}, {"branch", declaration.Branch}, {"tag", declaration.Tag}, + } { + if selector.value == "" { + continue + } + if source.DeclaredRef != "" { + return PythonGitSource{}, fmt.Errorf("cargo Git dependency must declare at most one reference selector") + } + reference, err := pythonGitReference(selector.value) + if err != nil { + return PythonGitSource{}, err + } + source.DeclaredRef, source.RefKind = reference, selector.kind + if selector.kind == "rev" && pythonFullCommit(reference) { + source.Commit = reference + } + } + return source, nil +} + +func ParseCargoGitLockSource(value string) (PythonGitSource, error) { + parsed, err := url.Parse(value) + if err != nil { + return PythonGitSource{}, fmt.Errorf("cargo Git lock source URL is malformed") + } + if parsed.RawQuery != "" { + return ParsePythonGitInventorySource(value, "", "") + } + commit := parsed.Fragment + parsed.Fragment, parsed.RawFragment = "", "" + source, err := ParsePythonGitInventorySource(parsed.String(), commit, "") + if err != nil { + return PythonGitSource{}, err + } + source.DeclaredRef = "" + return source, nil +} diff --git a/internal/repository/cargo_git_test.go b/internal/repository/cargo_git_test.go new file mode 100644 index 00000000..9d8bc8fd --- /dev/null +++ b/internal/repository/cargo_git_test.go @@ -0,0 +1,65 @@ +package repository + +import ( + "strings" + "testing" +) + +func TestCargoGitInventoryPreservesDeclarationWithoutInventingResolution(t *testing.T) { + t.Parallel() + for _, declaration := range []CargoDependencyDeclaration{ + {Git: "https://example.test/team/tool"}, + {Git: "https://example.test/team/tool", Branch: "release/stable"}, + {Git: "https://example.test/team/tool", Tag: "v1.2.3"}, + {Git: "https://example.test/team/tool", Revision: "ABCDEF0"}, + } { + source, err := ParseCargoGitDeclaration(declaration) + if err != nil || source.Commit != "" || source.ValidateExactPin() == nil { + t.Fatalf("historical declaration became an admitted pin: %+v, %v", source, err) + } + if declaration.Branch != "" && source.DeclaredRef != declaration.Branch { + t.Fatalf("branch declaration changed: %+v", source) + } + } + commit := strings.Repeat("a", 40) + source, err := ParseCargoGitDeclaration(CargoDependencyDeclaration{Git: "https://example.test/team/tool", Revision: commit}) + if err != nil || source.Commit != commit || source.ValidateExactPin() != nil { + t.Fatalf("full revision was not retained: %+v, %v", source, err) + } +} + +func TestCargoGitLockRetainsDefaultRefAndResolvedCommitSeparately(t *testing.T) { + t.Parallel() + commit := strings.Repeat("a", 40) + source, err := ParseCargoGitLockSource("git+https://example.test/team/tool#" + commit) + if err != nil || source.DeclaredRef != "" || source.Commit != commit || !strings.Contains(source.InventoryIdentity(), "commit="+commit) { + t.Fatalf("default-ref Cargo lock source = %+v, %v", source, err) + } + source, err = ParseCargoGitLockSource("git+https://example.test/team/tool?branch=release/stable#" + commit) + if err != nil || source.DeclaredRef != "release/stable" || source.RefKind != "branch" || source.Commit != commit { + t.Fatalf("branch Cargo lock source = %+v, %v", source, err) + } +} + +func TestCargoGitInventoryRejectsCredentialsAndAmbiguousReferences(t *testing.T) { + t.Parallel() + for _, declaration := range []CargoDependencyDeclaration{ + {Git: "https://TOKEN@example.test/tool", Revision: strings.Repeat("a", 40)}, + {Git: "https://example.test/tool?token=TOKEN", Revision: strings.Repeat("a", 40)}, + {Git: "https://example.test/tool", Tag: "v1", Branch: "main"}, + {Git: "https://example.test/tool", Revision: "main~1"}, + } { + if _, err := ParseCargoGitDeclaration(declaration); err == nil { + t.Fatal("unsafe Cargo declaration accepted") + } + } + for _, source := range []string{ + "git+https://TOKEN@example.test/tool#" + strings.Repeat("a", 40), + "git+https://example.test/tool#abcdef0", + "git+https://example.test/tool?tag=v1&branch=main#" + strings.Repeat("a", 40), + } { + if _, err := ParseCargoGitLockSource(source); err == nil { + t.Fatal("unsafe Cargo lock source accepted") + } + } +} diff --git a/internal/repository/cargo_lock.go b/internal/repository/cargo_lock.go index 35616eb3..28a7f277 100644 --- a/internal/repository/cargo_lock.go +++ b/internal/repository/cargo_lock.go @@ -71,7 +71,7 @@ func validateCargoLockedPackage(pkg CargoLockedPackage) error { if strings.TrimSpace(pkg.Name) == "" || !semver.IsValid("v"+pkg.Version) || strings.Count(core, ".") != 2 { return errors.New("package must have a name and complete semantic version") } - if strings.HasPrefix(pkg.Source, "registry+") { + if strings.HasPrefix(pkg.Source, "registry+") || strings.HasPrefix(pkg.Source, "sparse+") { checksum, err := hex.DecodeString(pkg.Checksum) if err != nil || len(checksum) != 32 { return errors.New("registry package must carry its complete SHA-256 archive checksum") diff --git a/internal/repository/cargo_lock_test.go b/internal/repository/cargo_lock_test.go index 85573ae1..7e22bb22 100644 --- a/internal/repository/cargo_lock_test.go +++ b/internal/repository/cargo_lock_test.go @@ -53,3 +53,16 @@ func TestCargoLockAllowsDifferentVersionsAndSourcesOfOnePackage(t *testing.T) { t.Fatalf("distinct immutable package identities were collapsed: %+v, %v", lock, err) } } + +func TestCargoLockRequiresChecksumsForSparseRegistries(t *testing.T) { + t.Parallel() + data := "version=4\n[[package]]\nname='private'\nversion='1.0.0'\nsource='sparse+https://private.example.test/index/'\n" + if _, err := ParseCargoLock("Cargo.lock", []byte(data)); err == nil { + t.Fatal("sparse registry package without its archive identity was accepted") + } + data += "checksum='" + strings.Repeat("a", 64) + "'\n" + lock, err := ParseCargoLock("Cargo.lock", []byte(data)) + if err != nil || len(lock.Packages) != 1 || lock.Packages[0].Source != "sparse+https://private.example.test/index/" { + t.Fatalf("sparse registry identity = %+v, %v", lock, err) + } +} diff --git a/internal/supplychain/cargo_lock.go b/internal/supplychain/cargo_lock.go index d44a3abf..7da7b3b3 100644 --- a/internal/supplychain/cargo_lock.go +++ b/internal/supplychain/cargo_lock.go @@ -28,7 +28,7 @@ func resolvedCargoPackages(lock repository.CargoLock) ([]resolvedPackage, []poli case pkg.Source == repository.CargoCratesIOSource: item.Source = resolvedSource{Kind: "registry", Registry: "https://crates.io"} case strings.HasPrefix(pkg.Source, "git+"): - source, err := repository.ParsePythonGitInventorySource(pkg.Source, "", "") + source, err := repository.ParseCargoGitLockSource(pkg.Source) if err != nil { findings = append(findings, cargoLockSourceFinding(lock.Path, pkg, err.Error())) continue diff --git a/internal/supplychain/cargo_review.go b/internal/supplychain/cargo_review.go new file mode 100644 index 00000000..cda4aa59 --- /dev/null +++ b/internal/supplychain/cargo_review.go @@ -0,0 +1,108 @@ +package supplychain + +import ( + "fmt" + "strings" + + "github.com/riteofstring/code-polishy/internal/repository" +) + +func directCargoRecords(source inventorySource, path string) ([]dependencyRecord, error) { + data, err := source.read(path) + if err != nil { + return nil, err + } + manifest, err := repository.ParseCargoManifest(path, data) + if err != nil { + return nil, fmt.Errorf("parse %s: Cargo dependency inventory is malformed, ambiguous, or unsafe", path) + } + records := []dependencyRecord{} + for _, dependency := range manifest.Dependencies { + record, err := cargoReviewDeclaration(manifest, dependency) + if err != nil { + return nil, fmt.Errorf("parse %s: Cargo dependency inventory is malformed, ambiguous, or unsafe", path) + } + records = append(records, record) + } + return records, nil +} + +func cargoReviewDeclaration(manifest repository.CargoManifest, dependency repository.CargoDependencyDeclaration) (dependencyRecord, error) { + if err := cargoDeclarationFields(dependency); err != nil { + return dependencyRecord{}, err + } + if dependency.Path != "" && dependency.Git != "" { + return dependencyRecord{}, fmt.Errorf("cargo dependency has conflicting source kinds") + } + record := dependencyRecord{Scope: manifest.Path, Directness: "direct", Usage: cargoReviewUsage(dependency), Name: dependency.Package} + if dependency.Table == "workspace.dependencies" { + record.Directness, record.Usage = "workspace", "definition" + } + if dependency.Table == "replace" || strings.HasPrefix(dependency.Table, "patch.") { + if registry, found := strings.CutPrefix(dependency.Table, "patch."); found { + if err := validateDependencyDeclaration(registry); err != nil { + return dependencyRecord{}, err + } + } + record.Directness, record.Usage = "override", dependency.Table + } + ecosystem, version, err := cargoReviewSource(manifest, dependency) + if err != nil { + return dependencyRecord{}, err + } + record.Ecosystem, record.Version = ecosystem, version + return record, nil +} + +func cargoReviewUsage(dependency repository.CargoDependencyDeclaration) string { + if strings.HasSuffix(dependency.Table, "dev-dependencies") { + return "development" + } + if strings.HasSuffix(dependency.Table, "build-dependencies") { + return "build" + } + if dependency.Optional { + return "optional" + } + return "runtime" +} + +func cargoReviewSource(manifest repository.CargoManifest, dependency repository.CargoDependencyDeclaration) (string, string, error) { + switch { + case dependency.Workspace: + version := "workspace" + if manifest.Package != nil && manifest.Package.Workspace != "" { + if err := validateInventoryLocalPath(manifest.Path, manifest.Package.Workspace); err != nil { + return "", "", err + } + version += ":" + manifest.Package.Workspace + } + return "cargo-workspace", version, nil + case dependency.Git != "": + source, err := repository.ParseCargoGitDeclaration(dependency) + return "git", source.InventoryIdentity(), err + case dependency.Path != "": + return "file", "file:" + dependency.Path, validateInventoryLocalPath(manifest.Path, dependency.Path) + default: + return cargoReviewRegistry(dependency) + } +} + +func cargoReviewRegistry(dependency repository.CargoDependencyDeclaration) (string, string, error) { + version := strings.TrimSpace(dependency.Version) + if err := validateDependencyDeclaration(version); err != nil { + return "", "", err + } + if exactCargoRequirement(version) { + version = strings.TrimSpace(strings.TrimPrefix(version, "=")) + } else if exactVersion.MatchString(version) { + version = "^" + version + } + if dependency.Registry != "" && dependency.Registry != "crates-io" { + if err := validateDependencyDeclaration(dependency.Registry); err != nil { + return "", "", err + } + return "cargo-registry", "registry:" + dependency.Registry + "@" + version, nil + } + return "crates.io", version, nil +} diff --git a/internal/supplychain/cargo_review_lock.go b/internal/supplychain/cargo_review_lock.go new file mode 100644 index 00000000..8af4661c --- /dev/null +++ b/internal/supplychain/cargo_review_lock.go @@ -0,0 +1,55 @@ +package supplychain + +import ( + "fmt" + "net/url" + "slices" + "strings" + + "github.com/riteofstring/code-polishy/internal/repository" +) + +func cargoReviewLock(data []byte, path string) ([]resolvedPackage, error) { + lock, err := repository.ParseCargoLock(path, data) + if err != nil { + return nil, fmt.Errorf("cargo lock dependency inventory is malformed, ambiguous, or unsafe") + } + packages := []resolvedPackage{} + for _, pkg := range lock.Packages { + if pkg.Source == "" { + continue + } + item := resolvedPackage{Name: pkg.Name, Version: pkg.Version, Scope: path} + if err := cargoReviewLockedSource(pkg.Source, &item); err != nil { + return nil, fmt.Errorf("cargo lock dependency inventory is malformed, ambiguous, or unsafe") + } + packages = append(packages, item) + } + return packages, nil +} + +func cargoReviewLockedSource(value string, item *resolvedPackage) error { + if strings.HasPrefix(value, "git+") { + source, err := repository.ParseCargoGitLockSource(value) + item.Ecosystem, item.Source = "git", resolvedSource{Kind: "git", Git: source} + return err + } + kind, registry, found := strings.Cut(value, "+") + if !found || !slices.Contains([]string{"registry", "sparse"}, kind) { + return fmt.Errorf("unsupported Cargo lock source identity") + } + if err := validateInventoryURL(registry); err != nil { + return err + } + parsed, err := url.Parse(registry) + if err != nil || parsed.Scheme == "" || parsed.Fragment != "" { + return fmt.Errorf("cargo registry identity must be an absolute URL without a fragment") + } + item.Ecosystem, item.Source = "cargo-registry", resolvedSource{Kind: "registry", Registry: registry} + if value == repository.CargoCratesIOSource { + item.Ecosystem = "crates.io" + return nil + } + item.Version = value + "@" + item.Version + return nil +} diff --git a/internal/supplychain/cargo_review_test.go b/internal/supplychain/cargo_review_test.go new file mode 100644 index 00000000..d4aa29ed --- /dev/null +++ b/internal/supplychain/cargo_review_test.go @@ -0,0 +1,169 @@ +package supplychain + +import ( + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "testing" + "time" +) + +func TestCargoDependencyReviewComparesDeclarationsAndLockedVersions(t *testing.T) { + t.Parallel() + repo := supplyRepository(t) + writeSupplyFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\n[dependencies]\nrenamed={package='crate-name',version='1.0.0'}\n[build-dependencies]\ncc='=1.0.0'\n") + writeSupplyFile(t, repo.Root, "Cargo.lock", reviewCargoLock("crate-name", "1.0.0", "registry+https://github.com/rust-lang/crates.io-index")) + writeSupplyFile(t, repo.Root, "build.rs", "compile_error!(\"dependency comparison must not execute code\");\n") + base := commitReviewInventory(t, repo) + writeSupplyFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\n[dependencies]\nrenamed={package='crate-name',version='=1.1.0'}\n[build-dependencies]\ncc='=1.1.0'\n") + writeSupplyFile(t, repo.Root, "Cargo.lock", reviewCargoLock("crate-name", "1.1.0", "registry+https://github.com/rust-lang/crates.io-index")) + changes, err := DependencyChanges(t.Context(), repo, base) + if err != nil || len(changes) != 3 { + t.Fatalf("Cargo comparison = %+v, %v", changes, err) + } + for _, change := range changes { + if change.Change != "updated" || change.Ecosystem != "crates.io" || change.To != "1.1.0" { + t.Fatalf("incorrect Cargo version change: %+v", change) + } + if change.Directness == "direct" && change.Package == "crate-name" && change.From != "^1.0.0" { + t.Fatalf("Cargo bare version was mistaken for an exact pin: %+v", change) + } + } + if _, err := os.Stat(filepath.Join(repo.Root, ".tools")); !os.IsNotExist(err) { + t.Fatalf("dependency comparison installed tools or executed Cargo: %v", err) + } +} + +func TestCargoDependencyReviewDoesNotGuessInheritedVersions(t *testing.T) { + t.Parallel() + repo := supplyRepository(t) + writeSupplyFile(t, repo.Root, "Cargo.toml", "[workspace]\nmembers=['app']\n[workspace.dependencies]\nrenamed={package='crate-name',version='=1.0.0'}\n") + writeSupplyFile(t, repo.Root, "app/Cargo.toml", "[package]\nname='app'\n[dependencies]\nrenamed={workspace=true,optional=true}\n") + base := commitReviewInventory(t, repo) + writeSupplyFile(t, repo.Root, "Cargo.toml", "[workspace]\nmembers=['app']\n[workspace.dependencies]\nrenamed={package='crate-name',version='=2.0.0'}\n") + changes, err := DependencyChanges(t.Context(), repo, base) + if err != nil || len(changes) != 1 || changes[0].Directness != "workspace" || changes[0].Package != "crate-name" || changes[0].From != "1.0.0" || changes[0].To != "2.0.0" { + t.Fatalf("workspace definition comparison = %+v, %v", changes, err) + } + writeSupplyFile(t, repo.Root, "app/Cargo.toml", "[package]\nname='app'\n[dependencies]\nrenamed={workspace=true}\n") + changes, err = DependencyChanges(t.Context(), repo, base) + if err != nil || len(changes) != 3 { + t.Fatalf("inherited usage comparison = %+v, %v", changes, err) + } + for _, change := range changes { + if change.Scope == "app/Cargo.toml" && (change.Ecosystem != "cargo-workspace" || change.Package != "renamed" || change.From != "workspace" && change.To != "workspace") { + t.Fatalf("inherited declaration invented package/version facts: %+v", change) + } + } +} + +func TestCargoDependencyReviewAllowsRemovingHistoricalMovingGitAndPrivateRegistrySources(t *testing.T) { + t.Parallel() + repo := supplyRepository(t) + commit := strings.Repeat("a", 40) + writeSupplyFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\n[dependencies]\nprivate-git={git='https://private.example.test/team/tool',branch='stable'}\nprivate-registry={version='1.0.0',registry='private'}\n") + lock := reviewCargoLock("private-git", "1.0.0", "git+https://private.example.test/team/tool?branch=stable#"+commit) + lock += strings.TrimPrefix(reviewCargoLock("private-registry", "1.0.0", "registry+https://private.example.test/index"), "version=4\n") + writeSupplyFile(t, repo.Root, "Cargo.lock", lock) + base := commitReviewInventory(t, repo) + if findings := checkCargoManifest(repo, "Cargo.toml"); len(findings) == 0 { + t.Fatal("historical source declarations bypassed current admission policy") + } + writeSupplyFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\n") + writeSupplyFile(t, repo.Root, "Cargo.lock", "version=4\n[[package]]\nname='app'\nversion='1.0.0'\n") + changes, err := DependencyChanges(t.Context(), repo, base) + if err != nil || len(changes) != 4 { + t.Fatalf("historical source removal = %+v, %v", changes, err) + } + for _, change := range changes { + if change.Change != "removed" || newDependencyAgeCandidate(change) || change.Ecosystem == "crates.io" { + t.Fatalf("private source was reclassified as public or admitted: %+v", change) + } + } +} + +func TestCargoDependencyReviewKeepsLockScopeAndSourceIdentity(t *testing.T) { + t.Parallel() + repo := supplyRepository(t) + writeSupplyFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\n[dependencies]\ncrate-name='=1.0.0'\n") + writeSupplyFile(t, repo.Root, "Cargo.lock", reviewCargoLock("crate-name", "1.0.0", "registry+https://github.com/rust-lang/crates.io-index")) + base := commitReviewInventory(t, repo) + writeSupplyFile(t, repo.Root, "nested/Cargo.lock", reviewCargoLock("crate-name", "1.0.0", "registry+https://github.com/rust-lang/crates.io-index")) + changes, err := DependencyChanges(t.Context(), repo, base) + if err != nil || len(changes) != 1 || changes[0].Scope != "nested/Cargo.lock" || changes[0].Directness != "resolved" { + t.Fatalf("direct declaration in another scope hid a lock change: %+v, %v", changes, err) + } + writeSupplyFile(t, repo.Root, "Cargo.lock", reviewCargoLock("crate-name", "1.0.0", "git+https://example.test/team/tool#"+strings.Repeat("a", 40))) + changes, err = DependencyChanges(t.Context(), repo, base) + if err != nil || !slices.ContainsFunc(changes, func(change DependencyChange) bool { + return change.Ecosystem == "git" && strings.Contains(change.To, "commit=") + }) { + t.Fatalf("Git source change was lost: %+v, %v", changes, err) + } +} + +func TestCargoDependencyReviewRetainsSparseAndInsecureHistoricalRegistryIdentity(t *testing.T) { + t.Parallel() + for _, source := range []string{"sparse+https://private.example.test/index/", "registry+http://private.example.test/index"} { + repo := supplyRepository(t) + writeSupplyFile(t, repo.Root, "Cargo.lock", reviewCargoLock("private", "1.0.0", source)) + base := commitReviewInventory(t, repo) + writeSupplyFile(t, repo.Root, "Cargo.lock", "version=4\n[[package]]\nname='app'\nversion='1.0.0'\n") + changes, err := DependencyChanges(t.Context(), repo, base) + if err != nil || len(changes) != 1 || changes[0].From != source+"@1.0.0" || changes[0].Ecosystem != "cargo-registry" || changes[0].Change != "removed" { + t.Fatalf("historical registry comparison = %+v, %v", changes, err) + } + } +} + +func TestCargoDependencyReviewRejectsUnsafeHistoryWithoutLeakingCredentials(t *testing.T) { + t.Parallel() + for _, declaration := range []string{ + "{git='https://SENSITIVE_TOKEN@example.test/tool',rev='" + strings.Repeat("a", 40) + "'}", + "{path='../outside'}", + "{git='https://example.test/tool',tag='v1',branch='main'}", + "{version='=1.0.0',token='SENSITIVE_TOKEN'}", + } { + repo := supplyRepository(t) + writeSupplyFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\n[dependencies]\ncrate="+declaration+"\n") + base := commitReviewInventory(t, repo) + writeSupplyFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\n") + if _, err := DependencyChanges(t.Context(), repo, base); err == nil || strings.Contains(err.Error(), "SENSITIVE_TOKEN") { + t.Fatalf("unsafe historical inventory = %v", err) + } + } +} + +func TestCargoDependencyReviewAddsAgeAdviceOnlyForExactPublicRuntimeDeclarations(t *testing.T) { + repo := supplyRepository(t) + repo.Config.SupplyChain.PreferredNewDependencyAgeDays = 90 + transport := &cargoAgeTransport{released: map[string]time.Time{"1.0.0": time.Now().UTC().AddDate(0, 0, -60)}} + installCargoAgeTransport(t, transport) + changes := []DependencyChange{ + {Ecosystem: "crates.io", Scope: "Cargo.toml", Directness: "direct", Usage: "runtime", Change: "added", Package: "crate-name", To: "1.0.0"}, + {Ecosystem: "crates.io", Scope: "app/Cargo.toml", Directness: "direct", Usage: "optional", Change: "updated", Package: "crate-name", To: "1.0.0"}, + {Ecosystem: "crates.io", Scope: "Cargo.toml", Directness: "direct", Usage: "build", Change: "added", Package: "build-tool", To: "1.0.0"}, + {Ecosystem: "crates.io", Scope: "Cargo.toml", Directness: "workspace", Usage: "definition", Change: "added", Package: "unused", To: "1.0.0"}, + {Ecosystem: "cargo-registry", Scope: "Cargo.toml", Directness: "direct", Usage: "runtime", Change: "added", Package: "private", To: "1.0.0"}, + {Ecosystem: "crates.io", Scope: "Cargo.toml", Directness: "direct", Usage: "runtime", Change: "added", Package: "range", To: "^1.0.0"}, + } + advisories := NewDependencyAgeAdvisories(t.Context(), repo, changes) + if len(advisories) != 2 || len(transport.requests) != 1 { + t.Fatalf("Cargo preferred-age advice = %+v, requests = %+v", advisories, transport.requests) + } + for _, advisory := range advisories { + if advisory.Subject != "crate-name@1.0.0" || advisory.Check != "supplyChain.newDependencyAge" { + t.Fatalf("incorrect Cargo age advice: %+v", advisory) + } + } +} + +func reviewCargoLock(name, version, source string) string { + lock := fmt.Sprintf("version=4\n[[package]]\nname=%q\nversion=%q\nsource=%q\n", name, version, source) + if strings.HasPrefix(source, "registry+") || strings.HasPrefix(source, "sparse+") { + lock += fmt.Sprintf("checksum=%q\n", strings.Repeat("a", 64)) + } + return lock +} diff --git a/internal/supplychain/review.go b/internal/supplychain/review.go index 9858d753..d66a61ae 100644 --- a/internal/supplychain/review.go +++ b/internal/supplychain/review.go @@ -6,6 +6,7 @@ import ( "fmt" "os/exec" "path/filepath" + "slices" "sort" "strings" "time" @@ -86,6 +87,8 @@ func dependencyInventory(ctx context.Context, repo repository.Repository, source records, err = directPythonRecords(source, path) case "go.mod": records, err = directGoRecords(source, path) + case "Cargo.toml": + records, err = directCargoRecords(source, path) } if err != nil { return nil, err @@ -228,8 +231,12 @@ func resolvedLockRecords(ctx context.Context, repo repository.Repository, source return nil, err } ecosystem, version := resolvedDependencyReviewIdentity(item) + directness := "transitive" + if filepath.Base(path) == "Cargo.lock" { + directness = "resolved" + } records = append(records, dependencyRecord{ - Ecosystem: ecosystem, Scope: path, Directness: "transitive", Usage: "resolved", + Ecosystem: ecosystem, Scope: path, Directness: directness, Usage: "resolved", Name: item.Name, Version: version, }) } @@ -238,7 +245,7 @@ func resolvedLockRecords(ctx context.Context, repo repository.Repository, source func readResolvedLockInventory(ctx context.Context, repo repository.Repository, source inventorySource, path string) ([]resolvedPackage, error) { name := filepath.Base(path) - if name != "package-lock.json" && name != "npm-shrinkwrap.json" && name != pnpmLockName && name != "uv.lock" { + if !slices.Contains([]string{"package-lock.json", "npm-shrinkwrap.json", pnpmLockName, "uv.lock", "Cargo.lock"}, name) { return nil, nil } data, err := source.read(path) @@ -253,6 +260,8 @@ func readResolvedLockInventory(ctx context.Context, repo repository.Repository, packages, err = revisionPNPMPackages(ctx, repo, data, path) case "uv.lock": packages, err = parseUVLockWith(data, path, parseUVInventorySourceFields) + case "Cargo.lock": + packages, err = cargoReviewLock(data, path) } if err != nil { return nil, fmt.Errorf("parse %s: resolved dependency inventory is malformed, ambiguous, or unsupported: %w", path, err) @@ -409,7 +418,7 @@ func newDependencyAgeCandidate(change DependencyChange) bool { strings.Contains(" runtime optional ", " "+change.Usage+" ") && change.To != "-" && dependencyAgeVersion(change) && - strings.Contains(" go npm pnpm pypi ", " "+change.Ecosystem+" ") + strings.Contains(" go npm pnpm pypi crates.io ", " "+change.Ecosystem+" ") } func dependencyAgeVersion(change DependencyChange) bool { diff --git a/internal/supplychain/review_inventory.go b/internal/supplychain/review_inventory.go index 42ab2995..421b2e05 100644 --- a/internal/supplychain/review_inventory.go +++ b/internal/supplychain/review_inventory.go @@ -49,7 +49,7 @@ func validateNodeInventoryDeclaration(manifest, value string) error { func addResolvedInventoryRecords(inventory dependencyInventoryMap, direct map[string]bool, records []dependencyRecord) { for _, record := range records { - if record.Ecosystem != "git" && direct[record.Ecosystem+"\x00"+record.Name+"\x00"+record.Version] { + if record.Directness != "resolved" && record.Ecosystem != "git" && direct[record.Ecosystem+"\x00"+record.Name+"\x00"+record.Version] { continue } addDependencyRecords(inventory, []dependencyRecord{record}) From e43bc4906f55daef3cfe0c1069b6d5dc3ae65fac Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 01:10:47 +0000 Subject: [PATCH 11/49] feat: govern complete Cargo advisory scans --- .code-polishy.json | 4 + docs/design/execution-observability.md | 3 + docs/design/git-evidence-policy.md | 6 + docs/design/rust-inputs.md | 32 ++ docs/plans/code-polishy-rust-support-plan.md | 11 +- internal/policy/cargo_vulnerability_test.go | 28 ++ internal/supplychain/cargo_resolution.go | 19 + internal/supplychain/osv.go | 60 +++- internal/supplychain/osv_cargo.go | 70 ++++ .../supplychain/osv_cargo_runtime_test.go | 78 ++++ internal/supplychain/osv_cargo_test.go | 336 ++++++++++++++++++ internal/supplychain/osv_uv.go | 52 +-- internal/supplychain/supplychain.go | 4 +- schema/code-polishy-supply-chain.schema.json | 2 +- 14 files changed, 662 insertions(+), 43 deletions(-) create mode 100644 internal/policy/cargo_vulnerability_test.go create mode 100644 internal/supplychain/osv_cargo.go create mode 100644 internal/supplychain/osv_cargo_runtime_test.go create mode 100644 internal/supplychain/osv_cargo_test.go diff --git a/.code-polishy.json b/.code-polishy.json index b55668e9..64603777 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -34,6 +34,7 @@ "internal/policy/rust_inputs_test.go", "internal/policy/supplychain_config.go", "internal/policy/cargo_release_age_test.go", + "internal/policy/cargo_vulnerability_test.go", "internal/repository/cargo_manifest.go", "internal/repository/cargo_manifest_test.go", "internal/repository/cargo_dependencies.go", @@ -62,6 +63,9 @@ "internal/supplychain/cargo_release_age_test.go", "internal/supplychain/cargo_registry.go", "internal/supplychain/cargo_registry_test.go", + "internal/supplychain/osv_cargo.go", + "internal/supplychain/osv_cargo_test.go", + "internal/supplychain/osv_cargo_runtime_test.go", "internal/supplychain/release_age.go", "internal/supplychain/online_profile.go", "internal/supplychain/supplychain.go", diff --git a/docs/design/execution-observability.md b/docs/design/execution-observability.md index ce2454de..4e811f22 100644 --- a/docs/design/execution-observability.md +++ b/docs/design/execution-observability.md @@ -68,6 +68,9 @@ in evidence, while malformed, empty, timed-out, canceled, and operational results remain failed commands. OSV-Scanner's exit status 1 is the first such protocol; vulnerability assessment policy, not process status, decides whether its accepted report permits the gate to pass. +Standalone scanner calls use the same exit conditions. A timeout or operational +failure remains a scanner finding even when an advisory was emitted before the +failure; assessing that advisory cannot erase the failed scan. Contained asset links have a read-only identity shared by formatting snapshots and provider context. It binds link text, canonical target, and bounded target-file diff --git a/docs/design/git-evidence-policy.md b/docs/design/git-evidence-policy.md index cf516a4f..c7e8d961 100644 --- a/docs/design/git-evidence-policy.md +++ b/docs/design/git-evidence-policy.md @@ -13,6 +13,12 @@ public registry vulnerability and minimum-age checks even in mixed Git and registry locks. Public scan projections omit Git coordinates and never invent registry versions or publication dates for them. +Cargo uses the same contained public-projection boundary, with its own complete +locked-package report checks described in [Rust registry advisories](rust-inputs.md#registry-advisories). +Its native lock extractor is disabled because the pinned scanner does not retain +Cargo source identity. This does not extend the signed Python inventory protocol +to Cargo or substitute registry evidence for Git contents. + Opting in retains complete inventory binding, signature verification, scanner trust, expiry, license, vulnerability, and authenticated-age requirements. Missing or invalid evidence blocks the selected policy. Its receipt remains diff --git a/docs/design/rust-inputs.md b/docs/design/rust-inputs.md index d8f4369f..41d72eed 100644 --- a/docs/design/rust-inputs.md +++ b/docs/design/rust-inputs.md @@ -99,6 +99,38 @@ entries may record a commit without a declared ref; that commit is preserved without inventing a branch or treating the commit as a declaration from the manifest. Historical moving refs remain reviewable but do not become exact pins. +## Registry advisories + +The pinned OSV-Scanner provides native advisory coverage for complete public +Cargo lock inventories, independent of the currently enabled features or host. +Its [version-bound Cargo extractor](https://github.com/google/osv-scalibr/blob/164402d9140e/extractor/filesystem/language/rust/cargolock/cargolock.go) +does not preserve source identity. Recursive scans therefore disable both Cargo +manifest and Cargo lock extraction. A bounded, data-only projection supplies +only exact public crates.io coordinates from the governed lock reader. Private +registries and unsupported source encodings fail before public scan planning; +local and Git packages never become registry requests. Optional Git policy keeps +its visible warning, and requiring Git evidence does not hide public registry +advisories from a mixed Cargo lock. + +Cargo reuses the existing immutable projection artifacts and common scanner +runner. Separate locks preserve their own original finding scopes. Planning does +not write artifacts; execution verifies their regular-file containment and exact +planned bytes. No transitive resolution or Rust call analysis is enabled, so an +advisory scan does not execute build scripts or procedural macros. + +Scanner scope must contain the actual workspace lock established by Cargo. A +member-only scope cannot claim an ancestor lock's public dependencies; it needs +workspace-level OSV coverage or a governed security provider. + +Projected Cargo scans request every package in the result. Missing, repeated, +foreign or wrong-version coordinates fail coverage, including an empty result for +a nonempty input. Scanner errors do not become clean reports. Complete advisory +reports retain aliases, exact versions, severity and Cargo.lock scope through the +existing report-bearing exit and vulnerability-assessment policies. Assessments +use the `crates.io` ecosystem and cannot approve a different lock or version. +The pinned runtime contract is exercised against a temporary synthetic offline +advisory database, without querying a live provider. + ## Shader inputs WGSL is executable shader source with its own built-in language identifier, diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index a0d53200..5dec400d 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -123,8 +123,7 @@ target-specific and workspace declaration tables, checks exact registry and Git pins, contains local paths, and validates versioned lock identities/checksums. It keeps Git and registry coordinates separate and retains explicit failures for unsupported source substitutions. These declaration and lock readers are -data-only. Complete Git evidence, vulnerability and license integration remain -in progress. +data-only. Complete Git evidence and license integration remain in progress. Native Cargo lock verification now uses bounded, complete resolved metadata through the ordinary supply-chain runner. Online verification may fetch already locked @@ -150,6 +149,14 @@ Historical branch, tag, alternate-registry and override declarations can be reviewed for removal without admitting them into current dependency policy. Credential-bearing, malformed or escaping inputs fail before comparison. +Native Cargo advisory coverage now uses the pinned OSV-Scanner through a +public-crates-only projection of the complete lock. Raw Cargo extraction and +compiler call analysis are disabled. The report must account for every projected +version, and exact `crates.io` vulnerability assessments preserve the original +lock scope. Tests cover source isolation, incomplete reports, scanner exits, +assessment identity and the real pinned scanner with a temporary offline advisory +database. Signed Cargo Git inventories and license integration remain unfinished. + ## Phase 0: define the complete coverage contract - [x] Audit current Go/Python owners and Rust gaps against the recorded base; diff --git a/internal/policy/cargo_vulnerability_test.go b/internal/policy/cargo_vulnerability_test.go new file mode 100644 index 00000000..cb5fc0d4 --- /dev/null +++ b/internal/policy/cargo_vulnerability_test.go @@ -0,0 +1,28 @@ +package policy + +import ( + "testing" + "time" +) + +func TestCargoVulnerabilityAssessmentRequiresExactRegistryCoordinates(t *testing.T) { + t.Parallel() + now := time.Now().UTC().Truncate(24 * time.Hour) + assessment := highNotAffectedAssessment(now) + assessment.Ecosystem, assessment.Package, assessment.Advisory, assessment.Scope = "crates.io", "example", "RUSTSEC-2026-0001", "Cargo.lock" + for _, version := range []string{"1.2.3", "1.2.3+native", "1.2.3-beta.1"} { + assessment.AffectedVersion = version + if err := validateVulnerabilityAssessmentSchema(t, assessment); err != nil { + t.Fatalf("Cargo vulnerability schema rejected exact version %s: %v", version, err) + } + if err := validateVulnerabilityAssessment(assessment, "assessment", map[string]bool{}, map[string]bool{}, now); err != nil { + t.Fatalf("Cargo vulnerability policy rejected exact version %s: %v", version, err) + } + } + for _, version := range []string{"=1.2.3", "^1.2.3", "1.2", "1.2.3-01", "*"} { + assessment.AffectedVersion = version + if err := validateVulnerabilityAssessment(assessment, "assessment", map[string]bool{}, map[string]bool{}, now); err == nil { + t.Fatalf("inexact Cargo vulnerability version %s accepted", version) + } + } +} diff --git a/internal/supplychain/cargo_resolution.go b/internal/supplychain/cargo_resolution.go index a93bda4d..21b2c775 100644 --- a/internal/supplychain/cargo_resolution.go +++ b/internal/supplychain/cargo_resolution.go @@ -27,7 +27,26 @@ func onlineCargoFindings(ctx context.Context, repo repository.Repository, files locks := []repository.CargoLock{} for _, resolution := range result.Resolutions { locks = append(locks, resolution.Lock) + findings = append(findings, cargoAdvisoryScopeFindings(repo, files, resolution)...) } findings = append(findings, cargoReleaseAgeFindings(ctx, repo, files, locks)...) return findings, index + count } + +func cargoAdvisoryScopeFindings(repo repository.Repository, files []string, resolution rustfacts.Resolution) []policy.Finding { + if !osvCoversScope(repo.Config, resolution.Manifest) || osvCoversScope(repo.Config, resolution.Lock.Path) { + return nil + } + for _, pkg := range resolution.Lock.Packages { + if pkg.Source != repository.CargoCratesIOSource { + continue + } + findings := manifestCapabilityFindings(repo, files, resolution.Manifest, "cargo", []string{"security"}) + for index := range findings { + findings[index].Path = resolution.Lock.Path + findings[index].Message = "the resolved Cargo workspace lock is outside the active OSV scope; cover the workspace lock with OSV or a governed security provider" + } + return findings + } + return nil +} diff --git a/internal/supplychain/osv.go b/internal/supplychain/osv.go index 13cee07c..39776936 100644 --- a/internal/supplychain/osv.go +++ b/internal/supplychain/osv.go @@ -43,18 +43,27 @@ func osvScanPlan(repo repository.Repository) ([]osvScan, error) { if err != nil { return nil, err } + cargo, err := osvLockInputs(repo, "Cargo.lock", osvCargoInventory) + if err != nil { + return nil, err + } scans := []osvScan{} for _, root := range activeOSVRoots(repo.Config) { command := osvCommand(repo, "osv-scan-"+safeName(root), root) command.Argv = append(command.Argv, "--recursive", "--experimental-disable-plugins", "python/uvlock", "--experimental-exclude", "g:**/.code-polishy-reports") - if rootHasUVInput(root, inputs) { + command.Argv = append(command.Argv, "--experimental-disable-plugins", "rust/cargolock", "--experimental-disable-plugins", "rust/cargotoml") + if rootHasOSVInput(root, inputs) || rootHasOSVInput(root, cargo) { command.Argv = append(command.Argv, "--allow-no-lockfiles") } command.Argv = append(command.Argv, ".") scans = append(scans, osvScan{Command: command, Root: root}) } uv, err := osvUVScans(repo, inputs) - return append(scans, uv...), err + if err != nil { + return nil, err + } + rust, err := osvCargoScans(repo, cargo) + return append(append(scans, uv...), rust...), err } func scanOSVWithCommands(ctx context.Context, repo repository.Repository, commands []policy.Command, commandRunner runner.Runner) []policy.Finding { @@ -114,7 +123,7 @@ func compactStrings(values []string) []string { func scanOSVRoot(ctx context.Context, repo repository.Repository, scan osvScan, commandRunner runner.Runner) []policy.Finding { root := scan.Root - output, runErr := runOSVCommandWithOutput(ctx, repo, scan, commandRunner) + result, output, runErr := runOSVCommandWithOutput(ctx, repo, scan, commandRunner) findings, parseErr := parseOSVScanReport(repo, scan, output.Stdout) if parseErr != nil { message := fmt.Sprintf("parse OSV-Scanner JSON: %v", parseErr) @@ -128,32 +137,38 @@ func scanOSVRoot(ctx context.Context, repo repository.Repository, scan osvScan, Message: message, }} } - if runErr != nil && len(findings) == 0 { + if runErr != nil && !acceptedOSVFindingExit(ctx, result, findings, runErr) { message := strings.TrimSpace(string(output.Stderr)) if message == "" { message = runErr.Error() } - return []policy.Finding{{Check: "policy.securityScanner", Path: root, Subject: "osv-scanner", Message: message}} + return append(findings, policy.Finding{Check: "policy.securityScanner", Path: root, Subject: "osv-scanner", Message: message}) } return findings } -func runOSVCommandWithOutput(ctx context.Context, repo repository.Repository, scan osvScan, commandRunner runner.Runner) (runner.Output, error) { +func runOSVCommandWithOutput(ctx context.Context, repo repository.Repository, scan osvScan, commandRunner runner.Runner) (runner.Result, runner.Output, error) { if observed, ok := commandRunner.(runner.ReportOutputRunner); ok { - _, output, err := observed.RunWithReportOutput(ctx, repo.Root, scan.Command, func(result runner.Result, output runner.Output, runErr error) bool { + return observed.RunWithReportOutput(ctx, repo.Root, scan.Command, func(result runner.Result, output runner.Output, runErr error) bool { return acceptedOSVReportExit(ctx, repo, scan, result, output, runErr) }) - return output, err } - return runCommandWithOutput(ctx, repo, scan.Command, commandRunner) + if observed, ok := commandRunner.(runner.OutputRunner); ok { + return observed.RunWithOutput(ctx, repo.Root, scan.Command) + } + return runner.Result{}, runner.Output{}, fmt.Errorf("common command runner cannot capture scanner output") } func acceptedOSVReportExit(ctx context.Context, repo repository.Repository, scan osvScan, result runner.Result, output runner.Output, runErr error) bool { + findings, err := parseOSVScanReport(repo, scan, output.Stdout) + return err == nil && acceptedOSVFindingExit(ctx, result, findings, runErr) +} + +func acceptedOSVFindingExit(ctx context.Context, result runner.Result, findings []policy.Finding, runErr error) bool { if ctx.Err() != nil || runErr == nil || result.ExitStatus != 1 || runner.FailureCategoryFor(ctx, result, runErr) != runner.FailureCommandExit { return false } - findings, err := parseOSVScanReport(repo, scan, output.Stdout) - if err != nil || len(findings) == 0 { + if len(findings) == 0 { return false } for _, finding := range findings { @@ -201,23 +216,34 @@ func parseOSVReport(repo repository.Repository, root string, payload []byte) ([] return parseOSVScanReport(repo, osvScan{Root: root}, payload) } -func parseOSVScanReport(repo repository.Repository, scan osvScan, payload []byte) ([]policy.Finding, error) { +func decodeOSVReport(payload []byte) (osvReport, error) { + var report osvReport if len(bytes.TrimSpace(payload)) == 0 { - return nil, errors.New("scanner returned no JSON report") + return report, errors.New("scanner returned no JSON report") } var fields map[string]json.RawMessage if err := json.Unmarshal(payload, &fields); err != nil { - return nil, err + return report, err } if _, exists := fields["results"]; !exists { - return nil, errors.New("scanner report omitted results") + return report, errors.New("scanner report omitted results") } decoder := json.NewDecoder(bytes.NewReader(payload)) - var report osvReport if err := decoder.Decode(&report); err != nil { - return nil, err + return report, err } if err := requireOneJSONValue(decoder); err != nil { + return report, err + } + return report, nil +} + +func parseOSVScanReport(repo repository.Repository, scan osvScan, payload []byte) ([]policy.Finding, error) { + report, err := decodeOSVReport(payload) + if err != nil { + return nil, err + } + if err := validateOSVCargoReport(scan, report); err != nil { return nil, err } findings := []policy.Finding{} diff --git a/internal/supplychain/osv_cargo.go b/internal/supplychain/osv_cargo.go new file mode 100644 index 00000000..feb2f2ce --- /dev/null +++ b/internal/supplychain/osv_cargo.go @@ -0,0 +1,70 @@ +package supplychain + +import ( + "encoding/json" + "fmt" + "path/filepath" + + "github.com/riteofstring/code-polishy/internal/repository" +) + +func osvCargoInventory(data []byte, path string) ([]resolvedPackage, error) { + lock, err := repository.ParseCargoLock(path, data) + if err != nil { + return nil, err + } + packages, findings := resolvedCargoPackages(lock) + if len(findings) != 0 { + return nil, fmt.Errorf("cargo inventory contains unsupported sources") + } + return packages, nil +} + +func osvCargoScans(repo repository.Repository, inputs []osvLockInput) ([]osvScan, error) { + scans := []osvScan{} + for _, input := range inputs { + if !osvCoversScope(repo.Config, input.Scope) { + continue + } + projection, err := publicLockProjection(input, "crates.io", "https://crates.io") + if err != nil { + return nil, err + } + if len(projection) == 0 { + continue + } + scan := osvProjectionScan(repo, "cargo", input.Scope, projection) + scan.Command.Argv = append(scan.Command.Argv, "--all-packages", "--no-call-analysis", "rust") + scans = append(scans, scan) + } + return scans, nil +} + +func validateOSVCargoReport(scan osvScan, report osvReport) error { + if scan.InputPath == "" || filepath.Base(scan.Scope) != "Cargo.lock" { + return nil + } + var projected osvReport + if err := json.Unmarshal(scan.Projection, &projected); err != nil { + return fmt.Errorf("planned Cargo public package projection is malformed") + } + expected := map[string]bool{} + for _, result := range projected.Results { + for _, pkg := range result.Packages { + expected[pkg.Package.Ecosystem+"\x00"+pkg.Package.Name+"\x00"+pkg.Package.Version] = true + } + } + for _, result := range report.Results { + for _, pkg := range result.Packages { + key := pkg.Package.Ecosystem + "\x00" + pkg.Package.Name + "\x00" + pkg.Package.Version + if !expected[key] { + return fmt.Errorf("cargo scanner report repeats a package or refers outside its public package projection") + } + delete(expected, key) + } + } + if len(expected) != 0 { + return fmt.Errorf("cargo scanner report omitted packages from its complete locked registry inventory") + } + return nil +} diff --git a/internal/supplychain/osv_cargo_runtime_test.go b/internal/supplychain/osv_cargo_runtime_test.go new file mode 100644 index 00000000..88eac4fc --- /dev/null +++ b/internal/supplychain/osv_cargo_runtime_test.go @@ -0,0 +1,78 @@ +package supplychain + +import ( + "archive/zip" + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestOSVCargoPinnedRuntimeUsesCompleteOfflineProjection(t *testing.T) { + t.Parallel() + repo := cargoOSVRepository(t, cargoOSVMixedLock()) + root, err := filepath.Abs("../..") + if err != nil { + t.Fatal(err) + } + repo.PolicyRoot = root + if !isExecutable(repo.PolicyTool("osv-scanner")) { + t.Fatal("pinned OSV-Scanner is required; run ./tools/install-policy-tools.sh") + } + scans, err := osvScanPlan(repo) + if err != nil || len(scans) != 2 { + t.Fatalf("Cargo runtime scan plan = %+v, %v", scans, err) + } + cache := cargoOSVDatabase(t) + for _, scan := range scans { + scan.Command.Argv = append(scan.Command.Argv, "--offline") + scan.Command.SealedEnvironment = true + scan.Command.EnvironmentOverrides = []string{"OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY=" + cache} + if err := prepareOSVInput(repo, scan); err != nil { + t.Fatal(err) + } + findings := scanOSVRoot(t.Context(), repo, scan, runner.OSRunner{}) + if scan.InputPath == "" { + if len(findings) != 0 { + t.Fatalf("recursive scanner read raw Cargo sources: %+v", findings) + } + continue + } + if len(findings) != 1 || findings[0].Vulnerability == nil || findings[0].Vulnerability.Advisory != "RUSTSEC-2026-0001" || findings[0].Vulnerability.Scope != "Cargo.lock" { + t.Fatalf("pinned Cargo scanner lost complete package or vulnerability evidence: %+v", findings) + } + } +} + +func cargoOSVDatabase(t *testing.T) string { + t.Helper() + buffer := bytes.Buffer{} + archive := zip.NewWriter(&buffer) + file, err := archive.Create("RUSTSEC-2026-0001.json") + if err != nil { + t.Fatal(err) + } + _, err = file.Write([]byte(`{ + "schema_version":"1.7.0","id":"RUSTSEC-2026-0001","modified":"2026-01-01T00:00:00Z", + "published":"2026-01-01T00:00:00Z","summary":"Offline scanner fixture", + "affected":[{"package":{"ecosystem":"crates.io","name":"registry-crate"}, + "ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"2.0.0"}]}]}] +}`)) + if err != nil { + t.Fatal(err) + } + if err := archive.Close(); err != nil { + t.Fatal(err) + } + cache := t.TempDir() + directory := filepath.Join(cache, "osv-scanner", "crates.io") + if err := os.MkdirAll(directory, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(directory, "all.zip"), buffer.Bytes(), 0o600); err != nil { + t.Fatal(err) + } + return cache +} diff --git a/internal/supplychain/osv_cargo_test.go b/internal/supplychain/osv_cargo_test.go new file mode 100644 index 00000000..1866b38a --- /dev/null +++ b/internal/supplychain/osv_cargo_test.go @@ -0,0 +1,336 @@ +package supplychain + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "testing" + "time" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" + "github.com/riteofstring/code-polishy/internal/rustfacts" +) + +func TestOSVCargoRetainsAllLockedRegistryVersionsWithoutPrivateSources(t *testing.T) { + t.Parallel() + repo := cargoOSVRepository(t, cargoOSVMixedLock()) + repo.Config.SupplyChain.GitEvidence.Required = true + commands, err := osvCommands(repo) + if err != nil || len(commands) != 2 { + t.Fatalf("Cargo public scan plan = %+v, %v", commands, err) + } + if _, err := os.Stat(filepath.Join(repo.Root, osvInputDirectory)); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("planning wrote a projection: %v", err) + } + boundary := &cargoOSVBoundary{t: t} + findings := scanOSVWithCommands(t.Context(), repo, commands, boundary) + if boundary.scans != 2 || len(findings) != 1 { + t.Fatalf("Cargo scan = %+v, commands %d", findings, boundary.scans) + } + identity := findings[0].Vulnerability + if identity == nil || identity.Ecosystem != "crates.io" || identity.Package != "registry-crate" || identity.AffectedVersion != "1.0.0" || identity.Scope != "Cargo.lock" { + t.Fatalf("Cargo advisory lost its exact identity: %+v", findings) + } + if identity.Advisory != "GHSA-cargo-example" || !slices.Contains(identity.Aliases, "RUSTSEC-2026-0001") || identity.Severity != "low" { + t.Fatalf("Cargo advisory lost aliases or severity: %+v", identity) + } +} + +func TestOSVCargoRejectsUnsupportedSourcesBeforePlanningPublicScans(t *testing.T) { + t.Parallel() + for _, source := range []string{ + "registry+https://private.example.test/index", "sparse+https://private.example.test/index/", + "registry+https://token@github.com/rust-lang/crates.io-index", "git+https://private.example.test/repo#short", + } { + repo := cargoOSVRepository(t, reviewCargoLock("private-crate", "1.0.0", source)) + commands, err := osvCommands(repo) + if err == nil || len(commands) != 0 || strings.Contains(err.Error(), "private") || strings.Contains(err.Error(), "token") { + t.Fatalf("unsafe Cargo scan plan = %+v, %v", commands, err) + } + } +} + +func TestOSVCargoRejectsLinkedLocksAndChangedPublicPlans(t *testing.T) { + t.Parallel() + repo := cargoOSVRepository(t, cargoOSVMixedLock()) + commands, err := osvCommands(repo) + if err != nil { + t.Fatal(err) + } + writeSupplyFile(t, repo.Root, "Cargo.lock", strings.ReplaceAll(cargoOSVMixedLock(), "1.0.0", "1.1.0")) + boundary := &cargoOSVBoundary{t: t} + findings := scanOSVWithCommands(t.Context(), repo, commands, boundary) + if boundary.scans != 0 || len(findings) != 1 || findings[0].Check != "policy.supplyChain" { + t.Fatalf("changed Cargo scan plan executed: %+v, %d scans", findings, boundary.scans) + } + if err := os.Rename(filepath.Join(repo.Root, "Cargo.lock"), filepath.Join(repo.Root, "original.lock")); err != nil { + t.Fatal(err) + } + if err := os.Symlink("original.lock", filepath.Join(repo.Root, "Cargo.lock")); err != nil { + t.Fatal(err) + } + if _, err := osvCommands(repo); err == nil { + t.Fatal("linked Cargo lock became a public scanner input") + } +} + +func TestOSVCargoPartitionKeepsIndependentNativeAndLockScopes(t *testing.T) { + t.Parallel() + repo := cargoOSVRepository(t, "version=4\n[[package]]\nname='local'\nversion='1.0.0'\n") + repo.Config.ActivePolicyModules = []policy.ActivePolicyModule{{Name: "osv", Root: "native"}, {Name: "osv", Root: "rust"}} + writeSupplyFile(t, repo.Root, "native/go.mod", "module example.test/native\n") + for _, scope := range []string{"rust/Cargo.lock", "rust/nested/Cargo.lock", "outside/Cargo.lock"} { + writeSupplyFile(t, repo.Root, scope, cargoOSVMixedLock()) + } + scans, err := osvScanPlan(repo) + if err != nil || len(scans) != 4 { + t.Fatalf("scoped Cargo scan plan = %+v, %v", scans, err) + } + if slices.Contains(scans[0].Command.Argv, "--allow-no-lockfiles") || !slices.Contains(scans[1].Command.Argv, "--allow-no-lockfiles") { + t.Fatalf("unrelated native missing-input coverage relaxed: %+v", scans) + } + if scans[2].Scope != "rust/Cargo.lock" || scans[3].Scope != "rust/nested/Cargo.lock" || scans[2].InputPath == scans[3].InputPath { + t.Fatalf("independent Cargo lock scopes collapsed: %+v", scans) + } +} + +func TestOSVCargoRejectsIncompleteOrForeignPackageReports(t *testing.T) { + t.Parallel() + repo := cargoOSVRepository(t, cargoOSVMixedLock()) + scans, err := osvScanPlan(repo) + if err != nil { + t.Fatal(err) + } + scan := scans[1] + for _, mutation := range []string{"missing", "duplicate", "foreign-version", "foreign-ecosystem", "foreign-source", "empty"} { + t.Run(mutation, func(t *testing.T) { + var report osvReport + if err := json.Unmarshal(scan.Projection, &report); err != nil { + t.Fatal(err) + } + report.Results[0].Source.Path = scan.InputPath + mutateCargoOSVReport(&report, mutation) + payload, err := json.Marshal(report) + if err != nil { + t.Fatal(err) + } + if _, err := parseOSVScanReport(repo, scan, payload); err == nil { + t.Fatalf("%s Cargo report satisfied locked package coverage", mutation) + } + }) + } +} + +func TestOSVCargoAssessmentAndReportExitUseObservedExactCoordinates(t *testing.T) { + t.Parallel() + repo := cargoOSVRepository(t, cargoOSVMixedLock()) + scans, err := osvScanPlan(repo) + if err != nil { + t.Fatal(err) + } + scan := scans[1] + output := runner.Output{Stdout: cargoOSVResponse(t, scan.Projection, scan.InputPath)} + failure := errors.New("advisories found") + result := runner.Result{ExitStatus: 1, FailureCategory: runner.FailureCommandExit} + if !acceptedOSVReportExit(t.Context(), repo, scan, result, output, failure) { + t.Fatal("complete Cargo vulnerability report lost report-bearing exit handling") + } + findings, err := parseOSVScanReport(repo, scan, output.Stdout) + if err != nil { + t.Fatal(err) + } + now := time.Now().UTC() + assessment := policy.VulnerabilityAssessment{ID: "cargo", Ecosystem: "crates.io", Advisory: "RUSTSEC-2026-0001", Package: "registry-crate", AffectedVersion: "1.0.0", Scope: "Cargo.lock", Severity: "low", Expires: policy.Date{Time: now.AddDate(0, 0, 1)}} + kept, accepted := policy.ApplyVulnerabilityAssessments(findings, []policy.VulnerabilityAssessment{assessment}, now, true) + if len(kept) != 0 || len(accepted) != 1 { + t.Fatalf("exact Cargo advisory assessment rejected: %+v, %+v", kept, accepted) + } + assessment.Scope = "nested/Cargo.lock" + kept, accepted = policy.ApplyVulnerabilityAssessments(findings, []policy.VulnerabilityAssessment{assessment}, now, false) + if len(kept) != 1 || len(accepted) != 0 { + t.Fatal("assessment for a different Cargo lock hid the advisory") + } +} + +func TestOSVCargoSecurityCapabilityRequiresActiveScannerScope(t *testing.T) { + t.Parallel() + repo := cargoOSVRepository(t, cargoOSVMixedLock()) + files := []string{"Cargo.toml", "other/Cargo.toml"} + repo.Config.ActivePolicyModules[0].Root = "other" + findings := CoverageFindings(repo, files) + if len(findings) != 1 || findings[0].Path != "Cargo.toml" || findings[0].Subject != "repository:security" { + t.Fatalf("Cargo scanner scope coverage = %+v", findings) + } + repo.Config.ActivePolicyModules[0].Root = "." + if findings := CoverageFindings(repo, files); len(findings) != 0 { + t.Fatalf("native Cargo advisory coverage still required a provider: %+v", findings) + } +} + +func TestOSVCargoMemberScopeDoesNotCoverAnAncestorWorkspaceLock(t *testing.T) { + t.Parallel() + repo := cargoOSVRepository(t, cargoOSVMixedLock()) + repo.Config.ActivePolicyModules[0].Root = "members/app" + lock, err := repo.ReadCargoLock("Cargo.lock") + if err != nil { + t.Fatal(err) + } + resolution := rustfacts.Resolution{Manifest: "members/app/Cargo.toml", Lock: lock} + files := []string{resolution.Manifest} + findings := cargoAdvisoryScopeFindings(repo, files, resolution) + if len(findings) != 1 || findings[0].Path != "Cargo.lock" || findings[0].Check != "policy.supplyChainCoverage" { + t.Fatalf("member-only scan claimed an unscanned workspace lock: %+v", findings) + } + repo.Config.Checks = []policy.Command{{Name: "workspace-security", Provides: []string{"security"}, RunOn: []string{"security"}}} + if findings := cargoAdvisoryScopeFindings(repo, files, resolution); len(findings) != 0 { + t.Fatalf("governed workspace security provider was ignored: %+v", findings) + } + repo.Config.Checks = nil + repo.Config.ActivePolicyModules[0].Root = "." + if findings := cargoAdvisoryScopeFindings(repo, files, resolution); len(findings) != 0 { + t.Fatalf("actual workspace scanner scope was rejected: %+v", findings) + } +} + +func TestOSVCargoKeepsOperationalFailuresBesideValidAdvisories(t *testing.T) { + t.Parallel() + repo := cargoOSVRepository(t, cargoOSVMixedLock()) + scans, err := osvScanPlan(repo) + if err != nil { + t.Fatal(err) + } + scan := scans[1] + output := runner.Output{Stdout: cargoOSVResponse(t, scan.Projection, scan.InputPath)} + for _, result := range []runner.Result{ + {ExitStatus: 1, FailureCategory: runner.FailureOperational}, + {ExitStatus: 2, FailureCategory: runner.FailureCommandExit}, + {ExitStatus: 124, FailureCategory: runner.FailureTimeout}, + } { + boundary := cargoOSVFailure{result: result, output: output} + findings := scanOSVRoot(t.Context(), repo, scan, boundary) + checks := supplyChecks(findings) + if !checks["policy.securityScanner"] || !checks["supplyChain.osvVulnerability"] { + t.Fatalf("scanner failure %v disappeared behind advisories: %+v", result, findings) + } + } +} + +type cargoOSVFailure struct { + result runner.Result + output runner.Output +} + +func (boundary cargoOSVFailure) Run(context.Context, string, policy.Command) error { + return fmt.Errorf("unexpected unstructured scan") +} + +func (boundary cargoOSVFailure) RunWithOutput(context.Context, string, policy.Command) (runner.Result, runner.Output, error) { + return boundary.result, boundary.output, fmt.Errorf("scanner failed") +} + +func cargoOSVRepository(t *testing.T, lock string) repository.Repository { + t.Helper() + repo := supplyRepository(t) + repo.PolicyRoot = repo.Root + repo.Config.ActivePolicyModules = []policy.ActivePolicyModule{{Name: "osv", Root: "."}} + writeSupplyFile(t, repo.Root, ".tools/bin/osv-scanner", "fixture") + if err := os.Chmod(repo.PolicyTool("osv-scanner"), 0o700); err != nil { + t.Fatal(err) + } + writeSupplyFile(t, repo.Root, "Cargo.lock", lock) + writeSupplyFile(t, repo.Root, "Cargo.toml", "[package]\nname='private-workspace'\nversion='1.0.0'\n[dependencies]\nprivate={git='https://private.example.test/repo', version='=1.0.0', rev='"+strings.Repeat("a", 40)+"'}\n") + return repo +} + +func cargoOSVMixedLock() string { + public := reviewCargoLock("registry-crate", "1.0.0", repository.CargoCratesIOSource) + newer := strings.TrimPrefix(reviewCargoLock("registry-crate", "2.0.0", repository.CargoCratesIOSource), "version=4\n") + git := strings.TrimPrefix(reviewCargoLock("registry-crate", "1.0.0", "git+https://private.example.test/repo#"+strings.Repeat("a", 40)), "version=4\n") + return public + newer + git + "[[package]]\nname='private-workspace'\nversion='1.0.0'\n" +} + +func mutateCargoOSVReport(report *osvReport, mutation string) { + result := &report.Results[0] + switch mutation { + case "missing": + result.Packages = result.Packages[:1] + case "duplicate": + result.Packages = append(result.Packages, result.Packages[0]) + case "foreign-version": + result.Packages[0].Package.Version = "3.0.0" + case "foreign-ecosystem": + result.Packages[0].Package.Ecosystem = "npm" + case "foreign-source": + result.Source.Path = "other/Cargo.lock" + case "empty": + report.Results = nil + } +} + +type cargoOSVBoundary struct { + t *testing.T + scans int +} + +func (boundary *cargoOSVBoundary) Run(context.Context, string, policy.Command) error { + return fmt.Errorf("unexpected unstructured Cargo scan") +} + +func (boundary *cargoOSVBoundary) RunWithOutput(_ context.Context, root string, command policy.Command) (runner.Result, runner.Output, error) { + boundary.t.Helper() + boundary.scans++ + input, projected := argumentAfter(command.Argv, "--lockfile") + if !projected { + for _, plugin := range []string{"rust/cargolock", "rust/cargotoml"} { + index := slices.Index(command.Argv, plugin) + if index < 1 || command.Argv[index-1] != "--experimental-disable-plugins" { + boundary.t.Fatal("recursive scanner can read raw Cargo sources") + } + } + return runner.Result{}, runner.Output{Stdout: []byte(`{"results":[]}`)}, nil + } + if !slices.Contains(command.Argv, "--all-packages") || !slices.Contains(command.Argv, "--no-resolve") || !slices.Contains(command.Argv, "--no-call-analysis") { + boundary.t.Fatal("Cargo scanner may resolve, compile, or omit locked packages") + } + path, _ := strings.CutPrefix(input, "osv-scanner:") + data, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(path))) + if err != nil { + boundary.t.Fatal(err) + } + for _, private := range []string{"private.example.test", "private-workspace", strings.Repeat("a", 40)} { + if strings.Contains(string(data), private) { + boundary.t.Fatal("private Cargo source reached a public scanner") + } + } + return runner.Result{ExitStatus: 1}, runner.Output{Stdout: cargoOSVResponse(boundary.t, data, path)}, fmt.Errorf("advisories found") +} + +func cargoOSVResponse(t *testing.T, projection []byte, path string) []byte { + t.Helper() + var report osvReport + if err := json.Unmarshal(projection, &report); err != nil { + t.Fatal(err) + } + packages := report.Results[0].Packages + if len(packages) != 2 || packages[0].Package.Name != "registry-crate" || packages[1].Package.Name != "registry-crate" || packages[0].Package.Version == packages[1].Package.Version { + t.Fatalf("complete Cargo lock inventory lost public versions: %+v", packages) + } + for index := range packages { + if packages[index].Package.Version == "1.0.0" { + packages[index].Groups = []osvGroup{{IDs: []string{"RUSTSEC-2026-0001", "GHSA-cargo-example"}, MaxSeverity: "low"}} + } + } + report.Results[0].Source.Path = path + data, err := json.Marshal(report) + if err != nil { + t.Fatal(err) + } + return data +} diff --git a/internal/supplychain/osv_uv.go b/internal/supplychain/osv_uv.go index 752de8c6..edee9fd3 100644 --- a/internal/supplychain/osv_uv.go +++ b/internal/supplychain/osv_uv.go @@ -20,34 +20,38 @@ type osvScan struct { Projection []byte } -type onlineUVInput struct { +type osvLockInput struct { Scope string Packages []resolvedPackage } -func onlineUVInputs(repo repository.Repository) ([]onlineUVInput, error) { +func onlineUVInputs(repo repository.Repository) ([]osvLockInput, error) { + return osvLockInputs(repo, "uv.lock", parseUVLock) +} + +func osvLockInputs(repo repository.Repository, name string, parse func([]byte, string) ([]resolvedPackage, error)) ([]osvLockInput, error) { files, err := repo.RawFiles() if err != nil { - return nil, fmt.Errorf("enumerate resolved Python lockfiles: %w", err) + return nil, fmt.Errorf("enumerate resolved %s inventory: %w", name, err) } - inputs := []onlineUVInput{} + inputs := []osvLockInput{} remaining := 32 << 20 for _, scope := range files { - if filepath.Base(scope) != "uv.lock" || repo.IsExcluded(scope) { + if filepath.Base(scope) != name || repo.IsExcluded(scope) { continue } if len(inputs) >= 512 || remaining <= 0 { - return nil, fmt.Errorf("resolved Python inventory exceeds its input limit") + return nil, fmt.Errorf("resolved %s inventory exceeds its input limit", name) } data, err := readGitEvidenceFile(repo, scope, int64(min(16<<20, remaining))) if err != nil { - return nil, fmt.Errorf("read resolved Python inventory at %s: %w", scope, err) + return nil, fmt.Errorf("read resolved inventory at %s: %w", scope, err) } - packages, err := parseUVLock(data, scope) + packages, err := parse(data, scope) if err != nil { - return nil, fmt.Errorf("resolved Python inventory at %s is malformed", scope) + return nil, fmt.Errorf("resolved inventory at %s is malformed or has unsupported sources", scope) } - inputs = append(inputs, onlineUVInput{Scope: scope, Packages: packages}) + inputs = append(inputs, osvLockInput{Scope: scope, Packages: packages}) remaining -= len(data) } return inputs, nil @@ -62,24 +66,20 @@ func gitLockPackages(packages []resolvedPackage) bool { return false } -func osvUVScans(repo repository.Repository, inputs []onlineUVInput) ([]osvScan, error) { +func osvUVScans(repo repository.Repository, inputs []osvLockInput) ([]osvScan, error) { scans := []osvScan{} for _, input := range inputs { if !osvCoversScope(repo.Config, input.Scope) || (repo.Config.SupplyChain.GitEvidence.Required && gitLockPackages(input.Packages)) { continue } - projection, err := publicUVProjection(input) + projection, err := publicLockProjection(input, "PyPI", "https://pypi.org/simple") if err != nil { return nil, err } if len(projection) == 0 { continue } - path := osvInputDirectory + "/" + gitEvidenceDigest(projection) + ".osv-scanner.json" - command := osvCommand(repo, "osv-uv-"+safeName(input.Scope), ".") - command.Argv = append(command.Argv, "--no-resolve", "--lockfile", "osv-scanner:"+path) - command.Paths = []string{input.Scope} - scans = append(scans, osvScan{Command: command, Root: ".", Scope: input.Scope, InputPath: path, Projection: projection}) + scans = append(scans, osvProjectionScan(repo, "uv", input.Scope, projection)) } return scans, nil } @@ -93,7 +93,7 @@ func osvCoversScope(config policy.Config, scope string) bool { return false } -func rootHasUVInput(root string, inputs []onlineUVInput) bool { +func rootHasOSVInput(root string, inputs []osvLockInput) bool { for _, input := range inputs { if scopeInsideOSVRoot(input.Scope, root) { return true @@ -106,17 +106,17 @@ func scopeInsideOSVRoot(scope, root string) bool { return root == "." || strings.HasPrefix(scope, strings.TrimSuffix(root, "/")+"/") } -func publicUVProjection(input onlineUVInput) ([]byte, error) { +func publicLockProjection(input osvLockInput, ecosystem, registry string) ([]byte, error) { packages := []osvPackageResult{} for _, item := range input.Packages { if item.Source.Kind == "local" || item.Source.Kind == "git" { continue } - if item.Source.Kind != "registry" || !publicPythonRegistry(item.Source.Registry) { - return nil, fmt.Errorf("resolved Python inventory at %s cannot be sent to a public scanner; provide authenticated coverage for its non-public or unsupported sources", input.Scope) + if item.Source.Kind != "registry" || strings.TrimSuffix(item.Source.Registry, "/") != registry { + return nil, fmt.Errorf("resolved inventory at %s cannot be sent to a public scanner; provide authenticated coverage for its non-public or unsupported sources", input.Scope) } observed := osvPackageResult{} - observed.Package.Name, observed.Package.Version, observed.Package.Ecosystem = item.Name, item.Version, "PyPI" + observed.Package.Name, observed.Package.Version, observed.Package.Ecosystem = item.Name, item.Version, ecosystem packages = append(packages, observed) } if len(packages) == 0 { @@ -127,6 +127,14 @@ func publicUVProjection(input onlineUVInput) ([]byte, error) { return json.Marshal(osvReport{Results: []osvResult{result}}) } +func osvProjectionScan(repo repository.Repository, kind, scope string, projection []byte) osvScan { + path := osvInputDirectory + "/" + gitEvidenceDigest(projection) + ".osv-scanner.json" + command := osvCommand(repo, "osv-"+kind+"-"+safeName(scope), ".") + command.Argv = append(command.Argv, "--no-resolve", "--lockfile", "osv-scanner:"+path) + command.Paths = []string{scope} + return osvScan{Command: command, Root: ".", Scope: scope, InputPath: path, Projection: projection} +} + func publicPythonRegistry(registry string) bool { return strings.TrimSuffix(registry, "/") == "https://pypi.org/simple" } diff --git a/internal/supplychain/supplychain.go b/internal/supplychain/supplychain.go index 0119474b..6f5c9819 100644 --- a/internal/supplychain/supplychain.go +++ b/internal/supplychain/supplychain.go @@ -114,7 +114,9 @@ func CoverageFindings(repo repository.Repository, files []string) []policy.Findi case "python-uv": findings = append(findings, manifestCapabilityFindings(repo, files, path, ecosystem, []string{"lock-sync"})...) case "cargo": - findings = append(findings, manifestCapabilityFindings(repo, files, path, ecosystem, []string{"security"})...) + if !osvCoversScope(repo.Config, path) { + findings = append(findings, manifestCapabilityFindings(repo, files, path, ecosystem, []string{"security"})...) + } case "unsupported": capabilities := []string{"dependency-policy", "lock-sync", "release-age", "security"} findings = append(findings, manifestCapabilityFindings(repo, files, path, ecosystem, capabilities)...) diff --git a/schema/code-polishy-supply-chain.schema.json b/schema/code-polishy-supply-chain.schema.json index cbddf729..1b762ce5 100644 --- a/schema/code-polishy-supply-chain.schema.json +++ b/schema/code-polishy-supply-chain.schema.json @@ -157,7 +157,7 @@ "required": ["id", "ecosystem", "advisory", "package"], "properties": { "id": { "$ref": "code-polishy.schema.json#/$defs/identifier" }, - "ecosystem": { "enum": ["go", "npm", "pnpm", "pypi"] }, + "ecosystem": { "enum": ["go", "npm", "pnpm", "pypi", "crates.io"] }, "advisory": { "$ref": "code-polishy.schema.json#/$defs/nonemptyString" }, From 0102b457e9c27b5a1584f646bf9f45568b3493df Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 01:29:55 +0000 Subject: [PATCH 12/49] feat: authenticate Cargo archive license evidence --- .code-polishy.json | 7 + docs/design/execution-observability.md | 7 + docs/design/rust-inputs.md | 34 +++ docs/plans/code-polishy-rust-support-plan.md | 13 +- .../repository/contained_directory_test.go | 52 +++++ internal/repository/contained_file.go | 49 ++++- internal/supplychain/cargo_archive.go | 91 ++++++++ internal/supplychain/cargo_archive_test.go | 97 +++++++++ internal/supplychain/cargo_licenses.go | 171 +++++++++++++++ .../cargo_licenses_runtime_test.go | 46 ++++ internal/supplychain/cargo_licenses_test.go | 200 ++++++++++++++++++ internal/supplychain/supplychain.go | 3 + 12 files changed, 765 insertions(+), 5 deletions(-) create mode 100644 internal/repository/contained_directory_test.go create mode 100644 internal/supplychain/cargo_archive.go create mode 100644 internal/supplychain/cargo_archive_test.go create mode 100644 internal/supplychain/cargo_licenses.go create mode 100644 internal/supplychain/cargo_licenses_runtime_test.go create mode 100644 internal/supplychain/cargo_licenses_test.go diff --git a/.code-polishy.json b/.code-polishy.json index 64603777..720e7273 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -57,6 +57,11 @@ "internal/supplychain/cargo_test.go", "internal/supplychain/cargo_lock.go", "internal/supplychain/cargo_lock_test.go", + "internal/supplychain/cargo_archive.go", + "internal/supplychain/cargo_archive_test.go", + "internal/supplychain/cargo_licenses.go", + "internal/supplychain/cargo_licenses_test.go", + "internal/supplychain/cargo_licenses_runtime_test.go", "internal/supplychain/cargo_resolution.go", "internal/supplychain/cargo_resolution_test.go", "internal/supplychain/cargo_release_age.go", @@ -230,6 +235,8 @@ "internal/repository/python_project.go", "internal/repository/repository.go", "internal/repository/control_inputs.go", + "internal/repository/contained_file.go", + "internal/repository/contained_directory_test.go", "internal/repository/test_ownership.go", "internal/engine/engine.go", "internal/engine/execution_telemetry.go", diff --git a/docs/design/execution-observability.md b/docs/design/execution-observability.md index 4e811f22..03ddbdd9 100644 --- a/docs/design/execution-observability.md +++ b/docs/design/execution-observability.md @@ -83,3 +83,10 @@ inputs while remaining in the protected outcome inventory. Ordinary content read and all provider edit targets retain the strict regular-file boundary, including every parent component; a link cannot become an authorized write route. No new asset-link configuration is required. + +Bounded directory inventory uses the same canonical, contained, non-linked parent +checks as regular-file reads. Opening must retain the inspected directory's +identity. It returns direct entries only and fails if the selected entry budget +is exceeded; callers still validate each selected file through the regular-file +boundary. Cargo's archive-license reader uses this to discover physical cache +directories without interpreting their opaque names as registry authority. diff --git a/docs/design/rust-inputs.md b/docs/design/rust-inputs.md index 41d72eed..d4726eea 100644 --- a/docs/design/rust-inputs.md +++ b/docs/design/rust-inputs.md @@ -131,6 +131,40 @@ use the `crates.io` ecosystem and cannot approve a different lock or version. The pinned runtime contract is exercised against a temporary synthetic offline advisory database, without querying a live provider. +## Configured license policy + +When `supplyChain.allowedLicenses` is configured, installed-license verification +checks every public crates.io package in each complete lock, including inactive +optional and platform dependencies. It reads the published Cargo.toml from cached +`.crate` archives under the governed Cargo home. The lock's authoritative archive +checksum authenticates the exact bytes; opaque registry directory names never +establish source identity. Each candidate archive is read and authenticated once +per operation, and only observations matching the requested lock checksum can +satisfy that lock. Separate lock scopes retain separate findings. + +This follows the pinned Cargo cache layout described by [Cargo Home](https://doc.rust-lang.org/cargo/guide/cargo-home.html), +whose internal layout is version-bound rather than a stable public API. Inputs +must be contained regular files beneath physical cache directories. Reads and +decompression are bounded: 256 registry directories, 128 MiB per archive, 1 GiB of +archive input per operation, 512 MiB per decompressed stream, 65,536 entries and +8 MiB per manifest. The shared license-facts timeout also applies. Missing, +corrupt, ambiguous or mismatched manifests are missing license coverage, not +permission to use an installed directory's unverified metadata. + +The common SPDX evaluator decides declared expressions, including `OR`, `AND` and +`WITH`. It does not guess a license from `license-file` names or infer one from +text. Cargo resolves workspace inheritance when it produces the published +manifest; a real pinned-Cargo packaging fixture verifies that boundary without +running build scripts. Local packages and Git contents do not become public +registry license claims; Git follows its separate optional assessment policy. + +Like Node, dependency review defers installed-license evidence until after the +candidate has been prepared. No license policy means no license scan. Ordinary +supply-chain verification reads prepared archives without downloading, unpacking +or executing them. Missing archives can be prepared with the pinned Cargo's +[`fetch --locked`](https://doc.rust-lang.org/cargo/commands/cargo-fetch.html) using +the governed Cargo home; this includes all lock targets without compiling code. + ## Shader inputs WGSL is executable shader source with its own built-in language identifier, diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 5dec400d..4a6c2b40 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -123,7 +123,7 @@ target-specific and workspace declaration tables, checks exact registry and Git pins, contains local paths, and validates versioned lock identities/checksums. It keeps Git and registry coordinates separate and retains explicit failures for unsupported source substitutions. These declaration and lock readers are -data-only. Complete Git evidence and license integration remain in progress. +data-only. Signed Cargo Git evidence remains in progress. Native Cargo lock verification now uses bounded, complete resolved metadata through the ordinary supply-chain runner. Online verification may fetch already locked @@ -155,7 +155,16 @@ compiler call analysis are disabled. The report must account for every projected version, and exact `crates.io` vulnerability assessments preserve the original lock scope. Tests cover source isolation, incomplete reports, scanner exits, assessment identity and the real pinned scanner with a temporary offline advisory -database. Signed Cargo Git inventories and license integration remain unfinished. +database. Signed Cargo Git inventories remain unfinished. + +Configured Cargo license policy now authenticates cached `.crate` archives against +the complete lock inventory and applies the common SPDX evaluator to their +published manifests. It covers inactive optional/platform releases, preserves lock +scopes, and never substitutes Git or local metadata for public crate evidence. +Missing, corrupt, linked, ambiguous and oversized inputs fail coverage. Pre-install +dependency review defers installed-license checks, matching the Node workflow; +normal verification uses prepared archives without downloading or executing code. +Pinned Cargo packaging fixtures cover inherited workspace license metadata. ## Phase 0: define the complete coverage contract diff --git a/internal/repository/contained_directory_test.go b/internal/repository/contained_directory_test.go new file mode 100644 index 00000000..fe7a56f0 --- /dev/null +++ b/internal/repository/contained_directory_test.go @@ -0,0 +1,52 @@ +package repository + +import ( + "os" + "path/filepath" + "slices" + "testing" +) + +func TestContainedDirectoryReadsOnlyBoundedDirectEntries(t *testing.T) { + t.Parallel() + repo := Repository{Root: t.TempDir()} + writeFile(t, repo.Root, "cache/one", "one") + writeFile(t, repo.Root, "cache/nested/two", "two") + entries, err := repo.ReadContainedDirectory("cache", 2) + if err != nil || len(entries) != 2 { + t.Fatalf("contained entries = %+v, %v", entries, err) + } + names := []string{entries[0].Name(), entries[1].Name()} + slices.Sort(names) + if !slices.Equal(names, []string{"nested", "one"}) { + t.Fatalf("directory read expanded or omitted children: %v", names) + } + for _, limit := range []int{0, 1} { + if _, err := repo.ReadContainedDirectory("cache", limit); err == nil { + t.Fatalf("entry limit %d was ignored", limit) + } + } + if _, err := repo.ReadContainedDirectory("cache/one", 2); err == nil { + t.Fatal("a file was accepted as a directory") + } + if _, err := repo.ReadContainedFile("cache", 1024); err == nil { + t.Fatal("a directory was accepted as a regular file") + } +} + +func TestContainedDirectoryRejectsNoncanonicalAndLinkedParents(t *testing.T) { + t.Parallel() + repo := Repository{Root: t.TempDir()} + writeFile(t, repo.Root, "cache/nested/one", "one") + if err := os.Symlink("cache", filepath.Join(repo.Root, "linked")); err != nil { + t.Fatal(err) + } + if err := os.Symlink(t.TempDir(), filepath.Join(repo.Root, "outside")); err != nil { + t.Fatal(err) + } + for _, path := range []string{"../cache", "cache/../cache", "cache\\nested", "linked", "linked/nested", "outside"} { + if _, err := repo.ReadContainedDirectory(path, 2); err == nil { + t.Fatalf("unsafe directory %q was read", path) + } + } +} diff --git a/internal/repository/contained_file.go b/internal/repository/contained_file.go index c63ad257..7e53193c 100644 --- a/internal/repository/contained_file.go +++ b/internal/repository/contained_file.go @@ -25,6 +25,17 @@ func (repo Repository) ReadContainedFile(path string, limit int) ([]byte, error) } func (repo Repository) containedRegularFileInfo(root *os.Root, path string) (os.FileInfo, error) { + info, err := repo.containedPathInfo(root, path) + if err != nil { + return nil, err + } + if !info.Mode().IsRegular() { + return nil, fmt.Errorf("input path must name a regular file") + } + return info, nil +} + +func (repo Repository) containedPathInfo(root *os.Root, path string) (os.FileInfo, error) { normalized, err := repo.NormalizePath(path) if err != nil || normalized != path || strings.ContainsAny(path, "\\\x00:") { return nil, fmt.Errorf("input path must be a canonical contained repository path") @@ -43,12 +54,44 @@ func (repo Repository) containedRegularFileInfo(root *os.Root, path string) (os. return nil, fmt.Errorf("input path parent is not a directory") } } - if !info.Mode().IsRegular() { - return nil, fmt.Errorf("input path must name a regular file") - } return info, nil } +func (repo Repository) ReadContainedDirectory(path string, limit int) ([]os.DirEntry, error) { + if limit <= 0 { + return nil, fmt.Errorf("directory entry limit must be positive") + } + root, err := os.OpenRoot(repo.Root) + if err != nil { + return nil, err + } + defer root.Close() + info, err := repo.containedPathInfo(root, path) + if err != nil { + return nil, err + } + if !info.IsDir() { + return nil, fmt.Errorf("input path must name a directory") + } + file, err := root.Open(filepath.FromSlash(path)) + if err != nil { + return nil, err + } + defer file.Close() + opened, err := file.Stat() + if err != nil || !os.SameFile(info, opened) { + return nil, fmt.Errorf("directory changed while opening") + } + entries, err := file.ReadDir(limit + 1) + if err != nil && err != io.EOF { + return nil, err + } + if len(entries) > limit { + return nil, fmt.Errorf("directory exceeds %d entries", limit) + } + return entries, nil +} + func readContainedFile(root *os.Root, path string, expected os.FileInfo, limit int) ([]byte, error) { if expected.Size() > int64(limit) { return nil, fmt.Errorf("input exceeds %d bytes", limit) diff --git a/internal/supplychain/cargo_archive.go b/internal/supplychain/cargo_archive.go new file mode 100644 index 00000000..0bc85fef --- /dev/null +++ b/internal/supplychain/cargo_archive.go @@ -0,0 +1,91 @@ +package supplychain + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "errors" + "fmt" + "io" + "path" + "strings" + + "github.com/pelletier/go-toml/v2" + "github.com/riteofstring/code-polishy/internal/repository" +) + +func cargoArchiveLicense(ctx context.Context, data []byte, pkg repository.CargoLockedPackage) (string, error) { + compressed, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return "", fmt.Errorf("crate archive is not readable gzip data") + } + defer compressed.Close() + limited := &io.LimitedReader{R: compressed, N: 512<<20 + 1} + archive := tar.NewReader(limited) + manifest, err := cargoArchiveManifest(ctx, archive, pkg.Name+"-"+pkg.Version+"/Cargo.toml") + if limited.N == 0 { + return "", fmt.Errorf("crate archive exceeds its decompressed byte budget") + } + if err != nil { + return "", err + } + if _, err := io.Copy(io.Discard, limited); err != nil || limited.N == 0 { + return "", fmt.Errorf("crate archive is malformed or exceeds its decompressed byte budget") + } + return cargoPublishedLicense(manifest, pkg) +} + +func cargoArchiveManifest(ctx context.Context, archive *tar.Reader, expected string) ([]byte, error) { + var manifest []byte + for count := 0; count < 65536; count++ { + if err := ctx.Err(); err != nil { + return nil, err + } + header, err := archive.Next() + if errors.Is(err, io.EOF) { + if manifest == nil { + return nil, fmt.Errorf("crate archive omitted its published Cargo.toml") + } + return manifest, nil + } + if err != nil { + return nil, fmt.Errorf("crate archive contains unreadable entries") + } + if path.Clean(strings.ReplaceAll(header.Name, "\\", "/")) != expected { + continue + } + if err := cargoManifestHeader(header, expected, manifest != nil); err != nil { + return nil, err + } + manifest, err = io.ReadAll(archive) + if err != nil { + return nil, fmt.Errorf("crate archive manifest is unreadable") + } + } + return nil, fmt.Errorf("crate archive exceeds 65536 entries") +} + +func cargoManifestHeader(header *tar.Header, expected string, repeated bool) error { + if header.Name != expected || header.Typeflag != tar.TypeReg || repeated || header.Size > 8<<20 { + return fmt.Errorf("crate archive has an ambiguous, linked or oversized Cargo.toml") + } + return nil +} + +func cargoPublishedLicense(data []byte, expected repository.CargoLockedPackage) (string, error) { + var manifest struct { + Package struct { + Name string `toml:"name"` + Version string `toml:"version"` + License string `toml:"license"` + } `toml:"package"` + } + if err := toml.Unmarshal(data, &manifest); err != nil { + return "", fmt.Errorf("published crate license metadata is malformed") + } + if manifest.Package.Name != expected.Name || manifest.Package.Version != expected.Version { + return "", fmt.Errorf("published crate manifest does not match the locked package identity") + } + return manifest.Package.License, nil +} diff --git a/internal/supplychain/cargo_archive_test.go b/internal/supplychain/cargo_archive_test.go new file mode 100644 index 00000000..f9069d7e --- /dev/null +++ b/internal/supplychain/cargo_archive_test.go @@ -0,0 +1,97 @@ +package supplychain + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/repository" +) + +func TestCargoArchiveRejectsAmbiguousOrMismatchedPublishedManifests(t *testing.T) { + t.Parallel() + manifest := "[package]\nname='crate'\nversion='1.0.0'\nlicense='MIT'\n" + valid := cargoTestEntry{Name: "crate-1.0.0/Cargo.toml", Content: manifest} + tests := map[string][]cargoTestEntry{ + "missing": {{Name: "crate-1.0.0/Cargo.toml.orig", Content: manifest}}, + "duplicate": {valid, valid}, + "alias": {valid, {Name: "crate-1.0.0/./Cargo.toml", Content: manifest}}, + "backslash alias": {{Name: "crate-1.0.0\\Cargo.toml", Content: manifest}}, + "symbolic link": {{Name: valid.Name, Kind: tar.TypeSymlink}}, + "hard link": {{Name: valid.Name, Kind: tar.TypeLink}}, + "wrong package": {{Name: valid.Name, Content: strings.Replace(manifest, "name='crate'", "name='other'", 1)}}, + "wrong version": {{Name: valid.Name, Content: strings.Replace(manifest, "1.0.0", "2.0.0", 1)}}, + "malformed TOML": {{Name: valid.Name, Content: "[package"}}, + "inherited license": {{Name: valid.Name, Content: "[package]\nname='crate'\nversion='1.0.0'\nlicense.workspace=true\n"}}, + } + for name, entries := range tests { + t.Run(name, func(t *testing.T) { + data := cargoArchiveFixture(t, entries) + pkg := repository.CargoLockedPackage{Name: "crate", Version: "1.0.0"} + if license, err := cargoArchiveLicense(t.Context(), data, pkg); err == nil { + t.Fatalf("%s archive established license %q", name, license) + } + }) + } +} + +func TestCargoArchiveRejectsTruncationAndArchiveResourceExhaustion(t *testing.T) { + t.Parallel() + pkg := repository.CargoLockedPackage{Name: "crate", Version: "1.0.0"} + manifest := cargoTestEntry{Name: "crate-1.0.0/Cargo.toml", Content: "[package]\nname='crate'\nversion='1.0.0'\nlicense='MIT'\n"} + complete := cargoArchiveFixture(t, []cargoTestEntry{manifest}) + entries := make([]cargoTestEntry, 65537) + entries[0] = manifest + for index := 1; index < len(entries); index++ { + entries[index] = cargoTestEntry{Name: "crate-1.0.0/empty"} + } + for name, data := range map[string][]byte{ + "not gzip": []byte("not an archive"), "truncated gzip": complete[:len(complete)-4], + "large manifest": cargoArchiveFixture(t, []cargoTestEntry{{Name: manifest.Name, Content: manifest.Content + strings.Repeat(" ", 8<<20)}}), + "entry count": cargoArchiveFixture(t, entries), + } { + if _, err := cargoArchiveLicense(t.Context(), data, pkg); err == nil { + t.Fatalf("%s archive passed bounded license decoding", name) + } + } + ctx, cancel := context.WithCancel(t.Context()) + cancel() + if _, err := cargoArchiveLicense(ctx, complete, pkg); err == nil { + t.Fatal("canceled archive inspection passed") + } +} + +func TestCargoArchiveChecksTheDecompressedStreamBudget(t *testing.T) { + t.Parallel() + buffer := bytes.Buffer{} + compressed := gzip.NewWriter(&buffer) + archive := tar.NewWriter(compressed) + if err := archive.WriteHeader(&tar.Header{Name: "crate-1.0.0/oversized", Size: 513 << 20, Mode: 0o600}); err != nil { + t.Fatal(err) + } + block := make([]byte, 1<<20) + for index := 0; index < 513; index++ { + if _, err := archive.Write(block); err != nil { + t.Fatal(err) + } + } + manifest := "[package]\nname='crate'\nversion='1.0.0'\nlicense='MIT'\n" + if err := archive.WriteHeader(&tar.Header{Name: "crate-1.0.0/Cargo.toml", Size: int64(len(manifest)), Mode: 0o600}); err != nil { + t.Fatal(err) + } + if _, err := archive.Write([]byte(manifest)); err != nil { + t.Fatal(err) + } + if err := archive.Close(); err != nil { + t.Fatal(err) + } + if err := compressed.Close(); err != nil { + t.Fatal(err) + } + if _, err := cargoArchiveLicense(t.Context(), buffer.Bytes(), repository.CargoLockedPackage{Name: "crate", Version: "1.0.0"}); err == nil || !strings.Contains(err.Error(), "decompressed byte budget") { + t.Fatalf("compressed archive budget was not enforced: %v", err) + } +} diff --git a/internal/supplychain/cargo_licenses.go b/internal/supplychain/cargo_licenses.go new file mode 100644 index 00000000..5d5b6c5a --- /dev/null +++ b/internal/supplychain/cargo_licenses.go @@ -0,0 +1,171 @@ +package supplychain + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" +) + +const cargoArchiveCache = ".tools/cargo-home/registry/cache" + +type cargoLicenseObservation struct { + License string + Err error +} + +type cargoArchiveObservation struct { + cargoLicenseObservation + Checksum string +} + +type cargoLicenseInventory struct { + Archives map[string]cargoArchiveObservation + Remaining int +} + +func cargoLicenseFindings(ctx context.Context, repo repository.Repository, files []string) []policy.Finding { + allowed := licenseAllowList(repo.Config.SupplyChain.AllowedLicenses) + if len(allowed) == 0 { + return nil + } + factsContext, cancel := context.WithTimeout(ctx, licenseFactsBudget) + defer cancel() + observed := map[[3]string]cargoLicenseObservation{} + inventory := cargoLicenseInventory{Archives: map[string]cargoArchiveObservation{}, Remaining: 1 << 30} + findings := []policy.Finding{} + for _, path := range files { + if filepath.Base(path) != "Cargo.lock" { + continue + } + lock, err := repo.ReadCargoLock(path) + if err != nil { + continue + } + for _, pkg := range lock.Packages { + if pkg.Source != repository.CargoCratesIOSource { + continue + } + key := [3]string{pkg.Name, pkg.Version, strings.ToLower(pkg.Checksum)} + observation, found := observed[key] + if !found { + license, err := cachedCargoLicense(factsContext, repo, pkg, &inventory) + observation = cargoLicenseObservation{License: license, Err: err} + observed[key] = observation + } + if finding := cargoLicenseFinding(path, pkg, observation, allowed); finding != nil { + findings = append(findings, *finding) + } + } + } + return uniqueFindings(findings) +} + +func cargoLicenseFinding(scope string, pkg repository.CargoLockedPackage, observation cargoLicenseObservation, allowed map[string]bool) *policy.Finding { + finding := policy.Finding{Path: scope, Subject: pkg.Name + "@" + pkg.Version} + if observation.Err != nil { + finding.Check, finding.Message = "supplyChain.licenseCoverage", observation.Err.Error() + return &finding + } + if err := licenseAdmitted(observation.License, allowed); err != nil { + finding.Check, finding.Message = "supplyChain.dependencyLicense", err.Error() + return &finding + } + return nil +} + +func cachedCargoLicense(ctx context.Context, repo repository.Repository, pkg repository.CargoLockedPackage, inventory *cargoLicenseInventory) (string, error) { + if !cargoArchiveName(pkg.Name) { + return "", fmt.Errorf("public crate name is not a safe registry identity") + } + if err := ctx.Err(); err != nil { + return "", err + } + directories, err := cargoArchiveDirectories(repo) + if err != nil { + return "", err + } + var problem error + for _, directory := range directories { + if err := ctx.Err(); err != nil { + return "", err + } + path := directory + "/" + pkg.Name + "-" + pkg.Version + ".crate" + observed := inventory.archive(ctx, repo, path, pkg) + if errors.Is(observed.Err, os.ErrNotExist) { + continue + } + if observed.Checksum == "" { + problem = observed.Err + continue + } + if !strings.EqualFold(observed.Checksum, pkg.Checksum) { + continue + } + return observed.License, observed.Err + } + return "", errors.Join(fmt.Errorf("no cached crate archive matches the Cargo.lock checksum; prepare the governed Cargo home with pinned cargo fetch --locked before installed-license verification"), problem) +} + +func (inventory *cargoLicenseInventory) archive(ctx context.Context, repo repository.Repository, path string, pkg repository.CargoLockedPackage) cargoArchiveObservation { + if observed, found := inventory.Archives[path]; found { + return observed + } + data, err := readCargoArchive(repo, path, &inventory.Remaining) + observed := cargoArchiveObservation{cargoLicenseObservation: cargoLicenseObservation{Err: err}} + if err == nil { + checksum := sha256.Sum256(data) + observed.Checksum = hex.EncodeToString(checksum[:]) + observed.License, observed.Err = cargoArchiveLicense(ctx, data, pkg) + } + inventory.Archives[path] = observed + return observed +} + +func readCargoArchive(repo repository.Repository, path string, remaining *int) ([]byte, error) { + if *remaining <= 0 { + return nil, fmt.Errorf("cargo license inventory exceeds its archive byte budget") + } + data, err := repo.ReadContainedFile(path, min(128<<20, *remaining)) + *remaining -= len(data) + return data, err +} + +func cargoArchiveDirectories(repo repository.Repository) ([]string, error) { + entries, err := repo.ReadContainedDirectory(cargoArchiveCache, 256) + if errors.Is(err, os.ErrNotExist) { + return nil, nil + } + if err != nil { + return nil, err + } + directories := []string{} + for _, entry := range entries { + if !entry.IsDir() { + continue + } + directories = append(directories, cargoArchiveCache+"/"+entry.Name()) + } + slices.Sort(directories) + return directories, nil +} + +func cargoArchiveName(name string) bool { + if name == "" || len(name) > 64 { + return false + } + for _, character := range name { + if !strings.ContainsRune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_", character) { + return false + } + } + return true +} diff --git a/internal/supplychain/cargo_licenses_runtime_test.go b/internal/supplychain/cargo_licenses_runtime_test.go new file mode 100644 index 00000000..80bbc362 --- /dev/null +++ b/internal/supplychain/cargo_licenses_runtime_test.go @@ -0,0 +1,46 @@ +package supplychain + +import ( + "path/filepath" + "testing" + + "github.com/riteofstring/code-polishy/internal/runner" + "github.com/riteofstring/code-polishy/internal/rustfacts" +) + +func TestCargoLicensesReadPinnedCargoPublishedWorkspaceMetadata(t *testing.T) { + t.Parallel() + repo := supplyRepository(t) + root, err := filepath.Abs("../..") + if err != nil { + t.Fatal(err) + } + repo.PolicyRoot = root + if err := rustfacts.ToolProblem(repo); err != nil { + t.Fatal(err) + } + writeSupplyFile(t, repo.Root, "Cargo.toml", "[workspace]\nmembers=['crates/package']\nresolver='3'\n[workspace.package]\nlicense='MIT OR Apache-2.0'\n") + writeSupplyFile(t, repo.Root, "crates/package/Cargo.toml", "[package]\nname='crate'\nversion='1.0.0'\nedition='2024'\nlicense.workspace=true\n") + writeSupplyFile(t, repo.Root, "crates/package/src/lib.rs", "pub fn answer() -> i32 { 42 }\n") + writeSupplyFile(t, repo.Root, "crates/package/build.rs", "compile_error!(\"package verification must not execute build scripts\");\n") + writeSupplyFile(t, repo.Root, "Cargo.lock", "version=4\n[[package]]\nname='crate'\nversion='1.0.0'\n") + command := rustfacts.Command(repo, "package-license-fixture", "cargo", "package", "--manifest-path", "crates/package/Cargo.toml", "--frozen", "--allow-dirty", "--no-verify") + if _, output, err := (runner.OSRunner{}).RunStructured(t.Context(), repo.Root, command); err != nil { + t.Fatalf("pinned Cargo packaging failed: %v, %s", err, output.Stderr) + } + data, err := repo.ReadContainedFile(".tools/rust-target/package/crate-1.0.0.crate", 8<<20) + if err != nil { + t.Fatal(err) + } + pkg := writeCargoTestArchive(t, repo, "opaque-registry-id", "crate", "1.0.0", data) + writeSupplyFile(t, repo.Root, "Cargo.lock", cargoLicenseTestLock(pkg)) + repo.Config.SupplyChain.AllowedLicenses = []string{"MIT"} + if findings := cargoLicenseFindings(t.Context(), repo, []string{"Cargo.lock"}); len(findings) != 0 { + t.Fatalf("published inherited license was not resolved by Cargo: %+v", findings) + } + repo.Config.SupplyChain.AllowedLicenses = []string{"BSD-3-Clause"} + findings := cargoLicenseFindings(t.Context(), repo, []string{"Cargo.lock"}) + if len(findings) != 1 || findings[0].Check != "supplyChain.dependencyLicense" || findings[0].Subject != "crate@1.0.0" { + t.Fatalf("published Cargo license did not reach shared policy: %+v", findings) + } +} diff --git a/internal/supplychain/cargo_licenses_test.go b/internal/supplychain/cargo_licenses_test.go new file mode 100644 index 00000000..f9b6e84f --- /dev/null +++ b/internal/supplychain/cargo_licenses_test.go @@ -0,0 +1,200 @@ +package supplychain + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/repository" +) + +func TestCargoLicensesApplySharedSPDXPolicyToEveryLockedVersion(t *testing.T) { + t.Parallel() + repo := supplyRepository(t) + repo.Config.SupplyChain.AllowedLicenses = []string{"MIT", "Apache-2.0"} + allowed := cachedCargoFixture(t, repo, "registry-one", "crate", "1.0.0", "MIT OR Apache-2.0") + refused := cachedCargoFixture(t, repo, "registry-one", "crate", "2.0.0", "AGPL-3.0-only") + lock := cargoLicenseTestLock(allowed, refused) + lock += "[[package]]\nname='crate'\nversion='1.0.0'\nsource='git+https://private.example.test/repo#" + strings.Repeat("a", 40) + "'\n" + writeSupplyFile(t, repo.Root, "Cargo.lock", lock) + writeSupplyFile(t, repo.Root, "nested/Cargo.lock", cargoLicenseTestLock(refused)) + files := []string{"Cargo.lock", "nested/Cargo.lock"} + findings := Static(t.Context(), repo, files) + if len(findings) != 2 || findings[0].Check != "supplyChain.dependencyLicense" || findings[1].Check != "supplyChain.dependencyLicense" || findings[0].Subject != "crate@2.0.0" || findings[1].Subject != "crate@2.0.0" || findings[0].Path == findings[1].Path { + t.Fatalf("complete locked license policy = %+v", findings) + } + if findings := DependencyReviewStatic(t.Context(), repo, files); len(findings) != 0 { + t.Fatalf("pre-install review enforced cached license evidence: %+v", findings) + } + repo.Config.SupplyChain.AllowedLicenses = nil + if findings := Static(t.Context(), repo, files); len(findings) != 0 { + t.Fatalf("unconfigured license policy enforced a license: %+v", findings) + } +} + +func TestCargoLicensesRequireArchiveIdentityNotCacheDirectoryNames(t *testing.T) { + t.Parallel() + repo := supplyRepository(t) + repo.Config.SupplyChain.AllowedLicenses = []string{"MIT"} + _ = cachedCargoFixture(t, repo, "aaa-private-index", "crate", "1.0.0", "AGPL-3.0-only") + public := cachedCargoFixture(t, repo, "zzz-public-index", "crate", "1.0.0", "MIT") + writeSupplyFile(t, repo.Root, cargoArchiveCache+"/cache-note", "unrelated cache metadata") + writeSupplyFile(t, repo.Root, "Cargo.lock", cargoLicenseTestLock(public)) + if findings := Static(t.Context(), repo, []string{"Cargo.lock"}); len(findings) != 0 { + t.Fatalf("matching artifact was selected by guessed registry directory: %+v", findings) + } + public.Checksum = strings.Repeat("b", 64) + writeSupplyFile(t, repo.Root, "other/Cargo.lock", cargoLicenseTestLock(public)) + findings := Static(t.Context(), repo, []string{"other/Cargo.lock", "Cargo.lock"}) + if len(findings) != 1 || findings[0].Path != "other/Cargo.lock" || findings[0].Check != "supplyChain.licenseCoverage" { + t.Fatalf("one lock's archive identity satisfied a different checksum: %+v", findings) + } +} + +func TestCargoLicensesReportMissingAndCorruptArchivesWithoutNetwork(t *testing.T) { + t.Parallel() + for _, missing := range []bool{true, false} { + repo := supplyRepository(t) + repo.Config.SupplyChain.AllowedLicenses = []string{"MIT"} + pkg := cachedCargoFixture(t, repo, "registry", "crate", "1.0.0", "MIT") + archive := filepath.Join(repo.Root, cargoArchiveCache, "registry", "crate-1.0.0.crate") + if missing { + if err := os.Remove(archive); err != nil { + t.Fatal(err) + } + } else { + writeSupplyFile(t, repo.Root, cargoArchiveCache+"/registry/crate-1.0.0.crate", "corrupt") + } + writeSupplyFile(t, repo.Root, "Cargo.lock", cargoLicenseTestLock(pkg)) + findings := Static(t.Context(), repo, []string{"Cargo.lock"}) + if len(findings) != 1 || findings[0].Check != "supplyChain.licenseCoverage" || !strings.Contains(findings[0].Message, "fetch --locked") { + t.Fatalf("missing or corrupt archive admitted: %+v", findings) + } + } +} + +func TestCargoLicensesDoNotInferSPDXFromLicenseFilenames(t *testing.T) { + t.Parallel() + repo := supplyRepository(t) + repo.Config.SupplyChain.AllowedLicenses = []string{"MIT"} + manifest := "[package]\nname='crate'\nversion='1.0.0'\nlicense-file='LICENSE-MIT'\n" + data := cargoArchiveFixture(t, []cargoTestEntry{{Name: "crate-1.0.0/Cargo.toml", Content: manifest}}) + pkg := writeCargoTestArchive(t, repo, "registry", "crate", "1.0.0", data) + writeSupplyFile(t, repo.Root, "Cargo.lock", cargoLicenseTestLock(pkg)) + findings := Static(t.Context(), repo, []string{"Cargo.lock"}) + if len(findings) != 1 || findings[0].Check != "supplyChain.dependencyLicense" { + t.Fatalf("license filename became a license expression: %+v", findings) + } +} + +func TestCargoLicensesRejectLinkedCacheInputs(t *testing.T) { + t.Parallel() + for _, parent := range []bool{true, false} { + repo := supplyRepository(t) + repo.Config.SupplyChain.AllowedLicenses = []string{"MIT"} + pkg := cachedCargoFixture(t, repo, "registry", "crate", "1.0.0", "MIT") + path := filepath.Join(repo.Root, cargoArchiveCache, "registry", "crate-1.0.0.crate") + if parent { + path = filepath.Dir(path) + } + original := path + "-original" + if parent { + original = filepath.Join(repo.Root, "relocated-registry") + } + if err := os.Rename(path, original); err != nil { + t.Fatal(err) + } + if err := os.Symlink(original, path); err != nil { + t.Fatal(err) + } + writeSupplyFile(t, repo.Root, "Cargo.lock", cargoLicenseTestLock(pkg)) + findings := Static(t.Context(), repo, []string{"Cargo.lock"}) + if len(findings) != 1 || findings[0].Check != "supplyChain.licenseCoverage" { + t.Fatalf("linked crate archive established license evidence: %+v", findings) + } + } +} + +func TestCargoLicensesHonorCancellationAndDoNotReadUnsafeNames(t *testing.T) { + t.Parallel() + repo := supplyRepository(t) + repo.Config.SupplyChain.AllowedLicenses = []string{"MIT"} + pkg := cachedCargoFixture(t, repo, "registry", "crate", "1.0.0", "MIT") + writeSupplyFile(t, repo.Root, "Cargo.lock", cargoLicenseTestLock(pkg)) + ctx, cancel := context.WithCancel(t.Context()) + cancel() + findings := cargoLicenseFindings(ctx, repo, []string{"Cargo.lock"}) + if len(findings) != 1 || !strings.Contains(findings[0].Message, "canceled") { + t.Fatalf("canceled license verification passed: %+v", findings) + } + pkg.Name = "../private" + writeSupplyFile(t, repo.Root, "Cargo.lock", cargoLicenseTestLock(pkg)) + findings = cargoLicenseFindings(t.Context(), repo, []string{"Cargo.lock"}) + if len(findings) != 1 || !strings.Contains(findings[0].Message, "safe registry identity") { + t.Fatalf("unsafe archive name was used as a path: %+v", findings) + } +} + +type cargoTestEntry struct { + Name string + Content string + Kind byte +} + +func cachedCargoFixture(t *testing.T, repo repository.Repository, registry, name, version, license string) repository.CargoLockedPackage { + t.Helper() + manifest := fmt.Sprintf("[package]\nname=%q\nversion=%q\nlicense=%q\n", name, version, license) + data := cargoArchiveFixture(t, []cargoTestEntry{{Name: name + "-" + version + "/Cargo.toml", Content: manifest}}) + return writeCargoTestArchive(t, repo, registry, name, version, data) +} + +func writeCargoTestArchive(t *testing.T, repo repository.Repository, registry, name, version string, data []byte) repository.CargoLockedPackage { + t.Helper() + writeSupplyFile(t, repo.Root, cargoArchiveCache+"/"+registry+"/"+name+"-"+version+".crate", string(data)) + checksum := sha256.Sum256(data) + return repository.CargoLockedPackage{Name: name, Version: version, Source: repository.CargoCratesIOSource, Checksum: hex.EncodeToString(checksum[:])} +} + +func cargoArchiveFixture(t *testing.T, entries []cargoTestEntry) []byte { + t.Helper() + buffer := bytes.Buffer{} + compressed := gzip.NewWriter(&buffer) + archive := tar.NewWriter(compressed) + for _, entry := range entries { + header := &tar.Header{Name: entry.Name, Size: int64(len(entry.Content)), Typeflag: entry.Kind, Mode: 0o600} + if entry.Kind == tar.TypeSymlink || entry.Kind == tar.TypeLink { + header.Linkname, header.Size = "foreign.toml", 0 + } + if err := archive.WriteHeader(header); err != nil { + t.Fatal(err) + } + if header.Size != 0 { + if _, err := archive.Write([]byte(entry.Content)); err != nil { + t.Fatal(err) + } + } + } + if err := archive.Close(); err != nil { + t.Fatal(err) + } + if err := compressed.Close(); err != nil { + t.Fatal(err) + } + return buffer.Bytes() +} + +func cargoLicenseTestLock(packages ...repository.CargoLockedPackage) string { + lock := "version=4\n" + for _, pkg := range packages { + lock += fmt.Sprintf("[[package]]\nname=%q\nversion=%q\nsource=%q\nchecksum=%q\n", pkg.Name, pkg.Version, pkg.Source, pkg.Checksum) + } + return lock +} diff --git a/internal/supplychain/supplychain.go b/internal/supplychain/supplychain.go index 6f5c9819..c5456208 100644 --- a/internal/supplychain/supplychain.go +++ b/internal/supplychain/supplychain.go @@ -69,6 +69,9 @@ func static(ctx context.Context, repo repository.Repository, files []string, inc } findings = append(findings, pnpmGovernanceFindings(ctx, repo, files)...) findings = append(findings, pnpmProjectFindings(ctx, repo, files, includeInstalledLicenses)...) + if includeInstalledLicenses { + findings = append(findings, cargoLicenseFindings(ctx, repo, files)...) + } return uniqueFindings(findings) } From 4011006a441188776a08eb0a3b1354080f5197bc Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 01:46:35 +0000 Subject: [PATCH 13/49] fix: separate Rust lint findings from build failures --- docs/design/cargo-facts.md | 21 ++- docs/plans/code-polishy-rust-support-plan.md | 10 +- internal/rustfacts/compilation.go | 14 +- internal/rustfacts/compilation_test.go | 56 ++++++-- internal/rustfacts/lints.go | 137 +++++++++++++++++++ internal/rustfacts/lints_test.go | 53 +++++++ 6 files changed, 267 insertions(+), 24 deletions(-) create mode 100644 internal/rustfacts/lints.go create mode 100644 internal/rustfacts/lints_test.go diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index b934679b..9735c806 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -80,9 +80,24 @@ invitation for the check to resolve dependencies. Checks select the manifest's ordinary Cargo packages, default features and all enabled targets, using the governed native host explicitly. They do not enable every feature or establish coverage of other target configurations. Compiler -warnings, Clippy's default lint group and unused-code lints are forbidden at the -invocation boundary. Parser-backed attribute governance and complete dead-code -coverage remain separate requirements. Project Clippy settings currently fail +warnings, Clippy's default lint group and unused-code lints use `--force-warn` at +the invocation boundary. Source `allow`, `expect`, `deny` and `forbid` attributes +cannot hide these diagnostics or turn policy lint collection into a compiler +failure. Code Polishy treats the resulting typed findings as blocking errors, +subject to its existing exact exception policy. Failed compilation always adds +a project failure, even when the compiler also reports typed source diagnostics; +an excepted lint cannot make a failed build or operational invocation pass. +Rust does not support forcing the aggregate `warnings` group. The release owns +the concrete warning-by-default lint inventory for its pinned compiler, checked +against that compiler's advertised lint interface during toolchain tests. Rust +compiler lints that default to deny retain their failure semantics; the policy does +not lower them to warnings or enable every opt-in restriction lint. +The advertised `tail-call-track-caller` warning requires unstable explicit tail +calls in this release and is not selectable on the supported stable toolchain. +Its exclusion is verified against the real compiler, not silently ignored at +runtime. +Parser-backed attribute governance and complete dead-code coverage remain +separate requirements. Project Clippy settings currently fail with an explicit unsupported-configuration diagnostic; they cannot silently override the release-owned defaults. diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 4a6c2b40..51010117 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -113,9 +113,13 @@ changed modules; add a quick suite when a new module is introduced. errors, lint defects, attempted suppression, absent/drifting locks, unavailable linkers, cancellation and build-script configuration output. Public CLI checks retain the remaining coverage failures instead of declaring full Rust support. -- Parser-backed source rules, architecture, Cargo supply-chain governance, - structured Rust test evidence, WGSL and installed-release integration remain - unfinished. Project Clippy overrides and non-native targets currently fail +- Compiler warnings and the selected Clippy/unused groups use forced warning + collection so typed policy findings remain independent of process failure. + Real compiler tests prove source lint levels cannot hide those findings and + type errors, default-denied compiler lints and build-script failures still fail. +- Parser-backed source rules, architecture, structured Rust test evidence, WGSL + and installed-release integration remain unfinished. Project Clippy overrides + and non-native targets currently fail explicitly rather than weakening the supported compiler configuration. Cargo dependency-input governance now parses ordinary, build, development, diff --git a/internal/rustfacts/compilation.go b/internal/rustfacts/compilation.go index 7ce53ea2..6da4ff9b 100644 --- a/internal/rustfacts/compilation.go +++ b/internal/rustfacts/compilation.go @@ -58,7 +58,10 @@ func compilationSources(repo repository.Repository, manifest string, allFiles [] } func compilationCommand(repo repository.Repository, manifest string) policy.Command { - command := Command(repo, "policy-clippy-"+manifest, "cargo-clippy", "clippy", "--frozen", "--all-targets", "--no-deps", "--message-format=json", "--target", HostTarget(), "--manifest-path", manifest, "--", "-F", "warnings", "-F", "clippy::all", "-F", "unused") + command := Command(repo, "policy-clippy-"+manifest, "cargo-clippy", "clippy", "--frozen", "--all-targets", "--no-deps", "--message-format=json", "--target", HostTarget(), "--manifest-path", manifest, "--", "--force-warn", "clippy::all", "--force-warn", "unused") + for _, lint := range strings.Fields(compilerWarningLints) { + command.Argv = append(command.Argv, "--force-warn", lint) + } command.Environment = append(command.Environment, "PATH", "LIB", "INCLUDE", "LIBPATH", "SDKROOT", "DEVELOPER_DIR") command.EnvironmentOverrides = append(command.EnvironmentOverrides, "CARGO="+repo.RustTool("cargo"), "CLIPPY_CONF_DIR="+filepath.Join(repo.PolicyRoot, "tools")) return command @@ -106,13 +109,13 @@ func compileManifest(ctx context.Context, repo repository.Repository, manifest s if !ok { return []policy.Finding{projectFinding(manifest, "compilation", "the runner cannot capture bounded Cargo compiler output")} } - result, output, runErr := structured.RunStructured(ctx, repo.Root, command) + _, output, runErr := structured.RunStructured(ctx, repo.Root, command) report, parseErr := ParseCompilation(repo, manifest, workspace, output.Stdout) findings := report.Findings if parseErr != nil { findings = append(findings, projectFinding(manifest, "compiler-output", fmt.Sprintf("invalid Cargo compiler evidence: %v; %s", parseErr, diagnosticOutput(output.Stderr)))) } - if runErr != nil && !compilerDiagnosticExit(ctx, result, report, parseErr, runErr) { + if runErr != nil { findings = append(findings, projectFinding(manifest, "compilation", fmt.Sprintf("Cargo compilation failed: %v; %s", runErr, diagnosticOutput(output.Stderr)))) } if parseErr == nil && runErr == nil && !report.Success { @@ -121,11 +124,6 @@ func compileManifest(ctx context.Context, repo repository.Repository, manifest s return findings } -func compilerDiagnosticExit(ctx context.Context, result runner.Result, report Compilation, parseErr, runErr error) bool { - return runErr != nil && parseErr == nil && !report.Success && len(report.Findings) > 0 && - result.ExitStatus == 101 && runner.FailureCategoryFor(ctx, result, runErr) == runner.FailureCommandExit -} - func compilationConfiguration(repo repository.Repository, manifest string) error { if HostTarget() == "" { return errors.New("governed Rust compilation is unavailable on this host") diff --git a/internal/rustfacts/compilation_test.go b/internal/rustfacts/compilation_test.go index 953e5b1c..8f46c8e7 100644 --- a/internal/rustfacts/compilation_test.go +++ b/internal/rustfacts/compilation_test.go @@ -51,21 +51,57 @@ func TestRustCompilationReportsOriginalTypeAndLintLocations(t *testing.T) { } } -func TestRustCompilationRejectsLintAttributeSuppression(t *testing.T) { +func TestRustCompilationCollectsLintsDespiteSourceLevels(t *testing.T) { t.Parallel() - for name, source := range map[string]string{ - "allow clippy": "#[allow(clippy::needless_bool)]\npub fn answer(value: bool) -> bool { if value { true } else { false } }\n", - "expect clippy": "#[expect(clippy::needless_bool)]\npub fn answer(value: bool) -> bool { if value { true } else { false } }\n", - "allow unused": "#[allow(dead_code)]\nfn unused() {}\npub fn answer() {}\n", + for name, test := range map[string]struct{ source, subject string }{ + "allow clippy": {"#[allow(clippy::needless_bool)]\npub fn answer(value: bool) -> bool { if value { true } else { false } }\n", "clippy::needless_bool"}, + "expect clippy": {"#[expect(clippy::needless_bool)]\npub fn answer(value: bool) -> bool { if value { true } else { false } }\n", "clippy::needless_bool"}, + "deny clippy": {"#[deny(clippy::needless_bool)]\npub fn answer(value: bool) -> bool { if value { true } else { false } }\n", "clippy::needless_bool"}, + "forbid clippy": {"#[forbid(clippy::needless_bool)]\npub fn answer(value: bool) -> bool { if value { true } else { false } }\n", "clippy::needless_bool"}, + "allow unused": {"#[allow(dead_code)]\nfn unused() {}\npub fn answer() {}\n", "dead_code"}, + "expect unused": {"#[expect(dead_code)]\nfn unused() {}\npub fn answer() {}\n", "dead_code"}, + "allow warning": {"#[allow(unreachable_code)]\npub fn answer() { return; println!(\"unreachable\"); }\n", "unreachable_code"}, } { t.Run(name, func(t *testing.T) { - repo := compilationRepository(t, source) + repo := compilationRepository(t, test.source) plan, discovery := compilationDiscovery(t, repo, "src/lib.rs") - findings := Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}) - if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { - return finding.Check == "quality.rustCompiler" && strings.Contains(finding.Message, "incompatible with previous forbid") && finding.Path == "src/lib.rs" + result, output, err := (runner.OSRunner{}).RunStructured(t.Context(), repo.Root, plan.Commands[0]) + if err != nil || result.ExitStatus != 0 { + t.Fatalf("lint collection failed compilation: %+v, %v; %s", result, err, output.Stderr) + } + report, err := ParseCompilation(repo, "Cargo.toml", discovery.Workspaces[0], output.Stdout) + if err != nil || !report.Success { + t.Fatalf("lint report lacks successful compilation evidence: %+v, %v", report, err) + } + if !slices.ContainsFunc(report.Findings, func(finding policy.Finding) bool { + return finding.Subject == test.subject && finding.Path == "src/lib.rs" && finding.Line > 0 }) { - t.Fatalf("suppression did not conflict with governed lint policy: %+v", findings) + t.Fatalf("source levels hid the governed lint: %+v", report.Findings) + } + }) + } +} + +func TestRustCompilationFailureCannotBecomeAnExceptableLint(t *testing.T) { + t.Parallel() + for name, test := range map[string]struct{ source, build, subject string }{ + "type error": {source: "pub fn answer() -> u32 { \"wrong\" }\n", subject: "E0308"}, + "denied lint": {source: "pub fn answer() { let mutex = std::sync::Mutex::new(0); let _ = mutex.lock().unwrap(); }\n", subject: "let_underscore_lock"}, + "build script": {source: "pub fn answer() {}\n", build: "fn main() { let unused = 1; panic!(\"deliberate build failure\"); }\n", subject: "unused_variables"}, + } { + t.Run(name, func(t *testing.T) { + repo := compilationRepository(t, test.source) + if test.build != "" { + write(t, repo.Root, "build.rs", test.build) + } + plan, discovery := compilationDiscovery(t, repo, "src/lib.rs") + findings := Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}) + for _, expected := range []struct{ check, subject string }{{"quality.rustCompiler", test.subject}, {"policy.rustProject", "compilation"}} { + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return finding.Check == expected.check && finding.Subject == expected.subject + }) { + t.Errorf("failed compilation lost %s/%s: %+v", expected.check, expected.subject, findings) + } } }) } diff --git a/internal/rustfacts/lints.go b/internal/rustfacts/lints.go new file mode 100644 index 00000000..7fedf609 --- /dev/null +++ b/internal/rustfacts/lints.go @@ -0,0 +1,137 @@ +package rustfacts + +const compilerWarningLints = ` +aarch64-softfloat-neon +ambiguous-derive-helpers +ambiguous-glob-imported-traits +ambiguous-glob-reexports +ambiguous-import-visibilities +ambiguous-panic-imports +ambiguous-wide-pointer-comparisons +anonymous-parameters +array-into-iter +asm-sub-register +async-fn-in-trait +bad-asm-style +bare-trait-objects +boxed-slice-into-iter +break-with-label-and-loop +clashing-extern-declarations +coherence-leak-check +confusable-idents +const-evaluatable-unchecked +const-item-interior-mutations +const-item-mutation +dangling-pointers-from-locals +dangling-pointers-from-temporaries +dead-code +deprecated +deprecated-where-clause-location +double-negations +dropping-copy-types +dropping-references +drop-bounds +duplicate-macro-attributes +dyn-drop +ellipsis-inclusive-range-patterns +exported-private-dependencies +float-literal-f32-fallback +forbidden-lint-groups +forgetting-copy-types +forgetting-references +for-loops-over-fallibles +function-casts-as-integer +function-item-references +hidden-glob-reexports +improper-ctypes +improper-ctypes-definitions +improper-gpu-kernel-arg +incomplete-features +inline-no-sanitize +integer-to-ptr-transmutes +internal-eq-trait-method-impls +internal-features +invalid-doc-attributes +invalid-from-utf8 +invalid-nan-comparisons +invalid-value +irrefutable-let-patterns +large-assignments +late-bound-lifetime-arguments +linker-messages +malformed-diagnostic-attributes +malformed-diagnostic-format-literals +map-unit-fn +mismatched-lifetime-syntaxes +misplaced-diagnostic-attributes +missing-abi +missing-gpu-kernel-export-name +mixed-script-confusables +named-arguments-used-positionally +non-camel-case-types +non-contiguous-range-endpoints +non-fmt-panics +non-local-definitions +non-shorthand-field-patterns +non-snake-case +non-upper-case-globals +noop-method-call +no-mangle-generic-items +opaque-hidden-inferred-bound +overlapping-range-endpoints +path-statements +private-bounds +private-interfaces +ptr-to-integer-transmute-in-consts +redundant-semicolons +refining-impl-trait-internal +refining-impl-trait-reachable +renamed-and-removed-lints +repr-c-enums-larger-than-int +rtsan-nonblocking-async +self-constructor-from-outer-item +special-module-name +stable-features +static-mut-refs +suspicious-double-ref-op +trivial-bounds +type-alias-bounds +tyvar-behind-raw-pointer +uncommon-codepoints +unconditional-recursion +uncovered-param-in-projection +unexpected-cfgs +unfulfilled-lint-expectations +ungated-async-fn-track-caller +unknown-diagnostic-attributes +unknown-lints +unnameable-test-items +unnecessary-transmutes +unpredictable-function-pointer-comparisons +unreachable-cfg-select-predicates +unreachable-code +unreachable-patterns +unstable-name-collisions +unstable-syntax-pre-expansion +unsupported-calling-conventions +unused-allocation +unused-assignments +unused-associated-type-bounds +unused-attributes +unused-braces +unused-comparisons +unused-doc-comments +unused-features +unused-imports +unused-labels +unused-macros +unused-must-use +unused-mut +unused-parens +unused-unsafe +unused-variables +unused-visibilities +useless-ptr-null-checks +uses-power-alignment +while-true +` diff --git a/internal/rustfacts/lints_test.go b/internal/rustfacts/lints_test.go new file mode 100644 index 00000000..b2c6ba89 --- /dev/null +++ b/internal/rustfacts/lints_test.go @@ -0,0 +1,53 @@ +package rustfacts + +import ( + "slices" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestRustLintCollectionCoversCompilerWarningsWithoutLoweringErrors(t *testing.T) { + t.Parallel() + repo := rustRepository(t) + _, output, err := (runner.OSRunner{}).RunStructured(t.Context(), repo.Root, Command(repo, "rust-lint-interface", "rustc", "-W", "help")) + if err != nil { + t.Fatalf("compiler lint interface: %v; %s", err, output.Stderr) + } + forced := map[string]bool{} + arguments := compilationCommand(repo, "Cargo.toml").Argv + for index, argument := range arguments[:len(arguments)-1] { + if argument == "--force-warn" { + forced[arguments[index+1]] = true + } + } + warnings, errors := 0, 0 + for _, line := range strings.Split(string(output.Stdout), "\n") { + fields := strings.Fields(line) + if len(fields) < 3 || slices.Contains([]string{"warnings", "tail-call-track-caller"}, fields[0]) || !slices.Contains([]string{"warn", "deny"}, fields[1]) { + continue + } + if fields[1] == "warn" { + warnings++ + } else { + errors++ + } + if forced[fields[0]] != (fields[1] == "warn") { + t.Errorf("policy lint collection changes the compiler's %s contract for %s", fields[1], fields[0]) + } + } + if warnings == 0 || errors == 0 { + t.Fatal("compiler lint interface omitted its default warning/error inventory") + } +} + +func TestRustCompilerTailCallWarningRequiresAnUnsupportedFeature(t *testing.T) { + t.Parallel() + repo := compilationRepository(t, "pub fn answer() {}\n") + command := Command(repo, "rust-unstable-lint", "rustc", "src/lib.rs", "--crate-type=lib", "--emit=metadata", "--out-dir", repo.Root, "--force-warn", "tail-call-track-caller") + _, output, err := (runner.OSRunner{}).RunStructured(t.Context(), repo.Root, command) + if err != nil || !strings.Contains(string(output.Stderr), "the `tail_call_track_caller` lint is unstable") { + t.Fatalf("excluded lint is no longer feature-gated: %v; %s", err, output.Stderr) + } +} From f81299da8a11452194b8632439071a6708dde7d8 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 02:29:42 +0000 Subject: [PATCH 14/49] feat: enforce parsed Rust source policy Add authenticated frozen Tree-sitter wheels, bounded source facts, comment and attribute policy, and independent Rust complexity limits. Preserve production limits beside inline tests. Exact Rust source/quality and coverage regressions pass; scoped policy checks pass. Full quality rerun remains after replacing obsolete unsupported-Rust fixtures. Architecture, structured test evidence and installed-release parity remain incomplete. --- .code-polishy.json | 6 + .gitignore | 1 + docs/design/cargo-facts.md | 38 +++ docs/design/rust-inputs.md | 11 + docs/design/test-ownership.md | 6 + docs/plans/code-polishy-rust-support-plan.md | 22 +- internal/policy/load.go | 2 + internal/policy/model.go | 4 + internal/policy/rust_quality_test.go | 35 +++ internal/quality/cargo.go | 1 + internal/quality/comments.go | 2 +- internal/quality/quality.go | 3 +- internal/quality/quality_test.go | 27 +- internal/quality/rust_source.go | 84 ++++++ internal/quality/rust_source_test.go | 72 +++++ internal/repository/providers.go | 2 +- internal/rustfacts/install.py | 280 +++++++++++++++++++ internal/rustfacts/install_test.go | 150 ++++++++++ internal/rustfacts/pyproject.toml | 8 + internal/rustfacts/source.go | 108 +++++++ internal/rustfacts/source_output.go | 190 +++++++++++++ internal/rustfacts/source_parser.py | 276 ++++++++++++++++++ internal/rustfacts/source_test.go | 166 +++++++++++ internal/rustfacts/uv.lock | 50 ++++ schema/code-polishy.schema.json | 2 + tools/install-policy-tools.ps1 | 22 +- tools/install-policy-tools.sh | 29 +- 27 files changed, 1553 insertions(+), 44 deletions(-) create mode 100644 internal/policy/rust_quality_test.go create mode 100644 internal/quality/rust_source.go create mode 100644 internal/quality/rust_source_test.go create mode 100644 internal/rustfacts/install.py create mode 100644 internal/rustfacts/install_test.go create mode 100644 internal/rustfacts/pyproject.toml create mode 100644 internal/rustfacts/source.go create mode 100644 internal/rustfacts/source_output.go create mode 100644 internal/rustfacts/source_parser.py create mode 100644 internal/rustfacts/source_test.go create mode 100644 internal/rustfacts/uv.lock diff --git a/.code-polishy.json b/.code-polishy.json index 720e7273..eda807e7 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -32,6 +32,7 @@ "sourcePaths": [ "internal/policy/data.go", "internal/policy/rust_inputs_test.go", + "internal/policy/rust_quality_test.go", "internal/policy/supplychain_config.go", "internal/policy/cargo_release_age_test.go", "internal/policy/cargo_vulnerability_test.go", @@ -52,6 +53,9 @@ "internal/quality/cargo_test.go", "internal/quality/rust_format.go", "internal/quality/rust_format_test.go", + "internal/quality/rust_source.go", + "internal/quality/rust_source_test.go", + "internal/quality/comments.go", "internal/quality/wgsl_test.go", "internal/supplychain/cargo.go", "internal/supplychain/cargo_test.go", @@ -77,6 +81,8 @@ "internal/engine/cargo_supplychain_test.go", "tools/install-rust.sh", "tools/install-rust.ps1", + "tools/install-policy-tools.sh", + "tools/install-policy-tools.ps1", "scripts/test-rust-runtime.sh" ] }, diff --git a/.gitignore b/.gitignore index e59cc562..6e168b79 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store .tools/ internal/pythonfacts/.venv/ +internal/rustfacts/.venv/ /.code-polishy-reports/ /.code-polishy-artifacts/ __pycache__/ diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index 9735c806..b799aae2 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -68,6 +68,44 @@ local-state digest. Read-only formatting, like Go's gofmt check, is an internal source operation; Cargo discovery and compilation use the check execution plan. The public format operation routes all child processes through its supplied runner. +## Parsed source facts + +Rust source syntax is parsed by the upstream Tree-sitter Rust grammar in the +already governed CPython helper runtime. The grammar and its Python binding are +exact dependencies in this module's manifest and lock. This supplies syntax, +comments and structural measurements without importing or compiling project +code. Cargo and rustc remain authoritative for crate identity, resolution, +conditional compilation, macro expansion and type correctness. Syntax facts +must not masquerade as a compiler-resolved semantic graph. + +Requests carry bounded UTF-8 source bytes through the common structured runner; +the parser does not choose additional project files or execute Rust attributes +and macros. Error-recovered or incomplete syntax cannot establish complete +source coverage. Parsed comments are distinct from comment-like raw strings; +attributes and macro token trees retain their original source locations. + +Source complexity is measured on written syntax, not expanded macro output. +Functions and closures have independent measurements. Conditions, loops, Boolean +short circuits, guards, or-patterns, `?` and let-else add decisions; an exhaustive +match adds one fewer decision than its arm count. Macro sites remain explicit +facts, and their opaque token trees do not become guessed executable syntax. +The compiler still checks the selected expansion. Source metrics include written +configuration branches rather than claiming coverage of every compiled variant. + +Rust owns separate `quality.complexity.rust` and `rustTest` limits, initially 12 +and 20 with the same strict-below comparison as Go. Projects may tighten them. +Built-in test functions and exact `cfg(test)` scopes receive the test limit; +file-level test ownership alone cannot relax production functions co-located +with inline tests. Custom test attributes and exclusive Cargo test-target scope +need further test-context evidence. Depth and parameter measurements are retained +as facts; TypeScript's thresholds do not silently become Rust restrictions. + +Comment policy uses parsed line/block comments and value-bearing doc attributes, +not text searches. Metadata such as `doc(hidden)` is not prose. Inline `allow`, +`expect` and `rustfmt::skip`, including nested `cfg_attr` forms, cannot bypass +native policy ownership. Exceptions belong to the existing exact policy mechanism +for the underlying finding. Source validation and command failure remain blocking. + ## Compiler checks Compilation uses the matching pinned cargo-clippy executable directly, with an diff --git a/docs/design/rust-inputs.md b/docs/design/rust-inputs.md index d4726eea..450eb7d7 100644 --- a/docs/design/rust-inputs.md +++ b/docs/design/rust-inputs.md @@ -205,3 +205,14 @@ the developer installer alone does not establish installed-release coverage. [Cargo project facts](cargo-facts.md) defines workspace discovery and its execution boundary separately from source classification. + +The source-facts parser uses Tree-sitter 0.26.0 and its Rust grammar 0.24.2 through +the existing pinned CPython runtime. Both upstream releases are older than the +minimum admission age and provide binary wheels for all five supported hosts. +The Rust facts manifest and `uv.lock` own dependency versions and archive +identities; the installer reads those lock entries rather than maintaining a +second checksum inventory. It selects only compatible binary wheels, authenticates +each download once, and never runs package installation scripts or source builds. +Staged packages must pass a real Rust parse before replacing the previous exact +distributions. Replacement retains recoverable backups until the installed +parser passes that same check. Wheel metadata and licenses remain installed. diff --git a/docs/design/test-ownership.md b/docs/design/test-ownership.md index 48332cc7..ac0a028d 100644 --- a/docs/design/test-ownership.md +++ b/docs/design/test-ownership.md @@ -21,6 +21,12 @@ real suite, without invented standalone commands. Suite commands remain responsible for executing tests and rejecting empty runs; path declarations alone do not prove execution. +Rust files may contain both production items and inline tests. File-level test +ownership does not relax production complexity limits. Parsed built-in test +attributes and lexical `cfg(test)` scopes select the separate Rust test limit; +unproven test contexts retain the production limit. The +[Cargo source facts](cargo-facts.md) contract defines that syntax-only boundary. + Ownership does not schedule tests. Existing cost, profile, changed-impact, required-kind, and gate rules select execution. Changing a test or its helper still selects the owner's focused coverage; the full profile retains its diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 51010117..e1919513 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -117,8 +117,13 @@ changed modules; add a quick suite when a new module is introduced. collection so typed policy findings remain independent of process failure. Real compiler tests prove source lint levels cannot hide those findings and type errors, default-denied compiler lints and build-script failures still fail. -- Parser-backed source rules, architecture, structured Rust test evidence, WGSL - and installed-release integration remain unfinished. Project Clippy overrides +- Parser-backed source rules now enforce comment, lint-attribute, format-skip and + Rust-specific complexity policy, with original locations and independent + function/closure measurements. Inline tests do not relax production limits. + The pinned Tree-sitter Rust grammar supplies written-source facts; it does not + pretend to resolve macro expansion or compiler configuration. +- Architecture, structured Rust test evidence, WGSL and installed-release + integration remain unfinished. Project Clippy overrides and non-native targets currently fail explicitly rather than weakening the supported compiler configuration. @@ -356,10 +361,15 @@ installation scripts. Linux runtime execution and installer integrity/recovery fixtures pass. macOS/Windows installers and checksums are present but host execution and release packaging still require verification. -The next milestone is one complete crate path: selected-file formatting, compiler -diagnostics, source facts, architecture, dependencies and nonempty test evidence. -Resolved dependency graphs, semantic analysis, supply-chain checks, structured Rust -test results and installed-release parity remain open. Existing missing-capability +Selected-file formatting, compiler diagnostics, parsed source policy and Cargo +dependency governance now have runnable beta implementations. Source parsing uses +exact authenticated binary wheels in the existing governed Python runtime; it +requires neither target package installation scripts nor a new Rust bootstrap. + +The next milestone completes architecture and nonempty test evidence for one crate +path, then extends workspace coverage and installed-release integration. Semantic +module resolution, structured Rust test results and installed-release parity +remain open. Existing missing-capability findings remain until those implementations and evidence exist. This beta is not yet usable as first-class Rust support for Masterbrainer. diff --git a/internal/policy/load.go b/internal/policy/load.go index 1719fa57..a81e6ab3 100644 --- a/internal/policy/load.go +++ b/internal/policy/load.go @@ -83,6 +83,8 @@ func EffectiveQuality(quality Quality) Quality { defaultInt(&quality.MaxTestFileLines, MaxTestFileLines) defaultInt(&quality.Complexity.Go, MaxGoComplexity) defaultInt(&quality.Complexity.GoTest, MaxGoTestComplexity) + defaultInt(&quality.Complexity.Rust, MaxRustComplexity) + defaultInt(&quality.Complexity.RustTest, MaxRustTestComplexity) defaultInt(&quality.Complexity.Python, MaxPythonComplexity) defaultInt(&quality.Complexity.TypeScript, MaxTypeScriptComplexity) defaultInt(&quality.Complexity.TypeScriptTest, MaxTypeScriptTestComplexity) diff --git a/internal/policy/model.go b/internal/policy/model.go index 8d2efa7b..04f2b529 100644 --- a/internal/policy/model.go +++ b/internal/policy/model.go @@ -23,6 +23,8 @@ const ( MaxTestFileLines = 2500 MaxGoComplexity = 12 MaxGoTestComplexity = 20 + MaxRustComplexity = 12 + MaxRustTestComplexity = 20 MaxPythonComplexity = 10 MaxTypeScriptComplexity = 10 MaxTypeScriptTestComplexity = 10 @@ -255,6 +257,8 @@ type ReliabilityReminder struct { type Complexity struct { Go int `json:"go,omitempty"` GoTest int `json:"goTest,omitempty"` + Rust int `json:"rust,omitempty"` + RustTest int `json:"rustTest,omitempty"` Python int `json:"python,omitempty"` TypeScript int `json:"typescript,omitempty"` TypeScriptTest int `json:"typescriptTest,omitempty"` diff --git a/internal/policy/rust_quality_test.go b/internal/policy/rust_quality_test.go new file mode 100644 index 00000000..89a69ff8 --- /dev/null +++ b/internal/policy/rust_quality_test.go @@ -0,0 +1,35 @@ +package policy + +import ( + "strings" + "testing" +) + +func TestRustQualityLimitsHaveIndependentNonBypassableBounds(t *testing.T) { + t.Parallel() + for name, quality := range map[string]string{ + "defaults": `{}`, + "tighter": `{"complexity":{"rust":5,"rustTest":8}}`, + } { + t.Run(name, func(t *testing.T) { + input := strings.Replace(minimalConfig(), `"quality":{}`, `"quality":`+quality, 1) + config, err := Load(writeConfig(t, input), "") + if err != nil { + t.Fatal(err) + } + production, test := 12, 20 + if name == "tighter" { + production, test = 5, 8 + } + if config.Quality.Complexity.Rust != production || config.Quality.Complexity.RustTest != test || config.Quality.Complexity.TypeScript != 10 { + t.Fatalf("Rust limits did not retain their independent policy: %+v", config.Quality.Complexity) + } + }) + } + for _, quality := range []string{`{"complexity":{"rust":13}}`, `{"complexity":{"rustTest":21}}`, `{"complexity":{"rust":-1}}`} { + input := strings.Replace(minimalConfig(), `"quality":{}`, `"quality":`+quality, 1) + if _, err := Load(writeConfig(t, input), ""); err == nil { + t.Fatalf("invalid Rust limit was accepted: %s", quality) + } + } +} diff --git a/internal/quality/cargo.go b/internal/quality/cargo.go index 118a55d0..8101aa52 100644 --- a/internal/quality/cargo.go +++ b/internal/quality/cargo.go @@ -17,6 +17,7 @@ func cargoDiscoveryFindings(ctx context.Context, repo repository.Repository, fil plan := rustfacts.PlanDiscovery(repo, files, allFiles) discovery := rustfacts.Discover(ctx, repo, plan, commandRunner) findings := append(discovery.Findings, rustFormatFindings(ctx, repo, files, discovery.Workspaces)...) + findings = append(findings, rustSourceFindings(ctx, repo, files, commandRunner)...) compilation := rustfacts.PlanCompilation(repo, plan, allFiles) return append(findings, rustfacts.Compile(ctx, repo, compilation, discovery, commandRunner)...) } diff --git a/internal/quality/comments.go b/internal/quality/comments.go index e4ed0af2..df849a8f 100644 --- a/internal/quality/comments.go +++ b/internal/quality/comments.go @@ -640,5 +640,5 @@ func nextLine(data []byte, index int) int { } func sourceCommentsHandledElsewhere(repo repository.Repository, path string) bool { - return repo.IsGenerated(path) || repo.AnalysisOwner(path, "lint", "").Pack != "" + return repo.IsGenerated(path) || repo.Language(path) == "rust" || repo.AnalysisOwner(path, "lint", "").Pack != "" } diff --git a/internal/quality/quality.go b/internal/quality/quality.go index d463533b..c712a78f 100644 --- a/internal/quality/quality.go +++ b/internal/quality/quality.go @@ -48,6 +48,7 @@ func CheckCommands(repo repository.Repository, selection repository.Selection, p if allFiles, err := repo.AllFiles(); err == nil { discovery := rustfacts.PlanDiscovery(repo, selection.Files, allFiles) commands = append(commands, discovery.Commands...) + commands = append(commands, rustfacts.PlanSources(repo, selection.Files).Commands...) commands = append(commands, rustfacts.PlanCompilation(repo, discovery, allFiles).Commands...) goCommands, _ := nativeGoToolCommands(repo, selection.Files, allFiles) commands = append(commands, goCommands...) @@ -925,7 +926,7 @@ func builtInCapability(language, capability string) bool { case "go", "typescript", "python": return slices.Contains(builtInCapabilities, capability) case "rust": - return slices.Contains([]string{"format", "lint", "typecheck"}, capability) + return slices.Contains([]string{"format", "lint", "typecheck", "complexity"}, capability) case "shell": return slices.Contains([]string{"format", "lint", "typecheck"}, capability) } diff --git a/internal/quality/quality_test.go b/internal/quality/quality_test.go index 0e121924..8911b0d5 100644 --- a/internal/quality/quality_test.go +++ b/internal/quality/quality_test.go @@ -156,8 +156,8 @@ func TestCoverageRequiresAdaptersForUnknownLanguages(t *testing.T) { repo := qualityRepository(t) repo.Config.Modules = []policy.Module{{Name: "sample", Paths: []string{"internal/sample/**"}}} repo.Config.ModuleByName = map[string]int{"sample": 0} - writeQualityFile(t, repo.Root, "internal/sample/lib.rs", "fn main() {}\n") - findings := CoverageFindings(repo, []string{"internal/sample/lib.rs"}) + writeQualityFile(t, repo.Root, "internal/sample/lib.dart", "void main() {}\n") + findings := CoverageFindings(repo, []string{"internal/sample/lib.dart"}) if len(withoutSourceCommentCoverage(findings)) != 12 || !findingChecks(findings)["policy.sourceCommentCoverage"] { t.Fatalf("findings = %+v", findings) } @@ -169,8 +169,8 @@ func TestGeneratedSourceStillRequiresModuleAndToolCoverage(t *testing.T) { repo.Config.Scope.Generated = []string{"generated/**"} repo.Config.Modules = []policy.Module{{Name: "generated", Paths: []string{"generated/**"}}} repo.Config.ModuleByName = map[string]int{"generated": 0} - writeQualityFile(t, repo.Root, "generated/client.rs", "fn generated() {}\n") - findings := CoverageFindings(repo, []string{"generated/client.rs"}) + writeQualityFile(t, repo.Root, "generated/client.dart", "void generated() {}\n") + findings := CoverageFindings(repo, []string{"generated/client.dart"}) if len(findings) != 9 || !findingChecks(findings)["policy.generationOwnership"] { t.Fatalf("generated executable source cannot bypass adapter coverage: %+v", findings) } @@ -186,9 +186,9 @@ func TestCoverageRequiresStructuredEvidenceBeyondModuleScopedCommand(t *testing. repo := qualityRepository(t) repo.Config.Modules = []policy.Module{{Name: "sample", Paths: []string{"internal/sample/**"}}} repo.Config.ModuleByName = map[string]int{"sample": 0} - repo.Config.Checks = []policy.Command{{Name: "rust", Provides: []string{"format", "lint", "typecheck", "complexity", "dead-code", "architecture"}, Modules: []string{"sample"}, RunOn: []string{"check", "gate"}}} - writeQualityFile(t, repo.Root, "internal/sample/lib.rs", "fn main() {}\n") - if findings := CoverageFindings(repo, []string{"internal/sample/lib.rs"}); len(withoutSourceCommentCoverage(findings)) != 12 || !findingChecks(findings)["policy.sourceCommentCoverage"] { + repo.Config.Checks = []policy.Command{{Name: "dart", Provides: []string{"format", "lint", "typecheck", "complexity", "dead-code", "architecture"}, Modules: []string{"sample"}, RunOn: []string{"check", "gate"}}} + writeQualityFile(t, repo.Root, "internal/sample/lib.dart", "void main() {}\n") + if findings := CoverageFindings(repo, []string{"internal/sample/lib.dart"}); len(withoutSourceCommentCoverage(findings)) != 12 || !findingChecks(findings)["policy.sourceCommentCoverage"] { t.Fatalf("an ordinary command cannot establish source coverage: %+v", findings) } } @@ -198,9 +198,9 @@ func TestCoverageRequiresProvidersInLocalAndGateProfiles(t *testing.T) { repo := qualityRepository(t) repo.Config.Modules = []policy.Module{{Name: "sample", Paths: []string{"internal/sample/**"}}} repo.Config.ModuleByName = map[string]int{"sample": 0} - repo.Config.Checks = []policy.Command{{Name: "rust", Provides: []string{"format", "lint", "typecheck", "complexity", "dead-code", "architecture"}, Modules: []string{"sample"}, RunOn: []string{"check"}}} - writeQualityFile(t, repo.Root, "internal/sample/lib.rs", "fn main() {}\n") - findings := CoverageFindings(repo, []string{"internal/sample/lib.rs"}) + repo.Config.Checks = []policy.Command{{Name: "dart", Provides: []string{"format", "lint", "typecheck", "complexity", "dead-code", "architecture"}, Modules: []string{"sample"}, RunOn: []string{"check"}}} + writeQualityFile(t, repo.Root, "internal/sample/lib.dart", "void main() {}\n") + findings := CoverageFindings(repo, []string{"internal/sample/lib.dart"}) findings = withoutSourceCommentCoverage(findings) if len(findings) != 12 || findings[0].Subject != "format:check" || findings[1].Subject != "format:gate" { t.Fatalf("findings = %+v", findings) @@ -248,12 +248,13 @@ func TestCoverageKeepsUnknownSourceUncoveredBesideNativeSource(t *testing.T) { repo.Config.Modules = []policy.Module{{Name: "sample", Paths: []string{"internal/sample/**"}}} repo.Config.ModuleByName = map[string]int{"sample": 0} repo.Config.Checks = []policy.Command{{ - Name: "rust", Provides: []string{"format", "lint", "typecheck", "complexity", "dead-code", "architecture"}, + Name: "dart", Provides: []string{"format", "lint", "typecheck", "complexity", "dead-code", "architecture"}, + Paths: []string{"internal/sample/**/*.dart"}, Modules: []string{"sample"}, RunOn: []string{"check", "gate"}, }} - writeQualityFile(t, repo.Root, "internal/sample/lib.rs", "fn main() {}\n") + writeQualityFile(t, repo.Root, "internal/sample/lib.dart", "void main() {}\n") writeQualityFile(t, repo.Root, "internal/sample/build.ts", "export const build = 1;\n") - if findings := CoverageFindings(repo, []string{"internal/sample/lib.rs", "internal/sample/build.ts"}); len(withoutSourceCommentCoverage(findings)) != 12 || !findingChecks(findings)["policy.sourceCommentCoverage"] { + if findings := CoverageFindings(repo, []string{"internal/sample/lib.dart", "internal/sample/build.ts"}); len(withoutSourceCommentCoverage(findings)) != 12 || !findingChecks(findings)["policy.sourceCommentCoverage"] { t.Fatalf("unstructured commands cannot establish unknown-language analysis: %+v", findings) } } diff --git a/internal/quality/rust_source.go b/internal/quality/rust_source.go new file mode 100644 index 00000000..46bffb87 --- /dev/null +++ b/internal/quality/rust_source.go @@ -0,0 +1,84 @@ +package quality + +import ( + "context" + "fmt" + "strconv" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" + "github.com/riteofstring/code-polishy/internal/rustfacts" +) + +func rustSourceFindings(ctx context.Context, repo repository.Repository, files []string, boundary runner.Runner) []policy.Finding { + result := rustfacts.ReadSources(ctx, repo, rustfacts.PlanSources(repo, files), boundary) + findings := result.Findings + for _, source := range result.Files { + if repo.NativeAnalysis(source.Path, "lint") && !repo.Config.Quality.CommentsAllowed() { + for _, comment := range source.Comments { + findings = append(findings, rustCommentFinding(source.Path, comment)) + } + } + findings = append(findings, rustAttributeFindings(repo, source.Path, source.Attributes)...) + if repo.NativeAnalysis(source.Path, "complexity") { + findings = append(findings, rustComplexityFindings(repo, source)...) + } + } + return findings +} + +func rustCommentFinding(path string, location rustfacts.SourceLocation) policy.Finding { + return rustLocatedFinding("policy.sourceComment", path, fmt.Sprintf("%d:%d", location.Line, location.Column), "prose comments and doc attributes are forbidden; move durable context to a design document", location) +} + +func rustAttributeFindings(repo repository.Repository, path string, attributes []rustfacts.SourceAttribute) []policy.Finding { + findings := []policy.Finding{} + for _, attribute := range attributes { + message := "" + switch attribute.Name { + case "allow", "expect": + if repo.NativeAnalysis(path, "lint") { + message = "inline Rust lint suppression must be replaced by an exact Code Polishy exception for the underlying finding" + } + case "rustfmt::skip": + if repo.NativeAnalysis(path, "format") { + message = "Rust attributes must not exclude governed source from formatting" + } + case "doc": + if attribute.HasValue && repo.NativeAnalysis(path, "lint") && !repo.Config.Quality.CommentsAllowed() { + findings = append(findings, rustCommentFinding(path, attribute.SourceLocation)) + } + } + if message != "" { + subject := fmt.Sprintf("%s:%d:%d", attribute.Name, attribute.Line, attribute.Column) + findings = append(findings, rustLocatedFinding("policy.rustAttribute", path, subject, message, attribute.SourceLocation)) + } + findings = append(findings, rustAttributeFindings(repo, path, attribute.Children)...) + } + return findings +} + +func rustComplexityFindings(repo repository.Repository, source rustfacts.Source) []policy.Finding { + limits := policy.EffectiveQuality(repo.Config.Quality).Complexity + findings := []policy.Finding{} + for _, function := range source.Functions { + limit := limits.Rust + if function.Test { + limit = limits.RustTest + } + if function.Complexity < limit { + continue + } + subject := fmt.Sprintf("%s:%d:%d", function.Name, function.Line, function.Column) + message := fmt.Sprintf("function %s has written-source cyclomatic complexity %d; must be below %d", function.Name, function.Complexity, limit) + finding := rustLocatedFinding("quality.rustComplexity", source.Path, subject, message, function.SourceLocation) + finding.Fields = map[string]string{"complexity": strconv.Itoa(function.Complexity), "limit": strconv.Itoa(limit), "measurementScope": "unexpanded-written-source"} + findings = append(findings, finding) + } + return findings +} + +func rustLocatedFinding(check, path, subject, message string, location rustfacts.SourceLocation) policy.Finding { + return policy.Finding{Check: check, Path: path, Subject: subject, Message: message, Line: location.Line, Column: location.Column, EndLine: location.EndLine, EndColumn: location.EndColumn} +} diff --git a/internal/quality/rust_source_test.go b/internal/quality/rust_source_test.go new file mode 100644 index 00000000..37188b35 --- /dev/null +++ b/internal/quality/rust_source_test.go @@ -0,0 +1,72 @@ +package quality + +import ( + "slices" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestRustSourcePolicyDistinguishesProseFromStringsAndDocMetadata(t *testing.T) { + t.Parallel() + repo := cargoQualityRepository(t) + denied := false + repo.Config.Quality.AllowComments = &denied + writeQualityFile(t, repo.Root, "src/lib.rs", "#![doc = \"crate prose\"]\n/* outer /* nested */ comment */\n#[doc(hidden)]\npub fn value() -> &'static str { r###\"// not prose\"### }\n") + findings := rustSourceFindings(t.Context(), repo, []string{"src/lib.rs"}, runner.OSRunner{}) + if len(findings) != 2 { + t.Fatalf("expected the doc attribute and one actual block comment: %+v", findings) + } + for _, finding := range findings { + if finding.Check != "policy.sourceComment" || finding.Path != "src/lib.rs" || finding.Line < 1 || finding.Line > 2 || finding.Column < 1 { + t.Errorf("prose finding lost its source location: %+v", finding) + } + } + allowed := true + repo.Config.Quality.AllowComments = &allowed + if findings := rustSourceFindings(t.Context(), repo, []string{"src/lib.rs"}, runner.OSRunner{}); len(findings) != 0 { + t.Fatalf("allowed Rust documentation failed: %+v", findings) + } +} + +func TestRustSourcePolicyRejectsConditionalLintAndFormatSuppression(t *testing.T) { + t.Parallel() + repo := cargoQualityRepository(t) + writeQualityFile(t, repo.Root, "src/lib.rs", "#[cfg_attr(feature = \"maybe\", r#allow(dead_code), rustfmt::skip)]\n#[expect(dead_code)]\nfn value() {}\n") + findings := rustSourceFindings(t.Context(), repo, []string{"src/lib.rs"}, runner.OSRunner{}) + for _, name := range []string{"allow", "expect", "rustfmt::skip"} { + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return finding.Check == "policy.rustAttribute" && strings.HasPrefix(finding.Subject, name+":") && finding.Path == "src/lib.rs" && finding.Line > 0 + }) { + t.Errorf("conditional suppression %s was not governed: %+v", name, findings) + } + } +} + +func TestRustComplexityDoesNotBorrowTestLimitsForInlineProductionCode(t *testing.T) { + t.Parallel() + repo := cargoQualityRepository(t) + repo.Config.Quality.Complexity.Rust = 3 + repo.Config.Quality.Complexity.RustTest = 5 + repo.Config.Tests.Paths = []string{"src/lib.rs"} + writeQualityFile(t, repo.Root, "src/lib.rs", "pub fn production(a: bool, b: bool) { if a {} if b {} }\n#[test]\nfn test_body() { if true {} if true {} }\n") + findings := rustSourceFindings(t.Context(), repo, []string{"src/lib.rs"}, runner.OSRunner{}) + if len(findings) != 1 || findings[0].Check != "quality.rustComplexity" || !strings.HasPrefix(findings[0].Subject, "production:") || findings[0].Fields["complexity"] != "3" || findings[0].Fields["limit"] != "3" { + t.Fatalf("inline tests relaxed production complexity or inherited TypeScript limits: %+v", findings) + } +} + +func TestRustSourcePolicyMissingParserDoesNotPassCommentCoverage(t *testing.T) { + t.Parallel() + repo := cargoQualityRepository(t) + denied := false + repo.Config.Quality.AllowComments = &denied + writeQualityFile(t, repo.Root, "src/lib.rs", "// prose\npub fn value() {}\n") + repo.PolicyRoot = t.TempDir() + findings := rustSourceFindings(t.Context(), repo, []string{"src/lib.rs"}, runner.OSRunner{}) + if len(findings) != 1 || findings[0].Check != "policy.rustSource" { + t.Fatalf("missing parser was accepted as comment coverage: %+v", findings) + } +} diff --git a/internal/repository/providers.go b/internal/repository/providers.go index cb9a84d4..0556e905 100644 --- a/internal/repository/providers.go +++ b/internal/repository/providers.go @@ -127,7 +127,7 @@ func (repo Repository) NativeCapability(path, capability string) bool { case "go", "python": return slices.Contains([]string{"format", "lint", "typecheck", "complexity", "dead-code", "architecture"}, capability) case "rust": - return slices.Contains([]string{"format", "lint", "typecheck"}, capability) + return slices.Contains([]string{"format", "lint", "typecheck", "complexity"}, capability) case "shell": return slices.Contains([]string{"format", "lint", "typecheck"}, capability) case "typescript": diff --git a/internal/rustfacts/install.py b/internal/rustfacts/install.py new file mode 100644 index 00000000..b437d016 --- /dev/null +++ b/internal/rustfacts/install.py @@ -0,0 +1,280 @@ +import hashlib +import json +import pathlib +import platform +import shutil +import stat +import subprocess +import sys +import sysconfig +import tempfile +import tomllib +import urllib.parse +import urllib.request +import zipfile + +DISTRIBUTIONS = {"tree-sitter": "tree_sitter", "tree-sitter-rust": "tree_sitter_rust"} +MAXIMUM_WHEEL_BYTES = 8 * 1024 * 1024 +MAXIMUM_UNPACKED_BYTES = 32 * 1024 * 1024 +PARSER_PROBE = """ +import importlib.metadata +import json +import pathlib +import sys +sys.path.insert(0, sys.argv[1]) +from tree_sitter import Language, Parser +import tree_sitter +import tree_sitter_rust +root = pathlib.Path(sys.argv[1]).resolve() +for module in (tree_sitter, tree_sitter_rust): + if not pathlib.Path(module.__file__).resolve().is_relative_to(root): + raise SystemExit("Rust parser imported an external distribution") +tree = Parser(Language(tree_sitter_rust.language())).parse(b"fn main() {}") +if tree.root_node.has_error or tree.root_node.type != "source_file": + raise SystemExit("Rust parser did not produce a complete syntax tree") +names = ("tree-sitter", "tree-sitter-rust") +print(json.dumps({name: importlib.metadata.version(name) for name in names})) +""" + + +def parser_versions(directory): + result = subprocess.run( + [sys.executable, "-I", "-B", "-c", PARSER_PROBE, str(directory)], + capture_output=True, + text=True, + timeout=30, + check=True, + ) + return json.loads(result.stdout) + + +def locked_packages(root): + project = root / "internal" / "rustfacts" + manifest = tomllib.loads((project / "pyproject.toml").read_text(encoding="utf-8")) + required = dict(entry.split("==") for entry in manifest["project"]["dependencies"]) + if set(required) != set(DISTRIBUTIONS): + raise ValueError( + "Rust parser dependencies differ from the supported distributions" + ) + lock = tomllib.loads((project / "uv.lock").read_text(encoding="utf-8")) + if lock["version"] != 1 or lock["requires-python"] != "==3.12.*": + raise ValueError("Rust parser requires a supported CPython 3.12 lock") + packages = [] + for package in lock["package"]: + name = package["name"] + if name not in required: + continue + if package["version"] != required[name] or package["source"] != { + "registry": "https://pypi.org/simple" + }: + raise ValueError( + "Rust parser lock does not match its exact public dependencies" + ) + packages.append(package) + if sorted(package["name"] for package in packages) != sorted(required): + raise ValueError("Rust parser lock has missing or duplicate packages") + return packages + + +def host_wheel(package): + host = sys.platform + machine = platform.machine().lower() + architecture = { + "amd64": "x86_64", + "x86_64": "x86_64", + "arm64": "aarch64", + "aarch64": "aarch64", + }.get(machine) + if ( + host not in {"linux", "darwin", "win32"} + or architecture is None + or (host == "win32" and architecture != "x86_64") + ): + raise ValueError("Rust syntax tools are unavailable on this host") + suffix = { + "linux": architecture, + "darwin": {"x86_64": "x86_64", "aarch64": "arm64"}[architecture], + "win32": "amd64", + }[host] + platform_tag = {"linux": "-manylinux", "darwin": "-macosx", "win32": "-win_"}[host] + abi = "-cp312-cp312-" if package["name"] == "tree-sitter" else "-cp39-abi3-" + wheels = [ + wheel + for wheel in package["wheels"] + if abi in wheel["url"] + and platform_tag in wheel["url"] + and wheel["url"].endswith(f"_{suffix}.whl") + ] + if len(wheels) != 1: + raise ValueError(f"Rust parser has no unique locked wheel for {host}/{machine}") + return wheels[0] + + +def download_wheel(wheel, scratch): + url = urllib.parse.urlsplit(wheel["url"]) + if ( + url.scheme != "https" + or url.netloc != "files.pythonhosted.org" + or url.query + or url.fragment + ): + raise ValueError("Rust parser wheel does not use the approved archive host") + expected = wheel["hash"].removeprefix("sha256:") + if ( + not wheel["hash"].startswith("sha256:") + or len(expected) != 64 + or any(character not in "0123456789abcdef" for character in expected) + ): + raise ValueError("Rust parser wheel has no exact SHA-256 identity") + with urllib.request.urlopen(wheel["url"], timeout=30) as response: + data = response.read(MAXIMUM_WHEEL_BYTES + 1) + if ( + len(data) > MAXIMUM_WHEEL_BYTES + or len(data) != wheel["size"] + or hashlib.sha256(data).hexdigest() != expected + ): + raise ValueError("Rust parser wheel failed archive authentication") + archive = scratch / pathlib.PurePosixPath(url.path).name + archive.write_bytes(data) + return archive + + +def unpack_wheel(archive_path, package, destination): + name = DISTRIBUTIONS[package["name"]] + prefixes = {name, f"{name}-{package['version']}.dist-info"} + total = 0 + with zipfile.ZipFile(archive_path) as archive: + for entry in archive.infolist(): + path = pathlib.PurePosixPath(entry.filename) + total += entry.file_size + if ( + total > MAXIMUM_UNPACKED_BYTES + or path.is_absolute() + or ".." in path.parts + or "\\" in entry.filename + ): + raise ValueError("Rust parser wheel exceeds safe extraction bounds") + if ( + not path.parts + or path.parts[0] not in prefixes + or stat.S_IFMT(entry.external_attr >> 16) + not in {0, stat.S_IFDIR, stat.S_IFREG} + ): + raise ValueError( + "Rust parser wheel contains an unsupported archive entry" + ) + target = destination.joinpath(*path.parts) + if entry.is_dir(): + target.mkdir(parents=True, exist_ok=True) + else: + target.parent.mkdir(parents=True, exist_ok=True) + with archive.open(entry) as source, target.open("xb") as output: + shutil.copyfileobj(source, output) + + +def existing_distributions(site_packages): + paths = [] + for name in DISTRIBUTIONS.values(): + package = site_packages / name + if package.exists() or package.is_symlink(): + paths.append(package) + paths.extend(site_packages.glob(f"{name}-*.dist-info")) + if any(path.is_symlink() or not path.is_dir() for path in paths): + raise ValueError( + "Rust parser installation contains linked or non-directory distributions" + ) + return paths + + +def replace_distributions(staging, site_packages, expected): + previous = existing_distributions(site_packages) + backup = pathlib.Path( + tempfile.mkdtemp(prefix=".rust-syntax-backup-", dir=site_packages.parent) + ) + moved = [] + installed = [] + try: + for path in previous: + path.rename(backup / path.name) + moved.append(path) + for path in staging.iterdir(): + target = site_packages / path.name + path.rename(target) + installed.append(target) + if parser_versions(site_packages) != expected: + raise ValueError("Installed Rust parser does not match its locked versions") + except BaseException: + for path in installed: + shutil.rmtree(path) + for path in moved: + (backup / path.name).rename(path) + backup.rmdir() + raise + shutil.rmtree(backup) + + +def checked_runtime(root): + release = ( + (root / "tools" / "python-version.txt").read_text(encoding="utf-8").strip() + ) + if platform.python_version() != release.split("+", 1)[0] or sys.version_info[ + :2 + ] != (3, 12): + raise ValueError( + "Rust parser installation requires the pinned CPython 3.12 runtime" + ) + runtime = pathlib.Path(sys.prefix).resolve() + if not runtime.is_relative_to(root / ".tools" / "python"): + raise ValueError("Rust parser installation requires the policy-owned runtime") + if (runtime / ".code-polishy-python-release").read_text( + encoding="utf-8" + ).strip() != release: + raise ValueError( + "Rust parser installation requires an authenticated Python release" + ) + site_packages = pathlib.Path(sysconfig.get_path("platlib")).resolve() + if not site_packages.is_relative_to(runtime): + raise ValueError("Rust parser installation names an external package directory") + return site_packages + + +def install(root): + site_packages = checked_runtime(root) + packages = locked_packages(root) + expected = {package["name"]: package["version"] for package in packages} + previous = existing_distributions(site_packages) + exact_paths = [site_packages / name for name in DISTRIBUTIONS.values()] + exact_paths.extend( + site_packages / f"{DISTRIBUTIONS[name]}-{version}.dist-info" + for name, version in expected.items() + ) + try: + if ( + sorted(previous) == sorted(exact_paths) + and parser_versions(site_packages) == expected + ): + print("The exact Rust syntax distributions are already installed.") + return + except ( + subprocess.CalledProcessError, + subprocess.TimeoutExpired, + json.JSONDecodeError, + ): + pass + with tempfile.TemporaryDirectory( + prefix=".rust-syntax-staging-", dir=site_packages.parent + ) as temporary: + scratch = pathlib.Path(temporary) + staging = scratch / "packages" + staging.mkdir() + for package in packages: + archive = download_wheel(host_wheel(package), scratch) + unpack_wheel(archive, package, staging) + if parser_versions(staging) != expected: + raise ValueError("Staged Rust parser does not match its locked versions") + replace_distributions(staging, site_packages, expected) + print("Installed the authenticated Rust syntax distributions.") + + +if __name__ == "__main__": + install(pathlib.Path(sys.argv[1]).resolve(strict=True)) diff --git a/internal/rustfacts/install_test.go b/internal/rustfacts/install_test.go new file mode 100644 index 00000000..8892ec1c --- /dev/null +++ b/internal/rustfacts/install_test.go @@ -0,0 +1,150 @@ +package rustfacts + +import ( + "path/filepath" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestRustParserInstallerSelectsEachSupportedHostWithoutSourceBuilds(t *testing.T) { + t.Parallel() + runInstallerContract(t, ` +packages = installer.locked_packages(policy_root) +hosts = [("linux", "x86_64"), ("linux", "aarch64"), ("darwin", "x86_64"), ("darwin", "arm64"), ("win32", "AMD64")] +urls = set() +for host, machine in hosts: + installer.sys.platform = host + installer.platform.machine = lambda: machine + for package in packages: + wheel = installer.host_wheel(package) + assert wheel["url"].endswith(".whl"), wheel + assert wheel["hash"].startswith("sha256:"), wheel + urls.add(wheel["url"]) +assert len(urls) == 10, urls +installer.sys.platform = "win32" +installer.platform.machine = lambda: "ARM64" +try: + installer.host_wheel(packages[0]) +except ValueError: + pass +else: + raise AssertionError("unsupported host borrowed a foreign wheel") +`) +} + +func TestRustParserInstallerAuthenticatesBeforeWriting(t *testing.T) { + t.Parallel() + runInstallerContract(t, ` +import hashlib +import io +payload = b"authenticated wheel fixture" +installer.urllib.request.urlopen = lambda *args, **kwargs: io.BytesIO(payload) +wheel = {"url": "https://files.pythonhosted.org/packages/parser.whl", "hash": "sha256:" + hashlib.sha256(payload).hexdigest(), "size": len(payload)} +archive = installer.download_wheel(wheel, root) +assert archive.read_bytes() == payload +archive.unlink() +wheel["hash"] = "sha256:" + "0" * 64 +try: + installer.download_wheel(wheel, root) +except ValueError: + pass +else: + raise AssertionError("wrong archive identity passed") +assert not archive.exists() +`) +} + +func TestRustParserInstallerRejectsUnsafeWheelEntries(t *testing.T) { + t.Parallel() + runInstallerContract(t, ` +import stat +import zipfile +package = {"name": "tree-sitter", "version": "1.0.0"} +cases = [("../escape", stat.S_IFREG), ("tree_sitter/link", stat.S_IFLNK), ("other/file", stat.S_IFREG), ("tree_sitter/pipe", stat.S_IFIFO)] +for index, (name, mode) in enumerate(cases): + archive = root / f"bad-{index}.whl" + with zipfile.ZipFile(archive, "w") as output: + entry = zipfile.ZipInfo(name) + entry.external_attr = (mode | 0o644) << 16 + output.writestr(entry, b"outside") + destination = root / f"unpacked-{index}" + destination.mkdir() + try: + installer.unpack_wheel(archive, package, destination) + except ValueError: + pass + else: + raise AssertionError(f"unsafe entry accepted: {name}") + assert not list(destination.iterdir()) +assert not (root / "escape").exists() +`) +} + +func TestRustParserInstallerRestoresPreviousPackagesAfterFailedProbe(t *testing.T) { + t.Parallel() + runInstallerContract(t, ` +site = root / "site-packages" +staging = root / "staging" +site.mkdir() +staging.mkdir() +old = site / "tree_sitter" +old.mkdir() +(old / "original").write_bytes(b"previous package") +metadata = site / "tree_sitter-0.25.0.dist-info" +metadata.mkdir() +(metadata / "METADATA").write_text("Name: tree-sitter\nVersion: 0.25.0\n", encoding="utf-8") +replacement = staging / "tree_sitter" +replacement.mkdir() +(replacement / "__init__.py").write_text("raise RuntimeError('broken parser')\n", encoding="utf-8") +try: + installer.replace_distributions(staging, site, {"tree-sitter": "0.26.0", "tree-sitter-rust": "0.24.2"}) +except installer.subprocess.CalledProcessError: + pass +else: + raise AssertionError("a broken parser was installed") +assert (old / "original").read_bytes() == b"previous package" +assert (metadata / "METADATA").read_text(encoding="utf-8") == "Name: tree-sitter\nVersion: 0.25.0\n" +assert not (old / "__init__.py").exists() +assert not list(root.glob(".rust-syntax-backup-*")) +`) +} + +func TestRustParserInstallerVerifiesRealStagedDistributions(t *testing.T) { + t.Parallel() + runInstallerContract(t, ` +import shutil +site = root / "site-packages" +staging = root / "staging" +site.mkdir() +staging.mkdir() +installed = installer.checked_runtime(policy_root) +packages = installer.locked_packages(policy_root) +expected = {package["name"]: package["version"] for package in packages} +for path in installer.existing_distributions(installed): + shutil.copytree(path, staging / path.name) +assert installer.parser_versions(staging) == expected +installer.replace_distributions(staging, site, expected) +assert installer.parser_versions(site) == expected +for name, version in expected.items(): + metadata = site / f"{installer.DISTRIBUTIONS[name]}-{version}.dist-info" + assert (metadata / "METADATA").is_file() + assert list(metadata.rglob("LICENSE*")), metadata +assert not list(root.glob(".rust-syntax-backup-*")) +`) +} + +func runInstallerContract(t *testing.T, body string) { + t.Helper() + repo := rustRepository(t) + program := "import pathlib, sys\nsys.path.insert(0, sys.argv[1])\nimport install as installer\npolicy_root = pathlib.Path(sys.argv[2])\nroot = pathlib.Path(sys.argv[3])\n" + body + command := policy.Command{ + Name: "rust-parser-installer-contract", Cwd: ".", Managed: true, SealedEnvironment: true, TimeoutSeconds: 30, + Argv: repo.PythonCommand(program, filepath.Join(repo.PolicyRoot, "internal", "rustfacts"), repo.PolicyRoot, repo.Root), + } + _, output, err := (runner.OSRunner{}).RunStructured(t.Context(), repo.Root, command) + if err != nil { + t.Fatalf("installer contract: %v; %s %s", err, output.Stdout, output.Stderr) + } +} diff --git a/internal/rustfacts/pyproject.toml b/internal/rustfacts/pyproject.toml new file mode 100644 index 00000000..c4c129c4 --- /dev/null +++ b/internal/rustfacts/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "code-polishy-rust-facts" +version = "0.0.0" +requires-python = "==3.12.*" +dependencies = ["tree-sitter==0.26.0", "tree-sitter-rust==0.24.2"] + +[tool.uv] +package = false diff --git a/internal/rustfacts/source.go b/internal/rustfacts/source.go new file mode 100644 index 00000000..4f13bf4f --- /dev/null +++ b/internal/rustfacts/source.go @@ -0,0 +1,108 @@ +package rustfacts + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "slices" + "unicode/utf8" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" +) + +const SourceProtocol = "rust-source/v1" + +//go:embed source_parser.py +var sourceParser string + +type SourceInput struct { + Path string `json:"path"` + Source string `json:"source"` +} + +type SourcePlan struct { + Inputs []SourceInput + Commands []policy.Command + Findings []policy.Finding +} + +type Sources struct { + Files []Source + Findings []policy.Finding +} + +func PlanSources(repo repository.Repository, paths []string) SourcePlan { + plan := SourcePlan{} + for _, path := range paths { + if !sourceAnalysisSelected(repo, path) { + continue + } + data, err := repo.ReadContainedFile(path, 4<<20) + if err != nil { + plan.Findings = append(plan.Findings, sourceFinding(path, err.Error())) + continue + } + if !utf8.Valid(data) { + plan.Findings = append(plan.Findings, sourceFinding(path, "Rust source is not UTF-8 text")) + continue + } + plan.Inputs = append(plan.Inputs, SourceInput{Path: path, Source: string(data)}) + } + if len(plan.Inputs) == 0 { + return plan + } + request, err := json.Marshal(struct { + Protocol string `json:"protocol"` + Sources []SourceInput `json:"sources"` + }{SourceProtocol, plan.Inputs}) + if err != nil || len(request) > 8<<20 || len(plan.Inputs) > 4096 { + plan.Findings = append(plan.Findings, sourceFinding(plan.Inputs[0].Path, "Rust syntax request exceeds 8388608 bytes or 4096 sources; select a smaller source scope")) + return plan + } + plan.Commands = []policy.Command{{Name: "policy-rust-source-facts", Argv: repo.PythonCommand(sourceParser), Cwd: ".", Stdin: request, Managed: true, SealedEnvironment: true, TimeoutSeconds: 60}} + return plan +} + +func sourceAnalysisSelected(repo repository.Repository, path string) bool { + if repo.Language(path) != "rust" || repo.IsGenerated(path) || repo.IsData(path) { + return false + } + return repo.NativeAnalysis(path, "lint") || repo.NativeAnalysis(path, "complexity") || repo.NativeAnalysis(path, "format") +} + +func ReadSources(ctx context.Context, repo repository.Repository, plan SourcePlan, boundary runner.Runner) Sources { + result := Sources{Findings: slices.Clone(plan.Findings)} + if len(plan.Commands) == 0 { + return result + } + structured, ok := boundary.(runner.StructuredRunner) + if !ok { + result.Findings = append(result.Findings, sourceFinding(plan.Inputs[0].Path, "the runner cannot capture bounded Rust syntax facts")) + return result + } + _, output, err := structured.RunStructured(ctx, repo.Root, plan.Commands[0]) + if err != nil { + result.Findings = append(result.Findings, sourceFinding(plan.Inputs[0].Path, fmt.Sprintf("Rust syntax analysis failed: %v; %s", err, diagnosticOutput(output.Stderr)))) + return result + } + sources, err := ParseSources(repo, plan.Inputs, output.Stdout) + if err != nil { + result.Findings = append(result.Findings, sourceFinding(plan.Inputs[0].Path, "invalid Rust syntax evidence: "+err.Error())) + return result + } + for _, source := range sources { + if source.Complete { + result.Files = append(result.Files, source) + } else { + result.Findings = append(result.Findings, sourceFinding(source.Path, source.Error)) + } + } + return result +} + +func sourceFinding(path, reason string) policy.Finding { + return policy.Finding{Check: "policy.rustSource", Path: path, Subject: "syntax", Message: reason} +} diff --git a/internal/rustfacts/source_output.go b/internal/rustfacts/source_output.go new file mode 100644 index 00000000..e37089f5 --- /dev/null +++ b/internal/rustfacts/source_output.go @@ -0,0 +1,190 @@ +package rustfacts + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "sort" + "strings" + "unicode/utf8" + + "github.com/riteofstring/code-polishy/internal/repository" +) + +type SourceLocation struct { + StartByte int `json:"startByte"` + EndByte int `json:"endByte"` + Line int `json:"line"` + Column int `json:"column"` + EndLine int `json:"endLine"` + EndColumn int `json:"endColumn"` +} + +type SourceAttribute struct { + SourceLocation + Name string `json:"name"` + HasValue bool `json:"hasValue"` + TestConfiguration bool `json:"testConfiguration"` + Children []SourceAttribute `json:"children"` +} + +type SourceFunction struct { + SourceLocation + Name string `json:"name"` + Test bool `json:"test"` + Parameters int `json:"parameters"` + Complexity int `json:"complexity"` + Depth int `json:"depth"` +} + +type SourceMacro struct { + SourceLocation + Name string `json:"name"` + Definition bool `json:"definition"` +} + +type Source struct { + Path string `json:"path"` + Complete bool `json:"complete"` + Error string `json:"error"` + Comments []SourceLocation `json:"comments"` + Attributes []SourceAttribute `json:"attributes"` + Functions []SourceFunction `json:"functions"` + Macros []SourceMacro `json:"macros"` +} + +type sourceCoordinates struct { + text string + lineStarts []int +} + +func indexSource(text string) sourceCoordinates { + index := sourceCoordinates{text: text, lineStarts: []int{0}} + for offset := range len(text) { + if text[offset] == '\n' { + index.lineStarts = append(index.lineStarts, offset+1) + } + } + return index +} + +func ParseSources(repo repository.Repository, inputs []SourceInput, data []byte) ([]Source, error) { + if len(data) > 8<<20 { + return nil, errors.New("rust syntax response exceeds 8388608 bytes") + } + var response struct { + Protocol string `json:"protocol"` + PythonVersion string `json:"pythonVersion"` + ParserVersion string `json:"parserVersion"` + GrammarVersion string `json:"grammarVersion"` + Sources []Source `json:"sources"` + } + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&response); err != nil { + return nil, err + } + if decoder.Decode(new(any)) != io.EOF { + return nil, errors.New("rust syntax response contains trailing data") + } + python, _, _ := strings.Cut(repo.ToolPin("python"), "+") + if response.Protocol != SourceProtocol || response.PythonVersion != python || response.ParserVersion != "0.26.0" || response.GrammarVersion != "0.24.2" { + return nil, errors.New("rust syntax protocol or runtime versions differ from the governed release") + } + if err := validateSourceInventory(inputs, response.Sources); err != nil { + return nil, err + } + return response.Sources, nil +} + +func validateSourceInventory(inputs []SourceInput, sources []Source) error { + requested := map[string]string{} + for _, input := range inputs { + requested[input.Path] = input.Source + } + if len(requested) != len(inputs) || len(sources) != len(inputs) { + return errors.New("rust syntax inventory does not match the requested sources") + } + for _, source := range sources { + text, ok := requested[source.Path] + if !ok { + return errors.New("rust syntax response repeats or invents a source path") + } + delete(requested, source.Path) + if err := validateSourceFacts(source, text); err != nil { + return fmt.Errorf("%s: %w", source.Path, err) + } + } + return nil +} + +func validateSourceFacts(source Source, text string) error { + if source.Comments == nil || source.Attributes == nil || source.Functions == nil || source.Macros == nil || source.Complete == (source.Error != "") { + return errors.New("rust source omitted complete facts or its coverage failure") + } + locations, err := sourceFactLocations(source) + if err != nil { + return err + } + coordinates := indexSource(text) + if err := validateSourceAttributes(source.Attributes, coordinates, 0); err != nil { + return err + } + for _, location := range locations { + if err := validateSourceLocation(location, coordinates); err != nil { + return err + } + } + return nil +} + +func sourceFactLocations(source Source) ([]SourceLocation, error) { + locations := append([]SourceLocation{}, source.Comments...) + for _, function := range source.Functions { + if function.Name == "" || function.Complexity < 1 || function.Parameters < 0 || function.Depth < 0 { + return nil, errors.New("rust function omitted its identity or structural measurements") + } + locations = append(locations, function.SourceLocation) + } + for _, macro := range source.Macros { + if macro.Name == "" { + return nil, errors.New("rust macro omitted its original source identity") + } + locations = append(locations, macro.SourceLocation) + } + return locations, nil +} + +func validateSourceAttributes(attributes []SourceAttribute, coordinates sourceCoordinates, depth int) error { + if depth > 64 { + return errors.New("rust attribute facts exceed 64 levels") + } + for _, attribute := range attributes { + if attribute.Name == "" || attribute.Children == nil { + return errors.New("rust attribute omitted its parsed metadata") + } + if err := validateSourceLocation(attribute.SourceLocation, coordinates); err != nil { + return err + } + if err := validateSourceAttributes(attribute.Children, coordinates, depth+1); err != nil { + return err + } + } + return nil +} + +func validateSourceLocation(location SourceLocation, source sourceCoordinates) error { + if location.StartByte < 0 || location.EndByte < location.StartByte || location.EndByte > len(source.text) { + return errors.New("rust syntax location lies outside its source") + } + for _, point := range []struct{ offset, line, column int }{{location.StartByte, location.Line, location.Column}, {location.EndByte, location.EndLine, location.EndColumn}} { + line := sort.Search(len(source.lineStarts), func(index int) bool { return source.lineStarts[index] > point.offset }) - 1 + prefix := source.text[source.lineStarts[line]:point.offset] + if !utf8.ValidString(prefix) || point.line != line+1 || point.column != utf8.RuneCountInString(prefix)+1 { + return errors.New("rust syntax coordinates do not match the original UTF-8 source") + } + } + return nil +} diff --git a/internal/rustfacts/source_parser.py b/internal/rustfacts/source_parser.py new file mode 100644 index 00000000..d3f70018 --- /dev/null +++ b/internal/rustfacts/source_parser.py @@ -0,0 +1,276 @@ +import importlib.metadata +import json +import platform +import sys + +import tree_sitter_rust +from tree_sitter import Language, Parser + +PROTOCOL = "rust-source/v1" +MAXIMUM_REQUEST = 8 * 1024 * 1024 +MAXIMUM_NODES = 500_000 +COMMENTS = {"line_comment", "block_comment"} +CALLABLES = {"function_item", "closure_expression"} +BRANCHES = { + "if_expression", + "for_expression", + "while_expression", + "loop_expression", + "try_expression", + "or_pattern", +} + + +def location(node, source): + start = node.start_point + end = node.end_point + return { + "startByte": node.start_byte, + "endByte": node.end_byte, + "line": start.row + 1, + "column": len(source[node.start_byte - start.column : node.start_byte].decode()) + + 1, + "endLine": end.row + 1, + "endColumn": len(source[node.end_byte - end.column : node.end_byte].decode()) + + 1, + } + + +def path_name(node): + parts = [] + pending = [node] + while pending: + current = pending.pop() + if current.type in {"identifier", "crate", "self", "super"}: + parts.append(current.text.decode().removeprefix("r#")) + elif current.type not in COMMENTS: + pending.extend(reversed(current.named_children)) + return "::".join(parts) + + +def token_groups(node): + groups = [[]] + for token in node.children[1:-1]: + if token.type == ",": + groups.append([]) + elif token.type not in COMMENTS: + groups[-1].append(token) + return [group for group in groups if group] + + +def attribute_tokens(tokens, source, depth=0): + if depth > 64: + raise ValueError("Rust attribute nesting exceeds 64 levels") + parts = [] + position = 0 + while position < len(tokens) and tokens[position].type in { + "identifier", + "scoped_identifier", + "::", + }: + if tokens[position].type != "::": + parts.append(path_name(tokens[position])) + position += 1 + name = "::".join(parts) + arguments = next((token for token in tokens if token.type == "token_tree"), None) + groups = token_groups(arguments) if arguments is not None else [] + children = [] + if name == "cfg_attr": + children = [attribute_tokens(group, source, depth + 1) for group in groups[1:]] + return { + **location(tokens[0], source), + "name": name, + "hasValue": any(token.type == "=" for token in tokens), + "testConfiguration": name == "cfg" + and len(groups) == 1 + and len(groups[0]) == 1 + and groups[0][0].type == "identifier" + and groups[0][0].text == b"test", + "children": children, + } + + +def attribute(node, source): + value = node.named_children[0] + tokens = [child for child in value.children if child.type not in COMMENTS] + if not tokens: + raise ValueError("Rust attribute omitted its parsed path") + return attribute_tokens(tokens, source) + + +def item_attributes(node, source): + attributes = [] + sibling = node.prev_named_sibling + while sibling is not None and sibling.type in COMMENTS | {"attribute_item"}: + if sibling.type == "attribute_item": + attributes.append(attribute(sibling, source)) + sibling = sibling.prev_named_sibling + return attributes + + +def test_configuration(node, source): + attributes = item_attributes(node, source) + body = node.child_by_field_name("body") + scope = body if body is not None else node + attributes.extend( + attribute(child, source) + for child in scope.named_children + if child.type == "inner_attribute_item" + ) + return any(value["testConfiguration"] for value in attributes) + + +def function_fact(node, source, test_context): + name = node.child_by_field_name("name") + parameters = node.child_by_field_name("parameters") + attributes = item_attributes(node, source) + return { + **location(node, source), + "name": name.text.decode() if name is not None else "", + "test": test_context or any(value["name"] == "test" for value in attributes), + "parameters": sum( + child.type not in COMMENTS | {"attribute_item"} + for child in parameters.named_children + ) + if parameters is not None + else 0, + "complexity": 1, + "depth": 0, + } + + +def decisions(node): + if node.type in BRANCHES: + return 1 + if node.type == "binary_expression": + operator = node.child_by_field_name("operator") + return int(operator is not None and operator.type in {"&&", "||"}) + if node.type == "match_expression": + body = node.child_by_field_name("body") + return max( + 0, sum(child.type == "match_arm" for child in body.named_children) - 1 + ) + if node.type in {"match_pattern", "let_declaration"}: + field = "condition" if node.type == "match_pattern" else "alternative" + return int(node.child_by_field_name(field) is not None) + return 0 + + +def collect_node(node, source, result): + if node.type in COMMENTS: + result["comments"].append(location(node, source)) + elif node.type in {"attribute_item", "inner_attribute_item"}: + result["attributes"].append(attribute(node, source)) + elif node.type in {"macro_invocation", "macro_definition"}: + name = node.child_by_field_name( + "macro" if node.type == "macro_invocation" else "name" + ) + result["macros"].append( + { + **location(node, source), + "name": path_name(name) if name is not None else "", + "definition": node.type == "macro_definition", + } + ) + + +class SyntaxCollector: + def __init__(self, source, result): + self.source = source + self.result = result + + def walk(self, root): + pending = [(root, None, False, 0)] + visited = 0 + while pending: + visited += 1 + if visited > MAXIMUM_NODES: + raise ValueError("Rust syntax exceeds 500000 nodes") + pending.extend(self.visit(*pending.pop())) + + def visit(self, node, function, test_context, depth): + if node.type in {"source_file", "mod_item", "function_item"}: + test_context = test_context or test_configuration(node, self.source) + collect_node(node, self.source, self.result) + body = None + if node.type in CALLABLES: + body = node.child_by_field_name("body") + function = function_fact(node, self.source, test_context) + self.result["functions"].append(function) + test_context = function["test"] + depth = 0 + elif function is not None: + function["complexity"] += decisions(node) + depth += int(node.type == "block") + function["depth"] = max(function["depth"], depth) + children = [] + if node.type not in COMMENTS: + for child in reversed(node.named_children): + owner = function if body is None or child == body else None + children.append((child, owner, test_context, depth)) + return children + + +def source_facts(parser, entry): + source = entry["source"].encode("utf-8") + tree = parser.parse(source) + result = { + "path": entry["path"], + "complete": False, + "error": "", + "comments": [], + "attributes": [], + "functions": [], + "macros": [], + } + if tree is None or tree.root_node.has_error: + result["error"] = ( + "Rust syntax is incomplete or unsupported by the pinned grammar" + ) + return result + try: + SyntaxCollector(source, result).walk(tree.root_node) + result["complete"] = True + except ValueError as error: + result["error"] = str(error) + return result + + +def read_request(): + data = sys.stdin.buffer.read(MAXIMUM_REQUEST + 1) + if len(data) > MAXIMUM_REQUEST: + raise ValueError("Rust syntax request exceeds 8388608 bytes") + request = json.loads(data) + if set(request) != {"protocol", "sources"} or request["protocol"] != PROTOCOL: + raise ValueError("Unsupported Rust syntax request") + if ( + not isinstance(request["sources"], list) + or not 1 <= len(request["sources"]) <= 4096 + ): + raise ValueError("Rust syntax request requires between 1 and 4096 sources") + for entry in request["sources"]: + if set(entry) != {"path", "source"} or not all( + isinstance(value, str) for value in entry.values() + ): + raise ValueError("Rust syntax source requires a path and UTF-8 text") + return request + + +def main(): + request = read_request() + parser = Parser(Language(tree_sitter_rust.language())) + result = { + "protocol": PROTOCOL, + "pythonVersion": platform.python_version(), + "parserVersion": importlib.metadata.version("tree-sitter"), + "grammarVersion": importlib.metadata.version("tree-sitter-rust"), + "sources": [source_facts(parser, entry) for entry in request["sources"]], + } + output = json.dumps(result) + if len(output.encode("utf-8")) > MAXIMUM_REQUEST: + raise ValueError("Rust syntax response exceeds 8388608 bytes") + sys.stdout.write(output + "\n") + + +if __name__ == "__main__": + main() diff --git a/internal/rustfacts/source_test.go b/internal/rustfacts/source_test.go new file mode 100644 index 00000000..89347067 --- /dev/null +++ b/internal/rustfacts/source_test.go @@ -0,0 +1,166 @@ +package rustfacts + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestRustSourceFactsUseSyntaxForCommentsClosuresAndTests(t *testing.T) { + t.Parallel() + repo := rustRepository(t) + write(t, repo.Root, "src/lib.rs", `#![doc = "crate prose"] +/* outer /* nested */ comment */ +pub fn choose(value: Option) -> u8 { + let marker = r###"// /* not comments */"###; + let closure = |x: u8| if x > 0 && x < 5 { 1 } else { 2 }; + match value { Some(x) if x > 0 => closure(x), _ => 0 } +} +#[cfg(test)] +mod tests { + #[test] + fn test_it() { assert_eq!(choose(None), 0); } + fn helper() {} +} +mod inner_tests { + #![cfg(test)] + fn inner_helper() {} +} +#[test] +fn direct_test() { let check = || if true { 1 } else { 0 }; } +`) + result := ReadSources(t.Context(), repo, PlanSources(repo, []string{"src/lib.rs"}), runner.OSRunner{}) + if len(result.Findings) != 0 || len(result.Files) != 1 { + t.Fatalf("Rust source facts failed: %+v", result) + } + source := result.Files[0] + if len(source.Comments) != 1 || source.Comments[0].Line != 2 || len(source.Macros) != 1 || source.Macros[0].Name != "assert_eq" { + t.Fatalf("comments/raw strings/macros were confused: %+v", source) + } + for _, expected := range []struct { + name string + complexity, parameters int + test bool + }{{"choose", 3, 1, false}, {"", 3, 1, false}, {"test_it", 1, 0, true}, {"helper", 1, 0, true}, {"inner_helper", 1, 0, true}, {"direct_test", 1, 0, true}, {"", 2, 0, true}} { + if !slices.ContainsFunc(source.Functions, func(function SourceFunction) bool { + return function.Name == expected.name && function.Complexity == expected.complexity && function.Parameters == expected.parameters && function.Test == expected.test + }) { + t.Errorf("missing scoped function measurement %+v: %+v", expected, source.Functions) + } + } +} + +func TestRustSourceFactsPreserveConditionalAndRawAttributePaths(t *testing.T) { + t.Parallel() + repo := rustRepository(t) + write(t, repo.Root, "src/lib.rs", "#[cfg_attr(feature = \"x\", cfg_attr(feature = \"y\", r#allow(dead_code)), rustfmt::skip)]\n#[doc(hidden)]\npub fn f() {}\n") + result := ReadSources(t.Context(), repo, PlanSources(repo, []string{"src/lib.rs"}), runner.OSRunner{}) + if len(result.Findings) != 0 || len(result.Files) != 1 { + t.Fatalf("Rust attribute facts failed: %+v", result) + } + attributes := result.Files[0].Attributes + if len(attributes) != 2 || attributes[0].Name != "cfg_attr" || len(attributes[0].Children) != 2 || len(attributes[0].Children[0].Children) != 1 || attributes[0].Children[0].Children[0].Name != "allow" || attributes[0].Children[1].Name != "rustfmt::skip" || attributes[1].HasValue { + t.Fatalf("conditional attribute paths were lost: %+v", attributes) + } +} + +func TestRustSourceFactsKeepValidSiblingsAndProtectNonSourceInputs(t *testing.T) { + t.Parallel() + repo := rustRepository(t) + write(t, repo.Root, "good.rs", "pub fn café() { let emoji = \"🦀\"; /* prose */ }\n") + write(t, repo.Root, "bad.rs", "pub fn broken( {\n") + write(t, repo.Root, "generated.rs", "this is protected generated input\n") + repo.Config.Scope.Generated = []string{"generated.rs"} + plan := PlanSources(repo, []string{"good.rs", "bad.rs", "generated.rs"}) + result := ReadSources(t.Context(), repo, plan, runner.OSRunner{}) + if len(result.Files) != 1 || result.Files[0].Path != "good.rs" || len(result.Findings) != 1 || result.Findings[0].Path != "bad.rs" { + t.Fatalf("source coverage discarded a valid sibling or inspected generated code: %+v", result) + } + comment := result.Files[0].Comments[0] + if comment.Column != 34 || comment.StartByte != 37 { + t.Fatalf("UTF-8 offsets and displayed columns were conflated: %+v", comment) + } +} + +func TestRustSourceFactsRejectFabricatedOrIncompleteResponses(t *testing.T) { + t.Parallel() + repo := rustRepository(t) + write(t, repo.Root, "src/lib.rs", "pub fn f() {}\n") + plan := PlanSources(repo, []string{"src/lib.rs"}) + _, output, err := (runner.OSRunner{}).RunStructured(t.Context(), repo.Root, plan.Commands[0]) + if err != nil { + t.Fatal(err) + } + for name, corrupt := range map[string]func(map[string]any){ + "runtime": func(value map[string]any) { value["parserVersion"] = "0.0.0" }, + "empty": func(value map[string]any) { value["sources"] = []any{} }, + "foreign path": func(value map[string]any) { wireSource(value)["path"] = "outside.rs" }, + "missing facts": func(value map[string]any) { delete(wireSource(value), "comments") }, + "partial": func(value map[string]any) { wireSource(value)["complete"] = false }, + "coordinates": func(value map[string]any) { + wireSource(value)["functions"].([]any)[0].(map[string]any)["column"] = 999 + }, + } { + t.Run(name, func(t *testing.T) { + var response map[string]any + if err := json.Unmarshal(output.Stdout, &response); err != nil { + t.Fatal(err) + } + corrupt(response) + data, err := json.Marshal(response) + if err != nil { + t.Fatal(err) + } + if _, err := ParseSources(repo, plan.Inputs, data); err == nil { + t.Fatal("invalid source evidence was accepted") + } + }) + } +} + +func TestRustSourceFactsFailOnCancellationAndUncontainedFiles(t *testing.T) { + t.Parallel() + repo := rustRepository(t) + write(t, repo.Root, "src/lib.rs", "pub fn f() {}\n") + ctx, cancel := context.WithCancel(t.Context()) + cancel() + result := ReadSources(ctx, repo, PlanSources(repo, []string{"src/lib.rs"}), runner.OSRunner{}) + if len(result.Files) != 0 || len(result.Findings) != 1 || !strings.Contains(result.Findings[0].Message, "canceled") { + t.Fatalf("canceled parser produced coverage: %+v", result) + } + outside := filepath.Join(t.TempDir(), "outside.rs") + if err := os.WriteFile(outside, []byte("pub fn outside() {}\n"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.Symlink(outside, filepath.Join(repo.Root, "linked.rs")); err != nil { + t.Fatal(err) + } + plan := PlanSources(repo, []string{"linked.rs"}) + if len(plan.Commands) != 0 || len(plan.Findings) != 1 || plan.Findings[0].Path != "linked.rs" { + t.Fatalf("linked source reached the parser: %+v", plan) + } +} + +func wireSource(value map[string]any) map[string]any { + return value["sources"].([]any)[0].(map[string]any) +} + +func TestRustSourceFactsRespectConfiguredProviderOwnership(t *testing.T) { + t.Parallel() + repo := rustRepository(t) + write(t, repo.Root, "owned.rs", "not valid Rust but owned by the provider\n") + for _, capability := range []string{"format", "lint", "complexity"} { + repo.Config.Checks = append(repo.Config.Checks, policy.Command{Name: capability, Paths: []string{"owned.rs"}, RunOn: []string{"check"}, Provides: []string{capability}, Adapter: &policy.PackAdapter{PackName: "owner", Capability: capability}}) + } + plan := PlanSources(repo.WithAnalysisProfile("check"), []string{"owned.rs"}) + if len(plan.Commands) != 0 || len(plan.Findings) != 0 { + t.Fatalf("native parser claimed provider-owned source: %+v", plan) + } +} diff --git a/internal/rustfacts/uv.lock b/internal/rustfacts/uv.lock new file mode 100644 index 00000000..62ae37a5 --- /dev/null +++ b/internal/rustfacts/uv.lock @@ -0,0 +1,50 @@ +version = 1 +revision = 3 +requires-python = "==3.12.*" + +[[package]] +name = "code-polishy-rust-facts" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "tree-sitter" }, + { name = "tree-sitter-rust" }, +] + +[package.metadata] +requires-dist = [ + { name = "tree-sitter", specifier = "==0.26.0" }, + { name = "tree-sitter-rust", specifier = "==0.24.2" }, +] + +[[package]] +name = "tree-sitter" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/03/5600b84aff2e6c4fe80cfebb4063fe2f50299521befe5f6092ab8c082f4a/tree_sitter-0.26.0.tar.gz", hash = "sha256:b40c219edccc4564530c96f8f1556f6202b37cda964d1cbd7bd2b7e68b40a245", size = 191423, upload-time = "2026-06-30T12:14:27.933852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/ca/565702c44815393e3a973552ad546db4e5ca081ca8698640b4e93d809f51/tree_sitter-0.26.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6cb2bd20efb2544c19ac54486ab7cb8ec7b36f913bbe1ce95df84acb96743d9c", size = 148934, upload-time = "2026-06-30T12:14:01.188352Z" }, + { url = "https://files.pythonhosted.org/packages/54/6f/8bb61957f16ec1b1d92410a006cdc84a952b6352a7313b2ad299f2d21484/tree_sitter-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:918d89529786873f0982a0f59c2a303cd065fbfd1b903d71a8e4e1584f67b42e", size = 140820, upload-time = "2026-06-30T12:14:02.087619Z" }, + { url = "https://files.pythonhosted.org/packages/78/0a/8a6f08559182643a814a4ab559948ae817b2851890fd9b995a4fff6541ce/tree_sitter-0.26.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30a88be89ff1f2755297f81e8080d88b795dd98720c3f9fa2acf93873182cc95", size = 638844, upload-time = "2026-06-30T12:14:03.428205Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2f/6e6781b31677231366cb3cf27bc8269157f6d4b03c9032865a4f5f2bbe7e/tree_sitter-0.26.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a6b333b0282d8bb0af741f9b018bd2523d4eecb2686bf6717066a625fecfaa4", size = 667487, upload-time = "2026-06-30T12:14:04.669977Z" }, + { url = "https://files.pythonhosted.org/packages/02/0b/0483078c8567445557a7015b0e5b187f6d7d4fda73464df9c4bdea7f7f3c/tree_sitter-0.26.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f3c44339dd34fe8eb2b8d5aa7610660499a795f70376b130bbee7a437337280", size = 647975, upload-time = "2026-06-30T12:14:05.797417Z" }, + { url = "https://files.pythonhosted.org/packages/27/68/da83ca72c984e96ab4eb3bee0db1a6ffb5de1c8c455f92bd9f420cde7f0e/tree_sitter-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:94550e13b6ae576969da40246f4c4abb206380b5375ad43f26dd9151d55438e3", size = 665018, upload-time = "2026-06-30T12:14:07.278774Z" }, + { url = "https://files.pythonhosted.org/packages/d1/36/4d67927fd47b89af4a00f65f55a7370e28778cd50e972c2430487e3ecc27/tree_sitter-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca89e361a276dbc934b28a43dd881199e25d34ff5493ee0ce45f3c52a6124a37", size = 129619, upload-time = "2026-06-30T12:14:08.373423Z" }, + { url = "https://files.pythonhosted.org/packages/ed/72/cdefad523eb78710679c6da6a79e3d90f5afd32b1c6aa5a17bac7eef99f6/tree_sitter-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:bc6cb01d5ee75c85424aa1f1c72a82d8f07fd52539a0f3c4a6ed3e8721079b84", size = 116545, upload-time = "2026-06-30T12:14:09.273520Z" }, +] + +[[package]] +name = "tree-sitter-rust" +version = "0.24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/87/75cbd22b927267d310f76cca1ab3c1d9d41035dfa3eb9cc95f96ee199440/tree_sitter_rust-0.24.2.tar.gz", hash = "sha256:54fb02a5911e345308b405174465112479f56dc39e3f1e7744d7568595f00db9", size = 339341, upload-time = "2026-03-27T21:08:55.629400Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/24/2b2d33af5e27c84a4fde4e8cd2594bb4ab1e1cf48756a9f40dadc84956cc/tree_sitter_rust-0.24.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3620cfd12340efa43082d45df76349ff511893a9c361da2f8d6d51e307020a59", size = 129507, upload-time = "2026-03-27T21:08:47.585440Z" }, + { url = "https://files.pythonhosted.org/packages/78/2a/cf39f881a545360b5a86bb1accba1f4acc713daab01fb9edd35b6e84f473/tree_sitter_rust-0.24.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:01a46622735498493f29f3e628a90de95c96a07bfbeb88996243eb986b1cee36", size = 136812, upload-time = "2026-03-27T21:08:48.761553Z" }, + { url = "https://files.pythonhosted.org/packages/ca/45/a051bbd3045a61182dde25b93ae9a33d2677c935b16952283e12eaf46051/tree_sitter_rust-0.24.2-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e033c5a93b57c88e0a835880de39fc802909ff69f57aaff6000211c196ea5190", size = 164706, upload-time = "2026-03-27T21:08:49.605425Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f6/a5a146df5c0a5daea3ffcd5d7245775fe7f084357770d5a313dd6245ae78/tree_sitter_rust-0.24.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d76d1208c3638b871236090759dfc13d478921320653a6c9da5336e7c58f65a", size = 170310, upload-time = "2026-03-27T21:08:50.424427Z" }, + { url = "https://files.pythonhosted.org/packages/95/a8/f85b1ca75e01361ca5f92d226593ca4857cea49551b9f6c8fa6fc08ea917/tree_sitter_rust-0.24.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87930163a462408c49ab62c667e74029bc26b4cc7123dd1bdc7352215786c64a", size = 168668, upload-time = "2026-03-27T21:08:51.404932Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e1/3519f866a4679ca36acd9f5a06a779ecb8a92b18887c5546458d521df557/tree_sitter_rust-0.24.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:da2b86099028fd42c6cd32878b7b16b01f8aac0f7b0e98742b7fa6bc3cf09b89", size = 162403, upload-time = "2026-03-27T21:08:52.588953Z" }, + { url = "https://files.pythonhosted.org/packages/34/71/7ef609894dbfe5699eb16f7471f9b8af1d958d8ba3e29c238d7607e8cb47/tree_sitter_rust-0.24.2-cp39-abi3-win_amd64.whl", hash = "sha256:4529c125d928882ddfb879fdc6bc0704913261ecc078b6fa7902559e0daf200d", size = 129422, upload-time = "2026-03-27T21:08:54.031415Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d8/050a781172745bc345f98abb7c56e72022ea0790f8e793de981c83c2ef15/tree_sitter_rust-0.24.2-cp39-abi3-win_arm64.whl", hash = "sha256:66ba90f61bd54f4c4f5d30434957daf64507c16b0313df76becb37d63f70a227", size = 128245, upload-time = "2026-03-27T21:08:54.803983Z" }, +] diff --git a/schema/code-polishy.schema.json b/schema/code-polishy.schema.json index 10e11300..7f054b75 100644 --- a/schema/code-polishy.schema.json +++ b/schema/code-polishy.schema.json @@ -434,6 +434,8 @@ "properties": { "go": { "type": "integer", "minimum": 1, "maximum": 12 }, "goTest": { "type": "integer", "minimum": 1, "maximum": 20 }, + "rust": { "type": "integer", "minimum": 1, "maximum": 12 }, + "rustTest": { "type": "integer", "minimum": 1, "maximum": 20 }, "python": { "type": "integer", "minimum": 1, "maximum": 10 }, "typescript": { "type": "integer", "minimum": 1, "maximum": 10 }, "typescriptTest": { "type": "integer", "minimum": 1, "maximum": 10 } diff --git a/tools/install-policy-tools.ps1 b/tools/install-policy-tools.ps1 index 8ed3f354..e5514ea8 100644 --- a/tools/install-policy-tools.ps1 +++ b/tools/install-policy-tools.ps1 @@ -427,8 +427,8 @@ with zipfile.ZipFile(wheel) as archive: $AstroidSource = Get-Verified "astroid-$AstroidVersion.tar.gz" "https://files.pythonhosted.org/packages/da/fd/24475b7cfb70298e8921bc077adb46a3fe77887422545d8a061573e130ee/astroid-$AstroidVersion.tar.gz" & (Join-Path $PolicyRoot 'tools/install-astroid.ps1') -Python $Python -PythonRoot $PythonRoot -Wheel $AstroidWheel -Source $AstroidSource -Version $AstroidVersion - $FactsEnvironment = Join-Path $PolicyRoot 'internal/pythonfacts/.venv' - $FactsPython = Join-Path $FactsEnvironment 'Scripts/python.exe' + & $Python -I -B (Join-Path $PolicyRoot 'internal/rustfacts/install.py') $PolicyRoot + if ($LASTEXITCODE -ne 0) { throw 'Rust syntax distribution installation failed.' } function Test-PythonFactsEnvironment { if (-not (Test-Path -LiteralPath (Join-Path $FactsEnvironment 'pyvenv.cfg') -PathType Leaf) -or -not (Test-Path -LiteralPath $FactsPython -PathType Leaf) -or @@ -439,16 +439,20 @@ with zipfile.ZipFile(wheel) as archive: return $LASTEXITCODE -eq 0 -and $FactsProbe.Count -eq 3 -and $FactsProbe[0].Trim() -eq $PythonVersion -and $FactsProbe[1].Trim() -eq $PackagingVersion -and $FactsProbe[2].Trim() -eq $AstroidVersion } - if (-not (Test-PythonFactsEnvironment)) { - if (Test-Path -LiteralPath $FactsEnvironment) { - Remove-Item -LiteralPath $FactsEnvironment -Recurse -Force - } - & $Python -I -B -m venv --without-pip --system-site-packages $FactsEnvironment - if ($LASTEXITCODE -ne 0 -or -not (Test-PythonFactsEnvironment)) { + foreach ($FactsModule in @('pythonfacts', 'rustfacts')) { + $FactsEnvironment = Join-Path $PolicyRoot "internal/$FactsModule/.venv" + $FactsPython = Join-Path $FactsEnvironment 'Scripts/python.exe' + if (-not (Test-PythonFactsEnvironment)) { if (Test-Path -LiteralPath $FactsEnvironment) { Remove-Item -LiteralPath $FactsEnvironment -Recurse -Force } - throw 'The contained Python facts environment failed verification.' + & $Python -I -B -m venv --without-pip --system-site-packages $FactsEnvironment + if ($LASTEXITCODE -ne 0 -or -not (Test-PythonFactsEnvironment)) { + if (Test-Path -LiteralPath $FactsEnvironment) { + Remove-Item -LiteralPath $FactsEnvironment -Recurse -Force + } + throw 'The contained Python facts environment failed verification.' + } } } diff --git a/tools/install-policy-tools.sh b/tools/install-policy-tools.sh index 070e71eb..43b221cc 100755 --- a/tools/install-policy-tools.sh +++ b/tools/install-policy-tools.sh @@ -27,8 +27,7 @@ case "$(uname -m)" in *) echo "Unsupported architecture for the Python facts environment: $(uname -m)" >&2; exit 1 ;; esac python_bin="${policy_root}/.tools/python/${python_os}-${python_arch}/bin/python3.12" -facts_environment="${policy_root}/internal/pythonfacts/.venv" -facts_python="${facts_environment}/bin/python" +"${python_bin}" -I -B "${policy_root}/internal/rustfacts/install.py" "${policy_root}" facts_environment_valid() { [[ -f "${facts_environment}/pyvenv.cfg" ]] && [[ -x "${facts_python}" ]] && [[ "$("${facts_python}" -I -B -c 'import sys; print(".".join(str(value) for value in sys.version_info[:3]))' 2>/dev/null || true)" == "${python_release%%+*}" ]] && @@ -36,20 +35,24 @@ facts_environment_valid() { [[ "$("${facts_python}" -I -B -c 'import importlib.metadata; print(importlib.metadata.version("astroid"))' 2>/dev/null || true)" == "${astroid_version}" ]] && [[ ! -e "${facts_environment}/bin/pip" ]] } -if ! facts_environment_valid; then - facts_staging="$(mktemp -d "${policy_root}/internal/pythonfacts/.venv.staging.XXXXXX")" - if ! "${python_bin}" -I -B -m venv --copies --without-pip --system-site-packages "${facts_staging}"; then - rm -rf "${facts_staging}" - exit 1 - fi - rm -rf "${facts_environment}" - mv "${facts_staging}" "${facts_environment}" +for facts_module in pythonfacts rustfacts; do + facts_environment="${policy_root}/internal/${facts_module}/.venv" + facts_python="${facts_environment}/bin/python" if ! facts_environment_valid; then + facts_staging="$(mktemp -d "${policy_root}/internal/${facts_module}/.venv.staging.XXXXXX")" + if ! "${python_bin}" -I -B -m venv --copies --without-pip --system-site-packages "${facts_staging}"; then + rm -rf "${facts_staging}" + exit 1 + fi rm -rf "${facts_environment}" - echo "The contained Python facts environment failed verification." >&2 - exit 1 + mv "${facts_staging}" "${facts_environment}" + if ! facts_environment_valid; then + rm -rf "${facts_environment}" + echo "The contained Python facts environment failed verification." >&2 + exit 1 + fi fi -fi +done "${policy_root}/tools/install-vulture.sh" "${policy_root}/tools/install-osv-scanner.sh" "${policy_root}/tools/install-gremlins.sh" From a7a9feb17794253a0c6538c7fafa865ffedd7abd Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 03:03:24 +0000 Subject: [PATCH 15/49] feat: integrate native Rust module architecture Resolve written module/use/alias/re-export edges through Cargo targets and feed existing ownership, dependency and cycle policy. Bind Rust evidence to native graph reports; expose compiler unused-code coverage. Full rustfacts and architecture suites plus exact quality/schema regressions pass. Plain-crate public CLI check passes. Broader quality/documentation suites are running. Macro expansion, structured tests and installed-release integration remain incomplete; WGSL is a separate follow-on capability. --- .code-polishy.json | 10 + docs/catalog.json | 8 + docs/design/cargo-facts.md | 33 +- docs/design/python-contract-evidence.md | 5 + docs/design/rust-architecture.md | 22 ++ docs/plans/code-polishy-rust-support-plan.md | 42 ++- internal/architecture/architecture.go | 1 + internal/architecture/rust.go | 104 ++++++ internal/architecture/rust_evidence.go | 77 +++++ internal/architecture/rust_test.go | 167 ++++++++++ internal/architecture/sourcegraph/facts.go | 14 +- .../architecture/sourcegraph/facts_test.go | 26 ++ internal/engine/merge_gate.go | 3 +- internal/engine/report_model_test.go | 29 +- internal/quality/cargo_test.go | 19 ++ internal/quality/quality.go | 2 +- internal/quality/quality_test.go | 2 +- internal/repository/providers.go | 2 +- internal/rustfacts/compilation.go | 2 +- internal/rustfacts/graph.go | 184 +++++++++++ internal/rustfacts/graph_modules.go | 126 ++++++++ internal/rustfacts/graph_paths.go | 149 +++++++++ internal/rustfacts/graph_syntax.go | 52 +++ internal/rustfacts/source.go | 2 +- internal/rustfacts/source_output.go | 6 +- internal/rustfacts/source_parser.py | 297 +++++++++++++++++- internal/rustfacts/source_test.go | 2 +- schema/code-polishy-report.schema.json | 10 +- 28 files changed, 1358 insertions(+), 38 deletions(-) create mode 100644 docs/design/rust-architecture.md create mode 100644 internal/architecture/rust.go create mode 100644 internal/architecture/rust_evidence.go create mode 100644 internal/architecture/rust_test.go create mode 100644 internal/rustfacts/graph.go create mode 100644 internal/rustfacts/graph_modules.go create mode 100644 internal/rustfacts/graph_paths.go create mode 100644 internal/rustfacts/graph_syntax.go diff --git a/.code-polishy.json b/.code-polishy.json index eda807e7..4d75b99b 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -27,6 +27,15 @@ "path": "docs/design/cargo-facts.md", "module": "rustfacts" }, + { + "path": "docs/design/rust-architecture.md", + "sourcePaths": [ + "internal/architecture/rust.go", + "internal/architecture/rust_evidence.go", + "internal/architecture/rust_test.go", + "internal/architecture/architecture.go" + ] + }, { "path": "docs/design/rust-inputs.md", "sourcePaths": [ @@ -428,6 +437,7 @@ "javascript", "policy", "pythonfacts", + "rustfacts", "repository", "runner", "pack" diff --git a/docs/catalog.json b/docs/catalog.json index b7f8a539..ab4a8f5f 100644 --- a/docs/catalog.json +++ b/docs/catalog.json @@ -294,6 +294,14 @@ "aliases": ["size-analysis-design"], "public": true }, + { + "id": "rust-architecture", + "path": "docs/design/rust-architecture.md", + "title": "Rust Architecture Integration", + "summary": "Understand native Rust module resolution, dependency direction, cycles and explicit coverage limits.", + "aliases": [], + "public": true + }, { "id": "rust-inputs", "path": "docs/design/rust-inputs.md", diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index b799aae2..88075b02 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -106,6 +106,29 @@ not text searches. Metadata such as `doc(hidden)` is not prose. Inline `allow`, native policy ownership. Exceptions belong to the existing exact policy mechanism for the underlying finding. Source validation and command failure remain blocking. +## Written module dependencies + +Rust architecture consumes parsed module declarations, use trees, aliases, +re-exports and qualified paths. Cargo target roots establish crate identity; +directory names do not. Conventional `mod` paths and literal `#[path]` attributes +must resolve to contained governed source. Imports resolve through module and +item bindings, not a text search for names. The core retains physical source +paths and applies its existing module-direction and source-cycle policy. + +This graph describes potential dependencies in written source, including +conditional branches, as native Go import analysis does. It is not a claim that +every branch was compiled. Conflicting conditional module identities, opaque +generated imports, ambiguous bindings and missing source facts make graph +coverage incomplete rather than selecting a convenient edge or reporting an +empty graph. Compiler checks separately validate the selected Cargo configuration. + +The initial graph handles conventional external/inline modules, literal external +module paths, nested use trees, renamed workspace dependencies and local aliases +and re-exports. Standard-library derives are syntax-known. Macro invocations, +non-builtin attribute expansion, conditional/inline path overrides, absolute +external-prelude paths and unresolved external glob bindings currently fail +coverage explicitly. These are beta limitations, not optional exemptions. + ## Compiler checks Compilation uses the matching pinned cargo-clippy executable directly, with an @@ -134,8 +157,14 @@ The advertised `tail-call-track-caller` warning requires unstable explicit tail calls in this release and is not selectable on the supported stable toolchain. Its exclusion is verified against the real compiler, not silently ignored at runtime. -Parser-backed attribute governance and complete dead-code coverage remain -separate requirements. Project Clippy settings currently fail +Native dead-code coverage uses rustc's forced unused-item/import/macro diagnostics +for the selected Cargo configuration. Public library APIs remain legitimate +entry points; a private item used only by a disabled feature is unused in the +checked configuration. This is compiler reachability, not a claim that every +public API, configuration or macro expansion is globally live. Lint, type-check +and dead-code providers cannot split one native compilation unit. Parsed +attribute policy remains independent of compiler collection. +Project Clippy settings currently fail with an explicit unsupported-configuration diagnostic; they cannot silently override the release-owned defaults. diff --git a/docs/design/python-contract-evidence.md b/docs/design/python-contract-evidence.md index f30f6a46..ce3eab3b 100644 --- a/docs/design/python-contract-evidence.md +++ b/docs/design/python-contract-evidence.md @@ -65,6 +65,11 @@ read set; an empty external-consumer set performs no dependency scan. The public declaration shapes and supported consumer syntax are documented in [Code Quality](../policies/code-quality.md#python-ruff-vulture-and-ty). +Canonical graph evidence validates each native analyzer's protocol, manifest and +language independently. Rust facts bind Cargo projects and Rust source; they +cannot stand in for Python project evidence. The +[Rust graph integration](rust-architecture.md) describes that additional consumer. + ## Repository-owned contracts and bundled integrations `scope.pythonContracts` is repository authority for runtime consumption that diff --git a/docs/design/rust-architecture.md b/docs/design/rust-architecture.md new file mode 100644 index 00000000..7c9da394 --- /dev/null +++ b/docs/design/rust-architecture.md @@ -0,0 +1,22 @@ +# Rust Architecture Integration + +The Rust facts boundary owns Cargo identity and written module resolution. +Architecture consumes its physical source closure and imports through the same +canonical graph, module ownership, direction and cycle checks as other native +languages. Module declarations establish source membership; declaration alone is +not a runtime import edge. Resolved uses and qualified references supply edges. + +The [Cargo facts](cargo-facts.md) contract distinguishes written-source dependency +coverage from compilation and describes unsupported resolution. Incomplete Rust +facts withhold the canonical graph, never fabricate a successful empty graph. +Focused selections retain the connected crate closure without reporting problems +from unrelated crate graphs. Generated or provider-owned sources cannot silently +enter a native closure without their required evidence. + +Discovery and syntax commands are present in the shared execution plan and use +the caller's common runner. Rust does not add another scheduler or receipt store. + +Canonical fact inputs bind each Cargo manifest/root to its exact source snapshot, +parsed facts, package metadata, source partition and resolved edges. These digests +are reusable graph evidence, not local change detectors. Rust and Python analyzer +protocols, languages and project manifests cannot substitute for one another. diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index e1919513..9afaaaf5 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -22,6 +22,12 @@ compiler/parser facts through a bounded child-process protocol. Do not embed language-specific heuristics in unrelated engine modules or reimplement Rust resolution with regular expressions. +Rust follows Go's built-in native routing. A separate conditional-module switch +is not necessary to activate or disable these baseline capabilities. WGSL remains +a distinct follow-on language capability for graphics projects, not a prerequisite +for ordinary Rust parity. Its existing source classification and unavailable +analysis findings remain honest until that validator is delivered. + Parity means the same ordinary user-facing commands and policy outcomes as Go, with Rust-native compilation units, Cargo conventions and honest scope. Features, targets, build scripts, procedural macros, tests and generated source need their @@ -91,8 +97,9 @@ Package paths below are under `internal/` in this repository. target/features, wire formatting, compiler checks, core quality/architecture, dependency policy and actual test evidence into existing commands. Unsupported layouts remain explicit failures. This is a beta subset, not general support. -4. **Workspace and semantic coverage:** expand the supported configuration - matrix to the Phase 1–5 cases below, including WGSL and native boundaries. +4. **Workspace and semantic coverage:** expand the supported Rust configuration + matrix to the Phase 1–5 cases below, including native boundaries. WGSL has a + separate follow-on acceptance track. 5. **Installed-release parity:** prove every acceptance cell on the supported hosts before advertising first-class support and adopting it in Masterbrainer. @@ -122,10 +129,22 @@ changed modules; add a quick suite when a new module is introduced. function/closure measurements. Inline tests do not relax production limits. The pinned Tree-sitter Rust grammar supplies written-source facts; it does not pretend to resolve macro expansion or compiler configuration. -- Architecture, structured Rust test evidence, WGSL and installed-release - integration remain unfinished. Project Clippy overrides +- Structured Rust test evidence, general macro/module resolution, WGSL and + installed-release integration remain unfinished. Project Clippy overrides and non-native targets currently fail explicitly rather than weakening the supported compiler configuration. +- Native dead-code coverage uses the pinned compiler's forced unused diagnostics + for the checked configuration, preserving public library entry points. It does + not claim whole-program public-API reachability or unused-code proof for + configurations that were not compiled. +- Written-module architecture now connects Cargo targets, conventional and literal + module paths, use trees, aliases, re-exports and qualified references to existing + dependency-direction and cycle policy. Real Cargo fixtures cover forbidden + edges, source cycles, renamed workspace dependencies, unrelated invalid crates + and command-plan agreement. Native graph evidence survives the report schema. + Macro expansion and other unresolved syntax still fail coverage explicitly. + A plain crate passes the complete public CLI `check`; this is a usable subset, + not evidence that macro-heavy or arbitrary target configurations are supported. Cargo dependency-input governance now parses ordinary, build, development, target-specific and workspace declaration tables, checks exact registry and Git @@ -189,9 +208,9 @@ Pinned Cargo packaging fixtures cover inherited workspace license metadata. the native host and default features. Additional feature/target selections need explicit analysis evidence. Installation hosts remain Linux/macOS on x64/arm64 and Windows x64; installation does not prove cross-compilation. -- [ ] Define a first-class Rust conditional module activated by governed Rust - source/Cargo workspaces. Unsupported or missing required analysis must block - completion rather than fall back to a successful generic command. +- [x] Route governed Rust through first-class native capabilities, like Go, + without a separate conditional-module switch. Unsupported or missing + analysis remains a blocking finding, not a successful generic command. | Capability | Required built-in behavior | | ------------ | ---------------------------------------------------------------------------------------------- | @@ -283,9 +302,10 @@ Pinned Cargo packaging fixtures cover inherited workspace license metadata. a blanket prohibition nor a global exemption is appropriate. - [ ] Govern Rust lint attributes and `#[expect]` without blanket allows. Keep diagnostic IDs stable; policy exceptions stay exact, justified and expiring. -- [ ] Validate WGSL with an exact compatible parser/validator, map shader assets +- [ ] Follow-on WGSL track: validate shaders with an exact compatible parser/validator, map shader assets to consumers, and invalidate receipts when shaders change. GPU execution and - visual correctness remain project-specific evidence. + visual correctness remain project-specific evidence. This is separate from + the core Rust delivery gate. ## Phase 4: Rust supply-chain governance @@ -366,8 +386,8 @@ dependency governance now have runnable beta implementations. Source parsing use exact authenticated binary wheels in the existing governed Python runtime; it requires neither target package installation scripts nor a new Rust bootstrap. -The next milestone completes architecture and nonempty test evidence for one crate -path, then extends workspace coverage and installed-release integration. Semantic +The next milestone extends module/macro resolution and nonempty test evidence, +then completes workspace coverage and installed-release integration. General module resolution, structured Rust test results and installed-release parity remain open. Existing missing-capability findings remain until those implementations and evidence exist. This beta is not diff --git a/internal/architecture/architecture.go b/internal/architecture/architecture.go index 43ce15cd..41bb6706 100644 --- a/internal/architecture/architecture.go +++ b/internal/architecture/architecture.go @@ -43,6 +43,7 @@ func AnalyzeWithRunner(ctx context.Context, repo repository.Repository, selected goSourceGraph(repo, nativeSelected, allFiles, goInventory), javascriptSourceGraph(ctx, repo, nativeSelected, allFiles), pythonSourceGraph(ctx, repo, nativeSelected, allFiles, commandRunner), + rustSourceGraph(ctx, repo, nativeSelected, allFiles, commandRunner), ) connectProviderImports(repo, allFiles, &part) if !part.incomplete { diff --git a/internal/architecture/rust.go b/internal/architecture/rust.go new file mode 100644 index 00000000..2e48a4e8 --- /dev/null +++ b/internal/architecture/rust.go @@ -0,0 +1,104 @@ +package architecture + +import ( + "context" + + "github.com/riteofstring/code-polishy/internal/architecture/sourcegraph" + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" + "github.com/riteofstring/code-polishy/internal/rustfacts" +) + +func rustSourceGraph(ctx context.Context, repo repository.Repository, selected, allFiles []string, boundary runner.Runner) sourceGraphPart { + selected = rustGraphSelection(repo, selected) + if len(selected) == 0 { + return sourceGraphPart{} + } + plan := rustGraphDiscovery(repo, selected, allFiles) + discovery := rustfacts.Discover(ctx, repo, plan, boundary) + sourcePlan := rustGraphSources(repo, allFiles) + sources := rustfacts.ReadSources(ctx, repo, sourcePlan, boundary) + graph := rustfacts.ResolveModules(repo, discovery.Workspaces, sources, selected, allFiles) + part := sourceGraphPart{findings: append(discovery.Findings, graph.Findings...)} + part.incomplete = len(part.findings) > 0 + governed := map[string]bool{} + for _, path := range allFiles { + governed[path] = true + } + for _, source := range graph.Sources { + owner, err := sourceModuleOwner(repo, source.Path) + if err != nil { + part.findings = append(part.findings, policy.Finding{Check: "architecture.importCoverage", Path: source.Path, Subject: "rust", Message: err.Error()}) + part.incomplete = true + continue + } + part.nodes = append(part.nodes, sourcegraph.Node{Path: source.Path, Language: "rust", Generated: repo.IsGenerated(source.Path), Test: repo.IsTest(source.Path), Root: source.Root, Module: owner, Resolution: "file:" + source.Path}) + } + seen := map[sourcegraph.Edge]bool{} + for _, imported := range graph.Imports { + kind := sourcegraph.EdgeRuntime + if imported.ReExport { + kind = sourcegraph.EdgeReExport + } + edge := sourcegraph.Edge{Source: imported.Source, Target: imported.Target, SourceResolution: "file:" + imported.Source, TargetResolution: "file:" + imported.Target, Line: imported.Line, Column: imported.Column, Ecosystem: "rust", Kind: kind} + if seen[edge] { + continue + } + seen[edge] = true + part.edges = append(part.edges, edge) + if finding, found := importModuleFinding(repo, governed, imported.Source, imported.Target, imported.Line); found { + finding.Line, finding.Column = imported.Line, imported.Column + part.findings = append(part.findings, finding) + } + } + if !part.incomplete { + inputs, err := rustGraphInputs(repo, part, sourcePlan, sources, discovery) + if err != nil { + part.incomplete = true + part.findings = append(part.findings, sourceGraphCoverageFinding(err.Error())) + } else { + part.inputs = inputs + } + } + return part +} + +func rustGraphSelection(repo repository.Repository, files []string) []string { + selected := []string{} + for _, path := range files { + if repo.Language(path) == "rust" && repo.NativeAnalysis(path, "architecture") { + selected = append(selected, path) + } + } + return selected +} + +func RustGraphCommands(repo repository.Repository, files []string) []policy.Command { + files = rustGraphSelection(repo, files) + if len(files) == 0 { + return nil + } + allFiles, err := repo.AllFiles() + if err != nil { + return nil + } + commands := rustGraphDiscovery(repo, files, allFiles).Commands + return append(commands, rustGraphSources(repo, allFiles).Commands...) +} + +func rustGraphDiscovery(repo repository.Repository, files, allFiles []string) rustfacts.DiscoveryPlan { + plan := rustfacts.PlanDiscovery(repo, files, allFiles) + for index := range plan.Commands { + plan.Commands[index].Name += "-architecture" + } + return plan +} + +func rustGraphSources(repo repository.Repository, allFiles []string) rustfacts.SourcePlan { + plan := rustfacts.PlanSources(repo, allFiles) + for index := range plan.Commands { + plan.Commands[index].Name += "-architecture" + } + return plan +} diff --git a/internal/architecture/rust_evidence.go b/internal/architecture/rust_evidence.go new file mode 100644 index 00000000..3a0729b5 --- /dev/null +++ b/internal/architecture/rust_evidence.go @@ -0,0 +1,77 @@ +package architecture + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "path/filepath" + + "github.com/riteofstring/code-polishy/internal/architecture/sourcegraph" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/rustfacts" +) + +func rustGraphInputs(repo repository.Repository, part sourceGraphPart, plan rustfacts.SourcePlan, sources rustfacts.Sources, discovery rustfacts.Discovery) ([]sourcegraph.FactInput, error) { + result := []sourcegraph.FactInput{} + for _, nodes := range providerNodeGroups(part.nodes) { + input := sourcegraph.FactInput{Analyzer: "rust-facts", Protocol: rustfacts.SourceProtocol, Root: nodes[0].Root} + input.Project = filepath.ToSlash(filepath.Join(input.Root, "Cargo.toml")) + selected := map[string]bool{} + for _, node := range nodes { + input.Paths = append(input.Paths, node.Path) + selected[node.Path] = true + } + facts := rustGraphProjectFacts(repo, plan, sources, discovery, input.Project, selected) + edges := []sourcegraph.Edge{} + for _, edge := range part.edges { + if selected[edge.Source] { + edges = append(edges, edge) + } + } + for _, evidence := range []struct { + destination *string + value any + }{ + {&input.FactsSHA256, facts}, {&input.PartitionsSHA256, nodes}, {&input.ResolutionSHA256, edges}, + } { + data, err := json.Marshal(evidence.value) + if err != nil { + return nil, err + } + digest := sha256.Sum256(data) + *evidence.destination = hex.EncodeToString(digest[:]) + } + result = append(result, input) + } + return result, nil +} + +type rustProjectGraphFacts struct { + Python string + Rust string + Sources []rustfacts.Source + Inputs []rustfacts.SourceInput + Package rustfacts.Package +} + +func rustGraphProjectFacts(repo repository.Repository, plan rustfacts.SourcePlan, sources rustfacts.Sources, discovery rustfacts.Discovery, manifest string, selected map[string]bool) rustProjectGraphFacts { + facts := rustProjectGraphFacts{Python: repo.ToolPin("python"), Rust: repo.ToolPin("rust")} + for _, source := range sources.Files { + if selected[source.Path] { + facts.Sources = append(facts.Sources, source) + } + } + for _, input := range plan.Inputs { + if selected[input.Path] { + facts.Inputs = append(facts.Inputs, input) + } + } + for _, workspace := range discovery.Workspaces { + for _, pkg := range workspace.Packages { + if pkg.Manifest == manifest { + facts.Package = pkg + } + } + } + return facts +} diff --git a/internal/architecture/rust_test.go b/internal/architecture/rust_test.go new file mode 100644 index 00000000..c4887696 --- /dev/null +++ b/internal/architecture/rust_test.go @@ -0,0 +1,167 @@ +package architecture + +import ( + "context" + "path/filepath" + "slices" + "testing" + + "github.com/riteofstring/code-polishy/internal/architecture/sourcegraph" + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestRustArchitectureResolvesUseTreesAliasesAndReExports(t *testing.T) { + t.Parallel() + repo := rustArchitectureRepository(t) + writeArchitectureFile(t, repo.Root, "src/lib.rs", "pub mod api;\npub mod domain;\npub use domain::Value;\n") + writeArchitectureFile(t, repo.Root, "src/api.rs", "use crate::domain::{self as model, Value as Alias};\npub fn value() -> Alias { model::make() }\n") + writeArchitectureFile(t, repo.Root, "src/domain.rs", "pub struct Value;\npub fn make() -> Value { Value }\n") + analysis := AnalyzeWithRunner(t.Context(), repo, []string{"src/api.rs"}, runner.OSRunner{}) + if analysis.Graph == nil || !slices.ContainsFunc(analysis.Findings, func(finding policy.Finding) bool { + return finding.Check == "architecture.moduleDependency" && finding.Path == "src/api.rs" && finding.Subject == "domain" && finding.Line == 1 + }) { + t.Fatalf("forbidden aliased dependency was lost: %+v", analysis) + } + repo.Config.Modules[1].DependsOn = []string{"domain"} + analysis = AnalyzeWithRunner(t.Context(), repo, []string{"src/api.rs"}, runner.OSRunner{}) + if analysis.Graph == nil || len(analysis.Findings) != 0 || len(analysis.Graph.Nodes) != 3 { + t.Fatalf("valid declared Rust graph failed: %+v", analysis) + } + if !slices.ContainsFunc(analysis.Graph.Edges, func(edge sourcegraph.Edge) bool { + return edge.Source == "src/lib.rs" && edge.Target == "src/domain.rs" && edge.Kind == sourcegraph.EdgeReExport + }) { + t.Fatalf("re-export lost its original file edge: %+v", analysis.Graph.Edges) + } +} + +func TestRustArchitectureReportsSourceCyclesWithoutDeclarationCycles(t *testing.T) { + t.Parallel() + repo := rustArchitectureRepository(t) + repo.Config.Modules[1].DependsOn = []string{"domain"} + repo.Config.Modules[2].DependsOn = []string{"api"} + writeArchitectureFile(t, repo.Root, "src/lib.rs", "pub mod api;\npub mod domain;\n") + writeArchitectureFile(t, repo.Root, "src/api.rs", "pub fn first() { crate::domain::second(); }\n") + writeArchitectureFile(t, repo.Root, "src/domain.rs", "pub fn second() { crate::api::first(); }\n") + analysis := AnalyzeWithRunner(t.Context(), repo, []string{"src/api.rs"}, runner.OSRunner{}) + finding := onlyCycleFinding(t, analysis.Findings, "architecture.fileCycle") + if analysis.Graph == nil || len(finding.DependencyComponent.Members) != 2 || slices.ContainsFunc(finding.DependencyComponent.Members, func(node policy.DependencyNode) bool { return node.Path == "src/lib.rs" }) { + t.Fatalf("module declaration became a fabricated import cycle: %+v", analysis) + } +} + +func TestRustArchitectureKeepsLiteralPathsInlineModulesAndUnrelatedCrates(t *testing.T) { + t.Parallel() + repo := rustArchitectureRepository(t) + writeArchitectureFile(t, repo.Root, "src/lib.rs", "#[path = \"domain.rs\"]\npub mod renamed;\nmod inline { pub use crate::renamed::Value as Alias; }\npub fn value() -> inline::Alias { renamed::Value }\n") + writeArchitectureFile(t, repo.Root, "src/domain.rs", "pub struct Value;\n") + writeArchitectureFile(t, repo.Root, "other/Cargo.toml", "[package]\nname='other'\nversion='1.0.0'\n[workspace]\n") + writeArchitectureFile(t, repo.Root, "other/src/lib.rs", "invalid Rust in an unrelated crate\n") + analysis := AnalyzeWithRunner(t.Context(), repo, []string{"src/lib.rs"}, runner.OSRunner{}) + if analysis.Graph == nil || len(analysis.Findings) != 0 || len(analysis.Graph.Nodes) != 2 { + t.Fatalf("literal path/inline alias or focused scope failed: %+v", analysis) + } +} + +func TestRustArchitectureCannotPassOpaqueOrMissingModuleEvidence(t *testing.T) { + t.Parallel() + for name, source := range map[string]string{ + "macro": "unknown_macro!();\n", + "attribute macro": "#[generate_imports]\npub fn entry() {}\n", + "conditional macro": "#[cfg_attr(feature = \"extra\", generate_imports)]\npub fn entry() {}\n", + "derive macro": "#[derive(External)]\npub struct Value;\n", + "absolute path": "pub fn call() { ::hidden::call(); }\n", + "missing": "mod absent;\n", + "escape": "#[path = \"../../outside.rs\"]\nmod outside;\n", + "conditional path": "#[cfg_attr(feature = \"alternate\", path = \"other.rs\")]\nmod other;\n", + "unresolved local path": "pub fn call() { crate::missing::call(); }\n", + } { + t.Run(name, func(t *testing.T) { + repo := rustArchitectureRepository(t) + writeArchitectureFile(t, repo.Root, "src/lib.rs", source) + analysis := AnalyzeWithRunner(t.Context(), repo, []string{"src/lib.rs"}, runner.OSRunner{}) + if analysis.Graph != nil || !slices.ContainsFunc(analysis.Findings, func(finding policy.Finding) bool { + return finding.Check == "architecture.importCoverage" && finding.Path == "src/lib.rs" + }) { + t.Fatalf("missing Rust graph evidence passed: %+v", analysis) + } + }) + } +} + +func TestRustArchitectureResolvesRenamedWorkspaceDependencyWithoutUnrelatedFailures(t *testing.T) { + t.Parallel() + repo := rustArchitectureRepository(t) + repo.Config.Modules = []policy.Module{ + {Name: "client", Paths: []string{"client/**"}}, + {Name: "model", Paths: []string{"model/**"}}, + } + repo.Config.ModuleByName = map[string]int{"client": 0, "model": 1} + writeArchitectureFile(t, repo.Root, "Cargo.toml", "[workspace]\nmembers=['client','model','unused']\nresolver='3'\n") + writeArchitectureFile(t, repo.Root, "client/Cargo.toml", "[package]\nname='client'\nversion='1.0.0'\nedition='2024'\n[dependencies]\nmodel_alias={package='shared-model',path='../model',version='=1.0.0'}\n") + writeArchitectureFile(t, repo.Root, "client/src/lib.rs", "pub use model_alias::Value;\n") + writeArchitectureFile(t, repo.Root, "model/Cargo.toml", "[package]\nname='shared-model'\nversion='1.0.0'\nedition='2024'\n") + writeArchitectureFile(t, repo.Root, "model/src/lib.rs", "pub struct Value;\n") + writeArchitectureFile(t, repo.Root, "unused/Cargo.toml", "[package]\nname='unused'\nversion='1.0.0'\nedition='2024'\n") + writeArchitectureFile(t, repo.Root, "unused/src/lib.rs", "invalid unrelated source\n") + selected := []string{"client/src/lib.rs"} + analysis := AnalyzeWithRunner(t.Context(), repo, selected, runner.OSRunner{}) + if analysis.Graph == nil || len(analysis.Findings) != 1 || analysis.Findings[0].Subject != "model" { + t.Fatalf("renamed local crate lost its dependency policy: %+v", analysis) + } + repo.Config.Modules[0].DependsOn = []string{"model"} + analysis = AnalyzeWithRunner(t.Context(), repo, selected, runner.OSRunner{}) + if analysis.Graph == nil || len(analysis.Findings) != 0 || len(analysis.Graph.Nodes) != 2 || len(analysis.Graph.Inputs) != 2 { + t.Fatalf("connected workspace graph included an unrelated member or lost evidence: %+v", analysis) + } +} + +func TestRustArchitectureCommandPlanMatchesRealExecution(t *testing.T) { + t.Parallel() + repo := rustArchitectureRepository(t) + writeArchitectureFile(t, repo.Root, "src/lib.rs", "#[derive(Default, Debug)]\npub struct Value;\n") + boundary := &rustGraphRecordingRunner{} + selected := []string{"src/lib.rs"} + plan := RustGraphCommands(repo, selected) + analysis := AnalyzeWithRunner(t.Context(), repo, selected, boundary) + if analysis.Graph == nil || len(analysis.Findings) != 0 { + t.Fatalf("real Rust graph failed: %+v", analysis) + } + expected := []string{} + for _, command := range plan { + expected = append(expected, command.Name) + } + if !slices.Equal(expected, boundary.names) || len(expected) < 4 { + t.Fatalf("execution %v differs from plan %v", boundary.names, expected) + } +} + +func rustArchitectureRepository(t *testing.T) repository.Repository { + t.Helper() + root := t.TempDir() + policyRoot, err := filepath.Abs("../..") + if err != nil { + t.Fatal(err) + } + repo := repository.Repository{Root: root, PolicyRoot: policyRoot, Config: policy.Config{ + Modules: []policy.Module{ + {Name: "crate", Paths: []string{"src/lib.rs"}, DependsOn: []string{"api", "domain"}}, + {Name: "api", Paths: []string{"src/api.rs"}}, + {Name: "domain", Paths: []string{"src/domain.rs"}}, + }, ModuleByName: map[string]int{"crate": 0, "api": 1, "domain": 2}, + }} + writeArchitectureFile(t, root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\nedition='2024'\n[workspace]\n") + writeArchitectureFile(t, root, "src/lib.rs", "pub fn entry() {}\n") + return repo +} + +type rustGraphRecordingRunner struct { + runner.OSRunner + names []string +} + +func (boundary *rustGraphRecordingRunner) RunStructured(ctx context.Context, root string, command policy.Command) (runner.Result, runner.Output, error) { + boundary.names = append(boundary.names, command.Name) + return boundary.OSRunner.RunStructured(ctx, root, command) +} diff --git a/internal/architecture/sourcegraph/facts.go b/internal/architecture/sourcegraph/facts.go index 92e41f34..eb4f5fe8 100644 --- a/internal/architecture/sourcegraph/facts.go +++ b/internal/architecture/sourcegraph/facts.go @@ -3,6 +3,7 @@ package sourcegraph import ( "encoding/hex" "fmt" + "path" "reflect" "slices" "strings" @@ -50,7 +51,11 @@ func normalizeFactInput(input FactInput) (FactInput, error) { return FactInput{}, fmt.Errorf("source dependency graph fact root: %w", err) } project, err := normalizePath(input.Project, false) - if err != nil || project != factProjectManifest(root) { + expected := factProjectManifest(root) + if input.Analyzer == "rust-facts" { + expected = path.Join(root, "Cargo.toml") + } + if err != nil || project != expected { return FactInput{}, fmt.Errorf("source dependency graph fact project does not match its root") } for _, digest := range []string{input.FactsSHA256, input.PartitionsSHA256, input.ResolutionSHA256} { @@ -72,6 +77,8 @@ func validFactAnalyzer(analyzer, protocol string) bool { return protocol == "python-facts/v3" case "ruff": return protocol == "ruff-graph-facts/v2" + case "rust-facts": + return protocol == "rust-source/v1" default: return false } @@ -94,11 +101,14 @@ func coverFactInput(input FactInput, nodes map[string]Node, covered map[string]b for _, path := range input.Paths { node, exists := nodes[path] languageMatches := node.Language == "python" + if input.Analyzer == "rust-facts" { + languageMatches = node.Language == "rust" + } if input.Provider != nil { languageMatches = slices.Contains(input.Provider.Languages, node.Language) } if !exists || !languageMatches || node.Root != input.Root || covered[path] { - return fmt.Errorf("source dependency graph fact path %q is absent, duplicated, or outside its Python project", path) + return fmt.Errorf("source dependency graph fact path %q is absent, duplicated, or outside its analyzer project", path) } covered[path] = true } diff --git a/internal/architecture/sourcegraph/facts_test.go b/internal/architecture/sourcegraph/facts_test.go index a81a4002..a32aea55 100644 --- a/internal/architecture/sourcegraph/facts_test.go +++ b/internal/architecture/sourcegraph/facts_test.go @@ -66,6 +66,32 @@ func TestGraphFactInputsAcceptRuffArchitectureEvidence(t *testing.T) { } } +func TestRustGraphFactsCannotReplacePythonOrForeignProjectEvidence(t *testing.T) { + t.Parallel() + node := Node{Path: "crate/src/lib.rs", Language: "rust", Root: "crate", Module: "app", Resolution: "file:crate/src/lib.rs"} + input := graphTestFactInput("crate", node.Path) + input.Analyzer, input.Protocol, input.Project = "rust-facts", "rust-source/v1", "crate/Cargo.toml" + if graph, err := New([]Node{node}, nil, []FactInput{input}, nil); err != nil || Validate(graph) != nil { + t.Fatalf("valid native Rust evidence failed: %+v, %v", graph, err) + } + for _, corrupt := range []func(*FactInput){ + func(input *FactInput) { input.Project = "crate/pyproject.toml" }, + func(input *FactInput) { input.Protocol = "python-facts/v3" }, + func(input *FactInput) { input.Paths = []string{"other/src/lib.rs"} }, + func(input *FactInput) { + input.Analyzer = "python-facts" + input.Protocol = "python-facts/v3" + input.Project = "crate/pyproject.toml" + }, + } { + changed := input + corrupt(&changed) + if _, err := New([]Node{node}, nil, []FactInput{changed}, nil); err == nil { + t.Fatalf("mismatched Rust graph evidence passed: %+v", changed) + } + } +} + func TestGraphFactEvidenceChangesIdentityWithoutChangingCycles(t *testing.T) { t.Parallel() node := Node{Path: "a.py", Language: "python", Root: ".", Module: "app", Resolution: "file:a.py"} diff --git a/internal/engine/merge_gate.go b/internal/engine/merge_gate.go index b97298c1..11bd53f6 100644 --- a/internal/engine/merge_gate.go +++ b/internal/engine/merge_gate.go @@ -320,7 +320,8 @@ func plannedPolicyCheckCommands(repo repository.Repository, selection repository func plannedArchitectureCommands(repo repository.Repository, files []string) []policy.Command { commands := architecture.ProviderCommands(repo, files) - return append(commands, architecture.PythonGraphCommands(repo, files)...) + commands = append(commands, architecture.PythonGraphCommands(repo, files)...) + return append(commands, architecture.RustGraphCommands(repo, files)...) } func (engine *Engine) mergeGateTestRequest(plan MergeGateExecutionPlan) (repository.Selection, testpolicy.Request, error) { diff --git a/internal/engine/report_model_test.go b/internal/engine/report_model_test.go index 1fe0d54d..0662051f 100644 --- a/internal/engine/report_model_test.go +++ b/internal/engine/report_model_test.go @@ -273,18 +273,25 @@ func TestSourceGraphAndCycleEvidenceValidateAgainstPinnedReportSchema(t *testing validateSchemaDocument(t, ReportSchemaURL, policyschema.CodePolishyReport, data) } -func TestPythonGraphFactEvidenceValidatesAgainstPinnedReportSchema(t *testing.T) { - graph, err := sourcegraph.New([]sourcegraph.Node{{Path: "app.py", Language: "python", Root: ".", Module: "app", Resolution: "file:app.py"}}, nil, - []sourcegraph.FactInput{{Analyzer: "python-facts", Protocol: "python-facts/v3", Project: "pyproject.toml", Root: ".", Paths: []string{"app.py"}, FactsSHA256: strings.Repeat("a", 64), PartitionsSHA256: strings.Repeat("b", 64), ResolutionSHA256: strings.Repeat("c", 64)}}, nil) - if err != nil { - t.Fatal(err) - } - report := (&Engine{}).normalizeReport(Report{Command: "architecture", ReportPath: ".code-polishy-reports/architecture/example/report.json", SourceDependencyGraph: &graph}) - data, err := JSONReport(report) - if err != nil { - t.Fatal(err) +func TestNativeGraphFactEvidenceValidatesAgainstPinnedReportSchema(t *testing.T) { + for _, test := range []struct{ language, path, analyzer, protocol, manifest string }{ + {"python", "app.py", "python-facts", "python-facts/v3", "pyproject.toml"}, + {"rust", "src/lib.rs", "rust-facts", "rust-source/v1", "Cargo.toml"}, + } { + t.Run(test.language, func(t *testing.T) { + graph, err := sourcegraph.New([]sourcegraph.Node{{Path: test.path, Language: test.language, Root: ".", Module: "app", Resolution: "file:" + test.path}}, nil, + []sourcegraph.FactInput{{Analyzer: test.analyzer, Protocol: test.protocol, Project: test.manifest, Root: ".", Paths: []string{test.path}, FactsSHA256: strings.Repeat("a", 64), PartitionsSHA256: strings.Repeat("b", 64), ResolutionSHA256: strings.Repeat("c", 64)}}, nil) + if err != nil { + t.Fatal(err) + } + report := (&Engine{}).normalizeReport(Report{Command: "architecture", ReportPath: ".code-polishy-reports/architecture/example/report.json", SourceDependencyGraph: &graph}) + data, err := JSONReport(report) + if err != nil { + t.Fatal(err) + } + validateSchemaDocument(t, ReportSchemaURL, policyschema.CodePolishyReport, data) + }) } - validateSchemaDocument(t, ReportSchemaURL, policyschema.CodePolishyReport, data) } func TestProviderFindingsAndGraphSurviveManagedReports(t *testing.T) { diff --git a/internal/quality/cargo_test.go b/internal/quality/cargo_test.go index a14edfce..01be5696 100644 --- a/internal/quality/cargo_test.go +++ b/internal/quality/cargo_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "reflect" "slices" + "strings" "testing" "github.com/riteofstring/code-polishy/internal/policy" @@ -73,6 +74,24 @@ func TestCargoAndGoCheckPlanMatchesObservedCommandOrder(t *testing.T) { } } +func TestCargoNativeDeadCodePreservesPublicAPIAndChecksSelectedFeatures(t *testing.T) { + t.Parallel() + repo := cargoQualityRepository(t) + manifest := "[package]\nname='app'\nversion='1.0.0'\nedition='2024'\n[features]\nactive=[]\ndefault=[]\n" + writeQualityFile(t, repo.Root, "Cargo.toml", manifest) + writeQualityFile(t, repo.Root, "Cargo.lock", "version=4\n[[package]]\nname='app'\nversion='1.0.0'\n") + writeQualityFile(t, repo.Root, "src/lib.rs", "fn feature_helper() {}\n#[cfg(feature = \"active\")]\npub fn entry() {\n feature_helper();\n}\npub fn public_api() {}\n") + selection := repository.Selection{Files: []string{"src/lib.rs"}} + findings := Check(t.Context(), repo, selection, runner.OSRunner{}, "check") + if !repo.NativeAnalysis("src/lib.rs", "dead-code") || len(findings) == 0 || slices.ContainsFunc(findings, func(finding policy.Finding) bool { return finding.Subject != "dead_code" || finding.Line != 1 }) { + t.Fatalf("native dead-code coverage did not reflect the checked configuration: %+v", findings) + } + writeQualityFile(t, repo.Root, "Cargo.toml", strings.Replace(manifest, "default=[]", "default=['active']", 1)) + if findings := Check(t.Context(), repo, selection, runner.OSRunner{}, "check"); len(findings) != 0 { + t.Fatalf("enabled private helper or legitimate public API was treated as dead: %+v", findings) + } +} + type cargoRecordingRunner struct { runner.OSRunner names []string diff --git a/internal/quality/quality.go b/internal/quality/quality.go index c712a78f..cd710f75 100644 --- a/internal/quality/quality.go +++ b/internal/quality/quality.go @@ -926,7 +926,7 @@ func builtInCapability(language, capability string) bool { case "go", "typescript", "python": return slices.Contains(builtInCapabilities, capability) case "rust": - return slices.Contains([]string{"format", "lint", "typecheck", "complexity"}, capability) + return slices.Contains([]string{"format", "lint", "typecheck", "complexity", "dead-code", "architecture"}, capability) case "shell": return slices.Contains([]string{"format", "lint", "typecheck"}, capability) } diff --git a/internal/quality/quality_test.go b/internal/quality/quality_test.go index 8911b0d5..d79f4f2d 100644 --- a/internal/quality/quality_test.go +++ b/internal/quality/quality_test.go @@ -249,7 +249,7 @@ func TestCoverageKeepsUnknownSourceUncoveredBesideNativeSource(t *testing.T) { repo.Config.ModuleByName = map[string]int{"sample": 0} repo.Config.Checks = []policy.Command{{ Name: "dart", Provides: []string{"format", "lint", "typecheck", "complexity", "dead-code", "architecture"}, - Paths: []string{"internal/sample/**/*.dart"}, + Paths: []string{"internal/sample/**/*.dart"}, Modules: []string{"sample"}, RunOn: []string{"check", "gate"}, }} writeQualityFile(t, repo.Root, "internal/sample/lib.dart", "void main() {}\n") diff --git a/internal/repository/providers.go b/internal/repository/providers.go index 0556e905..aa12e2a8 100644 --- a/internal/repository/providers.go +++ b/internal/repository/providers.go @@ -127,7 +127,7 @@ func (repo Repository) NativeCapability(path, capability string) bool { case "go", "python": return slices.Contains([]string{"format", "lint", "typecheck", "complexity", "dead-code", "architecture"}, capability) case "rust": - return slices.Contains([]string{"format", "lint", "typecheck", "complexity"}, capability) + return slices.Contains([]string{"format", "lint", "typecheck", "complexity", "dead-code", "architecture"}, capability) case "shell": return slices.Contains([]string{"format", "lint", "typecheck"}, capability) case "typescript": diff --git a/internal/rustfacts/compilation.go b/internal/rustfacts/compilation.go index 6da4ff9b..1026e9aa 100644 --- a/internal/rustfacts/compilation.go +++ b/internal/rustfacts/compilation.go @@ -28,7 +28,7 @@ func PlanCompilation(repo repository.Repository, discovery DiscoveryPlan, allFil } for _, manifest := range discovery.Manifests { files := compilationSources(repo, manifest, allFiles) - native, err := repo.NativeUnit(files, "lint", "typecheck") + native, err := repo.NativeUnit(files, "lint", "typecheck", "dead-code") if err != nil { plan.Findings = append(plan.Findings, projectFinding(manifest, "compilation-unit", err.Error())) continue diff --git a/internal/rustfacts/graph.go b/internal/rustfacts/graph.go new file mode 100644 index 00000000..05f67cc2 --- /dev/null +++ b/internal/rustfacts/graph.go @@ -0,0 +1,184 @@ +package rustfacts + +import ( + "fmt" + "path/filepath" + "slices" + "strings" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" +) + +type ModuleSource struct { + Path string + Root string +} + +type ModuleImport struct { + Source string + Target string + Line int + Column int + ReExport bool +} + +type ModuleGraph struct { + Sources []ModuleSource + Imports []ModuleImport + Findings []policy.Finding +} + +type rustModule struct { + crate *rustCrate + parent *rustModule + file string + directory string + inline bool + bindings map[string]rustBinding + globs []rustBinding + facts []SourceGraphFact +} + +type rustBinding struct { + module *rustModule + file string + external bool + alias []string + scope *rustModule +} + +type rustCrate struct { + pkg Package + target Target + root *rustModule + modules []*rustModule + files map[string]bool + findings []policy.Finding +} + +type moduleInventory struct { + repo repository.Repository + sources map[string]Source + available map[string]bool + crates []*rustCrate +} + +func ResolveModules(repo repository.Repository, workspaces []Workspace, sources Sources, selected, allFiles []string) ModuleGraph { + inventory := moduleInventory{repo: repo, sources: map[string]Source{}, available: map[string]bool{}} + for _, path := range allFiles { + inventory.available[path] = true + } + for _, source := range sources.Files { + inventory.sources[source.Path] = source + } + inventory.buildCrates(workspaces) + return inventory.resolveSelected(selected) +} + +func (inventory *moduleInventory) buildCrates(workspaces []Workspace) { + seen := map[string]bool{} + for _, workspace := range workspaces { + for _, pkg := range workspace.Packages { + for _, target := range pkg.Targets { + key := pkg.Manifest + ":" + target.Path + if seen[key] { + continue + } + seen[key] = true + crate := &rustCrate{pkg: pkg, target: target, files: map[string]bool{}} + crate.root = newRustModule(crate, nil, target.Path, filepath.ToSlash(filepath.Dir(target.Path)), false) + inventory.crates = append(inventory.crates, crate) + inventory.readModuleFile(crate.root, nil) + } + } + } +} + +func newRustModule(crate *rustCrate, parent *rustModule, file, directory string, inline bool) *rustModule { + module := &rustModule{crate: crate, parent: parent, file: file, directory: directory, inline: inline, bindings: map[string]rustBinding{}} + crate.modules = append(crate.modules, module) + crate.files[file] = true + return module +} + +func (inventory *moduleInventory) resolveSelected(selected []string) ModuleGraph { + result := ModuleGraph{} + queue := []*rustCrate{} + for _, path := range selected { + if inventory.repo.Language(path) != "rust" || !inventory.repo.NativeAnalysis(path, "architecture") { + continue + } + found := false + for _, crate := range inventory.crates { + if crate.files[path] { + queue = append(queue, crate) + found = true + } + } + if !found { + result.Findings = append(result.Findings, moduleCoverage(path, SourceGraphFact{}, "source has no parsed Cargo target/module membership")) + } + } + visited := map[*rustCrate]bool{} + paths := map[string]bool{} + for len(queue) > 0 { + crate := queue[0] + queue = queue[1:] + if visited[crate] { + continue + } + visited[crate] = true + result.Findings = append(result.Findings, crate.findings...) + for file := range crate.files { + if !paths[file] { + paths[file] = true + result.Sources = append(result.Sources, ModuleSource{Path: file, Root: filepath.ToSlash(filepath.Dir(crate.pkg.Manifest))}) + } + } + queue = append(queue, inventory.resolveCrate(crate, &result)...) + } + slices.SortFunc(result.Sources, func(left, right ModuleSource) int { return strings.Compare(left.Path, right.Path) }) + return result +} + +func (inventory *moduleInventory) resolveCrate(crate *rustCrate, result *ModuleGraph) []*rustCrate { + connected := []*rustCrate{} + for _, module := range crate.modules { + connected = append(connected, inventory.resolveModule(module, result)...) + } + return connected +} + +func (inventory *moduleInventory) resolveModule(module *rustModule, result *ModuleGraph) []*rustCrate { + connected := []*rustCrate{} + for _, fact := range module.facts { + if fact.Kind == "unsupported" { + result.Findings = append(result.Findings, moduleCoverage(module.file, fact, fact.Name)) + continue + } + if fact.Kind != "use" && fact.Kind != "reference" { + continue + } + binding, err := inventory.resolvePath(module, fact.Path, fact.Kind == "use", 0) + if err != nil { + result.Findings = append(result.Findings, moduleCoverage(module.file, fact, err.Error())) + continue + } + if binding.module != nil { + connected = append(connected, binding.module.crate) + binding.file = binding.module.file + } + if binding.scope != nil { + connected = append(connected, binding.scope.crate) + } + if binding.file != "" && binding.file != module.file { + result.Imports = append(result.Imports, ModuleImport{Source: module.file, Target: binding.file, Line: fact.Line, Column: fact.Column, ReExport: fact.Kind == "use" && fact.Public}) + } + } + return connected +} + +func moduleCoverage(path string, fact SourceGraphFact, reason string) policy.Finding { + return policy.Finding{Check: "architecture.importCoverage", Path: path, Subject: "rust", Line: fact.Line, Column: fact.Column, Message: fmt.Sprintf("Rust written-module graph is incomplete: %s", reason)} +} diff --git a/internal/rustfacts/graph_modules.go b/internal/rustfacts/graph_modules.go new file mode 100644 index 00000000..b50e3c34 --- /dev/null +++ b/internal/rustfacts/graph_modules.go @@ -0,0 +1,126 @@ +package rustfacts + +import ( + "errors" + "fmt" + "path/filepath" + "slices" + "strings" +) + +func (inventory *moduleInventory) readModuleFile(root *rustModule, ancestry []string) { + if len(ancestry) >= 128 || slices.Contains(ancestry, root.file) { + root.crate.findings = append(root.crate.findings, moduleCoverage(root.file, SourceGraphFact{}, "module source inclusion is recursive or exceeds 128 levels")) + return + } + source, found := inventory.sources[root.file] + if !found { + root.crate.findings = append(root.crate.findings, moduleCoverage(root.file, SourceGraphFact{}, "governed module source has no complete parsed syntax facts")) + return + } + if !inventory.repo.NativeAnalysis(root.file, "architecture") { + root.crate.findings = append(root.crate.findings, moduleCoverage(root.file, SourceGraphFact{}, "one Cargo target's module closure is split between analysis providers")) + return + } + scopes := map[string]*rustModule{"": root} + for _, fact := range source.Graph { + module := scopes[strings.Join(fact.Scope, "::")] + if module == nil { + continue + } + module.facts = append(module.facts, fact) + if err := inventory.addModuleFact(module, fact, scopes, append(ancestry, root.file)); err != nil { + root.crate.findings = append(root.crate.findings, moduleCoverage(root.file, fact, err.Error())) + } + } +} + +func (inventory *moduleInventory) addModuleFact(module *rustModule, fact SourceGraphFact, scopes map[string]*rustModule, ancestry []string) error { + switch fact.Kind { + case "module": + child, err := inventory.moduleChild(module, fact) + if err != nil { + return err + } + if err := bindRustName(module, fact.Name, rustBinding{module: child}); err != nil { + return err + } + if fact.Inline { + key := strings.Join(append(slices.Clone(fact.Scope), fact.Name), "::") + scopes[key] = child + } else { + inventory.readModuleFile(child, ancestry) + } + case "item": + return bindRustName(module, fact.Name, rustBinding{file: module.file, scope: module}) + case "use": + binding := rustBinding{alias: fact.Path, scope: module} + if fact.Alias == "*" { + module.globs = append(module.globs, binding) + } else if fact.Alias != "_" { + return bindRustName(module, fact.Alias, binding) + } + } + return nil +} + +func bindRustName(module *rustModule, name string, binding rustBinding) error { + if previous, found := module.bindings[name]; found { + if previous.file == binding.file && binding.file != "" { + return nil + } + return fmt.Errorf("binding %q has ambiguous written-source definitions", name) + } + module.bindings[name] = binding + return nil +} + +func (inventory *moduleInventory) moduleChild(parent *rustModule, fact SourceGraphFact) (*rustModule, error) { + directory := filepath.ToSlash(filepath.Join(parent.directory, fact.Name)) + if fact.Inline { + if fact.File != "" { + return nil, errors.New("inline module path overrides need resolved directory evidence") + } + return newRustModule(parent.crate, parent, parent.file, directory, true), nil + } + candidates := []string{directory + ".rs", directory + "/mod.rs"} + if fact.File != "" { + base := filepath.Dir(parent.file) + if parent.inline { + base = parent.directory + } + if filepath.IsAbs(fact.File) || strings.ContainsAny(fact.File, "\\\x00") { + return nil, errors.New("module path is not a contained portable relative path") + } + candidates = []string{filepath.ToSlash(filepath.Join(base, fact.File))} + } + path, err := inventory.uniqueModuleFile(candidates) + if err != nil { + return nil, err + } + childDirectory := filepath.ToSlash(filepath.Dir(path)) + if filepath.Base(path) != "mod.rs" { + childDirectory = strings.TrimSuffix(path, ".rs") + } + return newRustModule(parent.crate, parent, path, childDirectory, false), nil +} + +func (inventory *moduleInventory) uniqueModuleFile(candidates []string) (string, error) { + found := "" + for _, candidate := range candidates { + if candidate == ".." || strings.HasPrefix(candidate, "../") || filepath.Ext(candidate) != ".rs" { + return "", errors.New("module path escapes the checkout or is not Rust source") + } + if !inventory.available[candidate] { + continue + } + if found != "" { + return "", errors.New("module declaration has multiple governed source candidates") + } + found = candidate + } + if found == "" { + return "", fmt.Errorf("module declaration has no governed source among %s", strings.Join(candidates, ", ")) + } + return found, nil +} diff --git a/internal/rustfacts/graph_paths.go b/internal/rustfacts/graph_paths.go new file mode 100644 index 00000000..d51fd79b --- /dev/null +++ b/internal/rustfacts/graph_paths.go @@ -0,0 +1,149 @@ +package rustfacts + +import ( + "errors" + "fmt" + "path/filepath" + "slices" + "strings" +) + +func (inventory *moduleInventory) resolvePath(module *rustModule, names []string, importing bool, depth int) (rustBinding, error) { + if len(names) == 0 || depth >= 64 { + return rustBinding{}, errors.New("rust import path is empty or its alias resolution is recursive") + } + binding, err := inventory.pathAnchor(module, names[0], importing, depth) + if err != nil { + return binding, err + } + for _, name := range names[1:] { + if binding.module == nil { + return binding, nil + } + binding, err = inventory.pathMember(binding.module, name, depth+1) + if err != nil { + return binding, err + } + } + return binding, nil +} + +func (inventory *moduleInventory) pathAnchor(module *rustModule, first string, importing bool, depth int) (rustBinding, error) { + var binding rustBinding + var err error + switch first { + case "crate": + binding.module = module.crate.root + case "self": + binding.module = module + case "super": + if module.parent == nil { + return binding, errors.New("rust super path has no containing module") + } + binding.module = module.parent + case "Self": + binding.file = module.file + default: + if importing && module.crate.target.Edition == "2015" { + module = module.crate.root + } + binding, err = inventory.resolveName(module, first, depth+1) + } + return binding, err +} + +func (inventory *moduleInventory) pathMember(module *rustModule, name string, depth int) (rustBinding, error) { + if name != "super" { + return inventory.resolveName(module, name, depth) + } + if module.parent == nil { + return rustBinding{}, errors.New("rust super path escapes its crate") + } + return rustBinding{module: module.parent}, nil +} + +func (inventory *moduleInventory) resolveName(module *rustModule, name string, depth int) (rustBinding, error) { + if depth >= 64 { + return rustBinding{}, errors.New("rust name resolution exceeds 64 alias/glob steps") + } + if binding, found := module.bindings[name]; found { + if len(binding.alias) == 0 { + return binding, nil + } + if len(binding.alias) == 1 && binding.alias[0] == name { + return inventory.externalCrate(module, name) + } + return inventory.resolvePath(binding.scope, binding.alias, true, depth+1) + } + if len(module.globs) > 0 { + binding, found, err := inventory.resolveGlobName(module, name, depth+1) + if err != nil || found { + return binding, err + } + } + return inventory.externalCrate(module, name) +} + +func (inventory *moduleInventory) resolveGlobName(module *rustModule, name string, depth int) (rustBinding, bool, error) { + var result rustBinding + found := false + for _, glob := range module.globs { + target, err := inventory.resolvePath(glob.scope, glob.alias, true, depth+1) + if err != nil { + return result, false, err + } + if target.module == nil { + return result, false, errors.New("external glob imports need resolved binding evidence") + } + if _, exists := target.module.bindings[name]; !exists { + continue + } + candidate, err := inventory.resolveName(target.module, name, depth+1) + if err != nil { + return result, false, err + } + if found && (result.file != candidate.file || result.module != candidate.module || result.external != candidate.external) { + return result, false, fmt.Errorf("glob imports supply ambiguous binding %q", name) + } + result, found = candidate, true + } + return result, found, nil +} + +func (inventory *moduleInventory) externalCrate(module *rustModule, name string) (rustBinding, error) { + if slices.Contains([]string{"std", "core", "alloc", "proc_macro", "bool", "char", "str", "f32", "f64", "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize"}, name) { + return rustBinding{external: true}, nil + } + for _, crate := range inventory.crates { + if crate.pkg.Manifest == module.crate.pkg.Manifest && crate.target.Name == name && libraryTarget(crate.target) { + return rustBinding{module: crate.root}, nil + } + } + for _, dependency := range module.crate.pkg.Dependencies { + alias := dependency.Rename + if alias == "" { + alias = dependency.Name + } + if strings.ReplaceAll(alias, "-", "_") != name { + continue + } + return inventory.dependencyBinding(dependency, name) + } + return rustBinding{}, fmt.Errorf("path prefix %q has no resolved written module, item or Cargo dependency", name) +} + +func (inventory *moduleInventory) dependencyBinding(dependency Dependency, name string) (rustBinding, error) { + if dependency.Path == "" { + return rustBinding{external: true}, nil + } + for _, crate := range inventory.crates { + if crate.pkg.Manifest == filepath.ToSlash(filepath.Join(dependency.Path, "Cargo.toml")) && libraryTarget(crate.target) { + return rustBinding{module: crate.root}, nil + } + } + return rustBinding{}, fmt.Errorf("local dependency %q has no discovered Cargo library target", name) +} + +func libraryTarget(target Target) bool { + return slices.Contains(target.Kinds, "lib") || slices.Contains(target.Kinds, "proc-macro") +} diff --git a/internal/rustfacts/graph_syntax.go b/internal/rustfacts/graph_syntax.go new file mode 100644 index 00000000..f7ebc501 --- /dev/null +++ b/internal/rustfacts/graph_syntax.go @@ -0,0 +1,52 @@ +package rustfacts + +import ( + "errors" + "slices" + "strings" +) + +type SourceGraphFact struct { + SourceLocation + Scope []string `json:"scope"` + Kind string `json:"kind"` + Name string `json:"name"` + Path []string `json:"path"` + Alias string `json:"alias"` + Inline bool `json:"inline"` + File string `json:"file"` + Public bool `json:"public"` +} + +func validateSourceGraphFacts(facts []SourceGraphFact, coordinates sourceCoordinates) error { + for _, fact := range facts { + if err := validateSourceLocation(fact.SourceLocation, coordinates); err != nil { + return err + } + if fact.Scope == nil || fact.Path == nil || len(fact.Scope) > 128 || !validRustNames(fact.Scope) || !validRustNames(fact.Path) { + return errors.New("rust graph fact omitted its bounded lexical identity") + } + if !validGraphFactIdentity(fact) { + return errors.New("rust graph fact omitted its parsed dependency identity") + } + } + return nil +} + +func validGraphFactIdentity(fact SourceGraphFact) bool { + switch fact.Kind { + case "module", "item", "unsupported": + return fact.Name != "" + case "use": + return len(fact.Path) > 0 && fact.Alias != "" + case "reference": + return len(fact.Path) > 0 + } + return false +} + +func validRustNames(names []string) bool { + return !slices.ContainsFunc(names, func(name string) bool { + return name == "" || strings.ContainsAny(name, "/\\:\x00\n\r") + }) +} diff --git a/internal/rustfacts/source.go b/internal/rustfacts/source.go index 4f13bf4f..187680d8 100644 --- a/internal/rustfacts/source.go +++ b/internal/rustfacts/source.go @@ -70,7 +70,7 @@ func sourceAnalysisSelected(repo repository.Repository, path string) bool { if repo.Language(path) != "rust" || repo.IsGenerated(path) || repo.IsData(path) { return false } - return repo.NativeAnalysis(path, "lint") || repo.NativeAnalysis(path, "complexity") || repo.NativeAnalysis(path, "format") + return repo.NativeAnalysis(path, "lint") || repo.NativeAnalysis(path, "complexity") || repo.NativeAnalysis(path, "format") || repo.NativeAnalysis(path, "architecture") } func ReadSources(ctx context.Context, repo repository.Repository, plan SourcePlan, boundary runner.Runner) Sources { diff --git a/internal/rustfacts/source_output.go b/internal/rustfacts/source_output.go index e37089f5..66cf48b8 100644 --- a/internal/rustfacts/source_output.go +++ b/internal/rustfacts/source_output.go @@ -53,6 +53,7 @@ type Source struct { Attributes []SourceAttribute `json:"attributes"` Functions []SourceFunction `json:"functions"` Macros []SourceMacro `json:"macros"` + Graph []SourceGraphFact `json:"graph"` } type sourceCoordinates struct { @@ -121,7 +122,7 @@ func validateSourceInventory(inputs []SourceInput, sources []Source) error { } func validateSourceFacts(source Source, text string) error { - if source.Comments == nil || source.Attributes == nil || source.Functions == nil || source.Macros == nil || source.Complete == (source.Error != "") { + if source.Comments == nil || source.Attributes == nil || source.Functions == nil || source.Macros == nil || source.Graph == nil || source.Complete == (source.Error != "") { return errors.New("rust source omitted complete facts or its coverage failure") } locations, err := sourceFactLocations(source) @@ -129,6 +130,9 @@ func validateSourceFacts(source Source, text string) error { return err } coordinates := indexSource(text) + if err := validateSourceGraphFacts(source.Graph, coordinates); err != nil { + return err + } if err := validateSourceAttributes(source.Attributes, coordinates, 0); err != nil { return err } diff --git a/internal/rustfacts/source_parser.py b/internal/rustfacts/source_parser.py index d3f70018..f7b5f83c 100644 --- a/internal/rustfacts/source_parser.py +++ b/internal/rustfacts/source_parser.py @@ -19,6 +19,62 @@ "try_expression", "or_pattern", } +BUILTIN_ATTRIBUTES = { + "cfg", + "cfg_attr", + "allow", + "expect", + "deny", + "forbid", + "doc", + "path", + "test", + "ignore", + "should_panic", + "inline", + "cold", + "must_use", + "repr", + "no_std", + "no_main", + "macro_use", + "macro_export", + "unsafe", + "deprecated", + "non_exhaustive", + "track_caller", + "automatically_derived", + "no_mangle", + "export_name", + "link", + "link_name", + "link_section", + "link_ordinal", + "used", + "target_feature", + "instruction_set", + "panic_handler", + "global_allocator", + "recursion_limit", + "type_length_limit", + "windows_subsystem", + "crate_type", + "crate_name", + "proc_macro", + "proc_macro_derive", + "proc_macro_attribute", +} +BUILTIN_DERIVES = { + "Clone", + "Copy", + "Debug", + "Default", + "Eq", + "PartialEq", + "Ord", + "PartialOrd", + "Hash", +} def location(node, source): @@ -174,13 +230,241 @@ def collect_node(node, source, result): ) +def syntax_path(node): + if node is None: + return [] + if node.type in { + "identifier", + "type_identifier", + "crate", + "self", + "super", + "primitive_type", + }: + return [node.text.decode().removeprefix("r#")] + if node.type in {"scoped_identifier", "scoped_type_identifier"}: + return syntax_path(node.child_by_field_name("path")) + syntax_path( + node.child_by_field_name("name") + ) + if node.type in {"generic_type", "generic_function"}: + field = "type" if node.type == "generic_type" else "function" + return syntax_path(node.child_by_field_name(field)) + return [] + + +def use_paths(node, prefix=None): + prefix = [] if prefix is None else prefix + if node.type == "use_list": + return [ + value for child in node.named_children for value in use_paths(child, prefix) + ] + if node.type == "scoped_use_list": + base = prefix + syntax_path(node.child_by_field_name("path")) + return use_paths(node.child_by_field_name("list"), base) + if node.type == "use_as_clause": + path = prefix + syntax_path(node.child_by_field_name("path")) + alias = node.child_by_field_name("alias").text.decode().removeprefix("r#") + elif node.type == "use_wildcard": + path = prefix + syntax_path(next(iter(node.named_children), None)) + alias = "*" + else: + path = prefix + syntax_path(node) + alias = path[-1] if path else "" + if path and path[-1] == "self": + path = path[:-1] + alias = path[-1] if alias == "self" and path else alias + return [(node, path, alias)] + + +def graph_fact(node, source, scope, kind, **values): + return { + **location(node, source), + "scope": scope, + "kind": kind, + "name": "", + "path": [], + "alias": "", + "inline": False, + "file": "", + "public": False, + **values, + } + + +def module_file(node, source): + attributes = item_attributes(node, source) + if any(value["name"] == "cfg_attr" for value in attributes): + return "", "conditional module attributes need resolved path evidence" + sibling = node.prev_named_sibling + while sibling is not None and sibling.type in COMMENTS | {"attribute_item"}: + if sibling.type == "attribute_item": + value = sibling.named_children[0] + if path_name(value.named_children[0]) == "path": + literal = value.child_by_field_name("value") + try: + return json.loads(literal.text.decode()), "" + except (ValueError, AttributeError): + return "", "module path requires a supported literal string" + sibling = sibling.prev_named_sibling + return "", "" + + +def collect_graph_node(node, source, result, scope): + facts = result["graph"] + if node.type in {"attribute_item", "inner_attribute_item"}: + facts.extend(attribute_graph_facts(node, source, scope)) + elif node.type == "mod_item": + name = node.child_by_field_name("name").text.decode().removeprefix("r#") + file, problem = module_file(node, source) + kind = "unsupported" if problem else "module" + facts.append( + graph_fact( + node, + source, + scope, + kind, + name=problem or name, + file=file, + inline=node.child_by_field_name("body") is not None, + ) + ) + elif node.type == "use_declaration": + facts.extend(use_graph_facts(node, source, scope)) + elif node.type in {"scoped_identifier", "scoped_type_identifier"}: + facts.extend(reference_graph_facts(node, source, scope)) + elif node.type in { + "function_item", + "struct_item", + "enum_item", + "type_item", + "trait_item", + "const_item", + "static_item", + "union_item", + "type_parameter", + }: + name = node.child_by_field_name("name") + if name is not None: + facts.append( + graph_fact( + node, + source, + scope, + "item", + name=name.text.decode().removeprefix("r#"), + ) + ) + elif node.type == "extern_crate_declaration": + name = node.child_by_field_name("name").text.decode().removeprefix("r#") + alias = node.child_by_field_name("alias") + facts.append( + graph_fact( + node, + source, + scope, + "use", + path=[name], + alias=alias.text.decode() if alias is not None else name, + ) + ) + elif node.type in {"macro_invocation", "macro_definition"}: + facts.append( + graph_fact( + node, + source, + scope, + "unsupported", + name="macro expansion requires resolved import evidence", + ) + ) + + +def use_graph_facts(node, source, scope): + if node.child_by_field_name("argument").text.startswith(b"::"): + return [ + graph_fact( + node, + source, + scope, + "unsupported", + name="absolute use paths need external-prelude resolution", + ) + ] + public = any(child.type == "visibility_modifier" for child in node.named_children) + return [ + graph_fact( + position, source, scope, "use", path=path, alias=alias, public=public + ) + for position, path, alias in use_paths(node.child_by_field_name("argument")) + ] + + +def reference_graph_facts(node, source, scope): + excluded = { + "use_declaration", + "attribute_item", + "inner_attribute_item", + "visibility_modifier", + } + if any(parent.type in excluded for parent in node_ancestors(node)): + return [] + if node.text.startswith(b"::"): + return [ + graph_fact( + node, + source, + scope, + "unsupported", + name="absolute paths need external-prelude resolution", + ) + ] + path = syntax_path(node) + return [graph_fact(node, source, scope, "reference", path=path)] if path else [] + + +def attribute_graph_facts(node, source, scope): + parsed = attribute(node, source) + if parsed["name"] == "derive": + arguments = node.named_children[0].child_by_field_name("arguments") + groups = token_groups(arguments) if arguments is not None else [] + if groups and all( + len(group) == 1 and group[0].text.decode() in BUILTIN_DERIVES + for group in groups + ): + return [] + elif known_graph_attribute(parsed): + return [] + return [ + graph_fact( + node, + source, + scope, + "unsupported", + name="attribute expansion requires resolved import evidence", + ) + ] + + +def known_graph_attribute(parsed): + return parsed["name"] in BUILTIN_ATTRIBUTES and all( + known_graph_attribute(child) for child in parsed["children"] + ) + + +def node_ancestors(node): + parent = node.parent + while parent is not None: + yield parent + parent = parent.parent + + class SyntaxCollector: def __init__(self, source, result): self.source = source self.result = result def walk(self, root): - pending = [(root, None, False, 0)] + pending = [(root, None, False, 0, [])] visited = 0 while pending: visited += 1 @@ -188,10 +472,16 @@ def walk(self, root): raise ValueError("Rust syntax exceeds 500000 nodes") pending.extend(self.visit(*pending.pop())) - def visit(self, node, function, test_context, depth): + def visit(self, node, function, test_context, depth, scope): if node.type in {"source_file", "mod_item", "function_item"}: test_context = test_context or test_configuration(node, self.source) collect_node(node, self.source, self.result) + collect_graph_node(node, self.source, self.result, scope) + if node.type == "mod_item" and node.child_by_field_name("body") is not None: + scope = [ + *scope, + node.child_by_field_name("name").text.decode().removeprefix("r#"), + ] body = None if node.type in CALLABLES: body = node.child_by_field_name("body") @@ -207,7 +497,7 @@ def visit(self, node, function, test_context, depth): if node.type not in COMMENTS: for child in reversed(node.named_children): owner = function if body is None or child == body else None - children.append((child, owner, test_context, depth)) + children.append((child, owner, test_context, depth, scope)) return children @@ -222,6 +512,7 @@ def source_facts(parser, entry): "attributes": [], "functions": [], "macros": [], + "graph": [], } if tree is None or tree.root_node.has_error: result["error"] = ( diff --git a/internal/rustfacts/source_test.go b/internal/rustfacts/source_test.go index 89347067..e67a9e8f 100644 --- a/internal/rustfacts/source_test.go +++ b/internal/rustfacts/source_test.go @@ -156,7 +156,7 @@ func TestRustSourceFactsRespectConfiguredProviderOwnership(t *testing.T) { t.Parallel() repo := rustRepository(t) write(t, repo.Root, "owned.rs", "not valid Rust but owned by the provider\n") - for _, capability := range []string{"format", "lint", "complexity"} { + for _, capability := range []string{"format", "lint", "complexity", "architecture"} { repo.Config.Checks = append(repo.Config.Checks, policy.Command{Name: capability, Paths: []string{"owned.rs"}, RunOn: []string{"check"}, Provides: []string{capability}, Adapter: &policy.PackAdapter{PackName: "owner", Capability: capability}}) } plan := PlanSources(repo.WithAnalysisProfile("check"), []string{"owned.rs"}) diff --git a/schema/code-polishy-report.schema.json b/schema/code-polishy-report.schema.json index 5f38594c..f1cd65b3 100644 --- a/schema/code-polishy-report.schema.json +++ b/schema/code-polishy-report.schema.json @@ -1018,6 +1018,13 @@ "provider": false } }, + { + "properties": { + "analyzer": { "const": "rust-facts" }, + "protocol": { "const": "rust-source/v1" }, + "provider": false + } + }, { "required": ["provider"], "properties": { @@ -1038,12 +1045,13 @@ ], "properties": { "analyzer": { - "enum": ["python-facts", "ruff", "pack"] + "enum": ["python-facts", "ruff", "rust-facts", "pack"] }, "protocol": { "enum": [ "python-facts/v3", "ruff-graph-facts/v2", + "rust-source/v1", "code-polishy-pack/v3" ] }, From 04a27895f8ac3df9f45825451826fd1499aaf374 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 03:26:30 +0000 Subject: [PATCH 16/49] feat: resolve Rust standard macros and lexical scopes Preserve written macro argument edges and original locations; honor lexical imports, generics, standard/core preludes and shadowing. Custom expansions remain explicit coverage limits. Verified scoped policy check plus full rustfacts-unit and architecture-unit suites. Rust test evidence and release integration remain unfinished. The documentation catalog suite remains affected by caller-owned untracked docs/issues files. --- docs/design/cargo-facts.md | 26 ++- docs/plans/code-polishy-rust-support-plan.md | 4 + internal/architecture/rust_test.go | 133 +++++++++++ internal/rustfacts/graph.go | 30 ++- internal/rustfacts/graph_modules.go | 45 ++-- internal/rustfacts/graph_paths.go | 90 ++++++-- internal/rustfacts/graph_prelude.go | 81 +++++++ internal/rustfacts/graph_syntax.go | 8 +- internal/rustfacts/source_parser.py | 222 +++++++++++++++++-- 9 files changed, 571 insertions(+), 68 deletions(-) create mode 100644 internal/rustfacts/graph_prelude.go diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index 88075b02..7aa07995 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -122,12 +122,26 @@ generated imports, ambiguous bindings and missing source facts make graph coverage incomplete rather than selecting a convenient edge or reporting an empty graph. Compiler checks separately validate the selected Cargo configuration. -The initial graph handles conventional external/inline modules, literal external -module paths, nested use trees, renamed workspace dependencies and local aliases -and re-exports. Standard-library derives are syntax-known. Macro invocations, -non-builtin attribute expansion, conditional/inline path overrides, absolute -external-prelude paths and unresolved external glob bindings currently fail -coverage explicitly. These are beta limitations, not optional exemptions. +The graph handles conventional external/inline modules, literal external module +paths, nested use trees, renamed workspace dependencies and local aliases and +re-exports. Function, block, closure, implementation and generic bindings retain +lexical scope; they cannot leak into sibling scopes or substitute for a module's +members. Prelude lookup respects edition, `no_std`, `no_implicit_prelude` and +local shadowing. Qualified module members never inherit the implicit prelude. + +Known standard macro arguments are parsed using the same upstream Rust grammar +with their original byte locations retained. This includes assertion/formatting +expressions, vector element/repeat expressions and match patterns/guards. +Nested invocations keep the same resolution checks. Stringified tokens are data, +not guessed imports. Standard derives and macros must retain standard-library +identity; imports, glob ambiguity or macro-use expansion cannot silently stand +in for that identity. This analyzes written arguments, not macro-expanded code +or expanded complexity. + +Custom macro definitions/invocations, non-builtin attribute expansion, +conditional/inline path overrides, absolute external-prelude paths and unresolved +external glob bindings currently fail coverage explicitly. These are beta +limitations, not optional exemptions. ## Compiler checks diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 9afaaaf5..328136f2 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -142,6 +142,10 @@ changed modules; add a quick suite when a new module is introduced. dependency-direction and cycle policy. Real Cargo fixtures cover forbidden edges, source cycles, renamed workspace dependencies, unrelated invalid crates and command-plan agreement. Native graph evidence survives the report schema. +- Standard macros retain dependencies from parsed written arguments, including + nested assertions, formatting, vectors and match guards. Locations remain in + original source. Lexical imports/generics and edition-specific standard/core + preludes preserve scope and shadowing; unknown expansion remains explicit. Macro expansion and other unresolved syntax still fail coverage explicitly. A plain crate passes the complete public CLI `check`; this is a usable subset, not evidence that macro-heavy or arbitrary target configurations are supported. diff --git a/internal/architecture/rust_test.go b/internal/architecture/rust_test.go index c4887696..427e07a4 100644 --- a/internal/architecture/rust_test.go +++ b/internal/architecture/rust_test.go @@ -4,6 +4,7 @@ import ( "context" "path/filepath" "slices" + "strings" "testing" "github.com/riteofstring/code-polishy/internal/architecture/sourcegraph" @@ -137,6 +138,138 @@ func TestRustArchitectureCommandPlanMatchesRealExecution(t *testing.T) { } } +func TestRustArchitectureTracksStandardMacroArgumentsAtOriginalLocations(t *testing.T) { + t.Parallel() + repo := rustArchitectureRepository(t) + writeArchitectureFile(t, repo.Root, "src/lib.rs", "pub mod api;\npub mod domain;\n") + writeArchitectureFile(t, repo.Root, "src/domain.rs", "pub fn value() -> u8 { 1 }\npub enum Value { Present }\n") + writeArchitectureFile(t, repo.Root, "src/api.rs", `pub fn check() { + let crab = "🦀"; + assert_eq!(crate::domain::value(), 1, "{crab}"); + let values = vec![dbg!(crate::domain::value()); 2]; + assert!(matches!(crate::domain::Value::Present, crate::domain::Value::Present if crate::domain::value() > 0)); + println!("{}", format!("{}", crate::domain::value())); + let _ = stringify!(crate::absent::not_a_dependency); + let _ = Vec::::new(); + let _ = Option::::None; + assert!(!values.is_empty()); +} +`) + analysis := AnalyzeWithRunner(t.Context(), repo, []string{"src/api.rs"}, runner.OSRunner{}) + if analysis.Graph == nil { + t.Fatalf("standard macro or prelude syntax blocked Rust graph: %+v", analysis.Findings) + } + for _, line := range []int{3, 4, 5, 6} { + if !slices.ContainsFunc(analysis.Findings, func(finding policy.Finding) bool { + return finding.Check == "architecture.moduleDependency" && finding.Path == "src/api.rs" && finding.Subject == "domain" && finding.Line == line && finding.Column > 1 + }) { + t.Errorf("macro argument on original line %d lost dependency evidence: %+v", line, analysis.Findings) + } + } + repo.Config.Modules[1].DependsOn = []string{"domain"} + analysis = AnalyzeWithRunner(t.Context(), repo, []string{"src/api.rs"}, runner.OSRunner{}) + if analysis.Graph == nil || len(analysis.Findings) != 0 { + t.Fatalf("declared macro argument dependencies failed: %+v", analysis) + } +} + +func TestRustArchitectureRespectsPreludeAndMacroShadowing(t *testing.T) { + t.Parallel() + for name, source := range map[string]string{ + "local macro": "macro_rules! assert_eq { () => { use crate::missing; }; }\npub fn f() { assert_eq!(); }\n", + "external macro": "use external::assert_eq;\npub fn f() { assert_eq!(1, 1); }\n", + "macro use": "#[macro_use]\nextern crate external;\npub fn f() { assert_eq!(1, 1); }\n", + "qualified shadow": "mod std {}\npub fn f() { std::assert_eq!(1, 1); }\n", + "nested macro": "pub fn f() { assert_eq!(unknown!(), 1); }\n", + "no std type": "#![no_std]\npub fn f() { let _ = Vec::::new(); }\n", + "no std macro": "#![no_std]\npub fn f() { let _ = vec![1]; }\n", + "no implicit type": "#![no_implicit_prelude]\npub fn f() { let _ = Option::::None; }\n", + "no implicit macro": "#![no_implicit_prelude]\npub fn f() { assert_eq!(1, 1); }\n", + "conditional prelude": "#![cfg_attr(feature = \"bare\", no_std)]\npub fn f() {}\n", + } { + t.Run(name, func(t *testing.T) { + repo := rustArchitectureRepository(t) + writeArchitectureFile(t, repo.Root, "src/lib.rs", source) + analysis := AnalyzeWithRunner(t.Context(), repo, []string{"src/lib.rs"}, runner.OSRunner{}) + if analysis.Graph != nil || !slices.ContainsFunc(analysis.Findings, func(finding policy.Finding) bool { return finding.Check == "architecture.importCoverage" }) { + t.Fatalf("unresolved Rust prelude/expansion passed: %+v", analysis) + } + }) + } +} + +func TestRustArchitectureSupportsCorePreludeAndExplicitStandardMacroImports(t *testing.T) { + t.Parallel() + for _, source := range []string{ + "#![no_std]\npub fn f() { let _ = Option::::None; assert_eq!(1, 1); }\n", + "use std::assert_eq;\npub fn f() { assert_eq!(1, 1); }\n", + "use std::fmt::Debug;\n#[derive(Debug)]\npub struct Value;\n", + "pub fn f() { std::assert_eq!(1, 1); core::assert!(true); }\n", + "mod tests { use super::*; #[test] fn f() { assert_eq!(value(), 1); } }\npub fn value() -> u8 { 1 }\n", + } { + t.Run(strings.Split(source, "\n")[0], func(t *testing.T) { + repo := rustArchitectureRepository(t) + writeArchitectureFile(t, repo.Root, "src/lib.rs", source) + analysis := AnalyzeWithRunner(t.Context(), repo, []string{"src/lib.rs"}, runner.OSRunner{}) + if analysis.Graph == nil || len(analysis.Findings) != 0 { + t.Fatalf("valid standard/core prelude failed: %+v", analysis) + } + }) + } +} + +func TestRustArchitectureKeepsFunctionBlockAndGenericBindingsLexical(t *testing.T) { + t.Parallel() + repo := rustArchitectureRepository(t) + writeArchitectureFile(t, repo.Root, "src/lib.rs", "pub mod api;\npub mod domain;\n") + writeArchitectureFile(t, repo.Root, "src/domain.rs", "pub struct Value;\nimpl Value { pub fn value() -> u8 { 1 } }\n") + writeArchitectureFile(t, repo.Root, "src/api.rs", `pub fn first() { + use crate::domain::Value as Alias; + assert_eq!({ use crate::domain::Value as Local; Local::value() }, 1); + let _ = Alias::value(); +} +pub fn second() { + use std::string::String as Alias; + let _ = Alias::new(); + { use crate::domain::Value as Alias; let _ = Alias::value(); } + let _ = Alias::new(); +} +pub fn generic() -> T { T::default() } +`) + analysis := AnalyzeWithRunner(t.Context(), repo, []string{"src/api.rs"}, runner.OSRunner{}) + if analysis.Graph == nil { + t.Fatalf("block-local bindings leaked into other scopes: %+v", analysis.Findings) + } + for _, finding := range analysis.Findings { + if finding.Check != "architecture.moduleDependency" || finding.Path != "src/api.rs" || !slices.Contains([]int{2, 3, 4, 9}, finding.Line) { + t.Errorf("lexical binding produced an incorrect dependency: %+v", finding) + } + } + repo.Config.Modules[1].DependsOn = []string{"domain"} + analysis = AnalyzeWithRunner(t.Context(), repo, []string{"src/api.rs"}, runner.OSRunner{}) + if analysis.Graph == nil || len(analysis.Findings) != 0 { + t.Fatalf("declared lexical dependencies failed: %+v", analysis) + } +} + +func TestRustArchitectureCannotResolveModuleMembersThroughThePrelude(t *testing.T) { + t.Parallel() + for _, source := range []string{ + "pub fn f() { let _ = crate::String::new(); }\n", + "mod empty {}\npub fn f() { let _ = empty::Option::::None; }\n", + "pub fn first() -> T { T::default() }\npub fn second() { let _ = T::default(); }\n", + } { + t.Run(source, func(t *testing.T) { + repo := rustArchitectureRepository(t) + writeArchitectureFile(t, repo.Root, "src/lib.rs", source) + analysis := AnalyzeWithRunner(t.Context(), repo, []string{"src/lib.rs"}, runner.OSRunner{}) + if analysis.Graph != nil || len(analysis.Findings) == 0 { + t.Fatalf("unavailable member acquired an implicit binding: %+v", analysis) + } + }) + } +} + func rustArchitectureRepository(t *testing.T) repository.Repository { t.Helper() root := t.TempDir() diff --git a/internal/rustfacts/graph.go b/internal/rustfacts/graph.go index 05f67cc2..e2e0b3a7 100644 --- a/internal/rustfacts/graph.go +++ b/internal/rustfacts/graph.go @@ -35,6 +35,8 @@ type rustModule struct { file string directory string inline bool + lexical bool + noPrelude bool bindings map[string]rustBinding globs []rustBinding facts []SourceGraphFact @@ -44,6 +46,8 @@ type rustBinding struct { module *rustModule file string external bool + standard bool + extern bool alias []string scope *rustModule } @@ -55,6 +59,7 @@ type rustCrate struct { modules []*rustModule files map[string]bool findings []policy.Finding + noStd bool } type moduleInventory struct { @@ -153,18 +158,14 @@ func (inventory *moduleInventory) resolveCrate(crate *rustCrate, result *ModuleG func (inventory *moduleInventory) resolveModule(module *rustModule, result *ModuleGraph) []*rustCrate { connected := []*rustCrate{} for _, fact := range module.facts { - if fact.Kind == "unsupported" { - result.Findings = append(result.Findings, moduleCoverage(module.file, fact, fact.Name)) - continue - } - if fact.Kind != "use" && fact.Kind != "reference" { - continue - } - binding, err := inventory.resolvePath(module, fact.Path, fact.Kind == "use", 0) + binding, resolved, err := inventory.resolveFact(module, fact) if err != nil { result.Findings = append(result.Findings, moduleCoverage(module.file, fact, err.Error())) continue } + if !resolved { + continue + } if binding.module != nil { connected = append(connected, binding.module.crate) binding.file = binding.module.file @@ -179,6 +180,19 @@ func (inventory *moduleInventory) resolveModule(module *rustModule, result *Modu return connected } +func (inventory *moduleInventory) resolveFact(module *rustModule, fact SourceGraphFact) (rustBinding, bool, error) { + switch fact.Kind { + case "macro": + return rustBinding{}, false, inventory.resolveMacro(module, fact.Path, fact.Name, 0) + case "unsupported": + return rustBinding{}, false, fmt.Errorf("%s", fact.Name) + case "use", "extern", "reference": + binding, err := inventory.resolvePath(module, fact.Path, fact.Kind == "use", 0) + return binding, true, err + } + return rustBinding{}, false, nil +} + func moduleCoverage(path string, fact SourceGraphFact, reason string) policy.Finding { return policy.Finding{Check: "architecture.importCoverage", Path: path, Subject: "rust", Line: fact.Line, Column: fact.Column, Message: fmt.Sprintf("Rust written-module graph is incomplete: %s", reason)} } diff --git a/internal/rustfacts/graph_modules.go b/internal/rustfacts/graph_modules.go index b50e3c34..ae329eb1 100644 --- a/internal/rustfacts/graph_modules.go +++ b/internal/rustfacts/graph_modules.go @@ -26,6 +26,7 @@ func (inventory *moduleInventory) readModuleFile(root *rustModule, ancestry []st for _, fact := range source.Graph { module := scopes[strings.Join(fact.Scope, "::")] if module == nil { + root.crate.findings = append(root.crate.findings, moduleCoverage(root.file, fact, "syntax fact has no established containing scope")) continue } module.facts = append(module.facts, fact) @@ -37,24 +38,23 @@ func (inventory *moduleInventory) readModuleFile(root *rustModule, ancestry []st func (inventory *moduleInventory) addModuleFact(module *rustModule, fact SourceGraphFact, scopes map[string]*rustModule, ancestry []string) error { switch fact.Kind { - case "module": - child, err := inventory.moduleChild(module, fact) - if err != nil { - return err - } - if err := bindRustName(module, fact.Name, rustBinding{module: child}); err != nil { - return err - } - if fact.Inline { - key := strings.Join(append(slices.Clone(fact.Scope), fact.Name), "::") - scopes[key] = child + case "scope": + child := newRustModule(module.crate, module, module.file, module.directory, module.inline) + child.lexical = true + key := strings.Join(append(slices.Clone(fact.Scope), fact.Name), "::") + scopes[key] = child + case "prelude": + if fact.Name == "no_std" { + module.crate.noStd = true } else { - inventory.readModuleFile(child, ancestry) + module.noPrelude = true } + case "module": + return inventory.addDeclaredModule(module, fact, scopes, ancestry) case "item": return bindRustName(module, fact.Name, rustBinding{file: module.file, scope: module}) - case "use": - binding := rustBinding{alias: fact.Path, scope: module} + case "use", "extern": + binding := rustBinding{alias: fact.Path, scope: module, extern: fact.Kind == "extern"} if fact.Alias == "*" { module.globs = append(module.globs, binding) } else if fact.Alias != "_" { @@ -64,6 +64,23 @@ func (inventory *moduleInventory) addModuleFact(module *rustModule, fact SourceG return nil } +func (inventory *moduleInventory) addDeclaredModule(module *rustModule, fact SourceGraphFact, scopes map[string]*rustModule, ancestry []string) error { + child, err := inventory.moduleChild(module, fact) + if err != nil { + return err + } + if err := bindRustName(module, fact.Name, rustBinding{module: child}); err != nil { + return err + } + if fact.Inline { + key := strings.Join(append(slices.Clone(fact.Scope), fact.Name), "::") + scopes[key] = child + } else { + inventory.readModuleFile(child, ancestry) + } + return nil +} + func bindRustName(module *rustModule, name string, binding rustBinding) error { if previous, found := module.bindings[name]; found { if previous.file == binding.file && binding.file != "" { diff --git a/internal/rustfacts/graph_paths.go b/internal/rustfacts/graph_paths.go index d51fd79b..62b5685f 100644 --- a/internal/rustfacts/graph_paths.go +++ b/internal/rustfacts/graph_paths.go @@ -35,12 +35,13 @@ func (inventory *moduleInventory) pathAnchor(module *rustModule, first string, i case "crate": binding.module = module.crate.root case "self": - binding.module = module + binding.module = containingModule(module) case "super": + module = containingModule(module) if module.parent == nil { return binding, errors.New("rust super path has no containing module") } - binding.module = module.parent + binding.module = containingModule(module.parent) case "Self": binding.file = module.file default: @@ -54,34 +55,67 @@ func (inventory *moduleInventory) pathAnchor(module *rustModule, first string, i func (inventory *moduleInventory) pathMember(module *rustModule, name string, depth int) (rustBinding, error) { if name != "super" { - return inventory.resolveName(module, name, depth) + binding, found, err := inventory.moduleName(module, name, depth) + if err != nil || found { + return binding, err + } + return binding, fmt.Errorf("module has no resolved written member %q", name) } if module.parent == nil { return rustBinding{}, errors.New("rust super path escapes its crate") } - return rustBinding{module: module.parent}, nil + return rustBinding{module: containingModule(module.parent)}, nil } func (inventory *moduleInventory) resolveName(module *rustModule, name string, depth int) (rustBinding, error) { + for current := module; current != nil; current = lexicalParent(current) { + binding, found, err := inventory.moduleName(current, name, depth) + if err != nil || found { + return binding, err + } + } + return inventory.externalCrate(module, name) +} + +func containingModule(module *rustModule) *rustModule { + for module.lexical { + module = module.parent + } + return module +} + +func lexicalParent(module *rustModule) *rustModule { + if module.lexical { + return module.parent + } + return nil +} + +func (inventory *moduleInventory) moduleName(module *rustModule, name string, depth int) (rustBinding, bool, error) { if depth >= 64 { - return rustBinding{}, errors.New("rust name resolution exceeds 64 alias/glob steps") + return rustBinding{}, false, errors.New("rust name resolution exceeds 64 alias/glob steps") } if binding, found := module.bindings[name]; found { if len(binding.alias) == 0 { - return binding, nil + return binding, true, nil } - if len(binding.alias) == 1 && binding.alias[0] == name { - return inventory.externalCrate(module, name) - } - return inventory.resolvePath(binding.scope, binding.alias, true, depth+1) + resolved, err := inventory.resolveAlias(binding, name, depth) + return resolved, true, err } - if len(module.globs) > 0 { - binding, found, err := inventory.resolveGlobName(module, name, depth+1) - if err != nil || found { - return binding, err + return inventory.resolveGlobName(module, name, depth+1) +} + +func (inventory *moduleInventory) resolveAlias(binding rustBinding, name string, depth int) (rustBinding, error) { + if binding.extern { + if binding.alias[0] == "self" { + return rustBinding{module: binding.scope.crate.root}, nil } + return inventory.declaredCrate(binding.scope, binding.alias[0]) } - return inventory.externalCrate(module, name) + if len(binding.alias) == 1 && binding.alias[0] == name { + return inventory.externalCrate(binding.scope, name) + } + return inventory.resolvePath(binding.scope, binding.alias, true, depth+1) } func (inventory *moduleInventory) resolveGlobName(module *rustModule, name string, depth int) (rustBinding, bool, error) { @@ -95,13 +129,13 @@ func (inventory *moduleInventory) resolveGlobName(module *rustModule, name strin if target.module == nil { return result, false, errors.New("external glob imports need resolved binding evidence") } - if _, exists := target.module.bindings[name]; !exists { - continue - } - candidate, err := inventory.resolveName(target.module, name, depth+1) + candidate, exists, err := inventory.moduleName(target.module, name, depth+1) if err != nil { return result, false, err } + if !exists { + continue + } if found && (result.file != candidate.file || result.module != candidate.module || result.external != candidate.external) { return result, false, fmt.Errorf("glob imports supply ambiguous binding %q", name) } @@ -111,9 +145,22 @@ func (inventory *moduleInventory) resolveGlobName(module *rustModule, name strin } func (inventory *moduleInventory) externalCrate(module *rustModule, name string) (rustBinding, error) { - if slices.Contains([]string{"std", "core", "alloc", "proc_macro", "bool", "char", "str", "f32", "f64", "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize"}, name) { + if slices.Contains([]string{"bool", "char", "str", "f32", "f64", "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize"}, name) { return rustBinding{external: true}, nil } + if implicitPrelude(module) { + binding, err := inventory.declaredCrate(module, name) + if err == nil && !(binding.standard && name == "std" && module.crate.noStd) { + return binding, nil + } + } + if preludeItem(module, name) { + return rustBinding{external: true}, nil + } + return rustBinding{}, fmt.Errorf("path prefix %q has no resolved written module, item or Cargo dependency", name) +} + +func (inventory *moduleInventory) declaredCrate(module *rustModule, name string) (rustBinding, error) { for _, crate := range inventory.crates { if crate.pkg.Manifest == module.crate.pkg.Manifest && crate.target.Name == name && libraryTarget(crate.target) { return rustBinding{module: crate.root}, nil @@ -129,6 +176,9 @@ func (inventory *moduleInventory) externalCrate(module *rustModule, name string) } return inventory.dependencyBinding(dependency, name) } + if slices.Contains([]string{"std", "core", "alloc", "proc_macro"}, name) { + return rustBinding{external: true, standard: true}, nil + } return rustBinding{}, fmt.Errorf("path prefix %q has no resolved written module, item or Cargo dependency", name) } diff --git a/internal/rustfacts/graph_prelude.go b/internal/rustfacts/graph_prelude.go new file mode 100644 index 00000000..5e2122d9 --- /dev/null +++ b/internal/rustfacts/graph_prelude.go @@ -0,0 +1,81 @@ +package rustfacts + +import ( + "errors" + "fmt" + "slices" +) + +func implicitPrelude(module *rustModule) bool { + for current := module; current != nil; current = current.parent { + if current.noPrelude { + return false + } + } + return true +} + +func preludeItem(module *rustModule, name string) bool { + if !implicitPrelude(module) { + return false + } + if slices.Contains([]string{"Clone", "Copy", "Send", "Sized", "Sync", "Unpin", "Drop", "Fn", "FnMut", "FnOnce", "AsyncFn", "AsyncFnMut", "AsyncFnOnce", "AsRef", "AsMut", "From", "Into", "DoubleEndedIterator", "ExactSizeIterator", "Extend", "IntoIterator", "Iterator", "Option", "Some", "None", "Result", "Ok", "Err", "Default", "Eq", "Ord", "PartialEq", "PartialOrd"}, name) { + return true + } + if !module.crate.noStd && slices.Contains([]string{"Box", "String", "Vec", "ToOwned", "ToString"}, name) { + return true + } + if slices.Contains([]string{"2021", "2024"}, module.crate.target.Edition) && slices.Contains([]string{"TryFrom", "TryInto", "FromIterator"}, name) { + return true + } + return module.crate.target.Edition == "2024" && slices.Contains([]string{"Future", "IntoFuture"}, name) +} + +func (inventory *moduleInventory) resolveMacro(module *rustModule, path []string, namespace string, depth int) error { + if depth >= 64 { + return errors.New("macro alias resolution exceeds 64 levels") + } + if len(path) == 1 { + return inventory.preludeMacro(module, path[0], namespace, depth) + } + if !standardMacroPath(path, namespace) { + return errors.New("macro path is not a resolved standard-library macro") + } + binding, err := inventory.resolveName(module, path[0], depth+1) + if err != nil || !binding.standard { + return errors.New("standard-library macro namespace is shadowed") + } + return nil +} + +func standardMacroPath(path []string, namespace string) bool { + if len(path) < 2 || !(path[0] == "std" || path[0] == namespace) { + return false + } + if len(path) == 2 { + return true + } + if len(path) != 3 || namespace != "core" { + return false + } + modules := map[string]string{"Clone": "clone", "Copy": "marker", "Debug": "fmt", "Default": "default", "Eq": "cmp", "PartialEq": "cmp", "Ord": "cmp", "PartialOrd": "cmp", "Hash": "hash"} + return modules[path[2]] == path[1] +} + +func (inventory *moduleInventory) preludeMacro(module *rustModule, name, namespace string, depth int) error { + for current := module; current != nil; current = lexicalParent(current) { + if binding, found := current.bindings[name]; found && len(binding.alias) > 0 { + if binding.alias[len(binding.alias)-1] != name { + return errors.New("macro alias changes the standard macro's syntax identity") + } + return inventory.resolveMacro(binding.scope, binding.alias, namespace, depth+1) + } + if _, found, err := inventory.resolveGlobName(current, name, depth+1); err != nil || found { + return fmt.Errorf("macro %q has an unresolved glob binding", name) + } + } + if !implicitPrelude(module) || module.crate.noStd && namespace != "core" { + return fmt.Errorf("macro %q is not available in this crate's implicit prelude", name) + } + return nil +} diff --git a/internal/rustfacts/graph_syntax.go b/internal/rustfacts/graph_syntax.go index f7ebc501..08d46751 100644 --- a/internal/rustfacts/graph_syntax.go +++ b/internal/rustfacts/graph_syntax.go @@ -35,12 +35,16 @@ func validateSourceGraphFacts(facts []SourceGraphFact, coordinates sourceCoordin func validGraphFactIdentity(fact SourceGraphFact) bool { switch fact.Kind { - case "module", "item", "unsupported": + case "module", "item", "unsupported", "scope": return fact.Name != "" - case "use": + case "use", "extern": return len(fact.Path) > 0 && fact.Alias != "" case "reference": return len(fact.Path) > 0 + case "macro": + return len(fact.Path) > 0 && slices.Contains([]string{"std", "core", "alloc"}, fact.Name) + case "prelude": + return fact.Name == "no_std" || fact.Name == "no_implicit_prelude" } return false } diff --git a/internal/rustfacts/source_parser.py b/internal/rustfacts/source_parser.py index f7b5f83c..fc33ff2b 100644 --- a/internal/rustfacts/source_parser.py +++ b/internal/rustfacts/source_parser.py @@ -37,7 +37,6 @@ "repr", "no_std", "no_main", - "macro_use", "macro_export", "unsafe", "deprecated", @@ -75,6 +74,44 @@ "PartialOrd", "Hash", } +EXPRESSION_MACROS = { + "assert", + "assert_eq", + "assert_ne", + "debug_assert", + "debug_assert_eq", + "debug_assert_ne", + "format_args", + "panic", + "todo", + "unimplemented", + "unreachable", + "write", + "writeln", + "concat", + "env", + "option_env", + "compile_error", + "include_bytes", + "include_str", + "file", + "line", + "column", + "module_path", +} +STANDARD_MACROS = {"dbg", "print", "println", "eprint", "eprintln", "format"} +LEXICAL_SCOPES = { + "function_item", + "function_signature_item", + "closure_expression", + "block", + "impl_item", + "trait_item", + "struct_item", + "enum_item", + "type_item", + "union_item", +} def location(node, source): @@ -362,21 +399,27 @@ def collect_graph_node(node, source, result, scope): node, source, scope, - "use", + "extern", path=[name], alias=alias.text.decode() if alias is not None else name, ) ) elif node.type in {"macro_invocation", "macro_definition"}: - facts.append( - graph_fact( - node, - source, - scope, - "unsupported", - name="macro expansion requires resolved import evidence", - ) + facts.extend(expansion_graph_facts(node, source, scope)) + + +def expansion_graph_facts(node, source, scope): + if node.type == "macro_invocation": + return macro_graph_facts(node, source, scope) + return [ + graph_fact( + node, + source, + scope, + "unsupported", + name="macro expansion requires resolved import evidence", ) + ] def use_graph_facts(node, source, scope): @@ -405,6 +448,7 @@ def reference_graph_facts(node, source, scope): "attribute_item", "inner_attribute_item", "visibility_modifier", + "macro_invocation", } if any(parent.type in excluded for parent in node_ancestors(node)): return [] @@ -424,6 +468,8 @@ def reference_graph_facts(node, source, scope): def attribute_graph_facts(node, source, scope): parsed = attribute(node, source) + if parsed["name"] in {"no_std", "no_implicit_prelude"}: + return [graph_fact(node, source, scope, "prelude", name=parsed["name"])] if parsed["name"] == "derive": arguments = node.named_children[0].child_by_field_name("arguments") groups = token_groups(arguments) if arguments is not None else [] @@ -431,7 +477,17 @@ def attribute_graph_facts(node, source, scope): len(group) == 1 and group[0].text.decode() in BUILTIN_DERIVES for group in groups ): - return [] + return [ + graph_fact( + group[0], + source, + scope, + "macro", + name="core", + path=[group[0].text.decode()], + ) + for group in groups + ] elif known_graph_attribute(parsed): return [] return [ @@ -446,11 +502,146 @@ def attribute_graph_facts(node, source, scope): def known_graph_attribute(parsed): - return parsed["name"] in BUILTIN_ATTRIBUTES and all( + return parsed["name"] in BUILTIN_ATTRIBUTES - {"no_std"} and all( known_graph_attribute(child) for child in parsed["children"] ) +def macro_graph_facts(node, source, scope, depth=0): + macro = node.child_by_field_name("macro") + path = syntax_path(macro) + if not path or depth >= 64: + raise ValueError("Rust macro syntax is missing or exceeds 64 levels") + name = path[-1] + tokens = next(child for child in node.named_children if child.type == "token_tree") + facts = [ + graph_fact( + node, + source, + scope, + "macro", + path=path, + name="std" + if name in STANDARD_MACROS + else "alloc" + if name == "vec" + else "core", + ) + ] + try: + if macro.text.startswith(b"::"): + raise ValueError("Absolute macro paths need external-prelude resolution") + for prefix, start, end, suffix in macro_fragments(name, tokens): + facts.extend( + parse_graph_fragment(prefix, start, end, suffix, source, scope, depth) + ) + except ValueError: + return [ + graph_fact( + node, + source, + scope, + "unsupported", + name="macro arguments or expansion require resolved import evidence", + ) + ] + return facts + + +def macro_fragments(name, tokens): + start, end = tokens.start_byte + 1, tokens.end_byte - 1 + if name in EXPRESSION_MACROS | STANDARD_MACROS: + return [(b"fn cp(){cp(", start, end, b");}")] + if name == "vec": + return [(b"fn cp(){let _=[", start, end, b"];}")] + if name in {"stringify", "cfg"}: + return [] + if name != "matches": + raise ValueError("Unsupported macro syntax") + children = [child for child in tokens.children[1:-1] if child.type not in COMMENTS] + comma = next((child for child in children if child.type == ","), None) + if comma is None: + raise ValueError("matches requires an expression and a pattern") + if children[-1].type == ",": + end = children[-1].start_byte + return [ + (b"fn cp(){cp(", start, comma.start_byte, b");}"), + (b"fn cp(){match (){", comma.end_byte, end, b" => ()}}"), + ] + + +def parse_graph_fragment(prefix, start, end, suffix, source, scope, depth): + fragment = prefix + source[start:end] + suffix + tree = Parser(Language(tree_sitter_rust.language())).parse(fragment) + if tree.root_node.has_error: + raise ValueError("Macro arguments do not match supported Rust syntax") + facts = {"graph": []} + pending = [(tree.root_node, scope)] + visited = 0 + while pending: + current, current_scope = pending.pop() + visited += 1 + if visited > MAXIMUM_NODES: + raise ValueError("Rust macro arguments exceed 500000 syntax nodes") + within = current.start_byte >= len(prefix) and current.end_byte <= len( + fragment + ) - len(suffix) + if within: + if current.type == "macro_invocation": + facts["graph"].extend( + macro_graph_facts(current, fragment, current_scope, depth + 1) + ) + continue + current_scope = collect_graph_scope(current, fragment, facts, current_scope) + pending.extend( + (child, current_scope) for child in reversed(current.named_children) + ) + for fact in facts["graph"]: + fact["scope"] = fact["scope"][: len(scope)] + [ + shifted_scope(value, start - len(prefix)) + for value in fact["scope"][len(scope) :] + ] + if fact["kind"] == "scope": + fact["name"] = shifted_scope(fact["name"], start - len(prefix)) + fact.update( + offset_location( + source, + fact["startByte"] + start - len(prefix), + fact["endByte"] + start - len(prefix), + ) + ) + return facts["graph"] + + +def shifted_scope(name, offset): + return "@" + str(int(name[1:]) + offset) if name.startswith("@") else name + + +def collect_graph_scope(node, source, result, scope): + collect_graph_node(node, source, result, scope) + if node.type in LEXICAL_SCOPES: + name = "@" + str(node.start_byte) + result["graph"].append(graph_fact(node, source, scope, "scope", name=name)) + return [*scope, name] + if node.type == "mod_item" and node.child_by_field_name("body") is not None: + return [ + *scope, + node.child_by_field_name("name").text.decode().removeprefix("r#"), + ] + return scope + + +def offset_location(source, start, end): + return { + "startByte": start, + "endByte": end, + "line": source.count(b"\n", 0, start) + 1, + "column": len(source[source.rfind(b"\n", 0, start) + 1 : start].decode()) + 1, + "endLine": source.count(b"\n", 0, end) + 1, + "endColumn": len(source[source.rfind(b"\n", 0, end) + 1 : end].decode()) + 1, + } + + def node_ancestors(node): parent = node.parent while parent is not None: @@ -476,12 +667,7 @@ def visit(self, node, function, test_context, depth, scope): if node.type in {"source_file", "mod_item", "function_item"}: test_context = test_context or test_configuration(node, self.source) collect_node(node, self.source, self.result) - collect_graph_node(node, self.source, self.result, scope) - if node.type == "mod_item" and node.child_by_field_name("body") is not None: - scope = [ - *scope, - node.child_by_field_name("name").text.decode().removeprefix("r#"), - ] + scope = collect_graph_scope(node, self.source, self.result, scope) body = None if node.type in CALLABLES: body = node.child_by_field_name("body") From 1e99f63a2cc6f44af3e3e67679c96e2b8d6838f2 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 03:46:39 +0000 Subject: [PATCH 17/49] feat: provision a pinned Rust test runner Install authenticated cargo-nextest binaries through the governed Python runtime on the five supported hosts. Preserve working tools through archive, probe and replacement failures; register the runner in governed command environments. Dependency review and offline supply-chain passed. Scoped policy checks, installer regressions, repository-unit and rustfacts-unit passed. Real Linux Nextest probes distinguish passed, failed and all-ignored runs. Native suite, doctest and release integration remain unfinished. --- .code-polishy.json | 7 + docs/design/cargo-facts.md | 11 ++ docs/design/rust-inputs.md | 10 ++ internal/repository/repository.go | 1 + internal/rustfacts/install_nextest.py | 178 +++++++++++++++++++++ internal/rustfacts/nextest_install_test.go | 157 ++++++++++++++++++ tools/cargo-nextest-version.txt | 1 + tools/cargo-nextest_checksums.txt | 4 + tools/install-policy-tools.ps1 | 2 + tools/install-policy-tools.sh | 1 + 10 files changed, 372 insertions(+) create mode 100644 internal/rustfacts/install_nextest.py create mode 100644 internal/rustfacts/nextest_install_test.go create mode 100644 tools/cargo-nextest-version.txt create mode 100644 tools/cargo-nextest_checksums.txt diff --git a/.code-polishy.json b/.code-polishy.json index 4d75b99b..f2b51e53 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -1649,6 +1649,13 @@ }, "supplyChain": { "releaseArtifacts": [ + { + "name": "cargo-nextest", + "versionFile": "tools/cargo-nextest-version.txt", + "source": "github-release", + "locator": "nextest-rs/nextest", + "tagPrefix": "cargo-nextest-" + }, { "name": "astroid", "versionFile": "tools/astroid-version.txt", diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index 7aa07995..95349e75 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -200,3 +200,14 @@ invented line. Workspace metadata, not parsed package-ID strings, owns identity. [Rust input coverage](rust-inputs.md) defines control input selection and the governed toolchain. The [implementation plan](../plans/code-polishy-rust-support-plan.md) records the remaining Rust capabilities and acceptance work. + +## Test runner provisioning + +The already governed Python runtime installs the exact cargo-nextest binary on +all supported hosts, using one cross-platform implementation. Checked-in archive +identities are authenticated before reading the executable payload. The installer +extracts only that regular member, with bounded bytes, and runs a staged version +probe before atomic replacement. Corrupt downloads, invalid payloads, failed +probes and replacement failures preserve a previous working executable. Reuse +checks the installed release; it does not hash local state. This provisions a +runner, not test evidence or doctest execution. diff --git a/docs/design/rust-inputs.md b/docs/design/rust-inputs.md index 450eb7d7..9c8e7758 100644 --- a/docs/design/rust-inputs.md +++ b/docs/design/rust-inputs.md @@ -216,3 +216,13 @@ each download once, and never runs package installation scripts or source builds Staged packages must pass a real Rust parse before replacing the previous exact distributions. Replacement retains recoverable backups until the installed parser passes that same check. Wheel metadata and licenses remain installed. + +Rust unit and integration execution uses the independent pinned cargo-nextest +0.9.138 release, published 2026-06-21. Its stable JUnit output supplies actual +test-case outcomes on the stable compiler, whose built-in libtest JSON interface +still requires unstable options. The binary archives cover the same five hosts; +the macOS archive is universal. Checked-in identities come from the exact +[upstream release](https://github.com/nextest-rs/nextest/releases/tag/cargo-nextest-0.9.138). +Installation must authenticate archives and verify the staged executable before +replacing a working tool. No Cargo source build, install script or self-update +participates. Nextest does not run doctests; it cannot supply doctest coverage. diff --git a/internal/repository/repository.go b/internal/repository/repository.go index edd31143..1b5d0787 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -487,6 +487,7 @@ func (repo Repository) CommandEnvironment() GovernedEnvironment { {Name: "osv-scanner", Path: repo.PolicyTool("osv-scanner"), Version: repo.ToolPin("osv-scanner")}, {Name: "ruff", Path: repo.PolicyTool("ruff"), Version: repo.ToolPin("ruff")}, {Name: "ty", Path: repo.PolicyTool("ty"), Version: repo.ToolPin("ty")}, + {Name: "cargo-nextest", Path: repo.PolicyTool("cargo-nextest"), Version: repo.ToolPin("cargo-nextest")}, }}, {directory: shellcheckDirectory, tools: []GovernedTool{{Name: "shellcheck", Path: filepath.Join(shellcheckDirectory, executableName("shellcheck")), Version: repo.ToolPin("shellcheck")}}}, {directory: nodeDirectory, tools: []GovernedTool{{Name: "node", Path: filepath.Join(nodeDirectory, executableName("node")), Version: repo.ToolPin("node")}}}, diff --git a/internal/rustfacts/install_nextest.py b/internal/rustfacts/install_nextest.py new file mode 100644 index 00000000..f9578af6 --- /dev/null +++ b/internal/rustfacts/install_nextest.py @@ -0,0 +1,178 @@ +import hashlib +import os +import pathlib +import platform +import re +import stat +import subprocess +import sys +import tarfile +import tempfile +import urllib.request +import zipfile + +MAXIMUM_ARCHIVE_BYTES = 64 * 1024 * 1024 +MAXIMUM_BINARY_BYTES = 128 * 1024 * 1024 + + +def host_archive(version): + architecture = { + "x86_64": "x86_64", + "amd64": "x86_64", + "arm64": "aarch64", + "aarch64": "aarch64", + }.get(platform.machine().lower()) + if architecture is None or sys.platform not in {"linux", "darwin", "win32"}: + raise ValueError("cargo-nextest is unavailable on this host") + if sys.platform == "win32" and architecture != "x86_64": + raise ValueError("cargo-nextest Windows support requires x64") + target = { + "linux": architecture + "-unknown-linux-gnu.tar.gz", + "darwin": "universal-apple-darwin.tar.gz", + "win32": "x86_64-pc-windows-msvc.zip", + }[sys.platform] + return "cargo-nextest-" + version + "-" + target + + +def locked_archive(root): + version = (root / "tools/cargo-nextest-version.txt").read_text().strip() + if re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", version) is None: + raise ValueError("cargo-nextest requires an exact release version") + asset = host_archive(version) + checksums = {} + for line in (root / "tools/cargo-nextest_checksums.txt").read_text().splitlines(): + parts = line.split() + if ( + len(parts) != 2 + or parts[0] in checksums + or re.fullmatch(r"[0-9a-f]{64}", parts[1]) is None + ): + raise ValueError( + "cargo-nextest checksum inventory is malformed or duplicated" + ) + checksums[parts[0]] = parts[1] + if asset not in checksums: + raise ValueError("cargo-nextest has no pinned archive for this host") + return version, asset, checksums[asset] + + +def download_archive(version, asset, digest, scratch): + url = ( + "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-" + + version + + "/" + + asset + ) + with urllib.request.urlopen(url, timeout=30) as response: + data = response.read(MAXIMUM_ARCHIVE_BYTES + 1) + if len(data) > MAXIMUM_ARCHIVE_BYTES or hashlib.sha256(data).hexdigest() != digest: + raise ValueError("cargo-nextest archive failed authentication") + destination = scratch / asset + destination.write_bytes(data) + return destination + + +def unpack_binary(archive_path, destination): + if archive_path.suffix == ".zip": + data = zip_payload(archive_path) + else: + data = tar_payload(archive_path) + with destination.open("xb") as output: + output.write(data) + destination.chmod(0o755) + + +def read_binary(payload, size): + if not 0 < size <= MAXIMUM_BINARY_BYTES: + raise ValueError("cargo-nextest executable is empty or exceeds its size bound") + data = payload.read(MAXIMUM_BINARY_BYTES + 1) + if len(data) != size: + raise ValueError( + "cargo-nextest executable size differs from its archive record" + ) + return data + + +def zip_payload(archive_path): + with zipfile.ZipFile(archive_path) as archive: + entries = archive.infolist() + if ( + len(entries) != 1 + or entries[0].filename != "cargo-nextest.exe" + or stat.S_IFMT(entries[0].external_attr >> 16) not in {0, stat.S_IFREG} + ): + raise ValueError( + "cargo-nextest archive omitted its unique regular executable" + ) + with archive.open(entries[0]) as payload: + return read_binary(payload, entries[0].file_size) + + +def tar_payload(archive_path): + with tarfile.open(archive_path, "r:gz") as archive: + entry = archive.next() + if ( + entry is None + or entry.name != "cargo-nextest" + or entry.type not in {tarfile.REGTYPE, tarfile.AREGTYPE} + ): + raise ValueError( + "cargo-nextest archive omitted its unique regular executable" + ) + payload = archive.extractfile(entry) + if payload is None: + raise ValueError("cargo-nextest archive omitted its executable payload") + with payload: + data = read_binary(payload, entry.size) + if archive.next() is not None: + raise ValueError( + "cargo-nextest archive contains unexpected additional entries" + ) + return data + + +def verified_binary(path, version): + if path.is_symlink() or not path.is_file(): + return False + try: + result = subprocess.run( + [str(path), "--version"], + capture_output=True, + text=True, + check=False, + timeout=10, + ) + first = result.stdout.splitlines()[0].split() + return result.returncode == 0 and first[:2] == ["cargo-nextest", version] + except (OSError, subprocess.SubprocessError, IndexError, UnicodeError): + return False + + +def install(root): + version, asset, digest = locked_archive(root) + for directory in (root / ".tools", root / ".tools/bin"): + if directory.is_symlink(): + raise ValueError( + "cargo-nextest installation directory must not be a symlink" + ) + directory.mkdir(exist_ok=True) + name = "cargo-nextest.exe" if sys.platform == "win32" else "cargo-nextest" + destination = root / ".tools/bin" / name + if destination.is_symlink() or (destination.exists() and not destination.is_file()): + raise ValueError("cargo-nextest destination must be a regular executable") + if verified_binary(destination, version): + return + with tempfile.TemporaryDirectory( + prefix=".nextest-installation-", dir=root / ".tools" + ) as temporary: + scratch = pathlib.Path(temporary) + archive = download_archive(version, asset, digest, scratch) + staged = scratch / name + unpack_binary(archive, staged) + if not verified_binary(staged, version): + raise ValueError("staged cargo-nextest failed its exact release probe") + os.replace(staged, destination) + + +if __name__ == "__main__": + install(pathlib.Path(sys.argv[1]).resolve(strict=True)) diff --git a/internal/rustfacts/nextest_install_test.go b/internal/rustfacts/nextest_install_test.go new file mode 100644 index 00000000..ceb1529c --- /dev/null +++ b/internal/rustfacts/nextest_install_test.go @@ -0,0 +1,157 @@ +package rustfacts + +import "testing" + +func TestNextestInstallerSelectsOnlyPinnedSupportedHostArchives(t *testing.T) { + t.Parallel() + runInstallerContract(t, ` +import install_nextest as nextest +archives = set() +for host, machine, target in [("linux", "x86_64", "x86_64-unknown-linux-gnu.tar.gz"), ("linux", "aarch64", "aarch64-unknown-linux-gnu.tar.gz"), ("darwin", "x86_64", "universal-apple-darwin.tar.gz"), ("darwin", "arm64", "universal-apple-darwin.tar.gz"), ("win32", "AMD64", "x86_64-pc-windows-msvc.zip")]: + nextest.sys.platform = host + nextest.platform.machine = lambda: machine + version, asset, digest = nextest.locked_archive(policy_root) + assert asset == "cargo-nextest-" + version + "-" + target, asset + assert len(digest) == 64 + archives.add(asset) +assert len(archives) == 4, archives +nextest.sys.platform = "win32" +nextest.platform.machine = lambda: "ARM64" +try: + nextest.locked_archive(policy_root) +except ValueError: + pass +else: + raise AssertionError("unsupported host borrowed another architecture's binary") +`) +} + +func TestNextestInstallerAuthenticatesArchivesBeforeWriting(t *testing.T) { + t.Parallel() + runInstallerContract(t, ` +import hashlib +import io +import install_nextest as nextest +data = b"nextest archive fixture" +nextest.urllib.request.urlopen = lambda *args, **kwargs: io.BytesIO(data) +asset = "cargo-nextest-fixture.tar.gz" +archive = nextest.download_archive("0.9.138", asset, hashlib.sha256(data).hexdigest(), root) +assert archive.read_bytes() == data +archive.unlink() +try: + nextest.download_archive("0.9.138", asset, "0" * 64, root) +except ValueError: + pass +else: + raise AssertionError("unauthenticated nextest archive passed") +assert not archive.exists() +`) +} + +func TestNextestInstallerRejectsLinkedEscapingDuplicateAndOversizedPayloads(t *testing.T) { + t.Parallel() + runInstallerContract(t, ` +import io +import stat +import tarfile +import zipfile +import install_nextest as nextest +nextest.MAXIMUM_BINARY_BYTES = 16 +for name, kind, count, size in [("../escape", tarfile.REGTYPE, 1, 1), ("cargo-nextest", tarfile.SYMTYPE, 1, 0), ("cargo-nextest", tarfile.LNKTYPE, 1, 0), ("cargo-nextest", tarfile.REGTYPE, 2, 1), ("cargo-nextest", tarfile.REGTYPE, 1, 17)]: + archive = root / "payload.tar.gz" + with tarfile.open(archive, "w:gz") as output: + for _ in range(count): + entry = tarfile.TarInfo(name) + entry.type, entry.linkname, entry.size = kind, "../escape", size + output.addfile(entry, io.BytesIO(b"x" * size)) + try: + nextest.unpack_binary(archive, root / "binary") + except ValueError: + pass + else: + raise AssertionError("unsafe TAR payload passed") + assert not (root / "binary").exists() +for name, mode in [("../escape", stat.S_IFREG), ("cargo-nextest.exe", stat.S_IFLNK), ("cargo-nextest.exe", stat.S_IFIFO)]: + archive = root / "payload.zip" + with zipfile.ZipFile(archive, "w") as output: + entry = zipfile.ZipInfo(name) + entry.external_attr = (mode | 0o755) << 16 + output.writestr(entry, b"x") + try: + nextest.unpack_binary(archive, root / "binary") + except ValueError: + pass + else: + raise AssertionError("unsafe ZIP payload passed") + assert not (root / "binary").exists() +assert not (root.parent / "escape").exists() +`) +} + +func TestNextestInstallerPreservesWorkingBinaryUntilReplacementSucceeds(t *testing.T) { + t.Parallel() + runInstallerContract(t, ` +import io +import tarfile +import zipfile +import install_nextest as nextest +version, asset, digest = nextest.locked_archive(policy_root) +(root / "tools").mkdir() +(root / "tools/cargo-nextest-version.txt").write_text(version) +(root / "tools/cargo-nextest_checksums.txt").write_text(asset + " " + digest + "\n") +binary = root / ".tools/bin" / ("cargo-nextest.exe" if nextest.sys.platform == "win32" else "cargo-nextest") +binary.parent.mkdir(parents=True) +binary.write_bytes(b"previous working runner") +def fixture_download(version, asset, digest, scratch): + archive = scratch / asset + if asset.endswith(".zip"): + with zipfile.ZipFile(archive, "w") as output: + output.writestr("cargo-nextest.exe", b"new pinned runner") + else: + with tarfile.open(archive, "w:gz") as output: + entry = tarfile.TarInfo("cargo-nextest") + entry.size = len(b"new pinned runner") + output.addfile(entry, io.BytesIO(b"new pinned runner")) + return archive +nextest.download_archive = fixture_download +nextest.verified_binary = lambda path, version: False +try: + nextest.install(root) +except ValueError: + pass +else: + raise AssertionError("failed staged probe replaced the working runner") +assert binary.read_bytes() == b"previous working runner" +nextest.verified_binary = lambda path, version: path.read_bytes() == b"new pinned runner" +replace = nextest.os.replace +def denied_replace(source, destination): + raise OSError("replacement is unavailable") +nextest.os.replace = denied_replace +try: + nextest.install(root) +except OSError: + pass +else: + raise AssertionError("failed replacement passed") +assert binary.read_bytes() == b"previous working runner" +nextest.os.replace = replace +nextest.install(root) +assert binary.read_bytes() == b"new pinned runner" +def unexpected_download(*args): + raise AssertionError("installed release was downloaded again") +nextest.download_archive = unexpected_download +nextest.install(root) +assert not list((root / ".tools").glob(".nextest-installation-*")) +`) +} + +func TestNextestInstallerVerifiesTheRealPinnedExecutable(t *testing.T) { + t.Parallel() + runInstallerContract(t, ` +import install_nextest as nextest +name = "cargo-nextest.exe" if nextest.sys.platform == "win32" else "cargo-nextest" +version = (policy_root / "tools/cargo-nextest-version.txt").read_text().strip() +assert nextest.verified_binary(policy_root / ".tools/bin" / name, version) +assert not nextest.verified_binary(policy_root / ".tools/bin" / name, "0.0.0") +`) +} diff --git a/tools/cargo-nextest-version.txt b/tools/cargo-nextest-version.txt new file mode 100644 index 00000000..197e3687 --- /dev/null +++ b/tools/cargo-nextest-version.txt @@ -0,0 +1 @@ +0.9.138 diff --git a/tools/cargo-nextest_checksums.txt b/tools/cargo-nextest_checksums.txt new file mode 100644 index 00000000..70ce8b55 --- /dev/null +++ b/tools/cargo-nextest_checksums.txt @@ -0,0 +1,4 @@ +cargo-nextest-0.9.138-aarch64-unknown-linux-gnu.tar.gz e1c85dbe02dcf5b963803b3f96f4f561757f5c37cab68c932411dea20d57284e +cargo-nextest-0.9.138-universal-apple-darwin.tar.gz 124baa34d160355fa8a8bed053856270cfcf132659e76a0bc8bbcdfc093266b7 +cargo-nextest-0.9.138-x86_64-pc-windows-msvc.zip 6597c6ddbd4157df9a8363c55817dd98ed4905a505225aefa726bc55bf984be7 +cargo-nextest-0.9.138-x86_64-unknown-linux-gnu.tar.gz 3793bf0c27607b196f502c39b2108f571de89fcda7586ae6beefa11ee177b216 diff --git a/tools/install-policy-tools.ps1 b/tools/install-policy-tools.ps1 index e5514ea8..dda861d4 100644 --- a/tools/install-policy-tools.ps1 +++ b/tools/install-policy-tools.ps1 @@ -429,6 +429,8 @@ with zipfile.ZipFile(wheel) as archive: & $Python -I -B (Join-Path $PolicyRoot 'internal/rustfacts/install.py') $PolicyRoot if ($LASTEXITCODE -ne 0) { throw 'Rust syntax distribution installation failed.' } + & $Python -I -B (Join-Path $PolicyRoot 'internal/rustfacts/install_nextest.py') $PolicyRoot + if ($LASTEXITCODE -ne 0) { throw 'Rust test runner installation failed.' } function Test-PythonFactsEnvironment { if (-not (Test-Path -LiteralPath (Join-Path $FactsEnvironment 'pyvenv.cfg') -PathType Leaf) -or -not (Test-Path -LiteralPath $FactsPython -PathType Leaf) -or diff --git a/tools/install-policy-tools.sh b/tools/install-policy-tools.sh index 43b221cc..9a2637f9 100755 --- a/tools/install-policy-tools.sh +++ b/tools/install-policy-tools.sh @@ -28,6 +28,7 @@ case "$(uname -m)" in esac python_bin="${policy_root}/.tools/python/${python_os}-${python_arch}/bin/python3.12" "${python_bin}" -I -B "${policy_root}/internal/rustfacts/install.py" "${policy_root}" +"${python_bin}" -I -B "${policy_root}/internal/rustfacts/install_nextest.py" "${policy_root}" facts_environment_valid() { [[ -f "${facts_environment}/pyvenv.cfg" ]] && [[ -x "${facts_python}" ]] && [[ "$("${facts_python}" -I -B -c 'import sys; print(".".join(str(value) for value in sys.version_info[:3]))' 2>/dev/null || true)" == "${python_release%%+*}" ]] && From f349472793ef5bcfa56af282ff0f6f4e79841b8d Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 04:28:19 +0000 Subject: [PATCH 18/49] feat: integrate native Rust test evidence Add Cargo-native suite selectors and pinned Nextest execution through direct tests, gate plans, regression proofs and reusable receipts. Validate nonempty JUnit outcomes and keep compiler, empty-run and artifact failures operational. Preserve real exit status through execution telemetry and keep Nextest state outside read-only source views. Verification: policy, testartifact, testing, engine, behaviorreview, gaterun and rustfacts ordinary suites pass. Focused policy checks pass. Public CLI proves passing, empty and assertion-failed Rust suites plus a complete simple-crate check. Doctests, broader macro coverage and verified release packaging remain open; no publication or lock upgrade. --- .code-polishy.json | 31 ++- docs/README.md | 2 + docs/catalog.json | 8 + docs/design/execution-observability.md | 3 + docs/design/rust-inputs.md | 3 +- docs/design/rust-tests.md | 77 ++++++++ docs/plans/code-polishy-rust-support-plan.md | 28 ++- internal/behaviorreview/proof.go | 41 ++-- internal/behaviorreview/proof_test.go | 3 +- internal/behaviorreview/rust_proof_test.go | 50 +++++ internal/engine/behavior_review.go | 6 +- internal/engine/engine.go | 2 +- internal/engine/engine_test.go | 2 +- internal/engine/execution_telemetry.go | 15 +- internal/engine/gate_command_runner.go | 19 +- internal/engine/gate_run.go | 2 +- internal/engine/generation_receipts_test.go | 4 +- internal/engine/merge_gate.go | 24 +-- internal/engine/rust_tests_test.go | 179 +++++++++++++++++ internal/engine/test_evidence.go | 8 +- internal/engine/test_execution_view.go | 9 +- internal/engine/test_receipts.go | 11 +- internal/gaterun/identity.go | 6 +- internal/gaterun/validation.go | 2 +- internal/policy/control_inputs.go | 1 + internal/policy/load.go | 18 +- internal/policy/model.go | 11 +- internal/policy/rust_inputs_test.go | 2 +- internal/policy/rust_tests.go | 103 ++++++++++ internal/policy/rust_tests_test.go | 67 +++++++ internal/rustfacts/tests.go | 59 ++++++ internal/testartifact/execution.go | 17 +- internal/testartifact/execution_test.go | 12 +- internal/testartifact/nextest.go | 198 +++++++++++++++++++ internal/testartifact/nextest_config.go | 36 ++++ internal/testartifact/nextest_test.go | 126 ++++++++++++ internal/testartifact/xml.go | 24 ++- internal/testing/artifact_execution_test.go | 7 +- internal/testing/rust.go | 62 ++++++ internal/testing/rust_test.go | 186 +++++++++++++++++ internal/testing/testing.go | 53 ++--- internal/testing/testing_test.go | 1 + schema/code-polishy.schema.json | 31 ++- 43 files changed, 1422 insertions(+), 127 deletions(-) create mode 100644 docs/design/rust-tests.md create mode 100644 internal/behaviorreview/rust_proof_test.go create mode 100644 internal/engine/rust_tests_test.go create mode 100644 internal/policy/rust_tests.go create mode 100644 internal/policy/rust_tests_test.go create mode 100644 internal/rustfacts/tests.go create mode 100644 internal/testartifact/nextest.go create mode 100644 internal/testartifact/nextest_config.go create mode 100644 internal/testartifact/nextest_test.go create mode 100644 internal/testing/rust.go create mode 100644 internal/testing/rust_test.go diff --git a/.code-polishy.json b/.code-polishy.json index f2b51e53..dd489adb 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -27,6 +27,26 @@ "path": "docs/design/cargo-facts.md", "module": "rustfacts" }, + { + "path": "docs/design/rust-tests.md", + "sourcePaths": [ + "internal/policy/rust_tests.go", + "internal/policy/rust_tests_test.go", + "internal/rustfacts/tests.go", + "internal/testing/rust.go", + "internal/testing/rust_test.go", + "internal/engine/test_receipts.go", + "internal/engine/test_execution_view.go", + "internal/engine/rust_tests_test.go", + "internal/behaviorreview/proof.go", + "internal/behaviorreview/rust_proof_test.go", + "internal/testartifact/execution.go", + "internal/testartifact/nextest.go", + "internal/testartifact/nextest_config.go", + "internal/testartifact/nextest_test.go", + "internal/testartifact/xml.go" + ] + }, { "path": "docs/design/rust-architecture.md", "sourcePaths": [ @@ -388,7 +408,8 @@ "gaterun", "policy", "repository", - "runner" + "runner", + "testing" ] }, { @@ -446,7 +467,13 @@ { "name": "testing", "paths": ["internal/testing/**"], - "dependsOn": ["policy", "repository", "runner", "testartifact"] + "dependsOn": [ + "policy", + "repository", + "runner", + "rustfacts", + "testartifact" + ] }, { "name": "supplychain", diff --git a/docs/README.md b/docs/README.md index 0213e887..a7ca88c9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -72,6 +72,8 @@ and aliases. Search is local, bounded, and deterministic. strict no-comment boundary. - [Rust and Shader Input Coverage](design/rust-inputs.md) defines Rust control inputs, WGSL ownership, and the current analysis boundary. +- [Native Rust Test Evidence](design/rust-tests.md) defines Cargo selectors, + managed Nextest results, retries and reusable test receipts. - [Rust Support Plan](plans/code-polishy-rust-support-plan.md) tracks the beta implementation and the remaining requirements for first-class Rust support. - [Source Provenance](source-provenance.md) records public design references, diff --git a/docs/catalog.json b/docs/catalog.json index ab4a8f5f..b19dae13 100644 --- a/docs/catalog.json +++ b/docs/catalog.json @@ -310,6 +310,14 @@ "aliases": [], "public": true }, + { + "id": "rust-tests", + "path": "docs/design/rust-tests.md", + "title": "Native Rust Test Evidence", + "summary": "Understand native Cargo test selection, structured Nextest outcomes, artifact authority and doctest boundaries.", + "aliases": [], + "public": true + }, { "id": "cargo-facts", "path": "docs/design/cargo-facts.md", diff --git a/docs/design/execution-observability.md b/docs/design/execution-observability.md index 03ddbdd9..e29f39f6 100644 --- a/docs/design/execution-observability.md +++ b/docs/design/execution-observability.md @@ -25,6 +25,9 @@ Reports expose requested and expanded paths, analysis-context paths, graph size, total evaluation time, phase durations, exact analyzer commands, resource wait, analyzer-owned Vulture subphases, and cache activity so retained evidence identifies both scope and cost. +The observation wrapper forwards structured process results unchanged to suite +execution. Recording an exit status while discarding it from the caller would +make native test-artifact validation disagree with the command that produced it. Repository-wide Python dead-code analysis runs only for an explicit complete selection or merge gate. Focused Python checks run selected Ruff and ty work; diff --git a/docs/design/rust-inputs.md b/docs/design/rust-inputs.md index 9c8e7758..717209ee 100644 --- a/docs/design/rust-inputs.md +++ b/docs/design/rust-inputs.md @@ -7,7 +7,8 @@ settings change analysis. Their standard filenames are therefore sensitive control inputs at every repository depth, alongside Cargo manifests and locks. This includes both `.cargo/config` and `.cargo/config.toml`, `rust-toolchain` and `rust-toolchain.toml`, `rustfmt.toml` and `.rustfmt.toml`, and `clippy.toml` -and `.clippy.toml`. +and `.clippy.toml`. `.config/nextest.toml` is also a control input because its +profiles can change test selection and execution. The existing repository selection boundary expands changed or deleted control inputs to full governed scope. This is deliberately conservative until Cargo diff --git a/docs/design/rust-tests.md b/docs/design/rust-tests.md new file mode 100644 index 00000000..34df3157 --- /dev/null +++ b/docs/design/rust-tests.md @@ -0,0 +1,77 @@ +# Native Rust Test Evidence + +Rust suites use the existing ownership, cost, profile, selection, artifact and +receipt mechanisms. A suite's `rust` selector replaces its arbitrary `argv`; +it identifies a contained Cargo manifest, optional packages/features and a +Nextest filter. Cargo default features remain the default. Native execution +uses the pinned host compiler, frozen resolution and the governed Cargo cache. +It does not enable every feature or claim another target was tested. + +The pinned Nextest runner supplies unit and integration outcomes. Each attempt +receives a fresh managed artifact directory and release-owned Nextest settings. +Nextest's state directory also lives there, never in the read-only source view. +The settings bind a JUnit report to that suite and disable retries within Nextest. +User configuration is disabled. Project Nextest configuration currently requires +explicit support and fails preflight: silently ignoring default filters could +enable live probes, while importing setup scripts could add unapproved execution. +Existing suite retries remain the only outcome-bearing retry authority. +Configuration is written as data, with correctly escaped paths and names. +The shared artifact preparer retains native compiler environment overrides. +Rust, Cargo and Nextest control variables cannot be inherited through a suite's +application environment. Application diagnostics such as `RUST_LOG` and +`RUST_BACKTRACE` remain valid declared inputs. The host SDK search environment remains available and +participates in receipt identity alongside declared application inputs. + +Typed selectors, including package names, feature choices and filters, remain +part of the suite identity used for retries, historical comparisons and reusable +receipts. A reusable suite runs in the existing read-only input view while using +the governed Cargo cache and target directory. Native report declarations are +present in both direct execution and the gate plan. +Regression proofs use that same execution and artifact boundary. An assertion +failure can establish the expected red exit; compiler failures, empty runs or +invalid artifacts cannot become red evidence merely by matching a process code. + +The `nextest-junit` artifact contract validates complete case identities and +declared counts, not just an XML root. Empty, wholly skipped, partial, duplicated +or malformed evidence cannot establish a successful test run. A normal Nextest +test-failure exit must agree with recorded failures; other failed exits remain +operational failures. Cancellation and timeouts never become report-bearing +success. Required artifacts remain contained regular files, bounded and hashed +once for reusable evidence by the existing artifact owner. + +Doctests are not Nextest tests. Their compiler behavior, including compile-fail, +no-run and ignored examples, requires separate version-bound evidence before +doctest coverage can be claimed. This native suite contract does not turn a +successful build, a benchmark invocation or a live app probe into test evidence. + +## Suite configuration + +A module suite uses ordinary ownership and profile declarations, for example: + +```json +{ + "name": "core-unit", + "kind": "unit", + "scope": "module", + "modules": ["core"], + "rust": { + "manifest": "Cargo.toml", + "packages": ["core"] + }, + "runOn": ["focused", "recommended", "full"], + "cost": "quick" +} +``` + +The manifest defaults to `Cargo.toml`. Without `packages`, Cargo's selected +manifest and workspace default members determine the packages tested. Optional +`features`, `noDefaultFeatures` and Nextest `filter` fields refine selection. +Cargo validates package specifications and feature names; valid punctuation and +Unicode are not restricted to Code Polishy's module-identifier grammar. A filter +that selects no executed tests fails. Declare integration and expensive suites +separately when they require different ownership, profiles or resources. + +Do not supply `argv`, `retryArgv`, a non-root `cwd` or a replacement `nextest.xml` +artifact for a native Rust suite. Native execution owns those fields and requires +its result report. Frozen testing requires a reviewed lock and a prepared Cargo +cache; it never installs dependencies or repairs resolution. diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 328136f2..e9f8436a 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -129,7 +129,7 @@ changed modules; add a quick suite when a new module is introduced. function/closure measurements. Inline tests do not relax production limits. The pinned Tree-sitter Rust grammar supplies written-source facts; it does not pretend to resolve macro expansion or compiler configuration. -- Structured Rust test evidence, general macro/module resolution, WGSL and +- Doctest evidence, general macro/module resolution, WGSL and installed-release integration remain unfinished. Project Clippy overrides and non-native targets currently fail explicitly rather than weakening the supported compiler configuration. @@ -149,6 +149,17 @@ changed modules; add a quick suite when a new module is introduced. Macro expansion and other unresolved syntax still fail coverage explicitly. A plain crate passes the complete public CLI `check`; this is a usable subset, not evidence that macro-heavy or arbitrary target configurations are supported. +- Typed Rust suites now use the pinned Nextest runner through ordinary suite + selection, direct execution, gate plans and regression proofs. Structured + JUnit evidence requires nonempty executed tests, complete counts and unique + identities; assertions remain distinct from compiler/build/operational failures. + Cargo workspace defaults, package and feature selection, integration targets, + filters, cancellation, frozen locks and receipt invalidation have real runtime + fixtures. Application logging variables remain valid explicit inputs; ambient + Cargo/compiler/Nextest controls do not replace governed execution. Nextest state + stays outside reusable read-only source views. Project Nextest settings require + explicit support rather than silently dropping their default filters or scripts. + Doctests and ordinary macro-heavy project coverage remain unfinished. Cargo dependency-input governance now parses ordinary, build, development, target-specific and workspace declaration tables, checks exact registry and Git @@ -334,7 +345,7 @@ Pinned Cargo packaging fixtures cover inherited workspace license metadata. - [ ] Map Rust suites to focused/recommended/full/supplemental profiles. Support unit, integration, doctest, benchmark and native app workflows distinctly. -- [ ] Reject a required suite that selects zero tests, skips every test, or only +- [x] Reject a required native Rust suite that selects zero tests, skips every test, or only builds. Use structured, version-bound runner evidence; do not parse arbitrary terminal prose as authoritative results. Choose a supported structured runner deliberately if stable Cargo output cannot satisfy the evidence contract. @@ -390,12 +401,13 @@ dependency governance now have runnable beta implementations. Source parsing use exact authenticated binary wheels in the existing governed Python runtime; it requires neither target package installation scripts nor a new Rust bootstrap. -The next milestone extends module/macro resolution and nonempty test evidence, -then completes workspace coverage and installed-release integration. General -module resolution, structured Rust test results and installed-release parity -remain open. Existing missing-capability -findings remain until those implementations and evidence exist. This beta is not -yet usable as first-class Rust support for Masterbrainer. +Native unit/integration suites now retain nonempty structured results through +ordinary commands, gate artifacts, regression proofs and reusable receipts. The +[Rust test contract](../design/rust-tests.md) documents selectors and boundaries. +The next milestones extend ordinary macro/module and doctest coverage, then +complete installed-release integration. Existing coverage failures remain until +their implementations and evidence exist. This beta does not yet establish +first-class support for Masterbrainer's full project. Primary references: [Cargo metadata](https://doc.rust-lang.org/cargo/commands/cargo-metadata.html), [Cargo dependency semantics](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html), diff --git a/internal/behaviorreview/proof.go b/internal/behaviorreview/proof.go index 73d61bfa..ddc6a246 100644 --- a/internal/behaviorreview/proof.go +++ b/internal/behaviorreview/proof.go @@ -15,6 +15,7 @@ import ( "github.com/riteofstring/code-polishy/internal/policy" "github.com/riteofstring/code-polishy/internal/repository" "github.com/riteofstring/code-polishy/internal/runner" + testpolicy "github.com/riteofstring/code-polishy/internal/testing" ) const worktreeDirectory = "w" @@ -222,7 +223,7 @@ func replayProofExecution(ctx context.Context, repo repository.Repository, comma if err := ensureCandidateUnchanged(repo, candidate); err != nil { return err } - baselineResult, _, baselineErr := runSuite(ctx, commandRunner, baseline, suite) + baselineResult, _, baselineErr := runSuite(ctx, repo, commandRunner, baseline, suite) if err := ensureCandidateUnchanged(repo, candidate); err != nil { return err } @@ -232,7 +233,7 @@ func replayProofExecution(ctx context.Context, repo repository.Repository, comma if err := ensureReplayCandidateUnchanged(repo, candidateRepository, candidate); err != nil { return err } - candidateResult, _, candidateErr := runSuite(ctx, commandRunner, candidateWorktree, suite) + candidateResult, _, candidateErr := runSuite(ctx, repo, commandRunner, candidateWorktree, suite) if err := ensureReplayCandidateUnchanged(repo, candidateRepository, candidate); err != nil { return err } @@ -460,7 +461,7 @@ func applyAndRecord(ctx context.Context, root *artifactHandle, worktree string, } func runAndRecord(ctx context.Context, repo repository.Repository, commandRunner runner.OutputRunner, artifactRoot *artifactHandle, executionRoot, candidate string, suite policy.TestSuite, id, suffix string) (proofExecution, error) { - result, output, runErr := runSuite(ctx, commandRunner, executionRoot, suite) + result, output, runErr := runSuite(ctx, repo, commandRunner, executionRoot, suite) digest, err := writeProofArtifact(artifactRoot, id, suffix, suiteLog(result, output, runErr)) changedErr := ensureCandidateUnchanged(repo, candidate) if err != nil { @@ -526,16 +527,29 @@ func applyFailure(err error, exitStatus int) error { return operational("apply candidate evidence patch", err) } -func runSuite(ctx context.Context, commandRunner runner.OutputRunner, root string, suite policy.TestSuite) (runner.Result, runner.Output, error) { - return commandRunner.RunWithOutput(ctx, root, suiteCommand(suite)) +func runSuite(ctx context.Context, repo repository.Repository, commandRunner runner.OutputRunner, root string, suite policy.TestSuite) (runner.Result, runner.Output, error) { + repo.Root = root + capture := &proofSuiteRunner{OutputRunner: commandRunner} + execution := testpolicy.ExecuteSuite(ctx, repo, capture, suite, 1) + if !execution.Failed() { + return execution.Result, capture.output, nil + } + err := errors.New(execution.FailureMessage) + if execution.FailureCategory != runner.FailureCommandExit { + err = operational("run regression suite", err) + } + return execution.Result, capture.output, err } -func suiteCommand(suite policy.TestSuite) policy.Command { - return policy.Command{ - Name: suite.Name, Argv: append([]string{}, suite.Argv...), Cwd: suite.Cwd, Paths: append([]string{}, suite.Paths...), - Modules: append([]string{}, suite.Modules...), RunOn: append([]string{}, suite.RunOn...), Environment: append([]string{}, suite.Environment...), - ExclusiveResources: append([]string{}, suite.ExclusiveResources...), TimeoutSeconds: suite.TimeoutSeconds, - } +type proofSuiteRunner struct { + runner.OutputRunner + output runner.Output +} + +func (capture *proofSuiteRunner) RunWithResult(ctx context.Context, root string, command policy.Command) (runner.Result, error) { + result, output, err := capture.RunWithOutput(ctx, root, command) + capture.output = output + return result, err } func cloneCommand(command policy.Command) policy.Command { @@ -544,11 +558,12 @@ func cloneCommand(command policy.Command) policy.Command { Paths: slices.Clone(command.Paths), Modules: slices.Clone(command.Modules), RunOn: slices.Clone(command.RunOn), Environment: slices.Clone(command.Environment), ExclusiveResources: slices.Clone(command.ExclusiveResources), TimeoutSeconds: command.TimeoutSeconds, Managed: command.Managed, PassFiles: command.PassFiles, PassFilePaths: slices.Clone(command.PassFilePaths), SealedEnvironment: command.SealedEnvironment, + EnvironmentOverrides: slices.Clone(command.EnvironmentOverrides), TestArtifacts: slices.Clone(command.TestArtifacts), } } func executionOperational(ctx context.Context, exitStatus int, err error) bool { - if ctx.Err() != nil || exitStatus < 0 || exitStatus == 124 { + if ctx.Err() != nil || exitStatus < 0 || exitStatus == 124 || errors.Is(err, ErrOperational) { return true } return err != nil && exitStatus == 0 @@ -702,7 +717,7 @@ func replayProofPhases(repo repository.Repository, root *artifactHandle, review return nil, nil, err } digest := sha256Hex(data) - command := suiteCommand(suite) + command := testpolicy.SuiteCommand(repo, suite) phases = append(phases, ReplayProofPhase{ProofID: id, ProofSHA256: digest, Baseline: cloneCommand(command), Candidate: cloneCommand(command)}) references = append(references, ProofReference{ID: id, SHA256: digest}) } diff --git a/internal/behaviorreview/proof_test.go b/internal/behaviorreview/proof_test.go index cdddc7b1..67b17c1c 100644 --- a/internal/behaviorreview/proof_test.go +++ b/internal/behaviorreview/proof_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/riteofstring/code-polishy/internal/repository" + testpolicy "github.com/riteofstring/code-polishy/internal/testing" ) func TestProveCreatesRedGreenEvidenceAndCleansWorktree(t *testing.T) { @@ -250,7 +251,7 @@ func assertReplayPlanProofPhase(t *testing.T, repo repository.Repository, plan G t.Fatalf("replay phases = %+v", plan.Proofs) } phase := plan.Proofs[0] - wantCommand := suiteCommand(repo.Config.Tests.Suites[0]) + wantCommand := testpolicy.SuiteCommand(repo, repo.Config.Tests.Suites[0]) if phase.ProofID != proof.ID || phase.ProofSHA256 != plan.Receipt.Proofs[0].SHA256 || !reflect.DeepEqual(phase.Baseline, wantCommand) || !reflect.DeepEqual(phase.Candidate, wantCommand) { t.Fatalf("replay phase = %+v, want proof %q and command %+v", phase, proof.ID, wantCommand) } diff --git a/internal/behaviorreview/rust_proof_test.go b/internal/behaviorreview/rust_proof_test.go new file mode 100644 index 00000000..1caffba6 --- /dev/null +++ b/internal/behaviorreview/rust_proof_test.go @@ -0,0 +1,50 @@ +package behaviorreview + +import ( + "errors" + "path/filepath" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestRustRegressionExecutionRequiresRealTestOutcomes(t *testing.T) { + t.Parallel() + policyRoot, err := filepath.Abs("../..") + if err != nil { + t.Fatal(err) + } + suite := policy.TestSuite{Name: "rust-unit", Cwd: ".", TimeoutSeconds: 30, Rust: &policy.RustTestSuite{Manifest: "Cargo.toml"}} + for name, test := range map[string]struct { + source string + redExit int + operational bool + }{ + "assertion": {source: "#[test]\nfn behavior() { assert_eq!(2 + 2, 5); }\n", redExit: 100}, + "compiler": {source: "#[test]\nfn behavior() { missing_function(); }\n", redExit: 101, operational: true}, + "empty": {source: "pub fn answer() -> u32 { 42 }\n", redExit: 4, operational: true}, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + repo := repository.Repository{Root: t.TempDir(), PolicyRoot: policyRoot} + writeBehaviorFile(t, repo.Root, "Cargo.toml", "[package]\nname='proof'\nversion='1.0.0'\nedition='2024'\n[workspace]\n") + writeBehaviorFile(t, repo.Root, "Cargo.lock", "version=4\n[[package]]\nname='proof'\nversion='1.0.0'\n") + writeBehaviorFile(t, repo.Root, "src/lib.rs", test.source) + result, output, runErr := runSuite(t.Context(), repo, runner.OSRunner{}, repo.Root, suite) + if runErr == nil || len(output.Stdout)+len(output.Stderr) == 0 || errors.Is(runErr, ErrOperational) != test.operational { + t.Fatalf("native regression outcome: %+v, %v", result, runErr) + } + err := validateReplayBaseline(t.Context(), result.ExitStatus, runErr, regressionProof{ExpectedRedExitStatus: test.redExit}) + if (err == nil) == test.operational { + t.Fatalf("invalid regression red: %+v, %v", result, err) + } + writeBehaviorFile(t, repo.Root, "src/lib.rs", "#[test]\nfn behavior() { assert_eq!(2 + 2, 4); }\n") + green, _, err := runSuite(t.Context(), repo, runner.OSRunner{}, repo.Root, suite) + if err != nil || green.ExitStatus != 0 { + t.Fatalf("valid green rejected: %+v, %v", green, err) + } + }) + } +} diff --git a/internal/engine/behavior_review.go b/internal/engine/behavior_review.go index ab79764d..6ac3f394 100644 --- a/internal/engine/behavior_review.go +++ b/internal/engine/behavior_review.go @@ -265,7 +265,7 @@ func (engine *Engine) checkpointGatePreparation(ctx context.Context, base string if err != nil { return checkpointGatePreparation{}, err } - commands = append(commands, mergeGateSuiteCommands(tests.Suites)...) + commands = append(commands, mergeGateSuiteCommands(engine.Repository, tests.Suites)...) state, plan := checkpointGateStateAndPlan(base, candidate, selection, documentation, decision, tests, commands) state.ReliabilityReminder = engine.reliabilityReminder(selection.Candidate) return checkpointGatePreparation{ @@ -327,7 +327,7 @@ func (engine *Engine) executeCheckpointGate(ctx context.Context, execution check func (engine *Engine) executeDocumentationCheckpointGate(ctx context.Context, execution checkpointGateExecution) (Report, error) { report, gateErr := engine.documentationMergeGate(ctx, execution.selection) if gateErr == nil && !HasFindings(report) && len(execution.plan.Tests.Suites) > 0 { - plannedRunner := &mergeGatePlannedRunner{root: engine.Repository.Root, delegate: execution.controller.runner, expected: execution.commands} + plannedRunner := &mergeGatePlannedRunner{repo: engine.Repository, root: engine.Repository.Root, delegate: execution.controller.runner, expected: execution.commands} plannedEngine := *engine plannedEngine.Runner = plannedRunner tested, testErr := plannedEngine.testExactPlan(ctx, execution.plan.Tests, execution.selection, true) @@ -353,7 +353,7 @@ func (engine *Engine) finalizeCheckpointReplayFailure(ctx context.Context, execu } func (engine *Engine) executeChangedCheckpointGate(ctx context.Context, execution checkpointGateExecution) (Report, error) { - plannedRunner := &mergeGatePlannedRunner{root: engine.Repository.Root, delegate: execution.controller.runner, expected: execution.commands} + plannedRunner := &mergeGatePlannedRunner{repo: engine.Repository, root: engine.Repository.Root, delegate: execution.controller.runner, expected: execution.commands} plannedEngine := *engine plannedEngine.Runner = plannedRunner report, testErr := engine.checkpointChecksAndTests(ctx, plannedEngine, execution.selection, execution.plan.Tests) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 7e219cab..bead048b 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -970,7 +970,7 @@ func configuredToolFindings(repo repository.Repository) []policy.Finding { findings := []policy.Finding{} commands := append([]policy.Command{}, repo.Config.Checks...) for _, suite := range repo.Config.Tests.Suites { - commands = append(commands, policy.Command{Name: suite.Name, Argv: suite.Argv, Cwd: suite.Cwd, TimeoutSeconds: suite.TimeoutSeconds}) + commands = append(commands, testpolicy.SuiteCommand(repo, suite)) } for _, command := range commands { if command.Managed { diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go index 1f911e6d..b0fb5935 100644 --- a/internal/engine/engine_test.go +++ b/internal/engine/engine_test.go @@ -766,7 +766,7 @@ func TestSupplyChainAggregatesProvidersAfterStaticFinding(t *testing.T) { func TestMergeGateSuiteCommandsPreserveConfiguredKinds(t *testing.T) { t.Parallel() - commands := mergeGateSuiteCommands([]policy.TestSuite{ + commands := mergeGateSuiteCommands(repository.Repository{}, []policy.TestSuite{ {Name: "unit", Kind: "unit", Scope: "module", Cost: "quick"}, {Name: "browser", Kind: "browser", Scope: "repository", Cost: "expensive"}, }) diff --git a/internal/engine/execution_telemetry.go b/internal/engine/execution_telemetry.go index f22cc9e7..25d7b63c 100644 --- a/internal/engine/execution_telemetry.go +++ b/internal/engine/execution_telemetry.go @@ -143,15 +143,24 @@ func (commandRunner *executionRunner) RecordCommandPhases(ctx context.Context, c } func (commandRunner *executionRunner) Run(ctx context.Context, root string, command policy.Command) error { + _, err := commandRunner.RunWithResult(ctx, root, command) + return err +} + +func (commandRunner *executionRunner) RunWithResult(ctx context.Context, root string, command policy.Command) (runner.Result, error) { started := time.Now() if delegate, ok := commandRunner.delegate.(resultRunner); ok { result, err := delegate.RunWithResult(ctx, root, command) commandRunner.recorder.addCommand(ctx, command, result, time.Since(started), err) - return err + return result, err } err := commandRunner.delegate.Run(ctx, root, command) - commandRunner.recorder.addCommand(ctx, command, runner.Result{}, time.Since(started), err) - return err + result := runner.Result{ExecutionDuration: time.Since(started)} + if err != nil { + result.ExitStatus = -1 + } + commandRunner.recorder.addCommand(ctx, command, result, result.ExecutionDuration, err) + return result, err } func (commandRunner *executionRunner) RunWithOutput(ctx context.Context, root string, command policy.Command) (runner.Result, runner.Output, error) { diff --git a/internal/engine/gate_command_runner.go b/internal/engine/gate_command_runner.go index a99bc87b..54435458 100644 --- a/internal/engine/gate_command_runner.go +++ b/internal/engine/gate_command_runner.go @@ -246,12 +246,8 @@ func (commandRunner *gateArtifactRunner) execute(ctx context.Context, root strin return runner.Result{ExitStatus: -1, FailureCategory: runner.FailureOperational}, runner.Output{}, err } result, output, runErr := runGateCommand(ctx, commandRunner.delegate, root, actual, commandLog.Stdout(), commandLog.Stderr(), captureOutput) - artifacts := []testartifact.Record{} - if commandRunner.expected[index].Category == gaterun.OrdinaryTest { - var artifactErr error - artifacts, artifactErr = commandRunner.artifactExecution.ValidateCommand(actual) - runErr = errors.Join(runErr, artifactErr) - } + artifacts, artifactErr := commandRunner.validateTestArtifacts(index, actual, &result) + runErr = errors.Join(runErr, artifactErr) logResult, logErr := commandLog.Close() if logErr != nil { commandRunner.err = logErr @@ -282,6 +278,17 @@ func (commandRunner *gateArtifactRunner) execute(ctx context.Context, root strin return commandRunner.finishCommand(command, result, output, runErr, category, reportBearing, logResult, attempt.LogPath) } +func (commandRunner *gateArtifactRunner) validateTestArtifacts(index int, command policy.Command, result *runner.Result) ([]testartifact.Record, error) { + if commandRunner.expected[index].Category != gaterun.OrdinaryTest { + return []testartifact.Record{}, nil + } + artifacts, err := commandRunner.artifactExecution.ValidateCommand(command, result.ExitStatus) + if err != nil && (result.FailureCategory == "" || result.FailureCategory == runner.FailureCommandExit) { + result.FailureCategory = runner.FailureOperational + } + return artifacts, err +} + func gateCommandAttempt(ctx context.Context, result runner.Result, output runner.Output, runErr error, accept runner.ReportOutcomeAcceptor) (gaterun.CommandStatus, runner.FailureCategory, bool) { if runErr == nil { return gaterun.Passed, "", false diff --git a/internal/engine/gate_run.go b/internal/engine/gate_run.go index 289d4d90..d659dc0b 100644 --- a/internal/engine/gate_run.go +++ b/internal/engine/gate_run.go @@ -167,7 +167,7 @@ func newGateArtifactRunner(engine *Engine, run *gaterun.Run, commands []MergeGat receipts.RenderPlan() retryCommands := map[string]policy.Command{} for _, suite := range engine.Repository.Config.Tests.Suites { - retryCommands[suite.Name] = suiteCommand(testpolicy.RetrySuite(suite)) + retryCommands[suite.Name] = testpolicy.SuiteCommand(engine.Repository, testpolicy.RetrySuite(suite)) } return &gateArtifactRunner{ delegate: engine.Runner, run: run, expected: commands, reusable: reusable, diff --git a/internal/engine/generation_receipts_test.go b/internal/engine/generation_receipts_test.go index d6ee9e07..23831427 100644 --- a/internal/engine/generation_receipts_test.go +++ b/internal/engine/generation_receipts_test.go @@ -22,7 +22,7 @@ func TestGenerationVerificationRejectsDriftAndInvalidatesProducerEvidence(t *tes identity := generationReceiptIdentity(t, policyEngine, suite) var output bytes.Buffer commandRunner := runner.OSRunner{Stdout: &output, Stderr: &output} - execution := testpolicy.ExecuteSuite(t.Context(), policyEngine.Repository.Root, commandRunner, suite, 1) + execution := testpolicy.ExecuteSuite(t.Context(), policyEngine.Repository, commandRunner, suite, 1) if execution.Failed() { t.Fatalf("initial generation verification failed: %+v\n%s", execution, output.String()) } @@ -49,7 +49,7 @@ func TestGenerationVerificationRejectsDriftAndInvalidatesProducerEvidence(t *tes t.Fatalf("changed %s retained verification evidence: %v", change, err) } if change == "input" || change == "output" { - execution := testpolicy.ExecuteSuite(t.Context(), policyEngine.Repository.Root, runner.OSRunner{}, suite, 1) + execution := testpolicy.ExecuteSuite(t.Context(), policyEngine.Repository, runner.OSRunner{}, suite, 1) if !execution.Failed() { t.Fatal("generation drift passed declared verification") } diff --git a/internal/engine/merge_gate.go b/internal/engine/merge_gate.go index 11bd53f6..37884172 100644 --- a/internal/engine/merge_gate.go +++ b/internal/engine/merge_gate.go @@ -180,7 +180,7 @@ func (engine *Engine) finalizeMergeGateReplayFailure(ctx context.Context, base s } func (engine *Engine) executePlannedMergeGate(ctx context.Context, base string, plan MergeGateExecutionPlan, controller *gateRunController) (Report, error) { - plannedRunner := &mergeGatePlannedRunner{root: engine.Repository.Root, delegate: controller.runner, expected: plan.Commands} + plannedRunner := &mergeGatePlannedRunner{repo: engine.Repository, root: engine.Repository.Root, delegate: controller.runner, expected: plan.Commands} plannedEngine := *engine plannedEngine.Runner = plannedRunner report, gateErr := plannedEngine.runMergeGateLevel(ctx, plan) @@ -283,7 +283,7 @@ func (engine *Engine) planDocumentationMergeGateExecution(plan MergeGateExecutio return MergeGateExecutionPlan{}, err } plan.Tests = tests - plan.Commands = mergeGateSuiteCommands(plan.Tests.Suites) + plan.Commands = mergeGateSuiteCommands(engine.Repository, plan.Tests.Suites) return plan, nil } @@ -308,7 +308,7 @@ func (engine *Engine) planOrdinaryMergeGateExecution(plan MergeGateExecutionPlan } } plan.Commands = append(plan.Commands, mergeGateCheckCommands(gaterun.Check, plannedPolicyCheckCommands(engine.Repository, checkSelection, "gate"))...) - plan.Commands = append(plan.Commands, mergeGateSuiteCommands(plan.Tests.Suites)...) + plan.Commands = append(plan.Commands, mergeGateSuiteCommands(engine.Repository, plan.Tests.Suites)...) plan.Commands = append(plan.Commands, mergeGateCheckCommands(gaterun.Build, quality.CommandsForProfiles(engine.Repository, checkSelection, "build"))...) return engine.appendMergeGateSupplyChainCommands(plan) } @@ -371,7 +371,7 @@ func mergeGateCheckCommands(category gaterun.CommandCategory, commands []policy. return result } -func mergeGateSuiteCommands(suites []policy.TestSuite) []MergeGateExecutionCommand { +func mergeGateSuiteCommands(repo repository.Repository, suites []policy.TestSuite) []MergeGateExecutionCommand { result := make([]MergeGateExecutionCommand, 0, len(suites)) seen := map[string]bool{} for _, suite := range suites { @@ -379,7 +379,7 @@ func mergeGateSuiteCommands(suites []policy.TestSuite) []MergeGateExecutionComma continue } seen[suite.Name] = true - result = append(result, MergeGateExecutionCommand{Category: gaterun.OrdinaryTest, Kind: suite.Kind, Scope: suite.Scope, Cost: suite.Cost, Command: suiteCommand(suite)}) + result = append(result, MergeGateExecutionCommand{Category: gaterun.OrdinaryTest, Kind: suite.Kind, Scope: suite.Scope, Cost: suite.Cost, Command: testpolicy.SuiteCommand(repo, suite)}) } return result } @@ -474,18 +474,8 @@ func uniqueBehaviorReviewSuites(suites []policy.TestSuite, additional int) ([]po return selected, seen } -func suiteCommand(suite policy.TestSuite) policy.Command { - return policy.Command{ - Name: suite.Name, Argv: append([]string{}, suite.Argv...), Cwd: suite.Cwd, - Paths: append([]string{}, suite.Paths...), Modules: append([]string{}, suite.Modules...), - Environment: append([]string{}, suite.Environment...), ExclusiveResources: append([]string{}, suite.ExclusiveResources...), - TimeoutSeconds: suite.TimeoutSeconds, - SealedEnvironment: suite.Reusable, - TestArtifacts: append([]policy.TestArtifact{}, suite.Artifacts...), - } -} - type mergeGatePlannedRunner struct { + repo repository.Repository root string viewRoot string delegate runner.Runner @@ -525,7 +515,7 @@ func (commandRunner *mergeGatePlannedRunner) TestArtifacts(name string, attempt func (commandRunner *mergeGatePlannedRunner) ReuseSuite(suite policy.TestSuite, attempt int) (testpolicy.SuiteExecution, bool, error) { if commandRunner.err != nil || commandRunner.next >= len(commandRunner.expected) || - !samePolicyCommand(commandRunner.expected[commandRunner.next].Command, suiteCommand(suite)) { + !samePolicyCommand(commandRunner.expected[commandRunner.next].Command, testpolicy.SuiteCommand(commandRunner.repo, suite)) { return testpolicy.SuiteExecution{}, false, nil } controller, ok := commandRunner.delegate.(testpolicy.SuiteReuseController) diff --git a/internal/engine/rust_tests_test.go b/internal/engine/rust_tests_test.go new file mode 100644 index 00000000..b0467078 --- /dev/null +++ b/internal/engine/rust_tests_test.go @@ -0,0 +1,179 @@ +package engine + +import ( + "bytes" + "context" + "encoding/json" + "io" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/riteofstring/code-polishy/internal/gaterun" + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/runner" + testpolicy "github.com/riteofstring/code-polishy/internal/testing" +) + +func TestRustNativeGateRetainsRealValidatedEvidence(t *testing.T) { + t.Parallel() + root := nativeRustEngineRepository(t) + installBehaviorReviewTestGuidance(t, root) + initializeEngineGitRepository(t, root) + writeEngineFile(t, root, "content/data.json", "{\"changed\":true}\n", 0o600) + commitEngineCandidate(t, root, "change content") + engine, err := Open(root, enginePolicyRoot(t), "") + if err != nil { + t.Fatal(err) + } + engine.Runner = nativeRustGateRunner{} + report, err := engine.MergeGate(t.Context(), "main") + if err != nil || HasFindings(report) || report.GateRunPolicy == nil || report.GateRunPolicy.Status != "passed" { + t.Fatalf("Rust gate report: %+v, %v", report, err) + } + outcome := readGateCommandOutcome(t, root, report.GateRunPolicy.ReportPath, "focused") + if outcome.Status != gaterun.Passed || len(outcome.Attempts) != 1 || outcome.Attempts[0].ExitStatus != 0 { + t.Fatalf("native gate outcome: %+v", outcome) + } + data, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(report.GateRunPolicy.ReportPath))) + if err != nil { + t.Fatal(err) + } + var published gaterun.Report + if err := json.Unmarshal(data, &published); err != nil { + t.Fatal(err) + } + if len(published.TestEvidence) == 0 || len(published.TestEvidence[0].Artifacts) != 1 || published.TestEvidence[0].Artifacts[0].Type != policy.NextestJUnitArtifact { + t.Fatalf("missing native evidence: %+v", published.TestEvidence) + } +} + +func TestRustNativeReceiptsReuseOnlyTheSameSelectionAndInputs(t *testing.T) { + t.Parallel() + root := nativeRustEngineRepository(t) + engine, err := Open(root, enginePolicyRoot(t), "") + if err != nil { + t.Fatal(err) + } + suite := engine.Repository.Config.Tests.Suites[0] + suite.Reusable = true + engine.Repository.Config.Tests.Suites[0] = suite + controller, err := newTestReceiptController(engine, []policy.TestSuite{suite}, false) + if err != nil { + t.Fatal(err) + } + output := &bytes.Buffer{} + commandRunner := &testReceiptRunner{delegate: runner.OSRunner{Stdout: output, Stderr: output}, controller: controller} + run := testpolicy.RunWithEvidence(t.Context(), engine.Repository, commandRunner, testpolicy.Plan{Suites: []policy.TestSuite{suite}}, nil) + if len(run.Findings) != 0 || len(run.Executions) != 1 || run.Executions[0].ReceiptPath == "" { + t.Fatalf("native receipt publication: %+v, reasons=%v; %s", run, controller.reasons, output) + } + controller.allowReuse = true + reused, hit, err := controller.ReuseSuite(suite, 1) + if err != nil || !hit || !reused.Reused || len(reused.Artifacts) != 1 { + t.Fatalf("native receipt reuse: %+v, %v, %v", reused, hit, err) + } + suite.Rust.Filter = "test(=other)" + changed, err := newTestReceiptController(engine, []policy.TestSuite{suite}, true) + if err != nil { + t.Fatal(err) + } + if _, hit, err := changed.ReuseSuite(suite, 1); err != nil || hit { + t.Fatalf("changed selector reused evidence: hit=%v, %v", hit, err) + } + suite.Rust.Filter = "" + writeEngineFile(t, root, "content/lib.rs", "#[test]\nfn behavior() { assert_eq!(2 + 2, 5); }\n", 0o600) + changed, err = newTestReceiptController(engine, []policy.TestSuite{suite}, true) + if err != nil { + t.Fatal(err) + } + if _, hit, err := changed.ReuseSuite(suite, 1); err != nil || hit { + t.Fatalf("changed source reused evidence: hit=%v, %v", hit, err) + } +} + +func TestRustHistoricalSuiteComparisonKeepsTypedSelectors(t *testing.T) { + t.Parallel() + original := policy.TestSuite{Name: "rust", Rust: &policy.RustTestSuite{Manifest: "Cargo.toml", Packages: []string{"core"}, Features: []string{"default"}}} + for _, alter := range []func(*policy.RustTestSuite){ + func(value *policy.RustTestSuite) { value.Manifest = "nested/Cargo.toml" }, + func(value *policy.RustTestSuite) { value.Packages[0] = "other" }, + func(value *policy.RustTestSuite) { value.Features[0] = "optional" }, + func(value *policy.RustTestSuite) { value.Filter = "test(selected)" }, + func(value *policy.RustTestSuite) { value.NoDefaultFeatures = true }, + } { + changed := cloneReceiptSuite(original) + alter(changed.Rust) + if sameTestSuiteSemantics(original, changed) || !sameTestSuiteSemantics(original, cloneReceiptSuite(original)) { + t.Fatal("historical suite comparison lost native Cargo selection") + } + } +} + +func TestRustDirectTestTelemetryPreservesNativeExitStatus(t *testing.T) { + t.Parallel() + for name, test := range map[string]struct { + source string + exit int + category runner.FailureCategory + }{ + "assertion": {"#[test]\nfn behavior() { assert_eq!(2 + 2, 5); }\n", 100, runner.FailureCommandExit}, + "empty": {"pub fn answer() -> u32 { 42 }\n", 4, runner.FailureOperational}, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + root := nativeRustEngineRepository(t) + writeEngineFile(t, root, "content/lib.rs", test.source, 0o600) + engine, err := Open(root, enginePolicyRoot(t), "") + if err != nil { + t.Fatal(err) + } + engine.Output = io.Discard + engine.Runner = runner.OSRunner{} + engine.BeginExecution(time.Now(), 0) + report, err := engine.Test(t.Context(), testpolicy.Request{Suites: []string{"focused"}}) + if err != nil || !HasFindings(report) || len(report.TestCommands) != 1 { + t.Fatalf("native direct report: %+v, %v", report, err) + } + command := report.TestCommands[0] + if command.Result.ExitStatus != test.exit || command.FailureCategory != test.category { + t.Fatalf("instrumentation lost native outcome: %+v", command) + } + telemetry := engine.AttachExecution(report).Execution + if len(telemetry.Commands) != 1 || telemetry.Commands[0].ExitStatus != test.exit { + t.Fatalf("native telemetry is inconsistent: %+v", telemetry) + } + }) + } +} + +func nativeRustEngineRepository(t *testing.T) string { + t.Helper() + root := contentRepository(t, nil) + data, err := os.ReadFile(filepath.Join(root, policy.ConfigFilename)) + if err != nil { + t.Fatal(err) + } + configuration := strings.Replace(string(data), `"argv":["go","test","./..."]`, `"rust":{}`, 1) + writeEngineFile(t, root, policy.ConfigFilename, configuration, 0o600) + writeEngineFile(t, root, "Cargo.toml", "[package]\nname='native-tests'\nversion='1.0.0'\nedition='2024'\n[workspace]\n[lib]\npath='content/lib.rs'\n", 0o600) + writeEngineFile(t, root, "Cargo.lock", "version=4\n[[package]]\nname='native-tests'\nversion='1.0.0'\n", 0o600) + writeEngineFile(t, root, "content/lib.rs", "#[test]\nfn behavior() { assert_eq!(2 + 2, 4); }\n", 0o600) + return root +} + +type nativeRustGateRunner struct{} + +func (nativeRustGateRunner) Run(ctx context.Context, root string, command policy.Command) error { + _, err := (nativeRustGateRunner{}).RunWithWriters(ctx, root, command, io.Discard, io.Discard) + return err +} + +func (nativeRustGateRunner) RunWithWriters(ctx context.Context, root string, command policy.Command, stdout, stderr io.Writer) (runner.Result, error) { + if command.Managed { + return (runner.OSRunner{}).RunWithWriters(ctx, root, command, stdout, stderr) + } + return runner.Result{}, nil +} diff --git a/internal/engine/test_evidence.go b/internal/engine/test_evidence.go index 6fd595f1..0d74bf58 100644 --- a/internal/engine/test_evidence.go +++ b/internal/engine/test_evidence.go @@ -148,7 +148,7 @@ func (engine *Engine) diagnoseTestFailure(ctx context.Context, plan testpolicy.P if outcomeBearingRetry { retryRunner = testRetryRunner(engine.Runner) } - retry := testpolicy.ExecuteSuite(ctx, engine.Repository.Root, retryRunner, testpolicy.RetrySuite(original.Suite), original.Attempt+1) + retry := testpolicy.ExecuteSuite(ctx, engine.Repository, retryRunner, testpolicy.RetrySuite(original.Suite), original.Attempt+1) retryEvidence := engine.testCommandEvidence(plan, selection, []testpolicy.SuiteExecution{retry}, "working-tree")[0] retryEvidence.Diagnostic = !outcomeBearingRetry diagnostic := TestFailureDiagnostic{Suite: original.Suite.Name, CandidateRetry: &retryEvidence} @@ -257,7 +257,9 @@ func (engine *Engine) executeBaselineSuite(ctx context.Context, worktree string, if !available { return nil, false } - execution := testpolicy.ExecuteSuite(ctx, worktree, commandRunner, baseline, attempt) + baselineRepo := engine.Repository + baselineRepo.Root = worktree + execution := testpolicy.ExecuteSuite(ctx, baselineRepo, commandRunner, baseline, attempt) return &execution, true } @@ -277,7 +279,7 @@ func (engine *Engine) baselineSuite(worktree string, candidate policy.TestSuite) } func sameTestSuiteSemantics(candidate, baseline policy.TestSuite) bool { - return sameTestSuiteIdentity(candidate, baseline) && sameTestSuiteCollections(candidate, baseline) + return sameTestSuiteIdentity(candidate, baseline) && sameTestSuiteCollections(candidate, baseline) && candidate.Rust.Equal(baseline.Rust) } func sameTestSuiteIdentity(candidate, baseline policy.TestSuite) bool { diff --git a/internal/engine/test_execution_view.go b/internal/engine/test_execution_view.go index 88856da4..7d17c19a 100644 --- a/internal/engine/test_execution_view.go +++ b/internal/engine/test_execution_view.go @@ -29,8 +29,10 @@ func (controller *testReceiptController) PrepareSuiteView(suite policy.TestSuite if !found { return controller.root, func() error { return nil }, nil } - if err := validateReusableArguments(suite.Argv); err != nil { - return "", nil, err + if suite.Rust == nil { + if err := validateReusableArguments(suite.Argv); err != nil { + return "", nil, err + } } view, err := createSuiteExecutionView(controller.root, suite.Cwd, identity.Inputs) if err != nil { @@ -40,6 +42,9 @@ func (controller *testReceiptController) PrepareSuiteView(suite policy.TestSuite } func validateReusableArguments(argv []string) error { + if len(argv) == 0 { + return errors.New("reusable suite has no executable command") + } for _, argument := range argv[1:] { if strings.Contains(argument, "{artifactDir}") { continue diff --git a/internal/engine/test_receipts.go b/internal/engine/test_receipts.go index 62388368..a1b389ee 100644 --- a/internal/engine/test_receipts.go +++ b/internal/engine/test_receipts.go @@ -16,6 +16,7 @@ import ( "github.com/riteofstring/code-polishy/internal/policy" "github.com/riteofstring/code-polishy/internal/release" "github.com/riteofstring/code-polishy/internal/repository" + testpolicy "github.com/riteofstring/code-polishy/internal/testing" "github.com/riteofstring/code-polishy/internal/testreceipt" ) @@ -23,7 +24,8 @@ func (engine *Engine) suiteReceiptIdentity(suite policy.TestSuite) (testreceipt. if !suite.Reusable { return testreceipt.Identity{}, "suite is not explicitly reusable", nil } - if err := validateReusableArguments(suite.Argv); err != nil { + command := testpolicy.SuiteCommand(engine.Repository, suite) + if err := validateReusableArguments(command.Argv); err != nil { return testreceipt.Identity{}, err.Error(), nil } configurationSHA256, releaseIdentity, err := suiteReceiptReleaseIdentity(engine.Repository) @@ -34,9 +36,9 @@ func (engine *Engine) suiteReceiptIdentity(suite policy.TestSuite) (testreceipt. if err != nil || reason != "" { return testreceipt.Identity{}, reason, err } - environment := suiteReceiptEnvironment(generationReceiptEnvironment(engine.Repository, suite, selected)) + environment := suiteReceiptEnvironment(append(generationReceiptEnvironment(engine.Repository, suite, selected), command.Environment...)) tools := suiteReceiptTools(engine.Repository) - if reason := unboundedSuiteTool(suite.Argv, tools, selected); reason != "" { + if reason := unboundedSuiteTool(command.Argv, tools, selected); reason != "" { return testreceipt.Identity{}, reason, nil } identity := testreceipt.Identity{ @@ -278,6 +280,9 @@ func intersectsStrings(left, right []string) bool { } func cloneReceiptSuite(suite policy.TestSuite) policy.TestSuite { + if suite.Rust != nil { + suite.Rust = suite.Rust.Clone() + } suite.Modules = append([]string{}, suite.Modules...) suite.Argv = append([]string{}, suite.Argv...) suite.RetryArgv = append([]string{}, suite.RetryArgv...) diff --git a/internal/gaterun/identity.go b/internal/gaterun/identity.go index e254ab96..4bd7f9c9 100644 --- a/internal/gaterun/identity.go +++ b/internal/gaterun/identity.go @@ -245,7 +245,7 @@ func validateCommandSuiteIdentity(command CommandSpec) error { func validateArtifactSpecs(artifacts []ArtifactSpec) error { seen := map[string]bool{} for _, artifact := range artifacts { - if !validArtifactRelativePath(artifact.Path) || artifact.Type != "junit" && artifact.Type != "cobertura" || seen[artifact.Path] { + if !validArtifactRelativePath(artifact.Path) || !validArtifactType(artifact.Type) || seen[artifact.Path] { return fmt.Errorf("command artifact declaration is invalid") } seen[artifact.Path] = true @@ -253,6 +253,10 @@ func validateArtifactSpecs(artifacts []ArtifactSpec) error { return nil } +func validArtifactType(value string) bool { + return value == "junit" || value == "cobertura" || value == policy.NextestJUnitArtifact +} + func validCommandIdentity(command CommandSpec) bool { return validCategory(command.Category) && validToken(command.Scope) && validToken(command.Cost) && validToken(command.Name) } diff --git a/internal/gaterun/validation.go b/internal/gaterun/validation.go index 0c89dcad..3bb8cc9f 100644 --- a/internal/gaterun/validation.go +++ b/internal/gaterun/validation.go @@ -516,7 +516,7 @@ func validateTestArtifacts(artifacts []TestArtifact) error { seen := map[string]bool{} for _, artifact := range artifacts { if !validToken(artifact.Suite) || !validArtifactRelativePath(artifact.Path) || - artifact.Type != "junit" && artifact.Type != "cobertura" || artifact.Size < 0 || artifact.Size > 32<<20 || + !validArtifactType(artifact.Type) || artifact.Size < 0 || artifact.Size > 32<<20 || !validSHA256(artifact.SHA256) || seen[artifact.Path] { return fmt.Errorf("%w: test artifact evidence is invalid", ErrInvalidArtifact) } diff --git a/internal/policy/control_inputs.go b/internal/policy/control_inputs.go index d910ad45..19cfe4c2 100644 --- a/internal/policy/control_inputs.go +++ b/internal/policy/control_inputs.go @@ -14,6 +14,7 @@ var sensitiveControlPatterns = []string{ "**/Cargo.toml", "**/pom.xml", "**/build.gradle", "**/build.gradle.kts", "**/Gemfile", "**/composer.json", "**/Package.swift", "**/Package.resolved", "**/pubspec.yaml", "**/.cargo/config", "**/.cargo/config.toml", "**/rust-toolchain", "**/rust-toolchain.toml", "**/rustfmt.toml", "**/.rustfmt.toml", "**/clippy.toml", "**/.clippy.toml", + "**/.config/nextest.toml", "**/*.lock", "**/*-lock.*", "**/eslint.config.*", "**/.eslintrc*", "**/knip.*", "**/.knip.*", "**/prettier.config.*", "**/.prettierrc*", "**/tsconfig*.json", "**/ruff.toml", "**/.ruff.toml", "**/ty.toml", "**/osv-scanner.toml", diff --git a/internal/policy/load.go b/internal/policy/load.go index a81e6ab3..d746a7c7 100644 --- a/internal/policy/load.go +++ b/internal/policy/load.go @@ -146,6 +146,9 @@ func defaultStrings(target *[]string, value []string) { } func suiteDefaults(suite *TestSuite) { + if suite.Rust != nil { + defaultString(&suite.Rust.Manifest, "Cargo.toml") + } if suite.Kind == "performance" && !slices.Contains(suite.ExclusiveResources, "performance") { suite.ExclusiveResources = append(suite.ExclusiveResources, "performance") @@ -395,10 +398,10 @@ func validateTests(config *Config) error { func validateTestSuite(config *Config, suite *TestSuite, index int, names map[string]bool) error { label := fmt.Sprintf("tests.suites[%d]", index) command := Command{Name: suite.Name, Argv: suite.Argv, Cwd: suite.Cwd, Paths: suite.Paths, Modules: suite.Modules, Environment: suite.Environment, ExclusiveResources: suite.ExclusiveResources, TimeoutSeconds: suite.TimeoutSeconds} - if err := validateCommand(config, &command, label, names); err != nil { + if err := validateCommandMetadata(config, &command, label, names); err != nil { return err } - if err := validateTestCommandArgv(suite.Argv, label); err != nil { + if err := validateSuiteRunner(*suite, label); err != nil { return err } if err := validateTestRetry(suite, label); err != nil { @@ -550,10 +553,14 @@ func validateTestCommandArgv(argv []string, label string) error { } func validateCommand(config *Config, command *Command, label string, names map[string]bool) error { - if err := validateCommandIdentity(command, label, names); err != nil { + if err := validateCommandArgv(command.Argv, label); err != nil { return err } - if err := validateCommandArgv(command.Argv, label); err != nil { + return validateCommandMetadata(config, command, label, names) +} + +func validateCommandMetadata(config *Config, command *Command, label string, names map[string]bool) error { + if err := validateCommandIdentity(command, label, names); err != nil { return err } if err := ValidateExclusiveResources(command.ExclusiveResources); err != nil { @@ -586,6 +593,9 @@ func validateCommandIdentity(command *Command, label string, names map[string]bo } func validateCommandArgv(argv []string, label string) error { + if len(argv) == 0 { + return fmt.Errorf("%s.argv must identify an executable", label) + } if shellEvaluation(argv) { return fmt.Errorf("%s.argv must call a checked-in script instead of shell -c", label) } diff --git a/internal/policy/model.go b/internal/policy/model.go index 04f2b529..8e328436 100644 --- a/internal/policy/model.go +++ b/internal/policy/model.go @@ -377,7 +377,8 @@ type TestSuite struct { Reusable bool `json:"reusable,omitempty"` Cost string `json:"cost,omitempty"` Modules []string `json:"modules,omitempty"` - Argv []string `json:"argv"` + Argv []string `json:"argv,omitempty"` + Rust *RustTestSuite `json:"rust,omitempty"` RetryArgv []string `json:"retryArgv,omitempty"` Cwd string `json:"cwd,omitempty"` Paths []string `json:"paths,omitempty"` @@ -396,6 +397,14 @@ type TestArtifact struct { Required bool `json:"required,omitempty"` } +type RustTestSuite struct { + Manifest string `json:"manifest,omitempty"` + Packages []string `json:"packages,omitempty"` + Features []string `json:"features,omitempty"` + NoDefaultFeatures bool `json:"noDefaultFeatures,omitempty"` + Filter string `json:"filter,omitempty"` +} + type SupplyChain struct { MinimumReleaseAgeDays int `json:"minimumReleaseAgeDays,omitempty"` PreferredNewDependencyAgeDays int `json:"preferredNewDependencyAgeDays,omitempty"` diff --git a/internal/policy/rust_inputs_test.go b/internal/policy/rust_inputs_test.go index 2ebce2b1..9902bd2c 100644 --- a/internal/policy/rust_inputs_test.go +++ b/internal/policy/rust_inputs_test.go @@ -9,7 +9,7 @@ func TestRustConfigurationPathsAreSensitive(t *testing.T) { t.Parallel() for _, path := range []string{ "Cargo.toml", "Cargo.lock", ".cargo/config", ".cargo/config.toml", - "rust-toolchain", "rust-toolchain.toml", "rustfmt.toml", ".rustfmt.toml", "clippy.toml", ".clippy.toml", + "rust-toolchain", "rust-toolchain.toml", "rustfmt.toml", ".rustfmt.toml", "clippy.toml", ".clippy.toml", ".config/nextest.toml", } { for _, prefix := range []string{"", "crates/graphics/", "./crates/graphics/", `crates\graphics\`} { if !IsSensitiveControlInput(prefix + path) { diff --git a/internal/policy/rust_tests.go b/internal/policy/rust_tests.go new file mode 100644 index 00000000..d2eec01d --- /dev/null +++ b/internal/policy/rust_tests.go @@ -0,0 +1,103 @@ +package policy + +import ( + "fmt" + "path" + "slices" + "strings" +) + +const NextestJUnitArtifact = "nextest-junit" + +func validateSuiteRunner(suite TestSuite, label string) error { + if suite.Rust == nil { + if err := validateCommandArgv(suite.Argv, label); err != nil { + return err + } + return validateTestCommandArgv(suite.Argv, label) + } + if len(suite.Argv) != 0 || len(suite.RetryArgv) != 0 || suite.Cwd != "." { + return fmt.Errorf("%s.rust owns its command and repository-root invocation; argv, retryArgv and another cwd cannot replace it", label) + } + if err := validateRustTestSelection(*suite.Rust, label); err != nil { + return err + } + if err := validateRustTestEnvironment(suite.Environment, label); err != nil { + return err + } + return validateRustTestArtifacts(suite.Artifacts, label) +} + +func validateRustTestSelection(settings RustTestSuite, label string) error { + if path.Base(settings.Manifest) != "Cargo.toml" { + return fmt.Errorf("%s.rust.manifest must identify a Cargo.toml", label) + } + if err := repositoryPath(settings.Manifest, label+".rust.manifest"); err != nil { + return err + } + for _, argument := range append(append([]string{}, settings.Packages...), settings.Features...) { + if !ValidRustTestArgument(argument) { + return fmt.Errorf("%s.rust package and feature selectors must be individual Cargo arguments", label) + } + } + if strings.ContainsRune(settings.Filter, 0) { + return fmt.Errorf("%s.rust.filter cannot contain a NUL byte", label) + } + return nil +} + +func validateRustTestEnvironment(environment []string, label string) error { + for _, name := range environment { + if rustExecutionVariable(strings.ToUpper(name)) { + return fmt.Errorf("%s.rust owns Rust, Cargo and Nextest execution settings; environment cannot inherit %s", label, name) + } + } + return nil +} + +func rustExecutionVariable(name string) bool { + if slices.Contains([]string{"CARGO", "RUSTC", "RUSTDOC", "RUSTFLAGS", "RUSTDOCFLAGS"}, name) { + return true + } + for _, prefix := range []string{"CARGO_", "NEXTEST_", "RUSTC_", "RUSTDOC_", "RUSTUP_", "RUST_TEST_"} { + if strings.HasPrefix(name, prefix) { + return true + } + } + return false +} + +func validateRustTestArtifacts(artifacts []TestArtifact, label string) error { + for _, artifact := range artifacts { + if artifact.Path == "nextest.xml" || artifact.Path == ".code-polishy-nextest.toml" || artifact.Type == NextestJUnitArtifact { + return fmt.Errorf("%s.rust owns its required Nextest report and configuration", label) + } + } + return nil +} + +func (suite TestSuite) ArtifactDeclarations() []TestArtifact { + result := append([]TestArtifact{}, suite.Artifacts...) + if suite.Rust != nil { + result = append(result, TestArtifact{Path: "nextest.xml", Type: NextestJUnitArtifact, Required: true}) + } + return result +} + +func (settings RustTestSuite) Clone() *RustTestSuite { + settings.Packages = append([]string{}, settings.Packages...) + settings.Features = append([]string{}, settings.Features...) + return &settings +} + +func (settings *RustTestSuite) Equal(other *RustTestSuite) bool { + if settings == nil || other == nil { + return settings == other + } + return settings.Manifest == other.Manifest && settings.Filter == other.Filter && settings.NoDefaultFeatures == other.NoDefaultFeatures && + slices.Equal(settings.Packages, other.Packages) && slices.Equal(settings.Features, other.Features) +} + +func ValidRustTestArgument(value string) bool { + return value != "" && !strings.HasPrefix(value, "-") && !strings.ContainsAny(value, "\x00\n\r\t ") +} diff --git a/internal/policy/rust_tests_test.go b/internal/policy/rust_tests_test.go new file mode 100644 index 00000000..6c875cfb --- /dev/null +++ b/internal/policy/rust_tests_test.go @@ -0,0 +1,67 @@ +package policy + +import ( + "strings" + "testing" +) + +func TestRustSuiteConfigurationPreservesCargoSelection(t *testing.T) { + t.Parallel() + input := strings.Replace(minimalConfig(), `"argv":["go","test","./..."]`, `"rust":{"packages":["core_crate"],"features":["core_crate/serde"],"filter":"test(behavior)","noDefaultFeatures":true},"environment":["RUST_LOG","RUST_BACKTRACE"]`, 1) + config, err := Parse([]byte(input), ConfigFilename) + if err != nil { + t.Fatal(err) + } + suite := config.Tests.Suites[0] + if suite.Rust.Manifest != "Cargo.toml" || suite.Cwd != "." || !suite.Rust.NoDefaultFeatures || suite.Rust.Filter != "test(behavior)" || len(suite.Argv) != 0 { + t.Fatalf("Cargo selector changed: %+v", suite) + } + artifacts := suite.ArtifactDeclarations() + if len(artifacts) != 1 || artifacts[0].Type != NextestJUnitArtifact || !artifacts[0].Required { + t.Fatalf("native test evidence is not required: %+v", artifacts) + } + copy := suite.Rust.Clone() + copy.Packages[0] = "changed" + copy.Features[0] = "changed" + if suite.Rust.Packages[0] != "core_crate" || suite.Rust.Features[0] != "core_crate/serde" { + t.Fatal("mutating copied selection changed the original") + } +} + +func TestRustSuiteConfigurationRejectsCommandAndEvidenceOverrides(t *testing.T) { + t.Parallel() + for name, selector := range map[string]string{ + "two runners": `"rust":{},"argv":["cargo","test"]`, + "no runner": `"environment":[]`, + "retry replacement": `"rust":{},"retryArgv":["true"]`, + "cwd replacement": `"rust":{},"cwd":"crate"`, + "escaped manifest": `"rust":{"manifest":"../Cargo.toml"}`, + "wrong manifest": `"rust":{"manifest":"Cargo.lock"}`, + "flags as package": `"rust":{"packages":["--workspace"]}`, + "joined features": `"rust":{"features":["one two"]}`, + "nul filter": `"rust":{"filter":"test(\u0000)"}`, + "ambient flags": `"rust":{},"environment":["RUSTFLAGS"]`, + "ambient retry": `"rust":{},"environment":["NEXTEST_RETRIES"]`, + "report override": `"rust":{},"artifacts":[{"path":"nextest.xml","type":"junit"}]`, + } { + t.Run(name, func(t *testing.T) { + input := strings.Replace(minimalConfig(), `"argv":["go","test","./..."]`, selector, 1) + if _, err := Parse([]byte(input), ConfigFilename); err == nil { + t.Fatalf("unsafe Rust suite accepted: %s", selector) + } + }) + } +} + +func TestRustSuiteConfigurationRetainsCargoIdentifierConventions(t *testing.T) { + t.Parallel() + input := strings.Replace(minimalConfig(), `"argv":["go","test","./..."]`, `"rust":{"packages":["core_crate@1.2.3"],"features":["std+alloc","codec.v2","unicode_λ"]}`, 1) + config, err := Parse([]byte(input), ConfigFilename) + if err != nil { + t.Fatal(err) + } + settings := config.Tests.Suites[0].Rust + if settings.Packages[0] != "core_crate@1.2.3" || len(settings.Features) != 3 || settings.Features[2] != "unicode_λ" { + t.Fatalf("Cargo selectors were rewritten: %+v", settings) + } +} diff --git a/internal/rustfacts/tests.go b/internal/rustfacts/tests.go new file mode 100644 index 00000000..2609af2c --- /dev/null +++ b/internal/rustfacts/tests.go @@ -0,0 +1,59 @@ +package rustfacts + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" +) + +func TestProblem(repo repository.Repository, settings policy.RustTestSuite) error { + plan := PlanDiscovery(repo, []string{settings.Manifest}, []string{settings.Manifest}) + if len(plan.Findings) != 0 { + return errors.New(plan.Findings[0].Message) + } + if HostTarget() == "" { + return errors.New("native Rust testing is unavailable on this host") + } + if err := compilationTargetConfiguration(repo); err != nil { + return err + } + info, err := os.Stat(repo.PolicyTool("cargo-nextest")) + if err != nil || !info.Mode().IsRegular() || runtime.GOOS != "windows" && info.Mode()&0o111 == 0 || repo.ToolPin("cargo-nextest") == "" { + return errors.New("pinned cargo-nextest is unavailable; install the policy-owned test runner") + } + return nextestConfigurationProblem(repo, settings.Manifest) +} + +func nextestConfigurationProblem(repo repository.Repository, manifest string) error { + inventory := repo.InspectCargoManifests([]string{manifest}) + if len(inventory.Problems) > 0 || len(inventory.Manifests) != 1 { + return errors.New("nextest configuration requires a valid contained Cargo manifest") + } + if pkg := inventory.Manifests[0].Package; pkg != nil && pkg.Workspace != "" { + workspace, err := repo.NormalizePath(filepath.ToSlash(filepath.Join(filepath.Dir(manifest), pkg.Workspace, "Cargo.toml"))) + if err != nil { + return err + } + if err := nextestAncestorSettings(repo, workspace); err != nil { + return err + } + } + return nextestAncestorSettings(repo, manifest) +} + +func nextestAncestorSettings(repo repository.Repository, manifest string) error { + for directory := filepath.Dir(manifest); ; directory = filepath.Dir(directory) { + name := filepath.ToSlash(filepath.Join(directory, ".config", "nextest.toml")) + if _, err := os.Lstat(filepath.Join(repo.Root, filepath.FromSlash(name))); !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("project Nextest configuration %s requires governed settings support; native tests cannot silently replace its filters or setup scripts", name) + } + if directory == "." { + return nil + } + } +} diff --git a/internal/testartifact/execution.go b/internal/testartifact/execution.go index 1969bd81..c74393c1 100644 --- a/internal/testartifact/execution.go +++ b/internal/testartifact/execution.go @@ -115,13 +115,16 @@ func (execution *Execution) PrepareCommand(suite policy.TestSuite, command polic } } command.Argv = substituteArguments(command.Argv, suiteDirectory, execution.id) - command.EnvironmentOverrides = []string{ - EnvironmentDirectory + "=" + suiteDirectory, - EnvironmentID + "=" + execution.id, - } - command.TestArtifacts = append([]policy.TestArtifact{}, suite.Artifacts...) + command.EnvironmentOverrides = append(append([]string{}, command.EnvironmentOverrides...), + EnvironmentDirectory+"="+suiteDirectory, + EnvironmentID+"="+execution.id, + ) + command.TestArtifacts = suite.ArtifactDeclarations() command.TestArtifactSuite = suite.Name command.TestArtifactDirectory = suiteDirectory + if err := prepareNextest(command); err != nil { + return policy.Command{}, err + } return command, nil } @@ -134,14 +137,14 @@ func substituteArguments(argv []string, directory, executionID string) []string return result } -func (execution *Execution) ValidateCommand(command policy.Command) ([]Record, error) { +func (execution *Execution) ValidateCommand(command policy.Command, exitStatus int) ([]Record, error) { if command.TestArtifactDirectory == "" || command.TestArtifactSuite == "" { if len(command.TestArtifacts) == 0 { return []Record{}, nil } return nil, errors.New("test artifact command has no managed directory") } - return validateDeclared(command.TestArtifactDirectory, command.TestArtifactSuite, command.TestArtifacts) + return validateDeclared(command.TestArtifactDirectory, command.TestArtifactSuite, command.TestArtifacts, exitStatus) } func (execution *Execution) Complete() error { diff --git a/internal/testartifact/execution_test.go b/internal/testartifact/execution_test.go index d3ac4809..f3933a79 100644 --- a/internal/testartifact/execution_test.go +++ b/internal/testartifact/execution_test.go @@ -33,7 +33,7 @@ func TestExecutionExposesContainedSuiteDirectoryAndCapturesXML(t *testing.T) { } writeArtifactFile(t, command.TestArtifactDirectory, "junit.xml", "\n") writeArtifactFile(t, command.TestArtifactDirectory, "coverage/cobertura.xml", "\n") - records, err := execution.ValidateCommand(command) + records, err := execution.ValidateCommand(command, 0) if err != nil || len(records) != 2 || records[0].SHA256 == "" || records[1].Path != "unit/coverage/cobertura.xml" { t.Fatalf("records = %+v, error = %v", records, err) } @@ -69,7 +69,7 @@ func TestArtifactValidationFailsClosed(t *testing.T) { t.Fatal(err) } writeArtifactFile(t, command.TestArtifactDirectory, "result.xml", test.content) - if _, err := execution.ValidateCommand(command); err == nil || !strings.Contains(err.Error(), test.want) { + if _, err := execution.ValidateCommand(command, 0); err == nil || !strings.Contains(err.Error(), test.want) { t.Fatalf("error = %v, want %q", err, test.want) } }) @@ -88,7 +88,7 @@ func TestArtifactValidationRejectsMissingLinksAndOversize(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := execution.ValidateCommand(command); err == nil || !strings.Contains(err.Error(), "missing") { + if _, err := execution.ValidateCommand(command, 0); err == nil || !strings.Contains(err.Error(), "missing") { t.Fatalf("missing artifact error = %v", err) } outside := filepath.Join(t.TempDir(), "outside.xml") @@ -99,7 +99,7 @@ func TestArtifactValidationRejectsMissingLinksAndOversize(t *testing.T) { if err := os.Symlink(outside, target); err != nil { t.Fatal(err) } - if _, err := execution.ValidateCommand(command); err == nil || !strings.Contains(err.Error(), "linked") { + if _, err := execution.ValidateCommand(command, 0); err == nil || !strings.Contains(err.Error(), "linked") { t.Fatalf("linked artifact error = %v", err) } if err := os.Remove(target); err != nil { @@ -115,7 +115,7 @@ func TestArtifactValidationRejectsMissingLinksAndOversize(t *testing.T) { if err := os.Truncate(target, MaximumArtifactBytes+1); err != nil { t.Fatal(err) } - if _, err := execution.ValidateCommand(command); err == nil || !strings.Contains(err.Error(), "exceeds") { + if _, err := execution.ValidateCommand(command, 0); err == nil || !strings.Contains(err.Error(), "exceeds") { t.Fatalf("oversized artifact error = %v", err) } } @@ -131,7 +131,7 @@ func TestOptionalArtifactMayOmitItsParentDirectory(t *testing.T) { if err != nil { t.Fatal(err) } - records, err := execution.ValidateCommand(command) + records, err := execution.ValidateCommand(command, 0) if err != nil || len(records) != 0 { t.Fatalf("records = %+v, error = %v", records, err) } diff --git a/internal/testartifact/nextest.go b/internal/testartifact/nextest.go new file mode 100644 index 00000000..80c0302a --- /dev/null +++ b/internal/testartifact/nextest.go @@ -0,0 +1,198 @@ +package testartifact + +import ( + "bytes" + "encoding/xml" + "errors" + "fmt" + "io" + "strconv" + "strings" +) + +type nextestCounts struct { + Tests string `xml:"tests,attr"` + Failures string `xml:"failures,attr"` + Errors string `xml:"errors,attr"` +} + +type nextestReport struct { + Name string `xml:"name,attr"` + UUID string `xml:"uuid,attr"` + nextestCounts + Binaries []nextestBinary `xml:"testsuite"` +} + +type nextestBinary struct { + Name string `xml:"name,attr"` + Disabled string `xml:"disabled,attr"` + nextestCounts + Cases []nextestCase `xml:"testcase"` +} + +type nextestCase struct { + Name string `xml:"name,attr"` + Class string `xml:"classname,attr"` + Failures []struct{} `xml:"failure"` + Errors []struct{} `xml:"error"` + Skipped []struct{} `xml:"skipped"` +} + +type nextestTotals struct{ tests, failures, errors, skipped int } + +type nextestXMLState struct { + stack []string + root bool +} + +func validateNextest(data []byte, suite string, exitStatus int) error { + if err := nextestXMLShape(data); err != nil { + return fmt.Errorf("invalid Nextest evidence: %w", err) + } + var report nextestReport + if err := xml.Unmarshal(data, &report); err != nil { + return fmt.Errorf("invalid Nextest evidence: %w", err) + } + if report.Name != suite || strings.TrimSpace(report.UUID) == "" { + return errors.New("nextest report must identify the current suite and run") + } + totals, err := nextestReportTotals(report) + if err != nil { + return err + } + if err := report.nextestCounts.matches(totals); err != nil { + return err + } + return nextestOutcome(totals, exitStatus) +} + +func nextestReportTotals(report nextestReport) (nextestTotals, error) { + total := nextestTotals{} + seen := map[string]bool{} + for _, binary := range report.Binaries { + if strings.TrimSpace(binary.Name) == "" || seen[binary.Name] { + return total, errors.New("nextest report has missing or duplicate binary identities") + } + seen[binary.Name] = true + counts, err := nextestBinaryTotals(binary) + if err != nil { + return total, err + } + total.tests += counts.tests + total.failures += counts.failures + total.errors += counts.errors + total.skipped += counts.skipped + } + return total, nil +} + +func nextestBinaryTotals(binary nextestBinary) (nextestTotals, error) { + totals := nextestTotals{tests: len(binary.Cases)} + seen := map[string]bool{} + for _, test := range binary.Cases { + if strings.TrimSpace(test.Name) == "" || test.Class != binary.Name || seen[test.Name] { + return totals, errors.New("nextest report has missing or duplicate test identities") + } + seen[test.Name] = true + if len(test.Failures)+len(test.Errors)+len(test.Skipped) > 1 { + return totals, errors.New("nextest test has conflicting or repeated outcomes") + } + totals.failures += len(test.Failures) + totals.errors += len(test.Errors) + totals.skipped += len(test.Skipped) + } + if binary.Disabled != strconv.Itoa(totals.skipped) { + return totals, errors.New("nextest disabled count disagrees with its cases") + } + return totals, binary.nextestCounts.matches(totals) +} + +func (counts nextestCounts) matches(totals nextestTotals) error { + if counts.Tests != strconv.Itoa(totals.tests) || counts.Failures != strconv.Itoa(totals.failures) || counts.Errors != strconv.Itoa(totals.errors) { + return errors.New("nextest declared counts disagree with complete test cases") + } + return nil +} + +func nextestOutcome(totals nextestTotals, exitStatus int) error { + if totals.tests == totals.skipped { + return errors.New("nextest selected no executed tests") + } + failures := totals.failures + totals.errors + if exitStatus == 0 && failures == 0 || exitStatus == 100 && failures > 0 { + return nil + } + return fmt.Errorf("nextest exit %d disagrees with test evidence or indicates an operational failure", exitStatus) +} + +func nextestXMLShape(data []byte) error { + decoder := xml.NewDecoder(bytes.NewReader(data)) + state := nextestXMLState{} + for { + token, err := decoder.Token() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return err + } + if err := state.consume(token); err != nil { + return err + } + } + if !state.root || len(state.stack) != 0 { + return errors.New("incomplete Nextest report") + } + return nil +} + +func (state *nextestXMLState) consume(token xml.Token) error { + switch value := token.(type) { + case xml.StartElement: + if err := nextestElement(state.stack, value, state.root); err != nil { + return err + } + state.root = true + state.stack = append(state.stack, value.Name.Local) + case xml.EndElement: + state.stack = state.stack[:len(state.stack)-1] + case xml.Directive: + return errors.New("XML directives are forbidden") + case xml.CharData: + if len(state.stack) < 4 && strings.TrimSpace(string(value)) != "" { + return errors.New("unexpected text outside a test outcome") + } + } + return nil +} + +func nextestAttributes(element xml.StartElement) error { + if element.Name.Space != "" { + return errors.New("nextest XML namespaces are unsupported") + } + seen := map[xml.Name]bool{} + for _, attribute := range element.Attr { + if attribute.Name.Space != "" || seen[attribute.Name] { + return errors.New("duplicate or namespaced Nextest attribute") + } + seen[attribute.Name] = true + } + return nil +} + +func nextestElement(stack []string, element xml.StartElement, root bool) error { + if err := nextestAttributes(element); err != nil { + return err + } + path := strings.Join(append(append([]string{}, stack...), element.Name.Local), "/") + if path == "testsuites" && !root || path == "testsuites/testsuite" || path == "testsuites/testsuite/testcase" { + return nil + } + if len(stack) == 3 && stack[2] == "testcase" { + switch element.Name.Local { + case "failure", "error", "skipped", "system-out", "system-err": + return nil + } + } + return fmt.Errorf("unsupported Nextest element %q", path) +} diff --git a/internal/testartifact/nextest_config.go b/internal/testartifact/nextest_config.go new file mode 100644 index 00000000..a9c84d8d --- /dev/null +++ b/internal/testartifact/nextest_config.go @@ -0,0 +1,36 @@ +package testartifact + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/riteofstring/code-polishy/internal/policy" +) + +const NextestConfigName = ".code-polishy-nextest.toml" + +func prepareNextest(command policy.Command) error { + for _, artifact := range command.TestArtifacts { + if artifact.Type != policy.NextestJUnitArtifact { + continue + } + path, err := containedArtifactPath(command.TestArtifactDirectory, artifact.Path) + if err != nil { + return err + } + reportPath, _ := json.Marshal(path) + reportName, _ := json.Marshal(command.TestArtifactSuite) + storePath, _ := json.Marshal(filepath.Join(command.TestArtifactDirectory, "nextest-state")) + settings := fmt.Sprintf("[store]\ndir = %s\n[profile.code-polishy]\nretries = 0\nfail-fast = false\n[profile.code-polishy.junit]\npath = %s\nreport-name = %s\n", storePath, reportPath, reportName) + file, err := os.OpenFile(filepath.Join(command.TestArtifactDirectory, NextestConfigName), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) + if err != nil { + return fmt.Errorf("create Nextest settings: %w", err) + } + _, err = file.WriteString(settings) + return errors.Join(err, file.Close()) + } + return nil +} diff --git a/internal/testartifact/nextest_test.go b/internal/testartifact/nextest_test.go new file mode 100644 index 00000000..28bf1e4d --- /dev/null +++ b/internal/testartifact/nextest_test.go @@ -0,0 +1,126 @@ +package testartifact + +import ( + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/pelletier/go-toml/v2" + "github.com/riteofstring/code-polishy/internal/policy" +) + +func nextestTestReport(outcome string, tests, failures, errors, skipped int) string { + return fmt.Sprintf(`%s`, tests, failures, errors, tests, failures, errors, skipped, outcome) +} + +func TestNextestEvidenceAgreesWithExecutedCasesAndExit(t *testing.T) { + t.Parallel() + pass := nextestTestReport("", 1, 0, 0, 0) + failure := nextestTestReport(`captured & escaped`, 1, 1, 0, 0) + cases := []struct { + name, report string + exit int + valid bool + }{ + {"passing", pass, 0, true}, + {"assertion", failure, 100, true}, + {"test error", nextestTestReport(``, 1, 0, 1, 0), 100, true}, + {"failure hidden by success", failure, 0, false}, + {"failed exit with passing report", pass, 100, false}, + {"operational exit", failure, 101, false}, + {"all ignored", ``, 0, false}, + {"all skipped", nextestTestReport(``, 1, 0, 0, 1), 0, false}, + {"missing case", strings.Replace(pass, `tests="1"`, `tests="2"`, -1), 0, false}, + {"wrong suite", strings.Replace(pass, `name="rust-unit"`, `name="other"`, 1), 0, false}, + {"missing count", strings.Replace(pass, ` failures="0"`, "", 1), 0, false}, + {"conflicting outcome", nextestTestReport(``, 1, 1, 0, 1), 100, false}, + {"retried pass", nextestTestReport(``, 1, 0, 0, 0), 0, false}, + {"truncated", strings.TrimSuffix(pass, ""), 0, false}, + {"two roots", pass + pass, 0, false}, + {"directive", "" + pass, 0, false}, + {"duplicate attribute", strings.Replace(pass, `tests="1"`, `tests="0" tests="1"`, 1), 0, false}, + {"trailing text", pass + "incomplete", 0, false}, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + err := validateNextest([]byte(test.report), "rust-unit", test.exit) + if (err == nil) != test.valid { + t.Fatalf("evidence accepted=%v, want %v: %v", err == nil, test.valid, err) + } + }) + } +} + +func TestNextestEvidenceRejectsDuplicateCaseAndBinaryIdentity(t *testing.T) { + t.Parallel() + pass := nextestTestReport("", 1, 0, 0, 0) + item := `` + duplicate := strings.Replace(pass, item, item+item, 1) + duplicate = strings.ReplaceAll(duplicate, `tests="1"`, `tests="2"`) + if err := validateNextest([]byte(duplicate), "rust-unit", 0); err == nil || !strings.Contains(err.Error(), "duplicate test") { + t.Fatalf("duplicate test evidence: %v", err) + } + start := strings.Index(pass, "") + duplicate = strings.Replace(pass, "", pass[start:end]+"", 1) + if err := validateNextest([]byte(duplicate), "rust-unit", 0); err == nil || !strings.Contains(err.Error(), "duplicate binary") { + t.Fatalf("duplicate binary evidence: %v", err) + } +} + +func TestNextestPreparationRetainsCompilerEnvironmentAndFreshEvidence(t *testing.T) { + t.Parallel() + execution, err := Start(t.TempDir(), "") + if err != nil { + t.Fatal(err) + } + suite := policy.TestSuite{Name: "rust-unit", Rust: &policy.RustTestSuite{Manifest: "Cargo.toml"}} + command, err := execution.PrepareCommand(suite, policy.Command{ + Argv: []string{"cargo-nextest", "--config-file", DirectoryToken + "/" + NextestConfigName}, + EnvironmentOverrides: []string{"RUSTC=/governed/rustc", "CARGO_NET_OFFLINE=true"}, + }) + if err != nil || !slices.Contains(command.EnvironmentOverrides, "RUSTC=/governed/rustc") { + t.Fatalf("compiler environment: %+v, %v", command, err) + } + assertNextestSettings(t, command) + writeArtifactFile(t, command.TestArtifactDirectory, "nextest.xml", nextestTestReport("", 1, 1, 0, 0)) + artifacts, err := execution.ValidateCommand(command, 100) + if err != nil || len(artifacts) != 1 || artifacts[0].SHA256 == "" || !artifacts[0].Required { + t.Fatalf("failed test artifacts: %+v, %v", artifacts, err) + } + retry, err := execution.PrepareCommand(suite, command) + if err != nil || retry.TestArtifactDirectory == command.TestArtifactDirectory { + t.Fatalf("fresh retry: %+v, %v", retry, err) + } + if _, err := execution.ValidateCommand(retry, 0); err == nil || !strings.Contains(err.Error(), "missing") { + t.Fatalf("stale report satisfied retry: %v", err) + } +} + +func assertNextestSettings(t *testing.T, command policy.Command) { + t.Helper() + data, err := os.ReadFile(filepath.Join(command.TestArtifactDirectory, NextestConfigName)) + if err != nil { + t.Fatal(err) + } + var parsed struct { + Profile map[string]struct { + Retries int `toml:"retries"` + FailFast bool `toml:"fail-fast"` + JUnit struct { + Path string `toml:"path"` + Name string `toml:"report-name"` + } `toml:"junit"` + } `toml:"profile"` + } + if err := toml.Unmarshal(data, &parsed); err != nil { + t.Fatal(err) + } + profile, exists := parsed.Profile["code-polishy"] + if !exists || profile.Retries != 0 || profile.FailFast || profile.JUnit.Name != command.TestArtifactSuite || profile.JUnit.Path != filepath.Join(command.TestArtifactDirectory, "nextest.xml") { + t.Fatalf("Nextest settings: %s", data) + } +} diff --git a/internal/testartifact/xml.go b/internal/testartifact/xml.go index a8a2d533..738a1d00 100644 --- a/internal/testartifact/xml.go +++ b/internal/testartifact/xml.go @@ -15,21 +15,21 @@ import ( "github.com/riteofstring/code-polishy/internal/policy" ) -func validateDeclared(directory, suite string, declarations []policy.TestArtifact) ([]Record, error) { +func validateDeclared(directory, suite string, declarations []policy.TestArtifact, exitStatus int) ([]Record, error) { records := []Record{} for _, declaration := range declarations { - record, present, err := validateOne(directory, suite, declaration) - if err != nil { - return records, err - } + record, present, err := validateOne(directory, suite, declaration, exitStatus) if present { records = append(records, record) } + if err != nil { + return records, err + } } return records, nil } -func validateOne(directory, suite string, declaration policy.TestArtifact) (Record, bool, error) { +func validateOne(directory, suite string, declaration policy.TestArtifact, exitStatus int) (Record, bool, error) { path, err := containedArtifactPath(directory, declaration.Path) if err != nil { return Record{}, false, err @@ -48,13 +48,17 @@ func validateOne(directory, suite string, declaration policy.TestArtifact) (Reco if err != nil { return Record{}, false, err } + record := Record{ + Suite: suite, Path: filepath.Base(directory) + "/" + declaration.Path, Type: declaration.Type, + Required: declaration.Required, Size: int64(len(data)), SHA256: digest, + } + if declaration.Type == policy.NextestJUnitArtifact { + return record, true, validateNextest(data, suite, exitStatus) + } if err := validateXML(data, declaration.Type); err != nil { return Record{}, false, fmt.Errorf("test artifact %q: %w", declaration.Path, err) } - return Record{ - Suite: suite, Path: filepath.Base(directory) + "/" + declaration.Path, Type: declaration.Type, - Required: declaration.Required, Size: int64(len(data)), SHA256: digest, - }, true, nil + return record, true, nil } func regularArtifactInfo(path string, declaration policy.TestArtifact) (os.FileInfo, bool, error) { diff --git a/internal/testing/artifact_execution_test.go b/internal/testing/artifact_execution_test.go index 7452d364..af61e84e 100644 --- a/internal/testing/artifact_execution_test.go +++ b/internal/testing/artifact_execution_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" "github.com/riteofstring/code-polishy/internal/runner" "github.com/riteofstring/code-polishy/internal/testartifact" ) @@ -25,7 +26,7 @@ func TestExecuteSuiteCapturesManagedJUnitArtifact(t *testing.T) { Argv: []string{executable, "-test.run=^TestManagedArtifactHelper$"}, Environment: []string{artifactHelperMode}, Artifacts: []policy.TestArtifact{{Path: "junit.xml", Type: "junit", Required: true}}, } - execution := ExecuteSuite(t.Context(), root, runner.OSRunner{}, suite, 1) + execution := ExecuteSuite(t.Context(), repository.Repository{Root: root}, runner.OSRunner{}, suite, 1) if execution.Failed() || len(execution.Artifacts) != 1 || execution.Artifacts[0].SHA256 == "" || execution.Artifacts[0].Path != "artifact-helper/junit.xml" { t.Fatalf("execution = %+v", execution) @@ -47,7 +48,7 @@ func TestExecuteSuiteFailsWhenRequiredArtifactIsMissing(t *testing.T) { Argv: []string{executable, "-test.run=^TestManagedArtifactHelper$"}, Environment: []string{artifactHelperMode}, Artifacts: []policy.TestArtifact{{Path: "junit.xml", Type: "junit", Required: true}}, } - execution := ExecuteSuite(t.Context(), root, runner.OSRunner{}, suite, 1) + execution := ExecuteSuite(t.Context(), repository.Repository{Root: root}, runner.OSRunner{}, suite, 1) if !execution.Failed() || execution.FailureCategory != runner.FailureOperational || !strings.Contains(execution.FailureMessage, "required junit artifact") { t.Fatalf("execution = %+v", execution) } @@ -65,7 +66,7 @@ func TestExecuteSuiteKeepsCommandFailureAndCapturesArtifact(t *testing.T) { Argv: []string{executable, "-test.run=^TestManagedArtifactHelper$"}, Environment: []string{artifactHelperMode}, Artifacts: []policy.TestArtifact{{Path: "junit.xml", Type: "junit", Required: true}}, } - execution := ExecuteSuite(t.Context(), root, runner.OSRunner{}, suite, 1) + execution := ExecuteSuite(t.Context(), repository.Repository{Root: root}, runner.OSRunner{}, suite, 1) if !execution.Failed() || execution.FailureCategory != runner.FailureCommandExit || len(execution.Artifacts) != 1 { t.Fatalf("execution = %+v", execution) } diff --git a/internal/testing/rust.go b/internal/testing/rust.go new file mode 100644 index 00000000..d2bd31ca --- /dev/null +++ b/internal/testing/rust.go @@ -0,0 +1,62 @@ +package testing + +import ( + "slices" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/rustfacts" + "github.com/riteofstring/code-polishy/internal/testartifact" +) + +func SuiteCommand(repo repository.Repository, suite policy.TestSuite) policy.Command { + command := policy.Command{ + Name: suite.Name, Argv: slices.Clone(suite.Argv), Cwd: suite.Cwd, + Paths: slices.Clone(suite.Paths), Modules: slices.Clone(suite.Modules), + Environment: slices.Clone(suite.Environment), ExclusiveResources: slices.Clone(suite.ExclusiveResources), + TimeoutSeconds: suite.TimeoutSeconds, SealedEnvironment: suite.Reusable, + TestArtifacts: suite.ArtifactDeclarations(), + } + if suite.Rust == nil { + return command + } + native := rustfacts.Command(repo, suite.Name, "cargo") + command.Argv = append([]string{repo.PolicyTool("cargo-nextest")}, rustTestArguments(*suite.Rust)...) + command.Managed = true + command.SealedEnvironment = true + command.EnvironmentOverrides = append(native.EnvironmentOverrides, "CARGO="+repo.RustTool("cargo")) + for _, key := range append(native.Environment, "PATH", "LIB", "INCLUDE", "LIBPATH", "SDKROOT", "DEVELOPER_DIR") { + if !slices.Contains(command.Environment, key) { + command.Environment = append(command.Environment, key) + } + } + return command +} + +func rustTestArguments(settings policy.RustTestSuite) []string { + arguments := []string{ + "nextest", "run", "--frozen", "--manifest-path", settings.Manifest, "--target", rustfacts.HostTarget(), + "--config-file", testartifact.DirectoryToken + "/" + testartifact.NextestConfigName, + "--user-config-file", "none", "--profile", "code-polishy", "--retries", "0", "--no-fail-fast", "--no-tests", "fail", + } + for _, name := range settings.Packages { + arguments = append(arguments, "--package", name) + } + for _, feature := range settings.Features { + arguments = append(arguments, "--features", feature) + } + if settings.NoDefaultFeatures { + arguments = append(arguments, "--no-default-features") + } + if settings.Filter != "" { + arguments = append(arguments, "--filter-expr", settings.Filter) + } + return arguments +} + +func suiteProblem(repo repository.Repository, suite policy.TestSuite) error { + if suite.Rust != nil { + return rustfacts.TestProblem(repo, *suite.Rust) + } + return nil +} diff --git a/internal/testing/rust_test.go b/internal/testing/rust_test.go new file mode 100644 index 00000000..e7215ce9 --- /dev/null +++ b/internal/testing/rust_test.go @@ -0,0 +1,186 @@ +package testing + +import ( + "bytes" + "context" + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestNativeRustSuiteProducesNonemptyEvidence(t *testing.T) { + t.Parallel() + repo, suite := rustSuiteRepository(t, "#[test]\nfn behavior() { assert_eq!(2 + 2, 4); }\n") + before, err := os.ReadFile(filepath.Join(repo.Root, "Cargo.lock")) + if err != nil { + t.Fatal(err) + } + execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + if execution.Failed() || len(execution.Artifacts) != 1 || execution.Artifacts[0].Type != policy.NextestJUnitArtifact || execution.Artifacts[0].SHA256 == "" { + t.Fatalf("native Rust execution: %+v", execution) + } + after, err := os.ReadFile(filepath.Join(repo.Root, "Cargo.lock")) + if err != nil || !bytes.Equal(before, after) { + t.Fatalf("frozen testing changed the lock: %v", err) + } +} + +func TestNativeRustSuiteDistinguishesTestAndOperationalFailure(t *testing.T) { + t.Parallel() + for name, test := range map[string]struct { + source, build string + category runner.FailureCategory + artifact bool + }{ + "assertion": {source: "#[test]\nfn behavior() { assert_eq!(2 + 2, 5); }\n", category: runner.FailureCommandExit, artifact: true}, + "empty": {source: "pub fn answer() -> u32 { 42 }\n", category: runner.FailureOperational, artifact: true}, + "ignored": {source: "#[test]\n#[ignore]\nfn behavior() { panic!(\"ignored\"); }\n", category: runner.FailureOperational, artifact: true}, + "compiler": {source: "#[test]\nfn behavior() { let value: u32 = \"wrong\"; assert_eq!(value, 42); }\n", category: runner.FailureOperational}, + "build script": {source: "#[test]\nfn behavior() { assert_eq!(2 + 2, 4); }\n", build: "fn main() { panic!(\"build failed\"); }\n", category: runner.FailureOperational}, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + repo, suite := rustSuiteRepository(t, test.source) + if test.build != "" { + writeRustSuiteFile(t, repo.Root, "build.rs", test.build) + } + execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + if !execution.Failed() || execution.FailureCategory != test.category || (len(execution.Artifacts) == 1) != test.artifact { + t.Fatalf("failure classification: %+v", execution) + } + }) + } +} + +func TestNativeRustSuiteHonorsCargoFeaturesAndFilter(t *testing.T) { + t.Parallel() + repo, suite := rustSuiteRepository(t, "#[test]\nfn selected() { assert!(!cfg!(feature = \"ordinary\")); assert!(cfg!(feature = \"chosen\")); }\n#[test]\nfn outside_filter() { panic!(\"not selected\"); }\n") + writeRustSuiteFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\nedition='2024'\n[workspace]\n[features]\ndefault=['ordinary']\nordinary=[]\nchosen=[]\n") + suite.Rust.Features = []string{"chosen"} + suite.Rust.NoDefaultFeatures = true + suite.Rust.Filter = "test(=selected)" + execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + if execution.Failed() || len(execution.Artifacts) != 1 { + t.Fatalf("Cargo feature/filter selection: %+v", execution) + } + suite.Rust.Filter = "test(=absent)" + empty := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 2) + if !empty.Failed() || empty.FailureCategory != runner.FailureOperational { + t.Fatalf("empty filter was accepted: %+v", empty) + } +} + +func TestNativeRustSuiteUsesWorkspaceDefaultsAndExplicitPackages(t *testing.T) { + t.Parallel() + repo, suite := rustSuiteRepository(t, "pub fn outside_workspace() {}\n") + writeRustSuiteFile(t, repo.Root, "Cargo.toml", "[workspace]\nmembers=['crates/core','crates/service']\ndefault-members=['crates/core']\nresolver='3'\n") + writeRustSuiteFile(t, repo.Root, "Cargo.lock", "version=4\n[[package]]\nname='core'\nversion='1.0.0'\n[[package]]\nname='service'\nversion='1.0.0'\n") + writeRustSuiteFile(t, repo.Root, "crates/core/Cargo.toml", "[package]\nname='core'\nversion='1.0.0'\nedition='2024'\n") + writeRustSuiteFile(t, repo.Root, "crates/service/Cargo.toml", "[package]\nname='service'\nversion='1.0.0'\nedition='2024'\n") + writeRustSuiteFile(t, repo.Root, "crates/core/src/lib.rs", "#[test]\nfn behavior() { assert_eq!(2 + 2, 4); }\n") + writeRustSuiteFile(t, repo.Root, "crates/service/src/lib.rs", "#[test]\nfn behavior() { assert_eq!(2 + 2, 5); }\n") + defaultRun := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + if defaultRun.Failed() { + t.Fatalf("unselected workspace member was tested: %+v", defaultRun) + } + suite.Rust.Packages = []string{"service"} + explicit := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 2) + if !explicit.Failed() || explicit.FailureCategory != runner.FailureCommandExit { + t.Fatalf("explicit package was not tested: %+v", explicit) + } + suite.Rust.Packages = []string{"core"} + writeRustSuiteFile(t, repo.Root, "crates/core/tests/public.rs", "#[test]\nfn public_behavior() { assert_eq!(2 + 2, 5); }\n") + integration := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 3) + if !integration.Failed() || integration.FailureCategory != runner.FailureCommandExit { + t.Fatalf("integration target was not tested: %+v", integration) + } +} + +func TestNativeRustSuiteNeverRepairsLocksOrIgnoresProjectSettings(t *testing.T) { + t.Parallel() + repo, suite := rustSuiteRepository(t, "#[test]\nfn behavior() { assert_eq!(2 + 2, 4); }\n") + writeRustSuiteFile(t, repo.Root, "Cargo.lock", "version = 4\n") + execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + if !execution.Failed() || execution.FailureCategory != runner.FailureOperational { + t.Fatalf("stale lock was accepted: %+v", execution) + } + lock, err := os.ReadFile(filepath.Join(repo.Root, "Cargo.lock")) + if err != nil || string(lock) != "version = 4\n" { + t.Fatalf("stale lock was changed: %q, %v", lock, err) + } + writeRustSuiteFile(t, repo.Root, ".config/nextest.toml", "[profile.default]\ndefault-filter='not test(live_probe)'\n") + preflight := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 2) + if !strings.Contains(preflight.FailureMessage, "cannot silently replace") { + t.Fatalf("project settings were ignored: %+v", preflight) + } +} + +func TestNativeRustSuiteFindsExplicitWorkspaceNextestSettings(t *testing.T) { + t.Parallel() + repo, suite := rustSuiteRepository(t, "pub fn answer() -> u32 { 42 }\n") + writeRustSuiteFile(t, repo.Root, "crates/app/Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\nedition='2024'\nworkspace='../../workspace'\n") + writeRustSuiteFile(t, repo.Root, "workspace/Cargo.toml", "[workspace]\nmembers=['../crates/app']\nresolver='3'\n") + writeRustSuiteFile(t, repo.Root, "workspace/.config/nextest.toml", "[profile.default]\ndefault-filter='not test(live_probe)'\n") + suite.Rust.Manifest = "crates/app/Cargo.toml" + execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + if !execution.Failed() || !strings.Contains(execution.FailureMessage, "workspace/.config/nextest.toml") { + t.Fatalf("explicit workspace configuration was ignored: %+v", execution) + } +} + +func TestNativeRustCommandAndCopiesRetainGovernedSelection(t *testing.T) { + t.Parallel() + repo, suite := rustSuiteRepository(t, "#[test]\nfn behavior() { assert_eq!(2 + 2, 4); }\n") + suite.Rust.Packages = []string{"app"} + command := SuiteCommand(repo, suite) + if command.Argv[0] != repo.PolicyTool("cargo-nextest") || !command.Managed || !command.SealedEnvironment || + !slices.Contains(command.EnvironmentOverrides, "CARGO="+repo.RustTool("cargo")) || !slices.Contains(command.EnvironmentOverrides, "CARGO_NET_OFFLINE=true") { + t.Fatalf("ungoverned test command: %+v", command) + } + retry := RetrySuite(suite) + retry.Rust.Packages[0] = "other" + if suite.Rust.Packages[0] != "app" { + t.Fatal("retry changed the selected original package") + } + plan := ExecutionPlanForDirectTest(Plan{Suites: []policy.TestSuite{suite}}) + if len(plan.Commands) != 1 || !slices.Contains(plan.Commands[0].Argv, "--package") { + t.Fatalf("native command missing from execution plan: %+v", plan) + } + ctx, cancel := context.WithCancel(t.Context()) + cancel() + execution := ExecuteSuite(ctx, repo, runner.OSRunner{}, suite, 1) + if !execution.Failed() || execution.FailureCategory != runner.FailureCanceled { + t.Fatalf("canceled test became another outcome: %+v", execution) + } +} + +func rustSuiteRepository(t *testing.T, source string) (repository.Repository, policy.TestSuite) { + t.Helper() + policyRoot, err := filepath.Abs("../..") + if err != nil { + t.Fatal(err) + } + repo := repository.Repository{Root: t.TempDir(), PolicyRoot: policyRoot} + writeRustSuiteFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\nedition='2024'\n[workspace]\n") + writeRustSuiteFile(t, repo.Root, "Cargo.lock", "version = 4\n[[package]]\nname = 'app'\nversion = '1.0.0'\n") + writeRustSuiteFile(t, repo.Root, "src/lib.rs", source) + suite := policy.TestSuite{Name: "rust-unit", Kind: "unit", Scope: "repository", Cwd: ".", TimeoutSeconds: 30, Rust: &policy.RustTestSuite{Manifest: "Cargo.toml"}} + return repo, suite +} + +func writeRustSuiteFile(t *testing.T, root, name, content string) { + t.Helper() + path := filepath.Join(root, filepath.FromSlash(name)) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatal(err) + } +} diff --git a/internal/testing/testing.go b/internal/testing/testing.go index 542b794c..14ace681 100644 --- a/internal/testing/testing.go +++ b/internal/testing/testing.go @@ -450,7 +450,7 @@ func run(ctx context.Context, repo repository.Repository, commandRunner runner.R reuse := suiteReuseController(commandRunner) for index, suite := range plan.Suites { runContext := suiteRunContext(ctx, reporter, index) - execution := runOneSuite(runContext, repo.Root, commandRunner, suite, reuse, &artifactExecution) + execution := runOneSuite(runContext, repo, commandRunner, suite, reuse, &artifactExecution) runResult.Executions = append(runResult.Executions, execution) reportSuiteExecution(reporter, index, execution) if execution.Failed() { @@ -478,7 +478,10 @@ func suiteRunContext(ctx context.Context, reporter *ExecutionReporter, index int }) } -func runOneSuite(ctx context.Context, root string, commandRunner runner.Runner, suite policy.TestSuite, reuse SuiteReuseController, artifacts **testartifact.Execution) SuiteExecution { +func runOneSuite(ctx context.Context, repo repository.Repository, commandRunner runner.Runner, suite policy.TestSuite, reuse SuiteReuseController, artifacts **testartifact.Execution) SuiteExecution { + if err := suiteProblem(repo, suite); err != nil { + return failedSuiteExecution(suite, 1, err) + } execution, reused, err := reusableSuiteExecution(reuse, suite, 1) if err != nil { return failedSuiteExecution(suite, 1, err) @@ -486,14 +489,14 @@ func runOneSuite(ctx context.Context, root string, commandRunner runner.Runner, if reused { return execution } - if err := ensureRunArtifacts(root, commandRunner, artifacts); err != nil { + if err := ensureRunArtifacts(repo.Root, commandRunner, artifacts); err != nil { return failedSuiteExecution(suite, 1, err) } - executionRoot, cleanup, err := suiteExecutionRoot(root, commandRunner, suite) + executionRoot, cleanup, err := suiteExecutionRoot(repo.Root, commandRunner, suite) if err != nil { return failedSuiteExecution(suite, 1, err) } - execution = executeSuite(ctx, executionRoot, commandRunner, suite, 1, *artifacts) + execution = executeSuite(ctx, repo, executionRoot, commandRunner, suite, 1, *artifacts) finishSuiteExecution(&execution, cleanup(), reuse) return execution } @@ -575,15 +578,18 @@ func suiteReuseController(commandRunner runner.Runner) SuiteReuseController { return controller } -func ExecuteSuite(ctx context.Context, root string, commandRunner runner.Runner, suite policy.TestSuite, attempt int) SuiteExecution { +func ExecuteSuite(ctx context.Context, repo repository.Repository, commandRunner runner.Runner, suite policy.TestSuite, attempt int) SuiteExecution { + if err := suiteProblem(repo, suite); err != nil { + return failedSuiteExecution(suite, attempt, err) + } if runnerManagesTestArtifacts(commandRunner) { - return executeSuite(ctx, root, commandRunner, suite, attempt, nil) + return executeSuite(ctx, repo, repo.Root, commandRunner, suite, attempt, nil) } - execution, err := testartifact.Start(root, "") + execution, err := testartifact.Start(repo.Root, "") if err != nil { return failedSuiteExecution(suite, attempt, err) } - result := executeSuite(ctx, root, commandRunner, suite, attempt, execution) + result := executeSuite(ctx, repo, repo.Root, commandRunner, suite, attempt, execution) if completeErr := execution.Complete(); completeErr != nil { result.FailureCategory = runner.FailureOperational if result.FailureMessage == "" { @@ -605,8 +611,8 @@ func RetrySuite(suite policy.TestSuite) policy.TestSuite { return retry } -func executeSuite(ctx context.Context, root string, commandRunner runner.Runner, suite policy.TestSuite, attempt int, artifactExecution *testartifact.Execution) SuiteExecution { - command := suiteCommand(suite) +func executeSuite(ctx context.Context, repo repository.Repository, root string, commandRunner runner.Runner, suite policy.TestSuite, attempt int, artifactExecution *testartifact.Execution) SuiteExecution { + command := SuiteCommand(repo, suite) if artifactExecution != nil { prepared, err := artifactExecution.PrepareCommand(suite, command) if err != nil { @@ -618,7 +624,10 @@ func executeSuite(ctx context.Context, root string, commandRunner runner.Runner, artifacts := []testartifact.Record{} if artifactExecution != nil { var artifactErr error - artifacts, artifactErr = artifactExecution.ValidateCommand(command) + artifacts, artifactErr = artifactExecution.ValidateCommand(command, result.ExitStatus) + if artifactErr != nil && (result.FailureCategory == "" || result.FailureCategory == runner.FailureCommandExit) { + result.FailureCategory = runner.FailureOperational + } err = errors.Join(err, artifactErr) } else if provider, ok := commandRunner.(interface { TestArtifacts(string, int) []testartifact.Record @@ -652,18 +661,10 @@ func runnerManagesTestArtifacts(commandRunner runner.Runner) bool { return ok && managed.ManagesTestArtifacts() } -func suiteCommand(suite policy.TestSuite) policy.Command { - return policy.Command{ - Name: suite.Name, Argv: append([]string{}, suite.Argv...), Cwd: suite.Cwd, - Paths: append([]string{}, suite.Paths...), Modules: append([]string{}, suite.Modules...), - Environment: append([]string{}, suite.Environment...), ExclusiveResources: append([]string{}, suite.ExclusiveResources...), - TimeoutSeconds: suite.TimeoutSeconds, - SealedEnvironment: suite.Reusable, - TestArtifacts: append([]policy.TestArtifact{}, suite.Artifacts...), - } -} - func cloneSuite(suite policy.TestSuite) policy.TestSuite { + if suite.Rust != nil { + suite.Rust = suite.Rust.Clone() + } suite.Modules = append([]string{}, suite.Modules...) suite.Argv = append([]string{}, suite.Argv...) suite.RetryArgv = append([]string{}, suite.RetryArgv...) @@ -694,9 +695,13 @@ func runSuite(ctx context.Context, root string, commandRunner runner.Runner, com func ExecutionPlanForDirectTest(plan Plan) ExecutionPlan { commands := make([]ExecutionCommand, 0, len(plan.Suites)) for _, suite := range plan.Suites { + argv := slices.Clone(suite.Argv) + if suite.Rust != nil { + argv = append([]string{"cargo-nextest"}, rustTestArguments(*suite.Rust)...) + } commands = append(commands, ExecutionCommand{ Name: suite.Name, Kind: suite.Kind, Scope: suite.Scope, Cost: suite.Cost, - Argv: append([]string{}, suite.Argv...), Cwd: suite.Cwd, + Argv: argv, Cwd: suite.Cwd, ExclusiveResources: append([]string{}, suite.ExclusiveResources...), TimeoutSeconds: suite.TimeoutSeconds, }) } diff --git a/internal/testing/testing_test.go b/internal/testing/testing_test.go index 967fd8d8..cfbc46f4 100644 --- a/internal/testing/testing_test.go +++ b/internal/testing/testing_test.go @@ -540,6 +540,7 @@ func TestRepositoryPolicySelectsOwningModuleToolingContracts(t *testing.T) { repo := repository.Repository{Root: root, Config: config} toolingSuites := []string{ "mutation-wrapper-contract", "install-contract", "release-preflight-contract", + "rust-runtime-contract", "javascript-runtime-contract", "javascript-bundle-contract", "javascript-runner-contract", "javascript-project-contract", "installed-release-contract", "oci-image-contract", "javascript-provider-boundary", } diff --git a/schema/code-polishy.schema.json b/schema/code-polishy.schema.json index 7f054b75..17841a56 100644 --- a/schema/code-polishy.schema.json +++ b/schema/code-polishy.schema.json @@ -691,13 +691,15 @@ "testSuite": { "type": "object", "unevaluatedProperties": false, - "required": ["name", "kind", "scope", "argv"], + "required": ["name", "kind", "scope"], + "oneOf": [{ "required": ["argv"] }, { "required": ["rust"] }], "properties": { "kind": { "$ref": "#/$defs/identifier" }, "scope": { "enum": ["module", "repository"] }, "reusable": { "type": "boolean" }, "cost": { "enum": ["quick", "standard", "expensive"] }, "retryArgv": { "$ref": "#/$defs/argv" }, + "rust": { "$ref": "#/$defs/rustTestSuite" }, "extraInputs": { "$ref": "#/$defs/paths" }, "covers": { "$ref": "#/$defs/identifierArray" }, "artifacts": { @@ -822,13 +824,38 @@ "executionSuite": { "$ref": "#/$defs/identifier" } } }, + "rustTestSuite": { + "type": "object", + "additionalProperties": false, + "properties": { + "manifest": { "$ref": "#/$defs/concretePath" }, + "packages": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^[^-\\s\\u0000][^\\s\\u0000]*$" + } + }, + "features": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^[^-\\s\\u0000][^\\s\\u0000]*$" + } + }, + "noDefaultFeatures": { "type": "boolean" }, + "filter": { "type": "string", "minLength": 1, "maxLength": 4096 } + } + }, "testArtifact": { "type": "object", "additionalProperties": false, "required": ["path", "type"], "properties": { "path": { "$ref": "#/$defs/exactPath", "pattern": "\\.[xX][mM][lL]$" }, - "type": { "enum": ["junit", "cobertura"] }, + "type": { "enum": ["junit", "cobertura", "nextest-junit"] }, "required": { "type": "boolean", "default": false } } }, From d44dbf85bb936b0a0cb9b7fb1cc6da672de65153 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 04:49:43 +0000 Subject: [PATCH 19/49] feat: carry Rust tooling in versioned releases --- .code-polishy.json | 5 ++ cmd/code-polishy-launcher/main_test.go | 2 +- cmd/code-polishy/main_test.go | 2 +- docs/design/installed-release-verification.md | 22 +++++++++ docs/installation.md | 9 +++- docs/plans/code-polishy-rust-support-plan.md | 8 +++- internal/release/launcher_manifest.go | 13 +++-- internal/release/launcher_manifest_test.go | 37 ++++++++++---- internal/release/manifest_writer.go | 9 ++-- internal/release/manifest_writer_test.go | 4 ++ internal/release/publication_test.go | 2 +- internal/release/release.go | 14 +++++- internal/release/release_test.go | 9 +++- internal/release/rust_sbom_test.go | 48 +++++++++++++++++++ internal/release/sbom.go | 40 +++++++++++++++- scripts/build-release.ps1 | 30 +++++++++++- scripts/install.sh | 27 +++++++++++ scripts/release-manifest.sh | 12 ++++- scripts/test-install.sh | 40 +++++++++++++++- tools/cargo-nextest-license.txt | 28 +++++++++++ 20 files changed, 330 insertions(+), 31 deletions(-) create mode 100644 internal/release/rust_sbom_test.go create mode 100644 tools/cargo-nextest-license.txt diff --git a/.code-polishy.json b/.code-polishy.json index dd489adb..94c89e7f 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -243,6 +243,11 @@ { "path": "docs/design/installed-release-verification.md", "sourcePaths": [ + "internal/release/manifest_writer.go", + "internal/release/launcher_manifest.go", + "internal/release/sbom.go", + "scripts/release-manifest.sh", + "scripts/build-release.ps1", "scripts/build-oci-image.sh", "scripts/test-installed-release.sh", "scripts/test-installed-release-contract.sh", diff --git a/cmd/code-polishy-launcher/main_test.go b/cmd/code-polishy-launcher/main_test.go index 323fdb57..946076fc 100644 --- a/cmd/code-polishy-launcher/main_test.go +++ b/cmd/code-polishy-launcher/main_test.go @@ -52,7 +52,7 @@ func (installed store) install(t *testing.T, revision, engine string) release.Lo Tools: release.Tools{ Go: "1.26.6", Govulncheck: "1.3.0", Node: "24.18.0", OSVScanner: "2.4.0", PNPM: "11.13.0", Packaging: "26.3", Python: "3.12.13+20260728", Ruff: "0.16.0", Shellcheck: "0.11.0", Staticcheck: "0.7.0", - Ty: "0.0.65", Vulture: "2.16", + Ty: "0.0.65", Vulture: "2.16", Rust: "1.97.1", Nextest: "0.9.138", }, Entries: []release.Entry{ {Path: bundleLink, Symlink: "../.pnpm/tool"}, diff --git a/cmd/code-polishy/main_test.go b/cmd/code-polishy/main_test.go index 3182476d..0e4b187f 100644 --- a/cmd/code-polishy/main_test.go +++ b/cmd/code-polishy/main_test.go @@ -769,7 +769,7 @@ func installedRelease(t *testing.T, revision string) string { Tools: release.Tools{ Go: "1.26.6", Govulncheck: "1.3.0", Node: "24.18.0", OSVScanner: "2.4.0", PNPM: "11.13.0", Packaging: "26.3", Python: "3.12.13+20260728", Ruff: "0.16.0", Shellcheck: "0.11.0", Staticcheck: "0.7.0", - Ty: "0.0.65", Vulture: "2.16", + Ty: "0.0.65", Vulture: "2.16", Rust: "1.97.1", Nextest: "0.9.138", }, CapabilityCatalogSHA256: hex.EncodeToString(catalogDigest[:]), Entries: []release.Entry{ diff --git a/docs/design/installed-release-verification.md b/docs/design/installed-release-verification.md index 9fe68da7..33369014 100644 --- a/docs/design/installed-release-verification.md +++ b/docs/design/installed-release-verification.md @@ -67,3 +67,25 @@ LGPL-2.1-or-later license. The library remains a separate Python package that recipients can replace; modified installations do not retain the original release's verified digest identity. Target project environments are not used for analyzer imports. + +The Rust syntax helper's Tree-sitter distributions share that carried Python +runtime. Native builders carry their exact manifest and lock alongside the +installed wheel metadata and licenses. Release SBOM composition requires both +parser distributions to match the manifest; missing, duplicate, wrong-version +or undeclared Python distributions cannot be silently left out of publication. + +Rust is a built-in release capability. Each native archive carries the matching +Rust SDK (Cargo, rustc, rustdoc, rustfmt, Clippy, and host standard library) and +the pinned Nextest executable. Builders probe these tools before sealing the +archive; installed commands never acquire a toolchain from rustup or the target +project. Manifest version 7 binds the Rust and Nextest pins into the cross-host +release identity. The launcher's existing historical-manifest reader continues +to interpret versions 2 through 6 with their original tool fields and identity +rules. An older engine does not execute a version 7 release. + +The SDK retains upstream component licenses and copyright inventories; parser +wheels retain their distribution license files. Nextest's binary-only archive +requires a separately carried upstream license. Its pinned MIT text is recorded +with the corresponding source revision. That top-level notice does not establish +complete third-party notices or an exact linked-dependency inventory for Nextest; +those remain a publication requirement alongside native installed-Rust evidence. diff --git a/docs/installation.md b/docs/installation.md index b8d65a38..67b4ad9e 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -323,7 +323,7 @@ swap the code a release runs without changing an installed file. Each manifest version has an exact schema and identity calculation. The engine accepts only its own version. The stable launcher keeps explicit readers for -installed manifest versions 2 through 5 so installing a newer release does not +installed manifest versions 2 through 6 so installing a newer release does not strand repositories still locked to an older one. It verifies each older release using that version's original fields and never invents evidence added by a later schema. @@ -345,6 +345,13 @@ validates the selected catalog against the repository's exact lock; another installed release or a modified catalog cannot supply its capabilities. Older releases with no authenticated catalog report discovery evidence unavailable. +Manifest version 7 adds `tools.rust` and `tools.cargo-nextest`. Each native release +carries the pinned Rust SDK, host standard library, and Nextest runner; target +repositories do not install rustup or a separate language pack. Rust parser +libraries reside in the carried Python runtime with their exact dependency +manifest, lock, and distribution metadata. Changing either executable pin changes +the cross-host release identity. + `code-polishy release-manifest verify --root ` recomputes the installed entry evidence. A release that was truncated, changed after installation, or copied from another host fails verification and is reinstalled diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index e9f8436a..30c5d109 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -404,7 +404,13 @@ requires neither target package installation scripts nor a new Rust bootstrap. Native unit/integration suites now retain nonempty structured results through ordinary commands, gate artifacts, regression proofs and reusable receipts. The [Rust test contract](../design/rust-tests.md) documents selectors and boundaries. -The next milestones extend ordinary macro/module and doctest coverage, then +Native release builders now carry the Rust SDK, Nextest and parser dependencies. +Manifest version 7 binds the executable pins; the SBOM includes both parser +distributions and the top-level Rust tools. Native installed-Rust fixtures and +complete bundled dependency notices/inventory remain unverified publication +requirements. No beta release has been published or adopted. + +The next milestones extend ordinary macro/module and doctest coverage and complete installed-release integration. Existing coverage failures remain until their implementations and evidence exist. This beta does not yet establish first-class support for Masterbrainer's full project. diff --git a/internal/release/launcher_manifest.go b/internal/release/launcher_manifest.go index d2199993..f76067ab 100644 --- a/internal/release/launcher_manifest.go +++ b/internal/release/launcher_manifest.go @@ -61,6 +61,11 @@ type manifestToolsV4 struct { Vulture string `json:"vulture"` } +type manifestToolsV5 struct { + manifestToolsV4 + Packaging string `json:"packaging"` +} + func ReadLauncherManifest(releaseDir string) (Manifest, bool, error) { return readManifest(releaseDir, parseLauncherManifest) } @@ -107,12 +112,14 @@ func parseLauncherManifest(data []byte, source string) (Manifest, error) { } func parseHistoricalManifestTools(data []byte, source string, version int) (Tools, error) { - if version == 5 { - wire := Tools{} + if version >= 5 { + wire := manifestToolsV5{} if err := decodeExactly(data, source+" tools", &wire); err != nil { return Tools{}, err } - return wire, nil + result := wire.manifestToolsV4.tools() + result.Packaging = wire.Packaging + return result, nil } if version == 2 { wire := manifestToolsV2{} diff --git a/internal/release/launcher_manifest_test.go b/internal/release/launcher_manifest_test.go index af844f85..9fc4c9cd 100644 --- a/internal/release/launcher_manifest_test.go +++ b/internal/release/launcher_manifest_test.go @@ -8,7 +8,7 @@ import ( func TestLauncherManifestReadsHistoricalSchemasWithoutReinterpretingThem(t *testing.T) { t.Parallel() - for _, version := range []int{2, 3, 4, 5} { + for _, version := range []int{2, 3, 4, 5, 6} { data, expected := historicalManifest(t, version) manifest, err := parseLauncherManifest(data, ManifestFilename) if err != nil { @@ -45,11 +45,28 @@ func TestLauncherManifestRejectsUnsupportedSchemas(t *testing.T) { } } +func TestLauncherManifestRejectsRustToolsInHistoricalSchemas(t *testing.T) { + t.Parallel() + for _, version := range []int{5, 6} { + data, _ := historicalManifest(t, version) + for _, field := range []string{`"rust":"1.97.1",`, `"cargo-nextest":"0.9.138",`} { + changed := strings.Replace(string(data), `"tools":{`, `"tools":{`+field, 1) + if _, err := parseLauncherManifest([]byte(changed), ManifestFilename); err == nil { + t.Fatalf("version %d accepted the later tool field %s", version, field) + } + } + } +} + func historicalManifest(t *testing.T, version int) ([]byte, Manifest) { t.Helper() _, manifest := exampleRelease(t) manifest.ManifestVersion = version - manifest.CapabilityCatalogSHA256 = "" + if version < 6 { + manifest.CapabilityCatalogSHA256 = "" + } + manifest.Tools.Rust = "" + manifest.Tools.Nextest = "" manifest.Tools.Packaging = "" manifest.Tools.Python = "" manifest.Tools.Vulture = "" @@ -59,7 +76,7 @@ func historicalManifest(t *testing.T, version int) ([]byte, Manifest) { manifest.Tools.Python = "3.12.13+20260728" manifest.Tools.Vulture = "2.16" } - if version == 5 { + if version >= 5 { manifest.Tools.Packaging = "26.3" } manifest.ReleaseDigest = manifest.Identity() @@ -68,16 +85,14 @@ func historicalManifest(t *testing.T, version int) ([]byte, Manifest) { ManifestVersion: manifest.ManifestVersion, CodePolishyVersion: manifest.CodePolishyVersion, SourceRevision: manifest.SourceRevision, Host: manifest.Host, Features: manifest.Features, Tools: tools, ReleaseDigest: manifest.ReleaseDigest, ContentDigest: manifest.ContentDigest, - EntryCount: manifest.EntryCount, Entries: manifest.Entries, + CapabilityCatalogSHA256: manifest.CapabilityCatalogSHA256, + EntryCount: manifest.EntryCount, Entries: manifest.Entries, } return render(t, document), manifest } func historicalManifestTools(t *testing.T, manifest Manifest) json.RawMessage { t.Helper() - if manifest.ManifestVersion == 5 { - return render(t, manifest.Tools) - } base := manifestToolsV2{ Go: manifest.Tools.Go, Govulncheck: manifest.Tools.Govulncheck, Node: manifest.Tools.Node, OSVScanner: manifest.Tools.OSVScanner, PNPM: manifest.Tools.PNPM, Ruff: manifest.Tools.Ruff, @@ -93,9 +108,13 @@ func historicalManifestTools(t *testing.T, manifest Manifest) json.RawMessage { Ty: manifest.Tools.Ty, }) } - return render(t, manifestToolsV4{ + versionFour := manifestToolsV4{ Go: base.Go, Govulncheck: base.Govulncheck, Node: base.Node, OSVScanner: base.OSVScanner, PNPM: base.PNPM, Ruff: base.Ruff, Shellcheck: base.Shellcheck, Staticcheck: base.Staticcheck, Ty: manifest.Tools.Ty, Python: manifest.Tools.Python, Vulture: manifest.Tools.Vulture, - }) + } + if manifest.ManifestVersion >= 5 { + return render(t, manifestToolsV5{manifestToolsV4: versionFour, Packaging: manifest.Tools.Packaging}) + } + return render(t, versionFour) } diff --git a/internal/release/manifest_writer.go b/internal/release/manifest_writer.go index e0f9828e..7350fc49 100644 --- a/internal/release/manifest_writer.go +++ b/internal/release/manifest_writer.go @@ -16,12 +16,13 @@ import ( ) var releasePinPaths = struct { - version, goVersion, govulncheck, node, osv, pnpm, packaging, python, ruff, shellcheck, staticcheck, ty, vulture string + version, goVersion, govulncheck, node, osv, pnpm, packaging, python, ruff, shellcheck, staticcheck, ty, vulture, rust, nextest string }{ "VERSION", "scripts/go_version.txt", "tools/govulncheck-version.txt", "tools/node-version.txt", "tools/osv-scanner-version.txt", "tools/pnpm-version.txt", "tools/packaging-version.txt", "tools/python-version.txt", "tools/ruff-version.txt", "tools/shellcheck-version.txt", "tools/staticcheck-version.txt", "tools/ty-version.txt", "tools/vulture-version.txt", + "tools/rust-version.txt", "tools/cargo-nextest-version.txt", } func WriteManifest(releaseDir, sourceRevision string) (Manifest, error) { @@ -62,8 +63,8 @@ func composeReleaseManifest(root, sourceRevision string) (Manifest, error) { if err != nil { return Manifest{}, err } - pins := make([]string, 12) - for index, path := range []string{releasePinPaths.goVersion, releasePinPaths.govulncheck, releasePinPaths.node, releasePinPaths.osv, releasePinPaths.pnpm, releasePinPaths.packaging, releasePinPaths.python, releasePinPaths.ruff, releasePinPaths.shellcheck, releasePinPaths.staticcheck, releasePinPaths.ty, releasePinPaths.vulture} { + pins := make([]string, 14) + for index, path := range []string{releasePinPaths.goVersion, releasePinPaths.govulncheck, releasePinPaths.node, releasePinPaths.osv, releasePinPaths.pnpm, releasePinPaths.packaging, releasePinPaths.python, releasePinPaths.ruff, releasePinPaths.shellcheck, releasePinPaths.staticcheck, releasePinPaths.ty, releasePinPaths.vulture, releasePinPaths.rust, releasePinPaths.nextest} { pins[index], err = readReleasePin(root, path, true) if err != nil { return Manifest{}, err @@ -117,7 +118,7 @@ func readReleasePin(root, relative string, trimV bool) (string, error) { func newReleaseManifest(version, sourceRevision, host string, pins []string, entries []Entry) Manifest { manifest := Manifest{ManifestVersion: ManifestVersion, CodePolishyVersion: version, SourceRevision: sourceRevision, - Host: host, Features: []string{"javascript-bundle"}, Tools: Tools{Go: pins[0], Govulncheck: pins[1], Node: pins[2], OSVScanner: pins[3], PNPM: pins[4], Packaging: pins[5], Python: pins[6], Ruff: pins[7], Shellcheck: pins[8], Staticcheck: pins[9], Ty: pins[10], Vulture: pins[11]}, Entries: entries, EntryCount: len(entries)} + Host: host, Features: []string{"javascript-bundle"}, Tools: Tools{Go: pins[0], Govulncheck: pins[1], Node: pins[2], OSVScanner: pins[3], PNPM: pins[4], Packaging: pins[5], Python: pins[6], Ruff: pins[7], Shellcheck: pins[8], Staticcheck: pins[9], Ty: pins[10], Vulture: pins[11], Rust: pins[12], Nextest: pins[13]}, Entries: entries, EntryCount: len(entries)} for _, entry := range entries { if entry.Path == CapabilityCatalogPath && entry.Symlink == "" { manifest.CapabilityCatalogSHA256 = entry.SHA256 diff --git a/internal/release/manifest_writer_test.go b/internal/release/manifest_writer_test.go index 804ac8c2..3aefadb8 100644 --- a/internal/release/manifest_writer_test.go +++ b/internal/release/manifest_writer_test.go @@ -18,6 +18,9 @@ func TestWriteManifestPublishesAVerifiableImmutableRelease(t *testing.T) { if manifest.Tools.Packaging != "26.3" || manifest.Tools.Python != "3.12.13+20260728" || manifest.Tools.Vulture != "2.16" { t.Fatalf("tools = %+v", manifest.Tools) } + if manifest.Tools.Rust != "1.97.1" || manifest.Tools.Nextest != "0.9.138" { + t.Fatalf("Rust tools = %+v", manifest.Tools) + } read, present, err := ReadManifest(root) if err != nil || !present { t.Fatalf("read present=%v err=%v", present, err) @@ -43,6 +46,7 @@ func writableReleaseStage(t *testing.T) string { "tools/python-version.txt": "3.12.13+20260728\n", "tools/ruff-version.txt": "0.16.0\n", "tools/shellcheck-version.txt": "0.11.0\n", "tools/staticcheck-version.txt": "v0.7.0\n", "tools/ty-version.txt": "0.0.65\n", "tools/vulture-version.txt": "2.16\n", + "tools/rust-version.txt": "1.97.1\n", "tools/cargo-nextest-version.txt": "0.9.138\n", BinaryPath: "engine", LauncherBinaryPath: "launcher", } for relative, content := range files { diff --git a/internal/release/publication_test.go b/internal/release/publication_test.go index 7443e0a9..46f2b701 100644 --- a/internal/release/publication_test.go +++ b/internal/release/publication_test.go @@ -119,7 +119,7 @@ func assertReleaseSBOMComponents(t *testing.T, components []cdx.Component) { goModules++ } } - for _, required := range []string{"pkg:npm/fixture@1.0.0", "pkg:pypi/packaging@26.3", "pkg:pypi/vulture@2.16"} { + for _, required := range []string{"pkg:npm/fixture@1.0.0", "pkg:pypi/packaging@26.3", "pkg:pypi/vulture@2.16", "pkg:pypi/tree-sitter@0.26.0", "pkg:pypi/tree-sitter-rust@0.24.2", "pkg:generic/rust@1.97.1", "pkg:generic/cargo-nextest@0.9.138"} { if !seen[required] { t.Fatalf("SBOM omitted %s", required) } diff --git a/internal/release/release.go b/internal/release/release.go index 4e5bb4c5..a188bde1 100644 --- a/internal/release/release.go +++ b/internal/release/release.go @@ -26,7 +26,7 @@ const ( MaximumLockBytes = 32 << 10 LockVersion = 2 - ManifestVersion = 6 + ManifestVersion = 7 releasesDirectory = "releases" ) @@ -105,6 +105,8 @@ type Tools struct { Packaging string `json:"packaging"` Python string `json:"python"` Ruff string `json:"ruff"` + Rust string `json:"rust"` + Nextest string `json:"cargo-nextest"` Shellcheck string `json:"shellcheck"` Staticcheck string `json:"staticcheck"` Ty string `json:"ty"` @@ -309,6 +311,10 @@ func (manifest Manifest) Identity() string { if manifest.ManifestVersion >= 4 { fmt.Fprintf(identity, "tool.vulture=%s\n", manifest.Tools.Vulture) } + if manifest.ManifestVersion >= 7 { + fmt.Fprintf(identity, "tool.rust=%s\n", manifest.Tools.Rust) + fmt.Fprintf(identity, "tool.cargo-nextest=%s\n", manifest.Tools.Nextest) + } digest := sha256.Sum256([]byte(identity.String())) return hex.EncodeToString(digest[:]) } @@ -476,6 +482,12 @@ func validateManifestToolPins(manifest Manifest, source string) error { if manifest.ManifestVersion >= 5 { pins = append(pins, struct{ tool, version string }{"packaging", manifest.Tools.Packaging}) } + if manifest.ManifestVersion >= 7 { + pins = append(pins, + struct{ tool, version string }{"Rust", manifest.Tools.Rust}, + struct{ tool, version string }{"Nextest", manifest.Tools.Nextest}, + ) + } for _, pin := range pins { if !versionPattern.MatchString(pin.version) { return fmt.Errorf("%s records the unusable %s version %q", source, pin.tool, pin.version) diff --git a/internal/release/release_test.go b/internal/release/release_test.go index d4abc984..065a781b 100644 --- a/internal/release/release_test.go +++ b/internal/release/release_test.go @@ -42,6 +42,9 @@ func installedRelease(t *testing.T, files, links map[string]string) (string, Man files["tools/javascript_bundle_inventory.txt"] = "fixture@1.0.0\tMIT\n" } files["tools/astroid-version.txt"] = "4.1.2\n" + files["internal/rustfacts/pyproject.toml"] = "[project]\ndependencies=['tree-sitter==0.26.0','tree-sitter-rust==0.24.2']\n" + files[".tools/python/fixture/lib/python3.12/site-packages/tree_sitter-0.26.0.dist-info/METADATA"] = "Metadata-Version: 2.4\nName: tree-sitter\nVersion: 0.26.0\nLicense-Expression: MIT\n\n" + files[".tools/python/fixture/lib/python3.12/site-packages/tree_sitter_rust-0.24.2.dist-info/METADATA"] = "Metadata-Version: 2.4\nName: tree-sitter-rust\nVersion: 0.24.2\nLicense-Expression: MIT\n\n" files[".tools/python/fixture/lib/python3.12/site-packages/astroid-4.1.2.dist-info/METADATA"] = "Metadata-Version: 2.4\nName: astroid\nVersion: 4.1.2\nLicense-Expression: LGPL-2.1-or-later\n\n" files[".tools/python/fixture/lib/python3.12/site-packages/packaging-26.3.dist-info/METADATA"] = "Metadata-Version: 2.4\nName: packaging\nVersion: 26.3\n\n" files[".tools/python/fixture/lib/python3.12/site-packages/vulture-2.16.dist-info/METADATA"] = "Metadata-Version: 2.4\nName: vulture\nVersion: 2.16\nRequires-Dist: packaging>=25\n\n" @@ -72,7 +75,7 @@ func installedRelease(t *testing.T, files, links map[string]string) (string, Man Tools: Tools{ Go: "1.26.6", Govulncheck: "1.3.0", Node: "24.18.0", OSVScanner: "2.4.0", PNPM: "11.13.0", Packaging: "26.3", Python: "3.12.13+20260728", Ruff: "0.16.0", Shellcheck: "0.11.0", Staticcheck: "0.7.0", - Ty: "0.0.65", Vulture: "2.16", + Ty: "0.0.65", Vulture: "2.16", Rust: "1.97.1", Nextest: "0.9.138", }, ContentDigest: releaseEntriesDigest(entries), EntryCount: len(entries), Entries: entries, CapabilityCatalogSHA256: capabilityContentSHA256([]byte(files[CapabilityCatalogPath])), @@ -276,6 +279,8 @@ func TestParseManifestRejectsACorruptedRecord(t *testing.T) { "an unrecorded type checker": func(broken *Manifest) { broken.Tools.Ty = "" }, "an unrecorded dead-code tool": func(broken *Manifest) { broken.Tools.Vulture = "" }, "another manifest version": func(broken *Manifest) { broken.ManifestVersion = ManifestVersion + 1 }, + "missing Rust pin": func(broken *Manifest) { broken.Tools.Rust = "" }, + "missing Nextest pin": func(broken *Manifest) { broken.Tools.Nextest = "" }, } for name, corrupt := range cases { broken := manifest @@ -340,6 +345,8 @@ func TestIdentityNamesTheReleaseTheRecordDescribes(t *testing.T) { func(tools *Tools) { tools.Staticcheck = "0.7.1" }, func(tools *Tools) { tools.Ty = "0.0.66" }, func(tools *Tools) { tools.Vulture = "2.17" }, + func(tools *Tools) { tools.Rust = "1.97.2" }, + func(tools *Tools) { tools.Nextest = "0.9.139" }, } { other := manifest carried(&other.Tools) diff --git a/internal/release/rust_sbom_test.go b/internal/release/rust_sbom_test.go new file mode 100644 index 00000000..6ea94649 --- /dev/null +++ b/internal/release/rust_sbom_test.go @@ -0,0 +1,48 @@ +package release + +import ( + "os" + "path/filepath" + "slices" + "strings" + "testing" + + cdx "github.com/CycloneDX/cyclonedx-go" +) + +func TestReleaseSBOMRejectsIncompleteRustParserInventory(t *testing.T) { + const parserMetadata = ".tools/python/fixture/lib/python3.12/site-packages/tree_sitter-0.26.0.dist-info/METADATA" + for _, test := range []struct { + name, path, content, problem string + omit, append bool + }{ + {name: "missing wheel", path: parserMetadata, omit: true, problem: "inventory is incomplete"}, + {name: "wrong version", path: parserMetadata, content: "Metadata-Version: 2.4\nName: tree-sitter\nVersion: 0.25.0\n\n", problem: "does not match"}, + {name: "duplicate wheel", path: ".tools/python/extra/tree_sitter-0.26.0.dist-info/METADATA", content: "Metadata-Version: 2.4\nName: tree-sitter\nVersion: 0.26.0\n\n", append: true, problem: "inventory is incomplete"}, + {name: "unknown wheel", path: ".tools/python/extra/unreviewed-1.0.dist-info/METADATA", content: "Metadata-Version: 2.4\nName: unreviewed\nVersion: 1.0\n\n", append: true, problem: "ungoverned distribution"}, + {name: "missing pin", path: "internal/rustfacts/pyproject.toml", content: "[project]\ndependencies=['tree-sitter==0.26.0']\n", problem: "dependency inventory is incomplete"}, + {name: "nonexact pin", path: "internal/rustfacts/pyproject.toml", content: "[project]\ndependencies=['tree-sitter>=0.26.0','tree-sitter-rust==0.24.2']\n", problem: "exact version"}, + } { + t.Run(test.name, func(t *testing.T) { + root, manifest := installedRelease(t, map[string]string{BinaryPath: "fixture"}, nil) + if test.omit { + manifest.Entries = slices.DeleteFunc(manifest.Entries, func(entry Entry) bool { return entry.Path == test.path }) + } else { + path := filepath.Join(root, filepath.FromSlash(test.path)) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(test.content), 0o644); err != nil { + t.Fatal(err) + } + } + if test.append { + manifest.Entries = append(manifest.Entries, Entry{Path: test.path}) + } + inventory := releaseSBOMInventory{components: map[string]cdx.Component{}, dependencies: map[string]map[string]bool{}} + if err := inventory.addPython(root, "fixture", manifest); err == nil || !strings.Contains(err.Error(), test.problem) { + t.Fatalf("invalid parser inventory: %v; wanted %q", err, test.problem) + } + }) + } +} diff --git a/internal/release/sbom.go b/internal/release/sbom.go index e1ef92eb..1b53aa08 100644 --- a/internal/release/sbom.go +++ b/internal/release/sbom.go @@ -16,6 +16,7 @@ import ( "strings" cdx "github.com/CycloneDX/cyclonedx-go" + "github.com/pelletier/go-toml/v2" "github.com/riteofstring/code-polishy/internal/pythonfacts" ) @@ -86,7 +87,8 @@ func releaseSBOMTools(manifest Manifest) map[string]cdx.Component { "go": manifest.Tools.Go, "govulncheck": manifest.Tools.Govulncheck, "node": manifest.Tools.Node, "osv-scanner": manifest.Tools.OSVScanner, "pnpm": manifest.Tools.PNPM, "python": manifest.Tools.Python, "ruff": manifest.Tools.Ruff, "shellcheck": manifest.Tools.Shellcheck, "staticcheck": manifest.Tools.Staticcheck, - "ty": manifest.Tools.Ty, + "ty": manifest.Tools.Ty, + "rust": manifest.Tools.Rust, "cargo-nextest": manifest.Tools.Nextest, } result := make(map[string]cdx.Component, len(versions)) for name, version := range versions { @@ -300,6 +302,13 @@ func (inventory *releaseSBOMInventory) addPython(root, rootRef string, manifest return fmt.Errorf("read Astroid library pin: %w", err) } expected["astroid"] = strings.TrimSpace(string(astroid)) + parserPins, err := rustParserReleasePins(root) + if err != nil { + return err + } + for name, version := range parserPins { + expected[name] = version + } if len(inputs) != len(expected) { return errors.New("release Python distribution inventory is incomplete or contains an ungoverned distribution") } @@ -359,7 +368,7 @@ func pythonReleaseDistributions(inputs []pythonfacts.Input, expected map[string] func (inventory *releaseSBOMInventory) addPythonComponents(rootRef string, expected map[string]string, distributions map[string]pythonfacts.Distribution) (map[string]string, error) { refs := map[string]string{} - for _, name := range []string{"astroid", "packaging", "vulture"} { + for _, name := range []string{"astroid", "packaging", "vulture", "tree-sitter", "tree-sitter-rust"} { version := expected[name] distribution, found := distributions[name] if !found || distribution.Version != version { @@ -373,6 +382,33 @@ func (inventory *releaseSBOMInventory) addPythonComponents(rootRef string, expec return refs, nil } +func rustParserReleasePins(root string) (map[string]string, error) { + data, err := os.ReadFile(filepath.Join(root, "internal", "rustfacts", "pyproject.toml")) + if err != nil { + return nil, fmt.Errorf("read Rust parser dependency pins: %w", err) + } + var manifest struct { + Project struct { + Dependencies []string `toml:"dependencies"` + } `toml:"project"` + } + if err := toml.Unmarshal(data, &manifest); err != nil { + return nil, fmt.Errorf("parse Rust parser dependency pins: %w", err) + } + pins := map[string]string{} + for _, requirement := range manifest.Project.Dependencies { + name, version, exact := strings.Cut(requirement, "==") + if !exact || !slices.Contains([]string{"tree-sitter", "tree-sitter-rust"}, name) || !versionPattern.MatchString(version) || pins[name] != "" { + return nil, errors.New("rust parser release dependencies must name each supported distribution exactly once with an exact version") + } + pins[name] = version + } + if len(pins) != 2 { + return nil, errors.New("rust parser release dependency inventory is incomplete") + } + return pins, nil +} + func (inventory *releaseSBOMInventory) addPythonEdges(distributions map[string]pythonfacts.Distribution, refs map[string]string) error { for name, distribution := range distributions { for _, requirement := range distribution.Requirements { diff --git a/scripts/build-release.ps1 b/scripts/build-release.ps1 index 37080fd0..48f79d24 100644 --- a/scripts/build-release.ps1 +++ b/scripts/build-release.ps1 @@ -118,21 +118,47 @@ try { 'VERSION','LICENSE','README.md','CHANGELOG.md','docs','schema','templates','artifact-security', 'scripts/go_version.txt','tools/govulncheck-version.txt','tools/node-version.txt','tools/osv-scanner-version.txt', 'internal/pythonfacts/pyproject.toml','internal/pythonfacts/uv.lock','tools/packaging-version.txt','tools/packaging_wheel_checksums.txt','tools/astroid-version.txt','tools/astroid_checksums.txt', + 'internal/rustfacts/pyproject.toml','internal/rustfacts/uv.lock', + 'tools/rust-version.txt','tools/rust_checksums.txt','tools/cargo-nextest-version.txt','tools/cargo-nextest_checksums.txt', + 'tools/cargo-nextest-license.txt', 'tools/pnpm-version.txt','tools/python-version.txt','tools/python_runtime_checksums.txt','tools/ruff-version.txt', 'tools/shellcheck-version.txt','tools/staticcheck-version.txt','tools/ty-version.txt','tools/ty.toml', 'tools/vulture-version.txt','tools/vulture_wheel_checksums.txt', 'tools/trivy-version.txt','tools/javascript_bundle_inventory.txt','tools/javascript_runtime_binaries.txt', 'tools/javascript_runtime_checksums.txt','tools/windows_tool_checksums.txt', - '.tools/go/windows-amd64','.tools/shellcheck/windows-x86_64','.tools/javascript/windows-x64','.tools/python/windows-x64' + '.tools/go/windows-amd64','.tools/shellcheck/windows-x86_64','.tools/javascript/windows-x64','.tools/python/windows-x64', + '.tools/rust/windows-x64' )) { Copy-ReleasePath $Relative } Assert-ReleasePythonCarrier $Stage $StageBin = Join-Path $Stage '.tools/bin' New-Item -ItemType Directory -Force -Path $StageBin | Out-Null - foreach ($Tool in @('staticcheck.exe','govulncheck.exe','osv-scanner.exe','ruff.exe','ty.exe')) { + foreach ($Tool in @('staticcheck.exe','govulncheck.exe','osv-scanner.exe','ruff.exe','ty.exe','cargo-nextest.exe')) { Copy-Item -LiteralPath (Join-Path $PolicyRoot ".tools/bin/$Tool") -Destination (Join-Path $StageBin $Tool) } + $RustVersion = (Get-Content -Raw -LiteralPath (Join-Path $Stage 'tools/rust-version.txt')).Trim() + $RustRoot = Join-Path $Stage '.tools/rust/windows-x64' + foreach ($Tool in @('rustc','rustdoc','cargo','rustfmt','cargo-fmt','clippy-driver','cargo-clippy')) { + if (-not (Test-Path -LiteralPath (Join-Path $RustRoot "bin/$Tool.exe") -PathType Leaf)) { + throw "The carried Rust SDK is missing $Tool." + } + } + if ((Get-Content -Raw -LiteralPath (Join-Path $RustRoot '.code-polishy-rust-release')).Trim() -ne $RustVersion) { + throw 'The carried Rust SDK marker does not match its release pin.' + } + foreach ($Tool in @('rustc','rustdoc','cargo')) { + $Reported = @(& (Join-Path $RustRoot "bin/$Tool.exe") --version) + if ($LASTEXITCODE -ne 0 -or $Reported.Count -ne 1 -or ($Reported[0] -split '\s+')[1] -cne $RustVersion) { + throw "The carried $Tool does not match Rust $RustVersion." + } + } + $NextestVersion = (Get-Content -Raw -LiteralPath (Join-Path $Stage 'tools/cargo-nextest-version.txt')).Trim() + $Reported = @(& (Join-Path $StageBin 'cargo-nextest.exe') --version) + if ($LASTEXITCODE -ne 0 -or $Reported.Count -eq 0 -or ($Reported[0] -split '\s+')[1] -cne $NextestVersion) { + throw "The carried Nextest does not match $NextestVersion." + } + $Bin = Join-Path $Stage 'bin' New-Item -ItemType Directory -Path $Bin | Out-Null $Engine = Join-Path $Bin 'code-polishy.exe' diff --git a/scripts/install.sh b/scripts/install.sh index 14898c6d..a47031ee 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -305,6 +305,13 @@ release_contents=( "tools/osv-scanner-version.txt" "internal/pythonfacts/pyproject.toml" "internal/pythonfacts/uv.lock" + "internal/rustfacts/pyproject.toml" + "internal/rustfacts/uv.lock" + "tools/rust-version.txt" + "tools/rust_checksums.txt" + "tools/cargo-nextest-version.txt" + "tools/cargo-nextest_checksums.txt" + "tools/cargo-nextest-license.txt" "tools/astroid-version.txt" "tools/astroid_checksums.txt" "tools/packaging-version.txt" @@ -342,10 +349,13 @@ esac go_tool_dir=".tools/go/${javascript_os_tag}-${go_arch_tag}" shellcheck_tool_dir=".tools/shellcheck/${javascript_os_tag}-${shellcheck_arch_tag}" python_tool_dir=".tools/python/${javascript_platform_tag}" +rust_tool_dir=".tools/rust/${javascript_platform_tag}" policy_tools=( "${go_tool_dir}" "${shellcheck_tool_dir}" "${python_tool_dir}" + "${rust_tool_dir}" + ".tools/bin/cargo-nextest" ".tools/bin/staticcheck" ".tools/bin/govulncheck" ".tools/bin/osv-scanner" @@ -378,6 +388,10 @@ carried_tools=( "ruff:tools/ruff-version.txt" "ty:tools/ty-version.txt" "vulture:tools/vulture-version.txt" + "rustc:tools/rust-version.txt" + "rustdoc:tools/rust-version.txt" + "cargo:tools/rust-version.txt" + "cargo-nextest:tools/cargo-nextest-version.txt" ) carried_markers=( @@ -385,6 +399,7 @@ carried_markers=( "packaging:${python_tool_dir}/.code-polishy-packaging-release:tools/packaging-version.txt" "python:${python_tool_dir}/.code-polishy-python-release:tools/python-version.txt" "vulture:${python_tool_dir}/.code-polishy-vulture-release:tools/vulture-version.txt" + "rust:${rust_tool_dir}/.code-polishy-rust-release:tools/rust-version.txt" ) @@ -408,6 +423,12 @@ pinned_identity() { probed_version() { case "$1" in + rustc | rustdoc | cargo) + javascript_sealed_run "${policy_root}/${rust_tool_dir}/bin/$1" --version | awk 'NR == 1 { print $2 }' + ;; + cargo-nextest) + javascript_sealed_run "${policy_root}/.tools/bin/cargo-nextest" --version | awk 'NR == 1 { print $2 }' + ;; go) javascript_sealed_run "${policy_root}/${go_tool_dir}/go/bin/go" version | awk '{ sub(/^go/, "", $3); print $3 }' @@ -631,6 +652,12 @@ trap interrupted INT TERM HUP javascript_scratch_home="${scratch}/home" echo "Probing the exact version of every tool this release carries..." +for rust_tool in rustc rustdoc cargo rustfmt cargo-fmt clippy-driver cargo-clippy; do + if [[ ! -x "${policy_root}/${rust_tool_dir}/bin/${rust_tool}" ]]; then + echo "The carried Rust SDK is missing ${rust_tool}. Run ./tools/install-policy-tools.sh." >&2 + exit 1 + fi +done for carried in "${carried_tools[@]}"; do carried_tool="${carried%%:*}" carried_pin="${carried#*:}" diff --git a/scripts/release-manifest.sh b/scripts/release-manifest.sh index daf1c21c..82cb04ef 100755 --- a/scripts/release-manifest.sh +++ b/scripts/release-manifest.sh @@ -21,7 +21,7 @@ set -euo pipefail release_manifest_name="release-manifest.json" -release_manifest_version=6 +release_manifest_version=7 @@ -46,6 +46,8 @@ release_pin_files=( "tools/staticcheck-version.txt" "tools/ty-version.txt" "tools/vulture-version.txt" + "tools/rust-version.txt" + "tools/cargo-nextest-version.txt" ) case "$(uname -s)" in @@ -148,6 +150,8 @@ shellcheck_version="$(read_tool_pin "${release_pin_files[9]}")" staticcheck_version="$(read_tool_pin "${release_pin_files[10]}")" ty_version="$(read_tool_pin "${release_pin_files[11]}")" vulture_version="$(read_tool_pin "${release_pin_files[12]}")" +rust_version="$(read_tool_pin "${release_pin_files[13]}")" +nextest_version="$(read_tool_pin "${release_pin_files[14]}")" @@ -278,6 +282,8 @@ release_identity() { printf 'tool.staticcheck=%s\n' "${staticcheck_version}" printf 'tool.ty=%s\n' "${ty_version}" printf 'tool.vulture=%s\n' "${vulture_version}" + printf 'tool.rust=%s\n' "${rust_version}" + printf 'tool.cargo-nextest=%s\n' "${nextest_version}" } release_digest="$(release_identity | @@ -317,7 +323,9 @@ $(render_features) "shellcheck": "${shellcheck_version}", "staticcheck": "${staticcheck_version}", "ty": "${ty_version}", - "vulture": "${vulture_version}" + "vulture": "${vulture_version}", + "rust": "${rust_version}", + "cargo-nextest": "${nextest_version}" }, "releaseDigest": "${release_digest}", "contentDigest": "${content_digest}", diff --git a/scripts/test-install.sh b/scripts/test-install.sh index d63f4b6c..58fff543 100755 --- a/scripts/test-install.sh +++ b/scripts/test-install.sh @@ -207,6 +207,11 @@ EOF mkdir -p "${source_root}/internal/pythonfacts" printf '[project]\nname="fixture"\nversion="9.9.9"\n' >"${source_root}/internal/pythonfacts/pyproject.toml" printf 'version = 1\nrevision = 3\n' >"${source_root}/internal/pythonfacts/uv.lock" + copy_file "${policy_root}/internal/rustfacts/pyproject.toml" "${source_root}/internal/rustfacts/pyproject.toml" + copy_file "${policy_root}/internal/rustfacts/uv.lock" "${source_root}/internal/rustfacts/uv.lock" + for rust_pin in rust-version.txt rust_checksums.txt cargo-nextest-version.txt cargo-nextest_checksums.txt cargo-nextest-license.txt; do + copy_file "${policy_root}/tools/${rust_pin}" "${source_root}/tools/${rust_pin}" + done printf '3.12.13+20260728\n' >"${source_root}/tools/python-version.txt" printf '%s\n' 'cpython-3.12.13+20260728-x86_64-unknown-linux-gnu-install_only.tar.gz fd9d70e1e1ed3f6caccb4e2eefe570aa07589c8f86ddf0e87f68a96cd14272e1' \ >"${source_root}/tools/python_runtime_checksums.txt" @@ -369,6 +374,14 @@ EOF write_version_tool "${source_root}/.tools/bin/osv-scanner" --version "osv-scanner version: 2.4.0" write_version_tool "${source_root}/.tools/bin/ruff" --version "ruff 0.16.0" write_version_tool "${source_root}/.tools/bin/ty" --version "ty 0.0.65 (87de836df 2026-07-29)" + for rust_tool in rustc rustdoc cargo; do + write_version_tool "${source_root}/.tools/rust/${platform_tag}/bin/${rust_tool}" --version "${rust_tool} 1.97.1 (fixture)" + done + for rust_tool in rustfmt cargo-fmt clippy-driver cargo-clippy; do + write_stub_tool "${source_root}/.tools/rust/${platform_tag}/bin/${rust_tool}" + done + printf '1.97.1\n' >"${source_root}/.tools/rust/${platform_tag}/.code-polishy-rust-release" + write_version_tool "${source_root}/.tools/bin/cargo-nextest" --version "cargo-nextest 0.9.138 (fixture)" write_python_runtime_tool "${source_root}/.tools/python/${platform_tag}/python" "3.12.13" "2.16" "26.3" printf '3.12.13+20260728\n' >"${source_root}/.tools/python/${platform_tag}/.code-polishy-python-release" printf '26.3\n' >"${source_root}/.tools/python/${platform_tag}/.code-polishy-packaging-release" @@ -514,7 +527,7 @@ expected_revision="$("${real_git}" -C "${source_root}" rev-parse HEAD)" for carried in go:1.26.6 node:24.18.0 pnpm:11.13.0 packaging:26.3 shellcheck:0.11.0 \ staticcheck:0.7.0 govulncheck:1.3.0 osv-scanner:2.4.0 python:3.12.13+20260728 \ - ruff:0.16.0 ty:0.0.65 vulture:2.16; do + ruff:0.16.0 ty:0.0.65 vulture:2.16 rust:1.97.1 cargo-nextest:0.9.138; do [[ "$(manifest_field "${manifest}" "${carried%%:*}")" == "${carried##*:}" ]] || fail "the release does not record which ${carried%%:*} it carries" done @@ -536,6 +549,9 @@ for required in bin/code-polishy bin/code-polishy-launcher VERSION LICENSE READM tools/staticcheck-version.txt tools/govulncheck-version.txt \ tools/osv-scanner-version.txt tools/python-version.txt tools/python_runtime_checksums.txt \ internal/pythonfacts/pyproject.toml internal/pythonfacts/uv.lock \ + internal/rustfacts/pyproject.toml internal/rustfacts/uv.lock \ + tools/rust-version.txt tools/rust_checksums.txt tools/cargo-nextest-version.txt tools/cargo-nextest_checksums.txt \ + tools/cargo-nextest-license.txt \ tools/packaging-version.txt tools/packaging_wheel_checksums.txt \ tools/ruff-version.txt tools/ty-version.txt tools/ty.toml tools/vulture-version.txt tools/vulture_wheel_checksums.txt \ tools/trivy-version.txt tools/javascript_bundle_inventory.txt \ @@ -550,7 +566,8 @@ for required in bin/code-polishy bin/code-polishy-launcher VERSION LICENSE READM ".tools/go/${go_platform_tag}/go/bin/go" \ ".tools/go/${go_platform_tag}/go/bin/gofmt" \ ".tools/shellcheck/${shellcheck_platform_tag}/shellcheck" \ - .tools/bin/staticcheck .tools/bin/govulncheck .tools/bin/osv-scanner .tools/bin/ruff .tools/bin/ty; do + ".tools/rust/${platform_tag}/bin/rustc" ".tools/rust/${platform_tag}/bin/cargo-clippy" \ + .tools/bin/staticcheck .tools/bin/govulncheck .tools/bin/osv-scanner .tools/bin/ruff .tools/bin/ty .tools/bin/cargo-nextest; do [[ -e "${release}/${required}" ]] || fail "the release is missing ${required}" done [[ ! -e "${release}/scripts/automation" ]] || @@ -683,6 +700,25 @@ mv "${fixture_root}/held-osv-scanner" "${source_root}/.tools/bin/osv-scanner" [[ "$(installed_release_count)" == "1" ]] || fail "a checkout missing a pinned tool changed what is installed" +for rust_tool in rustc rustdoc cargo; do + write_version_tool "${source_root}/.tools/rust/${platform_tag}/bin/${rust_tool}" --version "${rust_tool} 1.97.2 (fixture)" + if install_release >"${fixture_root}/rust-version.log" 2>&1; then + fail "a checkout with the wrong ${rust_tool} installed a release" + fi + grep -q "${rust_tool}.*1.97.2" "${fixture_root}/rust-version.log" || + fail "the mismatched ${rust_tool} was rejected without its reported version" + write_version_tool "${source_root}/.tools/rust/${platform_tag}/bin/${rust_tool}" --version "${rust_tool} 1.97.1 (fixture)" +done +write_version_tool "${source_root}/.tools/bin/cargo-nextest" --version "cargo-nextest 0.9.139 (fixture)" +if install_release >"${fixture_root}/nextest-version.log" 2>&1; then + fail "a checkout with the wrong Nextest installed a release" +fi +grep -q 'cargo-nextest.*0.9.139' "${fixture_root}/nextest-version.log" || + fail "the mismatched Nextest was rejected without its reported version" +write_version_tool "${source_root}/.tools/bin/cargo-nextest" --version "cargo-nextest 0.9.138 (fixture)" +[[ "$(installed_release_count)" == "1" ]] || + fail "a mismatched Rust tool changed the installed release" + write_version_tool "${source_root}/.tools/bin/ruff" --version "ruff 0.99.0" if install_release >"${fixture_root}/tool-version.log" 2>&1; then fail "a checkout whose Ruff is not the pinned version installed a release" diff --git a/tools/cargo-nextest-license.txt b/tools/cargo-nextest-license.txt new file mode 100644 index 00000000..6699e0fe --- /dev/null +++ b/tools/cargo-nextest-license.txt @@ -0,0 +1,28 @@ +cargo-nextest 0.9.138 +Source: https://github.com/nextest-rs/nextest/blob/fc97e97bbe0a3927482a694247da00c099f4269e/LICENSE-MIT + +Copyright (c) The nextest Contributors + +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. From 1e57580b1515ec131ffcd6b4dc5debde417cd47b Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 04:59:45 +0000 Subject: [PATCH 20/49] fix: package Rust configuration and exercise installed workflows Installer contract and scoped policy checks pass. The native installed Rust fixture still awaits execution against this committed build. --- .code-polishy.json | 4 +- docs/design/installed-release-verification.md | 8 ++ scripts/build-release.ps1 | 1 + scripts/install.sh | 2 + scripts/test-install.sh | 3 +- scripts/test-installed-release-contract.sh | 4 + scripts/test-installed-release-rust.sh | 108 ++++++++++++++++++ scripts/test-installed-release.sh | 10 +- 8 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 scripts/test-installed-release-rust.sh diff --git a/.code-polishy.json b/.code-polishy.json index 94c89e7f..8b41d9da 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -251,7 +251,8 @@ "scripts/build-oci-image.sh", "scripts/test-installed-release.sh", "scripts/test-installed-release-contract.sh", - "scripts/test-installed-release-architecture.sh" + "scripts/test-installed-release-architecture.sh", + "scripts/test-installed-release-rust.sh" ] }, { @@ -1430,6 +1431,7 @@ "paths": [ "scripts/test-installed-release.sh", "scripts/test-installed-release-contract.sh", + "scripts/test-installed-release-rust.sh", "cmd/code-polishy/**", "internal/pack/**", "tools/fixtures/language-pack/**" diff --git a/docs/design/installed-release-verification.md b/docs/design/installed-release-verification.md index 33369014..d9b0e083 100644 --- a/docs/design/installed-release-verification.md +++ b/docs/design/installed-release-verification.md @@ -89,3 +89,11 @@ requires a separately carried upstream license. Its pinned MIT text is recorded with the corresponding source revision. That top-level notice does not establish complete third-party notices or an exact linked-dependency inventory for Nextest; those remain a publication requirement alongside native installed-Rust evidence. + +The installed Rust adoption fixture uses a dependency-free Cargo library in a +temporary repository. Every policy operation enters through the exact installed +launcher and lock. It exercises selected formatting, compiler diagnostics in JSON +and SARIF, a passing native suite, an assertion failure, and an empty selection. +The fixture checks the retained structured test outcome rather than treating the +CLI's nonzero exit as sufficient evidence. It never downloads target packages or +changes the caller's repository authority. diff --git a/scripts/build-release.ps1 b/scripts/build-release.ps1 index 48f79d24..32bbfe04 100644 --- a/scripts/build-release.ps1 +++ b/scripts/build-release.ps1 @@ -121,6 +121,7 @@ try { 'internal/rustfacts/pyproject.toml','internal/rustfacts/uv.lock', 'tools/rust-version.txt','tools/rust_checksums.txt','tools/cargo-nextest-version.txt','tools/cargo-nextest_checksums.txt', 'tools/cargo-nextest-license.txt', + 'tools/rustfmt.toml','tools/clippy.toml', 'tools/pnpm-version.txt','tools/python-version.txt','tools/python_runtime_checksums.txt','tools/ruff-version.txt', 'tools/shellcheck-version.txt','tools/staticcheck-version.txt','tools/ty-version.txt','tools/ty.toml', 'tools/vulture-version.txt','tools/vulture_wheel_checksums.txt', diff --git a/scripts/install.sh b/scripts/install.sh index a47031ee..5d430243 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -309,6 +309,8 @@ release_contents=( "internal/rustfacts/uv.lock" "tools/rust-version.txt" "tools/rust_checksums.txt" + "tools/rustfmt.toml" + "tools/clippy.toml" "tools/cargo-nextest-version.txt" "tools/cargo-nextest_checksums.txt" "tools/cargo-nextest-license.txt" diff --git a/scripts/test-install.sh b/scripts/test-install.sh index 58fff543..2e725734 100755 --- a/scripts/test-install.sh +++ b/scripts/test-install.sh @@ -209,7 +209,7 @@ EOF printf 'version = 1\nrevision = 3\n' >"${source_root}/internal/pythonfacts/uv.lock" copy_file "${policy_root}/internal/rustfacts/pyproject.toml" "${source_root}/internal/rustfacts/pyproject.toml" copy_file "${policy_root}/internal/rustfacts/uv.lock" "${source_root}/internal/rustfacts/uv.lock" - for rust_pin in rust-version.txt rust_checksums.txt cargo-nextest-version.txt cargo-nextest_checksums.txt cargo-nextest-license.txt; do + for rust_pin in rust-version.txt rust_checksums.txt rustfmt.toml clippy.toml cargo-nextest-version.txt cargo-nextest_checksums.txt cargo-nextest-license.txt; do copy_file "${policy_root}/tools/${rust_pin}" "${source_root}/tools/${rust_pin}" done printf '3.12.13+20260728\n' >"${source_root}/tools/python-version.txt" @@ -552,6 +552,7 @@ for required in bin/code-polishy bin/code-polishy-launcher VERSION LICENSE READM internal/rustfacts/pyproject.toml internal/rustfacts/uv.lock \ tools/rust-version.txt tools/rust_checksums.txt tools/cargo-nextest-version.txt tools/cargo-nextest_checksums.txt \ tools/cargo-nextest-license.txt \ + tools/rustfmt.toml tools/clippy.toml \ tools/packaging-version.txt tools/packaging_wheel_checksums.txt \ tools/ruff-version.txt tools/ty-version.txt tools/ty.toml tools/vulture-version.txt tools/vulture_wheel_checksums.txt \ tools/trivy-version.txt tools/javascript_bundle_inventory.txt \ diff --git a/scripts/test-installed-release-contract.sh b/scripts/test-installed-release-contract.sh index 4d799a0f..3faa8520 100755 --- a/scripts/test-installed-release-contract.sh +++ b/scripts/test-installed-release-contract.sh @@ -43,3 +43,7 @@ EOF --prefix "${fixture_root}/prefix" \ --lock "${fixture_root}/target/.code-polishy.lock.json" \ --fixture first-adoption +"${policy_root}/scripts/test-installed-release.sh" \ + --prefix "${fixture_root}/prefix" \ + --lock "${fixture_root}/target/.code-polishy.lock.json" \ + --fixture rust-adoption diff --git a/scripts/test-installed-release-rust.sh b/scripts/test-installed-release-rust.sh new file mode 100644 index 00000000..9d09b5c6 --- /dev/null +++ b/scripts/test-installed-release-rust.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash + +exercise_rust_adoption_fixture() { + local fixture_root="$1" real_git="$2" release="$3" launcher="$4" lock="$5" python + python="$(installed_fixture_python "${release}")" + "${python}" -I -B - "${fixture_root}" "${real_git}" "${release}" "${launcher}" "${lock}" <<'PY' +import json +import pathlib +import shutil +import subprocess +import sys + +fixture, git, release, launcher, lock = sys.argv[1:] +root = pathlib.Path(fixture) / "rust-adoption" +root.mkdir() + + +def write(path, content): + destination = root / path + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_text(content, encoding="utf-8") + + +def policy(*arguments, status=0, output="json"): + result = subprocess.run( + [launcher, "--repo-root", str(root), *arguments, "--format", output], + text=True, capture_output=True, timeout=300, check=False, + ) + if result.returncode != status: + raise AssertionError(f"{arguments}: exit {result.returncode}, expected {status}: {result.stdout[-8000:]} {result.stderr[-2000:]}") + return json.loads(result.stdout) + + +def rust_source(expected): + return f"""pub fn answer() -> u32 {{ + 42 +}} + +#[cfg(test)] +mod tests {{ + #[test] + fn answers() {{ + assert_eq!(super::answer(), {expected}); + }} +}} +""" + + +def assert_native_outcome(report, exit_status, category=""): + commands = report["testCommands"] + assert len(commands) == 1, commands + command = commands[0] + assert command["name"] == "rust-unit", command + assert command["result"]["exitStatus"] == exit_status, command + assert command.get("failureCategory", "") == category, command + if category != "operational": + artifacts = command["artifacts"] + assert len(artifacts) == 1 and artifacts[0]["type"] == "nextest-junit", command + assert artifacts[0]["size"] > 0 and len(artifacts[0]["sha256"]) == 64, command + executed = report["execution"]["commands"] + assert len(executed) == 1, executed + pathlib.Path(executed[0]["argv"][0]).resolve().relative_to(pathlib.Path(release).resolve()) + + +config = { + "version": 4, + "project": {"kind": "content"}, + "modules": [{"name": "library", "paths": ["Cargo.toml", "src/**"]}], + "tests": {"ownership": [], "suites": [ + {"name": "rust-unit", "kind": "unit", "scope": "module", "modules": ["library"], "rust": {}}, + {"name": "full", "kind": "unit", "scope": "repository", "rust": {}}, + ]}, +} +write(".code-polishy.json", json.dumps(config) + "\n") +write(".gitignore", "/.tools/\n/.code-polishy-artifacts/\n") +write("Cargo.toml", '[package]\nname="installed-rust"\nversion="1.0.0"\nedition="2024"\n[workspace]\n') +write("Cargo.lock", 'version=4\n[[package]]\nname="installed-rust"\nversion="1.0.0"\n') +write("src/lib.rs", rust_source(42)) +shutil.copyfile(lock, root / ".code-polishy.lock.json") +subprocess.run([git, "-C", str(root), "init", "--quiet"], check=True) +subprocess.run([launcher, "--repo-root", str(root), "agents", "install"], check=True) +policy("format", "--all") +subprocess.run([git, "-C", str(root), "add", "-A"], check=True) +subprocess.run([git, "-C", str(root), "-c", "user.name=Rust Fixture", "-c", "user.email=fixture@example.invalid", "commit", "--quiet", "-m", "fixture"], check=True) + +baseline = (root / "src/lib.rs").read_bytes() +write("src/lib.rs", rust_source(42).replace("pub fn answer() -> u32 {", "pub fn answer()->u32{")) +policy("format", "--files", "src/lib.rs") +assert (root / "src/lib.rs").read_bytes() == baseline +clean = policy("check", "--all") +assert clean["summary"]["errors"] == 0, clean +assert_native_outcome(policy("test", "--suite", "rust-unit"), 0) + +write("src/lib.rs", rust_source(41)) +assert_native_outcome(policy("test", "--suite", "rust-unit", status=1), 100, "command-exit") +write("src/lib.rs", "pub fn answer() -> u32 { 42 }\n") +assert_native_outcome(policy("test", "--suite", "rust-unit", status=1), 4, "operational") + +write("src/lib.rs", 'pub fn answer() -> u32 { "not an integer" }\n') +invalid = policy("check", "--files", "src/lib.rs", status=1) +assert any(finding["ruleId"] == "quality.rustCompiler" and finding["path"] == "src/lib.rs" for finding in invalid["findings"]), invalid +sarif = policy("check", "--files", "src/lib.rs", status=1, output="sarif") +assert any(result["ruleId"] == "quality.rustCompiler" for result in sarif["runs"][0]["results"]), sarif +write("src/lib.rs", rust_source(42)) +assert_native_outcome(policy("test", "--suite", "rust-unit"), 0) +print("Installed Rust formatting, compiler diagnostics, native evidence, failures, and recovery passed.") +PY +} diff --git a/scripts/test-installed-release.sh b/scripts/test-installed-release.sh index e3e145b0..430c1aa5 100755 --- a/scripts/test-installed-release.sh +++ b/scripts/test-installed-release.sh @@ -7,7 +7,7 @@ lock="${policy_root}/.code-polishy.lock.json" fixture="" usage() { - echo "usage: test-installed-release.sh [--prefix DIR] [--lock FILE] [--fixture first-adoption|scoped-analysis|python-adoption|python-reachability|python-pydantic]" >&2 + echo "usage: test-installed-release.sh [--prefix DIR] [--lock FILE] [--fixture first-adoption|scoped-analysis|python-adoption|python-reachability|python-pydantic|rust-adoption]" >&2 exit 2 } @@ -50,7 +50,7 @@ while (($#)); do esac done case "${fixture}" in - "" | first-adoption | scoped-analysis | python-adoption | python-reachability | python-pydantic) ;; + "" | first-adoption | scoped-analysis | python-adoption | python-reachability | python-pydantic | rust-adoption) ;; *) usage ;; esac @@ -260,8 +260,13 @@ source "${policy_root}/scripts/test-installed-release-scoped-analysis.sh" source "${policy_root}/scripts/test-installed-release-python-adoption.sh" source "${policy_root}/scripts/test-installed-release-python-reachability.sh" source "${policy_root}/scripts/test-installed-release-python-pydantic.sh" +source "${policy_root}/scripts/test-installed-release-rust.sh" case "${fixture}" in + rust-adoption) + exercise_rust_adoption_fixture "${fixture_root}" "${real_git}" "${release}" "${launcher}" "${lock}" + exit 0 + ;; scoped-analysis) exercise_scoped_analysis_fixture "${fixture_root}" "${release}" "${output}" echo "The installed release passed the scoped-analysis fixture." @@ -984,5 +989,6 @@ expect_finding "react" "quality.lint" "src/conditional.tsx" "react-hooks/rules-o exercise_python_adoption_fixture "${fixture_root}" "${real_git}" "${release}" exercise_python_reachability_fixture "${fixture_root}" "${real_git}" "${release}" "${launcher}" "${lock}" exercise_python_pydantic_fixture "${fixture_root}" "${real_git}" "${release}" "${launcher}" "${lock}" "${policy_root}" +exercise_rust_adoption_fixture "${fixture_root}" "${real_git}" "${release}" "${launcher}" "${lock}" echo "The installed release governed every disposable target shape." From f7984a778475feed22ed42a1689a1ecf94831bc3 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 05:04:59 +0000 Subject: [PATCH 21/49] docs: describe Rust adoption and verified Linux installation --- docs/ai-adoption.md | 13 ++++++++++--- docs/installation.md | 17 +++++++++++++++-- docs/plans/code-polishy-rust-support-plan.md | 14 +++++++++----- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/docs/ai-adoption.md b/docs/ai-adoption.md index 8a616077..4ee21335 100644 --- a/docs/ai-adoption.md +++ b/docs/ai-adoption.md @@ -358,13 +358,13 @@ ordinary full behavior suite as described in ## 5. Reuse policy-owned tools and add only project-specific providers -Allow source and dependency evidence to activate the shared Go, Shell, Ruff, +Allow source and dependency evidence to activate the shared Go, Rust, Shell, Ruff, Vulture, `ty`, Node/TypeScript, React, Electron, and OSV behavior. Do not copy equivalent commands into target JSON. Add target `checks` only for facts that cannot be shared safely, such as: -- an alias-aware non-Go architecture graph; +- architecture facts beyond native or installed language-pack analysis; - a production build; - a frozen workspace install or generated-lock consistency check; - a domain-specific content/schema validator; @@ -376,12 +376,19 @@ the modules and capability it covers, have a bounded timeout, and fail when its evidence is missing. It must not auto-install packages during a policy check. The installed release supplies the Go toolchain, ShellCheck, staticcheck, -govulncheck, Ruff, Vulture, its carried CPython runtime, `ty`, OSV-Scanner, and +govulncheck, Rust/Cargo/rustfmt/Clippy, Nextest, Ruff, Vulture, its carried CPython runtime, `ty`, OSV-Scanner, and the sealed JavaScript tooling. Do not add target-local copies of those tools or ask a target dependency to satisfy shared coverage. A missing or corrupted shared tool means the locked release must be installed or repaired; it is not a target dependency gap. +For Rust unit and integration suites, use the native `rust` selector described in +[Rust test evidence](design/rust-tests.md). Preserve Cargo workspace default +members and features unless the suite deliberately selects alternatives. Prepare +the reviewed Cargo lock and dependency cache before frozen checks or tests. Native +unit/integration evidence does not establish doctest, benchmark or live-provider +coverage; those workflows need their own explicit evidence and authorization. + Use existing target tools where honest for genuinely project-specific providers. If such a provider requires a new or updated target dependency, that dependency change is not authorized by the adoption request. First show: diff --git a/docs/installation.md b/docs/installation.md index 67b4ad9e..224181d5 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -251,6 +251,9 @@ One release identity has one self-contained policy root per supported host: - the `code-polishy` binary built from the reviewed commit with the pinned Go toolchain; - the sealed Node runtime and JavaScript tool bundle; +- Rust `1.97.1` with Cargo, rustdoc, rustfmt, Clippy and the host standard library; + Nextest `0.9.138`; and Tree-sitter's pinned Rust syntax libraries in the carried + Python runtime, with the policy-owned formatter and Clippy settings; - every other pinned tool the engine runs: the Go toolchain, ShellCheck, staticcheck, govulncheck, OSV-Scanner, Ruff, PyPA `packaging` `26.3`, Vulture `2.16`, `ty`, and the carried CPython `3.12.13+20260728` runtime from @@ -371,7 +374,7 @@ homes, temporary files, and target output remain outside the release tree. Before it stages anything, the installer asks every tool the release will carry what version it is and requires the answer to be the version the checked-in pin beside it names — the Go toolchain, Node, pnpm, ShellCheck, staticcheck, -govulncheck, OSV-Scanner, Ruff, PyPA `packaging`, Vulture, `ty`, and carried +govulncheck, OSV-Scanner, Ruff, Rust/Cargo/rustdoc, Nextest, PyPA `packaging`, Vulture, `ty`, and carried CPython. The manifest records those identities, and a present file and a byte inventory cannot show that a local tool cache holds the version the manifest would claim. The two Go analyzers are read out of their binaries with the pinned @@ -385,6 +388,12 @@ The installer verifies the CPython archive before extraction and the exact packages into that carried runtime. It removes `pip` and `ensurepip` and does not use a target `.venv` or target Python installation for either package. +Rust target builds also need the host's native linker and SDK: a C toolchain on +Linux, Xcode command-line tools on macOS, or the Visual Studio C++ build tools +and Windows SDK for MSVC. These host prerequisites are separate from the carried +Rust SDK; Code Polishy does not install system development packages. See the +[Rust platform prerequisites](https://doc.rust-lang.org/book/ch01-01-installation.html). + The installer stages a complete release, verifies the staged tree against the manifest it just wrote, and only then moves it into place under one name. A previously installed release is untouched until a verified replacement is ready. @@ -548,7 +557,7 @@ workspace-local. Self-hosting covers one target shape: a Go repository that also owns the sealed bundle's source. `./scripts/test-installed-release.sh` covers the others. It builds disposable repositories — Go with no JavaScript, a pnpm application, a -pnpm workspace, TypeScript, and React — gives each the lock this checkout +pnpm workspace, TypeScript, React, Python and Rust — gives each the lock this checkout requires, and governs them with the installed launcher. The Go target also runs the complete non-documentation flow: pre-coding intent capture, review preparation, red/green proof, finalization, checkpoint, and merge. It verifies @@ -561,6 +570,10 @@ network; it needs an installed release named by the selected lock. `--prefix` selects the release store, and `--lock` can select a temporary candidate lock without changing this repository's checked-in lock. +`--fixture rust-adoption` selects the dependency-free Cargo fixture. It exercises +the carried formatter, compiler and parser, JSON/SARIF findings, structured native +test outcomes, empty-test rejection, and recovery after correcting a failure. + What it proves is the release the selected lock names. By default that is this checkout's lock, which must be refreshed before validating a newly published release. A temporary lock can name a clean installed candidate before the public diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 30c5d109..32e2a7be 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -393,8 +393,9 @@ tools. [Cargo project facts](../design/cargo-facts.md) owns that boundary. Developer setup installs authenticated Rust 1.97.1 components without rustup or installation scripts. Linux runtime execution and installer integrity/recovery -fixtures pass. macOS/Windows installers and checksums are present but host execution -and release packaging still require verification. +fixtures pass. Native release packaging and a dependency-free installed Cargo +workflow are verified on Linux x64. The remaining host executions still require +verification; macOS/Windows installers and pinned archives alone are not proof. Selected-file formatting, compiler diagnostics, parsed source policy and Cargo dependency governance now have runnable beta implementations. Source parsing uses @@ -406,9 +407,12 @@ ordinary commands, gate artifacts, regression proofs and reusable receipts. The [Rust test contract](../design/rust-tests.md) documents selectors and boundaries. Native release builders now carry the Rust SDK, Nextest and parser dependencies. Manifest version 7 binds the executable pins; the SBOM includes both parser -distributions and the top-level Rust tools. Native installed-Rust fixtures and -complete bundled dependency notices/inventory remain unverified publication -requirements. No beta release has been published or adopted. +distributions and the top-level Rust tools. The installed Rust fixture passes on +Linux x64 against committed source `1e57580`: selected formatting, JSON/SARIF +compiler diagnostics, nonempty native tests, assertion failure, zero-test +rejection, and recovery through the verified launcher. All-host installed evidence +and complete bundled dependency notices/inventory remain publication requirements. +No beta release has been published or adopted. The next milestones extend ordinary macro/module and doctest coverage and complete installed-release integration. Existing coverage failures remain until From 16a7ea2be6b92bb6d209e8e5b230850ba1233e04 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 05:09:26 +0000 Subject: [PATCH 22/49] test: exercise installed Rust on every release host --- .code-polishy.json | 4 ++- .github/workflows/release.yml | 5 +++ docs/design/installed-release-verification.md | 7 ++++ docs/release-checklist.md | 5 ++- scripts/test-installed-release-contract.sh | 6 ++-- scripts/test-installed-release-rust.sh | 36 ++++++++++++++++++- 6 files changed, 56 insertions(+), 7 deletions(-) diff --git a/.code-polishy.json b/.code-polishy.json index 8b41d9da..294e2125 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -1479,6 +1479,7 @@ "scripts/test-javascript-project.sh", "scripts/test-installed-release.sh", "scripts/test-installed-release-contract.sh", + "scripts/test-installed-release-rust.sh", "scripts/test-build-oci-image.sh", "scripts/test-javascript-provider.sh" ], @@ -1661,7 +1662,8 @@ { "paths": [ "scripts/test-installed-release.sh", - "scripts/test-installed-release-contract.sh" + "scripts/test-installed-release-contract.sh", + "scripts/test-installed-release-rust.sh" ], "module": "tooling", "focusedSuite": "installed-release-contract" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fd5dd8a6..286b6d99 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -188,6 +188,7 @@ jobs: echo "The fresh installation did not report Code Polishy ${RELEASE_VERSION}." >&2 exit 1 fi + bash ./scripts/test-installed-release-rust.sh "${installed_root}" "${installation}/bin/code-polishy" - name: Build and smoke-test native archive if: matrix.build == 'windows' @@ -235,6 +236,10 @@ jobs: if ($LASTEXITCODE -ne 0 -or $reportedVersion -ne "code-polishy $env:RELEASE_VERSION") { throw "The fresh installation did not report Code Polishy $env:RELEASE_VERSION." } + $bash = Join-Path $env:ProgramFiles 'Git\bin\bash.exe' + if (-not (Test-Path -LiteralPath $bash -PathType Leaf)) { throw 'The native Rust fixture requires Git Bash.' } + & $bash ./scripts/test-installed-release-rust.sh $installedRoot (Join-Path $installation 'bin\code-polishy.exe') + if ($LASTEXITCODE -ne 0) { throw 'The installed Rust fixture failed.' } - name: Retain native publication uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 diff --git a/docs/design/installed-release-verification.md b/docs/design/installed-release-verification.md index d9b0e083..71d8e0d7 100644 --- a/docs/design/installed-release-verification.md +++ b/docs/design/installed-release-verification.md @@ -97,3 +97,10 @@ and SARIF, a passing native suite, an assertion failure, and an empty selection. The fixture checks the retained structured test outcome rather than treating the CLI's nonzero exit as sufficient evidence. It never downloads target packages or changes the caller's repository authority. + +Each native release job also invokes this fixture against its freshly installed +archive. The standalone fixture constructs an inert publication-shaped lock from +the selected manifest, uses the installed Python and launcher, and keeps target +state in a temporary directory. The same scenario runs on all five native hosts; +it does not rebuild the archive or repeat the ordinary source gate. The fixture's +URLs cannot acquire a release and never replace a consuming repository's lock. diff --git a/docs/release-checklist.md b/docs/release-checklist.md index 7403a7ed..eb787e94 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -73,7 +73,10 @@ manual dispatch authorizes the same operation for an existing tag. source-install contracts, or supplemental suites already owned by normal CI. Each host only verifies the generated manifest, installs its archive into a fresh prefix through the recorded SHA-256, and checks the installed - version. The retained publication contains the archive, internal manifest, + version. It then runs the dependency-free installed Rust fixture against that + same native installation, covering formatter/compiler/parser execution, + structured test outcomes, zero-test rejection and recovery. The retained + publication contains the archive, internal manifest, CycloneDX SBOM, deterministic in-toto/SLSA metadata, and release descriptor. 7. After every host succeeds, the publish job restores the five publication diff --git a/scripts/test-installed-release-contract.sh b/scripts/test-installed-release-contract.sh index 3faa8520..0da649eb 100755 --- a/scripts/test-installed-release-contract.sh +++ b/scripts/test-installed-release-contract.sh @@ -43,7 +43,5 @@ EOF --prefix "${fixture_root}/prefix" \ --lock "${fixture_root}/target/.code-polishy.lock.json" \ --fixture first-adoption -"${policy_root}/scripts/test-installed-release.sh" \ - --prefix "${fixture_root}/prefix" \ - --lock "${fixture_root}/target/.code-polishy.lock.json" \ - --fixture rust-adoption +bash "${policy_root}/scripts/test-installed-release-rust.sh" \ + "$(dirname "${manifest}")" "${fixture_root}/prefix/bin/code-polishy" diff --git a/scripts/test-installed-release-rust.sh b/scripts/test-installed-release-rust.sh index 9d09b5c6..c176cc19 100644 --- a/scripts/test-installed-release-rust.sh +++ b/scripts/test-installed-release-rust.sh @@ -21,6 +21,26 @@ def write(path, content): destination.write_text(content, encoding="utf-8") +def install_fixture_lock(): + if lock: + shutil.copyfile(lock, root / ".code-polishy.lock.json") + return + manifest = json.loads((pathlib.Path(release) / "release-manifest.json").read_text(encoding="utf-8")) + hosts = ["darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64", "windows-x64"] + value = { + "lockVersion": 2, + "codePolishyVersion": manifest["codePolishyVersion"], + "releaseDigest": manifest["releaseDigest"], + "features": manifest["features"], + "publication": { + "indexUrl": "https://example.invalid/release-index.json", + "indexSha256": "a" * 64, + "archives": [{"host": host, "url": f"https://example.invalid/{host}.zip", "sha256": "b" * 64, "size": 1} for host in hosts], + }, + } + write(".code-polishy.lock.json", json.dumps(value) + "\n") + + def policy(*arguments, status=0, output="json"): result = subprocess.run( [launcher, "--repo-root", str(root), *arguments, "--format", output], @@ -76,7 +96,7 @@ write(".gitignore", "/.tools/\n/.code-polishy-artifacts/\n") write("Cargo.toml", '[package]\nname="installed-rust"\nversion="1.0.0"\nedition="2024"\n[workspace]\n') write("Cargo.lock", 'version=4\n[[package]]\nname="installed-rust"\nversion="1.0.0"\n') write("src/lib.rs", rust_source(42)) -shutil.copyfile(lock, root / ".code-polishy.lock.json") +install_fixture_lock() subprocess.run([git, "-C", str(root), "init", "--quiet"], check=True) subprocess.run([launcher, "--repo-root", str(root), "agents", "install"], check=True) policy("format", "--all") @@ -106,3 +126,17 @@ assert_native_outcome(policy("test", "--suite", "rust-unit"), 0) print("Installed Rust formatting, compiler diagnostics, native evidence, failures, and recovery passed.") PY } + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + set -euo pipefail + if [[ "$#" -ne 2 || ! -d "$1" || ! -x "$2" ]]; then + echo 'usage: test-installed-release-rust.sh RELEASE_ROOT LAUNCHER' >&2 + exit 2 + fi + policy_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" + source "${policy_root}/scripts/test-installed-release-architecture.sh" + fail() { echo "installed Rust fixture failure: $1" >&2; exit 1; } + fixture_root="$(mktemp -d "${TMPDIR:-/tmp}/code-polishy-installed-rust.XXXXXX")" + trap 'rm -rf -- "${fixture_root}"' EXIT + exercise_rust_adoption_fixture "${fixture_root}" "$(command -v git)" "$1" "$2" "" +fi From e1f6cd0c68a0033df029269bd65194c34081c736 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 05:25:06 +0000 Subject: [PATCH 23/49] feat: bind Rust release SBOM to reviewed native artifacts --- .code-polishy.json | 2 + docs/design/installed-release-verification.md | 13 ++ docs/plans/code-polishy-rust-support-plan.md | 11 ++ internal/release/publication_test.go | 11 ++ internal/release/release_test.go | 1 + internal/release/rust_sbom.go | 176 ++++++++++++++++++ internal/release/rust_sbom_test.go | 174 +++++++++++++++++ internal/release/sbom.go | 5 + 8 files changed, 393 insertions(+) create mode 100644 internal/release/rust_sbom.go diff --git a/.code-polishy.json b/.code-polishy.json index 294e2125..0320c743 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -246,6 +246,8 @@ "internal/release/manifest_writer.go", "internal/release/launcher_manifest.go", "internal/release/sbom.go", + "internal/release/rust_sbom.go", + "internal/release/rust_sbom_test.go", "scripts/release-manifest.sh", "scripts/build-release.ps1", "scripts/build-oci-image.sh", diff --git a/docs/design/installed-release-verification.md b/docs/design/installed-release-verification.md index 71d8e0d7..3cf56d38 100644 --- a/docs/design/installed-release-verification.md +++ b/docs/design/installed-release-verification.md @@ -90,6 +90,19 @@ with the corresponding source revision. That top-level notice does not establish complete third-party notices or an exact linked-dependency inventory for Nextest; those remain a publication requirement alongside native installed-Rust evidence. +The SBOM links each Rust tool to the selected host's reviewed upstream archive +URLs and SHA-256 identities, reusing the installer's checksum inventory. Archive +digests describe those downloads, not the extracted executable. Missing, +duplicate, foreign-version or malformed archive rows fail composition. The +selected SDK's primary licenses and upstream copyright inventories must remain +nonempty regular files in the sealed manifest; Nextest's primary notice must name +its exact version and immutable source revision. Notice locations are recorded +on their components. CycloneDX compositions explicitly mark both native tools' +transitive inventories incomplete until linked-dependency evidence exists. +An upstream source lock is not an exact linked graph, and a dependency's declared +license expression is not its copyright notice. These references authenticate +reviewed inputs, not the upstream builder's identity. + The installed Rust adoption fixture uses a dependency-free Cargo library in a temporary repository. Every policy operation enters through the exact installed launcher and lock. It exercises selected formatting, compiler diagnostics in JSON diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 32e2a7be..e5d1929d 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -414,6 +414,17 @@ rejection, and recovery through the verified launcher. All-host installed eviden and complete bundled dependency notices/inventory remain publication requirements. No beta release has been published or adopted. +The Nextest source audit at `fc97e97bbe0a3927482a694247da00c099f4269e` +found 580 locked packages: 571 crates.io archives and 9 workspace packages, +including upstream test fixtures. All 571 archive checksums matched on +2026-09-25. A recursive notice-filename inventory found 979 files, but 45 crates +had no packaged license/notice/copyright files under those names. This is a +source-inventory observation, not proof of the linked binary graph or license +compliance. Completing the notice set requires exact upstream source evidence; +copying the lock or substituting generic license text does not resolve it. +Rust and Nextest SBOM entries retain explicit incomplete native compositions, +host-specific authenticated download references and carried notice locations. + The next milestones extend ordinary macro/module and doctest coverage and complete installed-release integration. Existing coverage failures remain until their implementations and evidence exist. This beta does not yet establish diff --git a/internal/release/publication_test.go b/internal/release/publication_test.go index 46f2b701..962b4045 100644 --- a/internal/release/publication_test.go +++ b/internal/release/publication_test.go @@ -323,6 +323,17 @@ func foreignRelease(t *testing.T, host string) (string, Manifest) { } } } + if host != manifest.Host { + before, after := ".tools/rust/"+manifest.Host, ".tools/rust/"+host + if err := os.Rename(filepath.Join(root, filepath.FromSlash(before)), filepath.Join(root, filepath.FromSlash(after))); err != nil { + t.Fatal(err) + } + for index := range manifest.Entries { + if rest, found := strings.CutPrefix(manifest.Entries[index].Path, before+"/"); found { + manifest.Entries[index].Path = after + "/" + rest + } + } + } manifest.Host = host slices.SortFunc(manifest.Entries, func(first, second Entry) int { return strings.Compare(first.Path, second.Path) }) manifest.ContentDigest = releaseEntriesDigest(manifest.Entries) diff --git a/internal/release/release_test.go b/internal/release/release_test.go index 065a781b..874cb704 100644 --- a/internal/release/release_test.go +++ b/internal/release/release_test.go @@ -48,6 +48,7 @@ func installedRelease(t *testing.T, files, links map[string]string) (string, Man files[".tools/python/fixture/lib/python3.12/site-packages/astroid-4.1.2.dist-info/METADATA"] = "Metadata-Version: 2.4\nName: astroid\nVersion: 4.1.2\nLicense-Expression: LGPL-2.1-or-later\n\n" files[".tools/python/fixture/lib/python3.12/site-packages/packaging-26.3.dist-info/METADATA"] = "Metadata-Version: 2.4\nName: packaging\nVersion: 26.3\n\n" files[".tools/python/fixture/lib/python3.12/site-packages/vulture-2.16.dist-info/METADATA"] = "Metadata-Version: 2.4\nName: vulture\nVersion: 2.16\nRequires-Dist: packaging>=25\n\n" + addRustReleaseInventoryFixture(t, files, host) paths := append(slices.Collect(maps.Keys(files)), slices.Collect(maps.Keys(links))...) slices.Sort(paths) entries := []Entry{} diff --git a/internal/release/rust_sbom.go b/internal/release/rust_sbom.go new file mode 100644 index 00000000..7fb1069b --- /dev/null +++ b/internal/release/rust_sbom.go @@ -0,0 +1,176 @@ +package release + +import ( + "fmt" + "os" + "strings" + + cdx "github.com/CycloneDX/cyclonedx-go" +) + +var rustReleaseTargets = map[string]string{ + "darwin-arm64": "aarch64-apple-darwin", + "darwin-x64": "x86_64-apple-darwin", + "linux-arm64": "aarch64-unknown-linux-gnu", + "linux-x64": "x86_64-unknown-linux-gnu", + "windows-x64": "x86_64-pc-windows-msvc", +} + +var rustReleaseComponents = []string{"cargo", "clippy", "rust-std", "rustc", "rustfmt"} + +func rustReleaseSBOMEvidence(root string, manifest Manifest, tools map[string]cdx.Component) ([]cdx.Composition, error) { + sealed, err := os.OpenRoot(root) + if err != nil { + return nil, err + } + defer sealed.Close() + for _, name := range []string{"rust", "cargo-nextest"} { + component := tools[name] + references, err := rustReleaseDistributions(sealed, manifest, name) + if err != nil { + return nil, err + } + properties, err := rustReleaseNotices(sealed, manifest, name) + if err != nil { + return nil, err + } + if name == "cargo-nextest" { + source, err := nextestReleaseSource(sealed, manifest.Tools.Nextest) + if err != nil { + return nil, err + } + references = append(references, cdx.ExternalReference{Type: cdx.ERTypeLicense, URL: source}) + } + component.ExternalReferences = &references + component.Properties = &properties + tools[name] = component + } + refs := []cdx.BOMReference{cdx.BOMReference(tools["cargo-nextest"].BOMRef), cdx.BOMReference(tools["rust"].BOMRef)} + return []cdx.Composition{{Aggregate: cdx.CompositionAggregateIncomplete, Assemblies: &refs, Dependencies: &refs}}, nil +} + +func rustReleaseArchives(manifest Manifest, name, host string) ([]string, error) { + target := rustReleaseTargets[host] + if target == "" { + return nil, fmt.Errorf("rust release inventory has no target for %q", host) + } + if name == "rust" { + archives := make([]string, 0, len(rustReleaseComponents)) + for _, component := range rustReleaseComponents { + archives = append(archives, component+"-"+manifest.Tools.Rust+"-"+target+".tar.xz") + } + return archives, nil + } + extension := ".tar.gz" + if strings.HasPrefix(host, "darwin-") { + target = "universal-apple-darwin" + } else if host == "windows-x64" { + extension = ".zip" + } + return []string{"cargo-nextest-" + manifest.Tools.Nextest + "-" + target + extension}, nil +} + +func rustReleaseDistributions(root *os.Root, manifest Manifest, name string) ([]cdx.ExternalReference, error) { + expected := map[string]string{} + for _, host := range supportedReleaseHosts { + archives, err := rustReleaseArchives(manifest, name, host) + if err != nil { + return nil, err + } + for _, archive := range archives { + expected[archive] = "" + } + } + relative := "tools/" + name + "_checksums.txt" + if !manifestRecordsCapabilityFile(manifest, relative, "") { + return nil, fmt.Errorf("%s release inventory is absent from the manifest", name) + } + data, err := readCapabilityFile(root, relative, 32<<10) + if err != nil { + return nil, err + } + if err := readRustArchiveChecksums(data, expected); err != nil { + return nil, fmt.Errorf("%s release inventory: %w", name, err) + } + archives, err := rustReleaseArchives(manifest, name, manifest.Host) + if err != nil { + return nil, err + } + prefix := "https://static.rust-lang.org/dist/" + if name == "cargo-nextest" { + prefix = "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-" + manifest.Tools.Nextest + "/" + } + references := make([]cdx.ExternalReference, 0, len(archives)) + for _, archive := range archives { + hashes := []cdx.Hash{{Algorithm: cdx.HashAlgoSHA256, Value: expected[archive]}} + references = append(references, cdx.ExternalReference{Type: cdx.ERTypeDistribution, URL: prefix + archive, Hashes: &hashes}) + } + return references, nil +} + +func readRustArchiveChecksums(data []byte, expected map[string]string) error { + for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") { + fields := strings.Fields(line) + if len(fields) != 2 || !digestPattern.MatchString(fields[1]) { + return fmt.Errorf("archive checksum row is malformed") + } + digest, present := expected[fields[0]] + if !present || digest != "" { + return fmt.Errorf("unexpected or duplicate archive %q", fields[0]) + } + expected[fields[0]] = fields[1] + } + for archive, digest := range expected { + if digest == "" { + return fmt.Errorf("missing archive %q", archive) + } + } + return nil +} + +func rustReleaseNoticePaths(host, name string) []string { + if name == "cargo-nextest" { + return []string{"tools/cargo-nextest-license.txt"} + } + prefix := ".tools/rust/" + host + "/share/" + paths := []string{prefix + "doc/rust/COPYRIGHT.html", prefix + "doc/rust/COPYRIGHT-library.html", prefix + "doc/cargo/LICENSE-THIRD-PARTY"} + for _, component := range rustReleaseComponents { + for _, license := range []string{"LICENSE-APACHE", "LICENSE-MIT"} { + paths = append(paths, prefix+"licenses/"+component+"/"+license) + } + } + return paths +} + +func rustReleaseNotices(root *os.Root, manifest Manifest, name string) ([]cdx.Property, error) { + var properties []cdx.Property + for _, relative := range rustReleaseNoticePaths(manifest.Host, name) { + if !manifestRecordsCapabilityFile(manifest, relative, "") { + return nil, fmt.Errorf("%s release notice is absent from the manifest: %s", name, relative) + } + info, err := capabilityFileInfo(root, relative) + if err != nil || info.Size() == 0 { + return nil, fmt.Errorf("%s release notice must be a nonempty contained regular file: %s", name, relative) + } + properties = append(properties, cdx.Property{Name: "code-polishy:notice-path", Value: relative}) + } + return properties, nil +} + +func nextestReleaseSource(root *os.Root, version string) (string, error) { + data, err := readCapabilityFile(root, "tools/cargo-nextest-license.txt", 32<<10) + if err != nil { + return "", err + } + header, body, found := strings.Cut(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n\n") + lines := strings.Split(header, "\n") + if !found || len(lines) != 2 || lines[0] != "cargo-nextest "+version || strings.TrimSpace(body) == "" { + return "", fmt.Errorf("cargo-nextest notice does not identify its exact release and source") + } + const prefix = "Source: https://github.com/nextest-rs/nextest/blob/" + revision, found := strings.CutPrefix(lines[1], prefix) + if !found || !strings.HasSuffix(revision, "/LICENSE-MIT") || !revisionPattern.MatchString(strings.TrimSuffix(revision, "/LICENSE-MIT")) { + return "", fmt.Errorf("cargo-nextest notice must identify an immutable upstream license") + } + return strings.TrimPrefix(lines[1], "Source: "), nil +} diff --git a/internal/release/rust_sbom_test.go b/internal/release/rust_sbom_test.go index 6ea94649..08a01279 100644 --- a/internal/release/rust_sbom_test.go +++ b/internal/release/rust_sbom_test.go @@ -1,6 +1,7 @@ package release import ( + "bytes" "os" "path/filepath" "slices" @@ -10,6 +11,179 @@ import ( cdx "github.com/CycloneDX/cyclonedx-go" ) +func addRustReleaseInventoryFixture(t *testing.T, files map[string]string, host string) { + t.Helper() + for _, name := range []string{"rust_checksums.txt", "cargo-nextest_checksums.txt", "cargo-nextest-license.txt"} { + data, err := os.ReadFile(filepath.Join("..", "..", "tools", name)) + if err != nil { + t.Fatal(err) + } + files["tools/"+name] = string(data) + } + for _, relative := range rustReleaseNoticePaths(host, "rust") { + files[relative] = "upstream notice\n" + } +} + +func TestReleaseSBOMBindsRustDownloadsToTheSelectedHost(t *testing.T) { + for _, test := range []struct { + host, target, nextestArchive string + }{ + {"darwin-arm64", "aarch64-apple-darwin", "universal-apple-darwin.tar.gz"}, + {"darwin-x64", "x86_64-apple-darwin", "universal-apple-darwin.tar.gz"}, + {"linux-arm64", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-gnu.tar.gz"}, + {"linux-x64", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-gnu.tar.gz"}, + {"windows-x64", "x86_64-pc-windows-msvc", "x86_64-pc-windows-msvc.zip"}, + } { + t.Run(test.host, func(t *testing.T) { + root, manifest := installedRelease(t, map[string]string{BinaryPath: "fixture"}, nil) + manifest.Host = test.host + sealed, err := os.OpenRoot(root) + if err != nil { + t.Fatal(err) + } + defer sealed.Close() + for _, name := range []string{"rust", "cargo-nextest"} { + references, err := rustReleaseDistributions(sealed, manifest, name) + if err != nil { + t.Fatal(err) + } + wanted := []string{"https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-0.9.138/cargo-nextest-0.9.138-" + test.nextestArchive} + if name == "rust" { + wanted = nil + for _, component := range []string{"cargo", "clippy", "rust-std", "rustc", "rustfmt"} { + wanted = append(wanted, "https://static.rust-lang.org/dist/"+component+"-1.97.1-"+test.target+".tar.xz") + } + } + assertRustArchiveReferences(t, root, name, references, wanted) + } + }) + } +} + +func assertRustArchiveReferences(t *testing.T, root, name string, references []cdx.ExternalReference, wanted []string) { + t.Helper() + data, err := os.ReadFile(filepath.Join(root, "tools", name+"_checksums.txt")) + if err != nil || len(references) != len(wanted) { + t.Fatalf("archive inventory = %+v, error = %v", references, err) + } + for index, reference := range references { + if reference.Type != cdx.ERTypeDistribution || reference.URL != wanted[index] || reference.Hashes == nil || len(*reference.Hashes) != 1 { + t.Fatalf("archive reference = %+v, wanted %s", reference, wanted[index]) + } + hash := (*reference.Hashes)[0] + row := filepath.Base(reference.URL) + " " + hash.Value + "\n" + if hash.Algorithm != cdx.HashAlgoSHA256 || !strings.Contains(string(data), row) { + t.Fatalf("archive identity does not come from its checked-in pin: %+v", reference) + } + } +} + +func TestReleaseSBOMRetainsNativeNoticeLocationsAndIncompleteComposition(t *testing.T) { + root, manifest := installedRelease(t, map[string]string{BinaryPath: "fixture"}, nil) + data, err := renderReleaseSBOM(manifest, root, "release.zip", exampleDigest) + if err != nil { + t.Fatal(err) + } + document := &cdx.BOM{} + if err := cdx.NewBOMDecoder(bytes.NewReader(data), cdx.BOMFileFormatJSON).Decode(document); err != nil { + t.Fatal(err) + } + refs := []cdx.BOMReference{"pkg:generic/cargo-nextest@0.9.138", "pkg:generic/rust@1.97.1"} + if document.Compositions == nil || len(*document.Compositions) != 1 { + t.Fatalf("native dependency coverage omitted: %+v", document.Compositions) + } + composition := (*document.Compositions)[0] + if composition.Aggregate != cdx.CompositionAggregateIncomplete || composition.Assemblies == nil || composition.Dependencies == nil || !slices.Equal(*composition.Assemblies, refs) || !slices.Equal(*composition.Dependencies, refs) { + t.Fatalf("native dependency coverage = %+v", composition) + } + for _, component := range *document.Components { + if component.Name != "rust" && component.Name != "cargo-nextest" { + continue + } + assertRustNoticeEvidence(t, manifest.Host, component) + } +} + +func assertRustNoticeEvidence(t *testing.T, host string, component cdx.Component) { + t.Helper() + if component.Hashes != nil || component.Properties == nil || component.ExternalReferences == nil { + t.Fatalf("native archive and component identities conflated: %+v", component) + } + wanted := "tools/cargo-nextest-license.txt" + if component.Name == "rust" { + wanted = ".tools/rust/" + host + "/share/doc/rust/COPYRIGHT.html" + } + if !slices.Contains(*component.Properties, cdx.Property{Name: "code-polishy:notice-path", Value: wanted}) { + t.Fatalf("native notice omitted: %+v", component) + } + if component.Name == "cargo-nextest" { + license := cdx.ExternalReference{Type: cdx.ERTypeLicense, URL: "https://github.com/nextest-rs/nextest/blob/fc97e97bbe0a3927482a694247da00c099f4269e/LICENSE-MIT"} + if !slices.Contains(*component.ExternalReferences, license) { + t.Fatalf("immutable Nextest notice source omitted: %+v", component) + } + } +} + +func TestReleaseSBOMRejectsMissingOrInconsistentNativeEvidence(t *testing.T) { + for _, test := range []struct { + name, path, problem string + change func(string) string + omit bool + }{ + {name: "unsealed inventory", path: "tools/rust_checksums.txt", omit: true, problem: "absent from the manifest"}, + {name: "missing host", path: "tools/rust_checksums.txt", change: func(value string) string { _, rest, _ := strings.Cut(value, "\n"); return rest }, problem: "missing archive"}, + {name: "duplicate archive", path: "tools/cargo-nextest_checksums.txt", change: func(value string) string { first, _, _ := strings.Cut(value, "\n"); return value + first + "\n" }, problem: "duplicate archive"}, + {name: "foreign version", path: "tools/cargo-nextest_checksums.txt", change: func(value string) string { return strings.ReplaceAll(value, "0.9.138", "0.9.137") }, problem: "unexpected"}, + {name: "invalid digest", path: "tools/rust_checksums.txt", change: func(string) string { return "rustc-1.97.1-x86_64-unknown-linux-gnu.tar.xz invalid\n" }, problem: "malformed"}, + {name: "empty inventory", path: "tools/rust_checksums.txt", change: func(string) string { return "" }, problem: "malformed"}, + {name: "oversized inventory", path: "tools/rust_checksums.txt", change: func(string) string { return strings.Repeat(" ", 33<<10) }, problem: "exceeds"}, + {name: "missing nextest notice", path: "tools/cargo-nextest-license.txt", omit: true, problem: "notice is absent"}, + {name: "empty nextest notice", path: "tools/cargo-nextest-license.txt", change: func(string) string { return "" }, problem: "nonempty"}, + {name: "wrong notice version", path: "tools/cargo-nextest-license.txt", change: func(value string) string { return strings.ReplaceAll(value, "0.9.138", "0.9.137") }, problem: "exact release"}, + {name: "mutable notice source", path: "tools/cargo-nextest-license.txt", change: func(value string) string { + return strings.ReplaceAll(value, "fc97e97bbe0a3927482a694247da00c099f4269e", "main") + }, problem: "immutable"}, + {name: "unsealed SDK copyright", path: ".tools/rust/HOST/share/doc/rust/COPYRIGHT.html", omit: true, problem: "notice is absent"}, + {name: "empty SDK license", path: ".tools/rust/HOST/share/licenses/rustc/LICENSE-MIT", change: func(string) string { return "" }, problem: "nonempty"}, + } { + t.Run(test.name, func(t *testing.T) { + root, manifest := installedRelease(t, map[string]string{BinaryPath: "fixture"}, nil) + relative := strings.ReplaceAll(test.path, "HOST", manifest.Host) + if test.omit { + manifest.Entries = slices.DeleteFunc(manifest.Entries, func(entry Entry) bool { return entry.Path == relative }) + } else { + path := filepath.Join(root, filepath.FromSlash(relative)) + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(test.change(string(data))), 0o644); err != nil { + t.Fatal(err) + } + } + if _, err := renderReleaseSBOM(manifest, root, "release.zip", exampleDigest); err == nil || !strings.Contains(err.Error(), test.problem) { + t.Fatalf("native inventory error = %v, wanted %q", err, test.problem) + } + }) + } +} + +func TestReleaseSBOMAcceptsNextestNoticeWithNativeLineEndings(t *testing.T) { + root, manifest := installedRelease(t, map[string]string{BinaryPath: "fixture"}, nil) + path := filepath.Join(root, "tools", "cargo-nextest-license.txt") + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n")), 0o644); err != nil { + t.Fatal(err) + } + if _, err := renderReleaseSBOM(manifest, root, "release.zip", exampleDigest); err != nil { + t.Fatal(err) + } +} + func TestReleaseSBOMRejectsIncompleteRustParserInventory(t *testing.T) { const parserMetadata = ".tools/python/fixture/lib/python3.12/site-packages/tree_sitter-0.26.0.dist-info/METADATA" for _, test := range []struct { diff --git a/internal/release/sbom.go b/internal/release/sbom.go index 1b53aa08..d505c743 100644 --- a/internal/release/sbom.go +++ b/internal/release/sbom.go @@ -40,6 +40,10 @@ func renderReleaseSBOM(manifest Manifest, releaseRoot, archiveName, archiveDiges rootRef := "pkg:generic/code-polishy@" + manifest.CodePolishyVersion inventory := releaseSBOMInventory{components: map[string]cdx.Component{}, dependencies: map[string]map[string]bool{rootRef: {}}} toolRefs := releaseSBOMTools(manifest) + compositions, err := rustReleaseSBOMEvidence(releaseRoot, manifest, toolRefs) + if err != nil { + return nil, err + } for name, component := range toolRefs { inventory.add(component) inventory.edge(rootRef, component.BOMRef) @@ -75,6 +79,7 @@ func renderReleaseSBOM(manifest Manifest, releaseRoot, archiveName, archiveDiges }} document.Components = &components document.Dependencies = &dependencies + document.Compositions = &compositions buffer := &bytes.Buffer{} if err := cdx.NewBOMEncoder(buffer, cdx.BOMFileFormatJSON).SetPretty(true).SetEscapeHTML(false).EncodeVersion(document, cdx.SpecVersion1_6); err != nil { return nil, fmt.Errorf("encode CycloneDX release SBOM: %w", err) From a40e416aba3ee9382672b4431dac2343e760ecba Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 05:39:24 +0000 Subject: [PATCH 24/49] feat: verify signed Git assessments for Cargo locks --- .code-polishy.json | 4 + docs/design/git-evidence-policy.md | 18 +- docs/design/rust-inputs.md | 5 +- docs/git-dependency-evidence.md | 23 +- docs/plans/code-polishy-rust-support-plan.md | 8 +- internal/engine/supplychain_evidence_test.go | 161 ++++++++------ .../supplychain/cargo_git_evidence_test.go | 210 ++++++++++++++++++ internal/supplychain/cargo_git_state_test.go | 74 ++++++ internal/supplychain/cargo_release_age.go | 15 +- internal/supplychain/git_evidence.go | 36 ++- internal/supplychain/git_evidence_state.go | 25 +-- .../supplychain/git_evidence_validation.go | 4 +- internal/supplychain/osv_uv.go | 13 +- 13 files changed, 472 insertions(+), 124 deletions(-) create mode 100644 internal/supplychain/cargo_git_evidence_test.go create mode 100644 internal/supplychain/cargo_git_state_test.go diff --git a/.code-polishy.json b/.code-polishy.json index 0320c743..e3efb16d 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -151,7 +151,11 @@ "sourcePaths": [ "internal/policy/git_evidence.go", "internal/supplychain/git_evidence.go", + "internal/supplychain/git_evidence_validation.go", "internal/supplychain/git_evidence_state.go", + "internal/supplychain/cargo_git_evidence_test.go", + "internal/supplychain/cargo_git_state_test.go", + "internal/engine/supplychain_evidence_test.go", "internal/supplychain/osv_uv.go" ] }, diff --git a/docs/design/git-evidence-policy.md b/docs/design/git-evidence-policy.md index c7e8d961..f1df1c2c 100644 --- a/docs/design/git-evidence-policy.md +++ b/docs/design/git-evidence-policy.md @@ -16,8 +16,22 @@ registry versions or publication dates for them. Cargo uses the same contained public-projection boundary, with its own complete locked-package report checks described in [Rust registry advisories](rust-inputs.md#registry-advisories). Its native lock extractor is disabled because the pinned scanner does not retain -Cargo source identity. This does not extend the signed Python inventory protocol -to Cargo or substitute registry evidence for Git contents. +Cargo source identity. Cargo locks also use the existing signed assessment +protocol when required, without substituting registry evidence for Git contents. +The complete remote inventory retains crates.io coordinates separately from Git +coordinates, including multiple packages in one Git repository and inactive +optional/platform releases. Cargo supplies no lock-level package subdirectory; +the Git subject covers the complete repository snapshot at the exact revision, +with one named subject per package. No directory is guessed from its crate name. + +Gate evidence inventories both Cargo and uv locks through one bounded, data-only +reader. The combined inventory shares the 512-lock and 32-MiB limits; Cargo also +retains its stricter 8-MiB per-lock parser bound. Unsupported sources prevent +complete assessment. Each signature and inventory is authenticated once per +evidence snapshot and its identity is reused in the receipt binding. Disabled +policy reads neither ecosystem's signed artifacts. Exact declarations and Cargo's +separate frozen/locked resolution still establish agreement with the manifests. +Public crates.io scanning and release-age checks remain active for mixed locks. Opting in retains complete inventory binding, signature verification, scanner trust, expiry, license, vulnerability, and authenticated-age requirements. diff --git a/docs/design/rust-inputs.md b/docs/design/rust-inputs.md index 717209ee..720e2a85 100644 --- a/docs/design/rust-inputs.md +++ b/docs/design/rust-inputs.md @@ -90,8 +90,9 @@ ecosystem and the actual Cargo.lock scope. It cannot fill missing observations. Git, local paths and alternate registries never become public crates.io queries. Optional Git assessment policy retains its visible coverage warning; required -Cargo Git evidence currently fails explicitly until the signed inventory protocol -supports Cargo locks. Registry age evidence does not establish vulnerability or +Cargo Git evidence uses the shared signed inventory protocol for complete Cargo +locks, with exact repository/commit/package identity and no guessed subdirectory. +Registry age evidence does not establish vulnerability or license coverage and is not collected by the offline profile. The [historical dependency comparison policy](historical-review-policy.md#cargo-dependency-comparison) diff --git a/docs/git-dependency-evidence.md b/docs/git-dependency-evidence.md index bd61c068..882719d7 100644 --- a/docs/git-dependency-evidence.md +++ b/docs/git-dependency-evidence.md @@ -2,7 +2,7 @@ Signed CI assessments are optional. Ordinary local dependency review does not require an attestation service. By default, public registry vulnerability and -minimum-age checks still run, including registry packages sharing a `uv.lock` +minimum-age checks still run, including registry packages sharing a `uv.lock` or `Cargo.lock` with Git dependencies. Git identities are not sent to public registry scanners. A nonblocking `supplyChain.gitSourceCoverage` warning identifies unverified Git vulnerability, license, integrity, and authenticated age coverage. Passing the @@ -32,7 +32,7 @@ Each provider has these exact fields: | `scanners` | Approved scanner identities, each with exact `name`, `version`, and lowercase binary `sha256`. | Each attestation has `scope`, `provider`, and `path`. The scope is the exact -repository-relative `uv.lock` path; the provider names one configured trust +repository-relative `uv.lock` or `Cargo.lock` path; the provider names one configured trust entry. The path names one signed artifact. Paths must be canonical and contained, without globs, traversal, or symbolic links. Each scope has one artifact, and every artifact path is distinct. @@ -106,7 +106,15 @@ sort lexicographically by the four values joined with NUL characters. Git packages use ecosystem `git` and source `git+:///@`, followed by `#subdirectory=` when present. Registry packages use ecosystem -`pypi` and source `registry:`. Local packages are omitted; +`pypi` and source `registry:` for uv locks. Cargo's public +registry packages use ecosystem `crates.io` and source `registry:https://crates.io`. +Cargo Git packages keep ecosystem `git`; their lock has no package-subdirectory +field, so the subject's subdirectory is empty and its snapshot covers the whole +repository. Multiple crates at one repository/commit need separate named subjects. +Candidate Git sources must declare and resolve to the same full revision; a +branch, tag or missing declared revision cannot establish exact-pin evidence. +Include inactive optional and platform packages from the complete Cargo lock. +Local packages are omitted; unsupported sources fail coverage. Use JSON string escaping without ASCII-only Unicode escaping; escape `<`, `>`, `&`, U+2028, and U+2029 as the corresponding lowercase `\u` sequences, matching the protocol's Go JSON encoding. @@ -139,7 +147,7 @@ identity, freshness, and complete inventory checks remain separate requirements. Arrays must be present and non-null. Each signed artifact and decoded payload is bounded to 4 MiB, with at most 1,024 Git subjects and 8,192 vulnerability or license results. The repository inventory is separately bounded to 512 lockfiles, -16 MiB per lock, and 32 MiB in total. +16 MiB per uv lock, 8 MiB per Cargo lock, and 32 MiB across both ecosystems. ## Verify and retain evidence @@ -155,8 +163,11 @@ retain the original lockfile scope. A lock containing Git sources receives vulnerability and license coverage from its complete signed assessment. Public registry release-age checks remain active. Non-public registry identities are never queried at PyPI; absent authenticated publication coverage -for such packages remains an explicit failure. Other ecosystems retain their -native OSV scan inputs. +for such packages remains an explicit failure. Cargo's raw manifest and lock +extractors are disabled; its public crates.io projection is scanned separately, +including in mixed Git/registry locks. Signed Git evidence never turns private +Cargo sources into public scanner inputs. Other ecosystems retain their native +OSV scan inputs. JSON and SARIF reports retain verified artifact receipts, including provider, path, artifact digest, assessment-policy digest, lock and inventory digests, diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index e5d1929d..7ae6e652 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -166,7 +166,8 @@ target-specific and workspace declaration tables, checks exact registry and Git pins, contains local paths, and validates versioned lock identities/checksums. It keeps Git and registry coordinates separate and retains explicit failures for unsupported source substitutions. These declaration and lock readers are -data-only. Signed Cargo Git evidence remains in progress. +data-only. Required signed Cargo Git assessments use the existing optional +Git-evidence protocol, with complete remote inventories and exact lock scope. Native Cargo lock verification now uses bounded, complete resolved metadata through the ordinary supply-chain runner. Online verification may fetch already locked @@ -198,7 +199,10 @@ compiler call analysis are disabled. The report must account for every projected version, and exact `crates.io` vulnerability assessments preserve the original lock scope. Tests cover source isolation, incomplete reports, scanner exits, assessment identity and the real pinned scanner with a temporary offline advisory -database. Signed Cargo Git inventories remain unfinished. +database. Signed Cargo Git inventories retain separate Git and crates.io +identities, including same-name/version packages and multiple crates from one +repository. They share signature/trust/expiry checks and gate receipt identity +with Python without sending private coordinates to public registries. Configured Cargo license policy now authenticates cached `.crate` archives against the complete lock inventory and applies the common SPDX evaluator to their diff --git a/internal/engine/supplychain_evidence_test.go b/internal/engine/supplychain_evidence_test.go index c2c0c368..12c86648 100644 --- a/internal/engine/supplychain_evidence_test.go +++ b/internal/engine/supplychain_evidence_test.go @@ -20,44 +20,52 @@ import ( func TestGitEvidenceReportsRetainVerifiedIdentityAcrossJSONAndSARIF(t *testing.T) { t.Parallel() - engine := gitEvidenceEngine(t) - findings, evidence, _ := engine.onlineSupplyChain(t.Context(), []string{"uv.lock"}, nil) - if len(findings) != 0 || len(evidence) != 1 { - t.Fatalf("online evidence = %+v, %+v", findings, evidence) - } - report := engine.normalizeReport(Report{Command: "supply-chain", GitEvidence: evidence}) - evidence[0].RetrievedAt = evidence[0].RetrievedAt.Add(time.Minute) - report = engine.combine(report, Report{GitEvidence: evidence}) - report.Command = "supply-chain" - if len(report.GitEvidence) != 1 || !report.GitEvidence[0].RetrievedAt.Equal(evidence[0].RetrievedAt) { - t.Fatal("combining reports must retain the latest retrieval of the same verified artifact") - } - encoded, err := JSONReport(report) - if err != nil { - t.Fatal(err) - } - decoded := Report{} - if err := json.Unmarshal(encoded, &decoded); err != nil || len(decoded.GitEvidence) != 1 || decoded.GitEvidence[0].SHA256 != evidence[0].SHA256 { - t.Fatalf("JSON evidence lost its identity: %+v, %v", decoded.GitEvidence, err) - } - sarif, err := SARIF(report) - if err != nil || !strings.Contains(string(sarif), evidence[0].SHA256) || !strings.Contains(string(sarif), `"gitEvidence"`) { - t.Fatalf("SARIF evidence lost its identity: %v", err) - } - report.GitEvidence[0].SHA256 = "invalid" - if _, err := JSONReport(report); err == nil { - t.Fatal("invalid evidence identity satisfied the shipped report schema") + for _, scope := range []string{"uv.lock", "Cargo.lock"} { + t.Run(scope, func(t *testing.T) { + engine := gitEvidenceEngine(t, scope) + findings, evidence, _ := engine.onlineSupplyChain(t.Context(), []string{scope}, nil) + if len(findings) != 0 || len(evidence) != 1 || evidence[0].Scope != scope { + t.Fatalf("online evidence = %+v, %+v", findings, evidence) + } + report := engine.normalizeReport(Report{Command: "supply-chain", GitEvidence: evidence}) + evidence[0].RetrievedAt = evidence[0].RetrievedAt.Add(time.Minute) + report = engine.combine(report, Report{GitEvidence: evidence}) + report.Command = "supply-chain" + if len(report.GitEvidence) != 1 || !report.GitEvidence[0].RetrievedAt.Equal(evidence[0].RetrievedAt) { + t.Fatal("combining reports must retain the latest retrieval of the same verified artifact") + } + encoded, err := JSONReport(report) + if err != nil { + t.Fatal(err) + } + decoded := Report{} + if err := json.Unmarshal(encoded, &decoded); err != nil || len(decoded.GitEvidence) != 1 || decoded.GitEvidence[0].SHA256 != evidence[0].SHA256 { + t.Fatalf("JSON evidence lost its identity: %+v, %v", decoded.GitEvidence, err) + } + sarif, err := SARIF(report) + if err != nil || !strings.Contains(string(sarif), evidence[0].SHA256) || !strings.Contains(string(sarif), `"gitEvidence"`) { + t.Fatalf("SARIF evidence lost its identity: %v", err) + } + report.GitEvidence[0].SHA256 = "invalid" + if _, err := JSONReport(report); err == nil { + t.Fatal("invalid evidence identity satisfied the shipped report schema") + } + }) } } func TestGitEvidenceChangesDuringConfiguredSecurityChecksPreventAcceptance(t *testing.T) { t.Parallel() - engine := gitEvidenceEngine(t) - engine.Repository.Config.Checks = []policy.Command{{Name: "security", Argv: []string{"assess"}, Cwd: ".", RunOn: []string{"security"}, TimeoutSeconds: 30}} - engine.Runner = gitEvidenceChangingRunner{} - findings, receipts, _ := engine.onlineSupplyChain(t.Context(), []string{"uv.lock"}, nil) - if len(findings) != 1 || findings[0].Check != "supplyChain.gitEvidence" || findings[0].Subject != "verification-state" || len(receipts) != 0 { - t.Fatalf("changed evidence was accepted after a configured security check: %+v, %+v", findings, receipts) + for _, scope := range []string{"uv.lock", "Cargo.lock"} { + t.Run(scope, func(t *testing.T) { + engine := gitEvidenceEngine(t, scope) + engine.Repository.Config.Checks = []policy.Command{{Name: "security", Argv: []string{"assess"}, Cwd: ".", RunOn: []string{"security"}, TimeoutSeconds: 30}} + engine.Runner = gitEvidenceChangingRunner{} + findings, receipts, _ := engine.onlineSupplyChain(t.Context(), []string{scope}, nil) + if len(findings) != 1 || findings[0].Check != "supplyChain.gitEvidence" || findings[0].Subject != "verification-state" || len(receipts) != 0 { + t.Fatalf("changed evidence was accepted after a configured security check: %+v, %+v", findings, receipts) + } + }) } } @@ -78,40 +86,44 @@ func (gitEvidenceChangingRunner) Run(_ context.Context, root string, _ policy.Co func TestGateRunRejectsChangedUntrackedGitEvidence(t *testing.T) { t.Parallel() - engine := gitEvidenceEngine(t) - head, err := engine.Repository.CleanHead() - if err != nil { - t.Fatal(err) - } - commands := []MergeGateExecutionCommand{{Category: gaterun.Check, Scope: "repository", Cost: "quick", Command: policy.Command{Name: "evidence", Argv: []string{"verify"}, Cwd: ".", TimeoutSeconds: 30}}} - identity, err := gateRunIdentity(engine, gaterun.MergeGate, "main", head, head, "recommended", commands, testGateRunBehaviorReview(), nil) - if err != nil || identity.GitEvidenceSHA256 == "" { - t.Fatalf("gate identity omitted evidence: %+v, %v", identity, err) - } - storeGitEvidenceGate(t, engine.Repository.Root, identity) - if _, err := gaterun.LoadReport(engine.Repository.Root, identity); err != nil { - t.Fatal(err) - } - path := filepath.Join(engine.Repository.Root, ".code-polishy-reports/git-evidence/proof.json") - data, err := os.ReadFile(path) - if err != nil { - t.Fatal(err) - } - writeEngineFile(t, engine.Repository.Root, ".code-polishy-reports/git-evidence/proof.json", string(append(data, '\n')), 0o600) - current, err := engine.Repository.CleanHead() - if err != nil || current != head { - t.Fatalf("external evidence changed the committed candidate: %s, %v", current, err) - } - changed, err := gateRunIdentity(engine, gaterun.MergeGate, "main", head, head, "recommended", commands, testGateRunBehaviorReview(), nil) - if err != nil || changed.GitEvidenceSHA256 == identity.GitEvidenceSHA256 { - t.Fatalf("changed evidence retained gate identity: %+v, %v", changed, err) - } - if _, err := gaterun.LoadReport(engine.Repository.Root, changed); err == nil { - t.Fatal("changed external evidence reused an earlier passed gate") - } - controller := &gateRunController{candidate: head, gitEvidenceSHA256: identity.GitEvidenceSHA256} - if err := controller.candidateIntegrityError(engine); err == nil || !strings.Contains(err.Error(), "evidence changed") { - t.Fatalf("evidence changed during a gate without failing publication: %v", err) + for _, scope := range []string{"uv.lock", "Cargo.lock"} { + t.Run(scope, func(t *testing.T) { + engine := gitEvidenceEngine(t, scope) + head, err := engine.Repository.CleanHead() + if err != nil { + t.Fatal(err) + } + commands := []MergeGateExecutionCommand{{Category: gaterun.Check, Scope: "repository", Cost: "quick", Command: policy.Command{Name: "evidence", Argv: []string{"verify"}, Cwd: ".", TimeoutSeconds: 30}}} + identity, err := gateRunIdentity(engine, gaterun.MergeGate, "main", head, head, "recommended", commands, testGateRunBehaviorReview(), nil) + if err != nil || identity.GitEvidenceSHA256 == "" { + t.Fatalf("gate identity omitted evidence: %+v, %v", identity, err) + } + storeGitEvidenceGate(t, engine.Repository.Root, identity) + if _, err := gaterun.LoadReport(engine.Repository.Root, identity); err != nil { + t.Fatal(err) + } + path := filepath.Join(engine.Repository.Root, ".code-polishy-reports/git-evidence/proof.json") + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + writeEngineFile(t, engine.Repository.Root, ".code-polishy-reports/git-evidence/proof.json", string(append(data, '\n')), 0o600) + current, err := engine.Repository.CleanHead() + if err != nil || current != head { + t.Fatalf("external evidence changed the committed candidate: %s, %v", current, err) + } + changed, err := gateRunIdentity(engine, gaterun.MergeGate, "main", head, head, "recommended", commands, testGateRunBehaviorReview(), nil) + if err != nil || changed.GitEvidenceSHA256 == identity.GitEvidenceSHA256 { + t.Fatalf("changed evidence retained gate identity: %+v, %v", changed, err) + } + if _, err := gaterun.LoadReport(engine.Repository.Root, changed); err == nil { + t.Fatal("changed external evidence reused an earlier passed gate") + } + controller := &gateRunController{candidate: head, gitEvidenceSHA256: identity.GitEvidenceSHA256} + if err := controller.candidateIntegrityError(engine); err == nil || !strings.Contains(err.Error(), "evidence changed") { + t.Fatalf("evidence changed during a gate without failing publication: %v", err) + } + }) } } @@ -137,12 +149,15 @@ func storeGitEvidenceGate(t *testing.T, root string, identity gaterun.Identity) } } -func gitEvidenceEngine(t *testing.T) *Engine { +func gitEvidenceEngine(t *testing.T, scope string) *Engine { t.Helper() root := t.TempDir() commit := strings.Repeat("a", 40) lock := fmt.Sprintf("[[package]]\nname = \"private-kit\"\nversion = \"1.0.0\"\nsource = { git = \"ssh://git@private.example.test/kit.git?rev=%s#%s\" }\n", commit, commit) - writeEngineFile(t, root, "uv.lock", lock, 0o600) + if scope == "Cargo.lock" { + lock = fmt.Sprintf("version=4\n[[package]]\nname='private-kit'\nversion='1.0.0'\nsource='git+ssh://git@private.example.test/kit.git?rev=%s#%s'\n", commit, commit) + } + writeEngineFile(t, root, scope, lock, 0o600) writeEngineFile(t, root, ".gitignore", ".code-polishy-reports/\n", 0o600) initializeEngineGitRepository(t, root) public, private, err := ed25519.GenerateKey(rand.Reader) @@ -150,7 +165,7 @@ func gitEvidenceEngine(t *testing.T) *Engine { t.Fatal(err) } provider := policy.GitEvidenceProvider{Name: "ci", Kind: "ed25519-ci/v1", Issuer: "https://ci.example.test/attestor", PublicKey: base64.StdEncoding.EncodeToString(public), PolicySHA256: strings.Repeat("b", 64), Scanners: []policy.GitEvidenceScanner{{Name: "private-scanner", Version: "1.0.0", SHA256: strings.Repeat("c", 64)}}} - statement := engineGitStatement(provider, commit, lock) + statement := engineGitStatement(provider, commit, scope, lock) payload, err := json.Marshal(statement) if err != nil { t.Fatal(err) @@ -164,16 +179,16 @@ func gitEvidenceEngine(t *testing.T) *Engine { } path := ".code-polishy-reports/git-evidence/proof.json" writeEngineFile(t, root, path, string(encoded), 0o600) - config := policy.Config{SupplyChain: policy.SupplyChain{MinimumReleaseAgeDays: 30, AllowedLicenses: []string{"MIT"}, GitEvidence: policy.GitEvidence{Required: true, Providers: []policy.GitEvidenceProvider{provider}, Attestations: []policy.GitEvidenceAttestation{{Scope: "uv.lock", Provider: "ci", Path: path}}}}} + config := policy.Config{SupplyChain: policy.SupplyChain{MinimumReleaseAgeDays: 30, AllowedLicenses: []string{"MIT"}, GitEvidence: policy.GitEvidence{Required: true, Providers: []policy.GitEvidenceProvider{provider}, Attestations: []policy.GitEvidenceAttestation{{Scope: scope, Provider: "ci", Path: path}}}}} return &Engine{Repository: repository.Repository{Root: root, PolicyRoot: root, Config: config}, Runner: &recordingEngineRunner{}} } -func engineGitStatement(provider policy.GitEvidenceProvider, commit, lock string) map[string]any { +func engineGitStatement(provider policy.GitEvidenceProvider, commit, scope, lock string) map[string]any { now := time.Now().UTC().Add(-time.Minute) source := "git+ssh://private.example.test/kit.git@" + commit inventory := fmt.Sprintf(`[{"ecosystem":"git","name":"private-kit","version":"1.0.0","source":%q}]`, source) return map[string]any{ - "protocol": "git-evidence/v1", "issuer": provider.Issuer, "policySha256": provider.PolicySHA256, "scope": "uv.lock", + "protocol": "git-evidence/v1", "issuer": provider.Issuer, "policySha256": provider.PolicySHA256, "scope": scope, "lockSha256": gaterun.ContentSHA256([]byte(lock)), "inventorySha256": gaterun.ContentSHA256([]byte(inventory)), "issuedAt": now, "expiresAt": now.Add(time.Hour), "subjects": []map[string]any{{"ecosystem": "git", "name": "private-kit", "version": "1.0.0", "repository": "git+ssh://private.example.test/kit.git", "commit": commit, "subdirectory": "", "treeSha256": strings.Repeat("d", 64), "observation": map[string]any{"kind": "first-observed", "record": provider.Issuer + "/records/kit", "timestamp": now.AddDate(0, 0, -90)}}}, "scan": map[string]any{"coverage": "complete", "target": "git-contents-and-resolved-lock/v1", "scanner": provider.Scanners[0], "completedAt": now, "advisoryVersion": "snapshot-v1", "advisoryUpdated": now.Add(-time.Hour), "vulnerabilities": []any{}, "licenses": []map[string]string{{"ecosystem": "git", "name": "private-kit", "version": "1.0.0", "source": source, "expression": "MIT"}}}, diff --git a/internal/supplychain/cargo_git_evidence_test.go b/internal/supplychain/cargo_git_evidence_test.go new file mode 100644 index 00000000..b39c297c --- /dev/null +++ b/internal/supplychain/cargo_git_evidence_test.go @@ -0,0 +1,210 @@ +package supplychain + +import ( + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "testing" + "time" + + "github.com/riteofstring/code-polishy/internal/policy" +) + +func newCargoGitEvidenceFixture(t *testing.T, private bool) gitEvidenceFixture { + t.Helper() + fixture := newGitEvidenceFixture(t, private) + if err := os.Remove(filepath.Join(fixture.repo.Root, "uv.lock")); err != nil { + t.Fatal(err) + } + subject := &fixture.statement.Subjects[0] + subject.Name, subject.Subdirectory = "crate-name", "" + source := subject.Repository + "@" + subject.Commit + url := subject.Repository + if private { + url = strings.Replace(url, "ssh://", "ssh://git@", 1) + } + lock := fmt.Sprintf("version=4\n[[package]]\nname='app'\nversion='0.1.0'\n[[package]]\nname='crate-name'\nversion='1.0.0'\nsource='%s?rev=%s#%s'\n[[package]]\nname='crate-name'\nversion='1.0.0'\nsource='registry+https://github.com/rust-lang/crates.io-index'\nchecksum='%s'\n", url, subject.Commit, subject.Commit, strings.Repeat("a", 64)) + writeSupplyFile(t, fixture.repo.Root, "Cargo.lock", lock) + packages, err := osvCargoInventory([]byte(lock), "Cargo.lock") + if err != nil { + t.Fatal(err) + } + fixture.packages = packages + fixture.repo.Config.SupplyChain.GitEvidence.Attestations[0].Scope = "Cargo.lock" + fixture.statement.Scope, fixture.statement.LockSHA256 = "Cargo.lock", fixtureSHA256([]byte(lock)) + inventory := fmt.Sprintf(`[{"ecosystem":"crates.io","name":"crate-name","version":"1.0.0","source":"registry:https://crates.io"},{"ecosystem":"git","name":"crate-name","version":"1.0.0","source":%q}]`, source) + fixture.statement.InventorySHA256 = fixtureSHA256([]byte(inventory)) + fixture.statement.Scan.Licenses = []gitEvidenceLicense{ + {Ecosystem: "crates.io", Name: "crate-name", Version: "1.0.0", Source: "registry:https://crates.io", Expression: "MIT"}, + {Ecosystem: "git", Name: "crate-name", Version: "1.0.0", Source: source, Expression: "MIT"}, + } + refreshGitEvidenceFixture(&fixture) + return fixture +} + +func refreshGitEvidenceFixture(fixture *gitEvidenceFixture) { + fixture.now = time.Now().UTC() + issued := fixture.now.Add(-time.Minute) + fixture.statement.IssuedAt, fixture.statement.ExpiresAt = issued, issued.Add(time.Hour) + fixture.statement.Scan.CompletedAt, fixture.statement.Scan.AdvisoryUpdated = issued, issued.Add(-time.Hour) + for index := range fixture.statement.Subjects { + fixture.statement.Subjects[index].Observation.Timestamp = issued.AddDate(0, 0, -90) + } +} + +func TestCargoGitEvidenceAuthenticatesSourcesWithoutRegistrySubstitution(t *testing.T) { + t.Parallel() + for _, private := range []bool{false, true} { + fixture := newCargoGitEvidenceFixture(t, private) + fixture.write(t) + findings, receipt := verifyGitLockEvidence(fixture.repo, "Cargo.lock", fixture.packages, fixture.now) + if len(findings) != 0 || receipt == nil || receipt.Scope != "Cargo.lock" || receipt.InventorySHA256 != fixture.statement.InventorySHA256 { + t.Fatalf("Cargo signed evidence = %+v, %+v", findings, receipt) + } + if receipt.LockSHA256 != fixture.statement.LockSHA256 || !receipt.RetrievedAt.Equal(fixture.now) { + t.Fatalf("Cargo receipt lost its authenticated identity: %+v", receipt) + } + state, err := ReadGitEvidenceState(fixture.repo, fixture.now) + if err != nil || state.IdentitySHA256 == "" || len(state.Receipts) != 1 || state.Receipts[0] != *receipt { + t.Fatalf("Cargo gate state omitted its signed inventory: %+v, %v", state, err) + } + } +} + +func TestCargoGitEvidenceRejectsIncompleteAndForeignSignedInventories(t *testing.T) { + t.Parallel() + for _, test := range []struct { + name, category string + change func(*gitEvidenceStatement) + }{ + {"Python scope", "stale", func(statement *gitEvidenceStatement) { statement.Scope = "uv.lock" }}, + {"invented crate directory", "stale", func(statement *gitEvidenceStatement) { statement.Subjects[0].Subdirectory = "src/crate-name" }}, + {"registry subject", "stale", func(statement *gitEvidenceStatement) { statement.Subjects[0].Ecosystem = "crates.io" }}, + {"wrong commit", "stale", func(statement *gitEvidenceStatement) { statement.Subjects[0].Commit = strings.Repeat("b", 40) }}, + {"missing registry license", "coverage", func(statement *gitEvidenceStatement) { statement.Scan.Licenses = statement.Scan.Licenses[1:] }}, + {"registry substitutes Git", "coverage", func(statement *gitEvidenceStatement) { statement.Scan.Licenses[1] = statement.Scan.Licenses[0] }}, + {"missing Git subject", "coverage", func(statement *gitEvidenceStatement) { statement.Subjects = []gitEvidenceSubject{} }}, + {"unsigned lock contents", "stale", func(statement *gitEvidenceStatement) { statement.LockSHA256 = strings.Repeat("b", 64) }}, + {"stale assessment", "expired", func(statement *gitEvidenceStatement) { statement.ExpiresAt = statement.IssuedAt }}, + } { + t.Run(test.name, func(t *testing.T) { + fixture := newCargoGitEvidenceFixture(t, true) + test.change(&fixture.statement) + fixture.write(t) + findings, receipt := verifyGitLockEvidence(fixture.repo, "Cargo.lock", fixture.packages, fixture.now) + if receipt != nil || len(findings) != 1 || findings[0].Check != "supplyChain.gitEvidence" || findings[0].Subject != test.category { + t.Fatalf("invalid Cargo assessment = %+v, %+v", findings, receipt) + } + }) + } +} + +func TestCargoGitEvidenceEnforcesSignedFindingsAndIndependentRegistryAge(t *testing.T) { + fixture := newCargoGitEvidenceFixture(t, true) + item := fixture.statement.Scan.Licenses[1] + exploited := false + fixture.statement.Scan.Vulnerabilities = []gitEvidenceVulnerability{{Ecosystem: item.Ecosystem, Name: item.Name, Version: item.Version, Source: item.Source, ID: "CVE-2026-12345", Severity: "critical", KnownExploited: &exploited}} + fixture.statement.Scan.Licenses[1].Expression = "AGPL-3.0-only" + fixture.statement.Subjects[0].Observation.Timestamp = fixture.now.Add(-time.Hour) + fixture.write(t) + transport := &cargoAgeTransport{released: map[string]time.Time{"1.0.0": fixture.now.Add(-time.Hour)}} + installCargoAgeTransport(t, transport) + findings := Online(t.Context(), fixture.repo, []string{"Cargo.lock"}, &cargoSupplyRunner{}) + checks := supplyChecks(findings) + if len(findings) != 4 || len(transport.requests) != 1 || !checks["supplyChain.gitVulnerability"] || !checks["supplyChain.dependencyLicense"] || !checks["supplyChain.releaseAge"] { + t.Fatalf("Cargo signed policy or registry age omitted: %+v, requests = %+v", findings, transport.requests) + } + for _, ecosystem := range []string{"git", "crates.io"} { + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return finding.ReleaseAge != nil && finding.ReleaseAge.Ecosystem == ecosystem && finding.Path == "Cargo.lock" + }) { + t.Fatalf("%s release-age identity omitted: %+v", ecosystem, findings) + } + } +} + +func TestCargoGitEvidenceReceiptInvalidatesChangedLockAndProof(t *testing.T) { + t.Parallel() + for _, change := range []string{"lock bytes", "registry checksum", "proof bytes", "expiry", "trust"} { + t.Run(change, func(t *testing.T) { + fixture := newCargoGitEvidenceFixture(t, true) + fixture.write(t) + before, err := ReadGitEvidenceState(fixture.repo, fixture.now) + if err != nil || len(before.Receipts) != 1 { + t.Fatalf("initial state = %+v, %v", before, err) + } + unchanged, err := ReadGitEvidenceState(fixture.repo, fixture.now.Add(time.Minute)) + if err != nil || len(unchanged.Receipts) != 1 || unchanged.IdentitySHA256 != before.IdentitySHA256 || !unchanged.Receipts[0].RetrievedAt.Equal(fixture.now.Add(time.Minute)) { + t.Fatalf("unchanged Cargo proof could not be reused: %+v, %v", unchanged, err) + } + mutateCargoEvidenceState(t, &fixture, change) + after, err := ReadGitEvidenceState(fixture.repo, fixture.now) + if err != nil || after.IdentitySHA256 == before.IdentitySHA256 { + t.Fatalf("%s retained Cargo proof identity: %+v, %v", change, after, err) + } + if change != "proof bytes" && len(after.Receipts) != 0 { + t.Fatalf("%s retained an invalid receipt: %+v", change, after) + } + }) + } +} + +func mutateCargoEvidenceState(t *testing.T, fixture *gitEvidenceFixture, change string) { + t.Helper() + switch change { + case "expiry": + fixture.now = fixture.now.Add(2 * time.Hour) + case "trust": + fixture.repo.Config.SupplyChain.GitEvidence.Providers[0].PolicySHA256 = strings.Repeat("b", 64) + default: + path := "Cargo.lock" + if change == "proof bytes" { + path = "evidence/git.dsse.json" + } + data, err := os.ReadFile(filepath.Join(fixture.repo.Root, path)) + if err != nil { + t.Fatal(err) + } + if change == "registry checksum" { + data = []byte(strings.ReplaceAll(string(data), strings.Repeat("a", 64), strings.Repeat("b", 64))) + } else { + data = append(data, '\n') + } + writeSupplyFile(t, fixture.repo.Root, path, string(data)) + } +} + +func TestCargoGitEvidenceRequiresEachPackageFromTheSameRepository(t *testing.T) { + t.Parallel() + fixture := newCargoGitEvidenceFixture(t, true) + data, err := os.ReadFile(filepath.Join(fixture.repo.Root, "Cargo.lock")) + if err != nil { + t.Fatal(err) + } + subject := fixture.statement.Subjects[0] + lock := string(data) + fmt.Sprintf("[[package]]\nname='sibling-crate'\nversion='2.0.0'\nsource='%s?rev=%s#%s'\n", subject.Repository, subject.Commit, subject.Commit) + writeSupplyFile(t, fixture.repo.Root, "Cargo.lock", lock) + fixture.packages, err = osvCargoInventory([]byte(lock), "Cargo.lock") + if err != nil { + t.Fatal(err) + } + fixture.statement.LockSHA256 = fixtureSHA256([]byte(lock)) + source := subject.Repository + "@" + subject.Commit + inventory := fmt.Sprintf(`[{"ecosystem":"crates.io","name":"crate-name","version":"1.0.0","source":"registry:https://crates.io"},{"ecosystem":"git","name":"crate-name","version":"1.0.0","source":%q},{"ecosystem":"git","name":"sibling-crate","version":"2.0.0","source":%q}]`, source, source) + fixture.statement.InventorySHA256 = fixtureSHA256([]byte(inventory)) + fixture.write(t) + findings, receipt := verifyGitLockEvidence(fixture.repo, "Cargo.lock", fixture.packages, fixture.now) + if receipt != nil || len(findings) != 1 || findings[0].Subject != "coverage" { + t.Fatalf("one Git subject covered two Cargo packages: %+v, %+v", findings, receipt) + } + subject.Name, subject.Version = "sibling-crate", "2.0.0" + fixture.statement.Subjects = append(fixture.statement.Subjects, subject) + fixture.statement.Scan.Licenses = append(fixture.statement.Scan.Licenses, gitEvidenceLicense{Ecosystem: "git", Name: "sibling-crate", Version: "2.0.0", Source: source, Expression: "MIT"}) + fixture.write(t) + findings, receipt = verifyGitLockEvidence(fixture.repo, "Cargo.lock", fixture.packages, fixture.now) + if len(findings) != 0 || receipt == nil { + t.Fatalf("complete same-repository Cargo assessment failed: %+v, %+v", findings, receipt) + } +} diff --git a/internal/supplychain/cargo_git_state_test.go b/internal/supplychain/cargo_git_state_test.go new file mode 100644 index 00000000..1e1762a8 --- /dev/null +++ b/internal/supplychain/cargo_git_state_test.go @@ -0,0 +1,74 @@ +package supplychain + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" +) + +func TestGitEvidenceStateRetainsBothCargoAndPythonLocks(t *testing.T) { + t.Parallel() + cargo := newCargoGitEvidenceFixture(t, true) + cargo.write(t) + python := newGitEvidenceFixture(t, true) + python.key = cargo.key + python.statement.Scope = "python/uv.lock" + refreshGitEvidenceFixture(&python) + python.write(t) + for _, pair := range [][2]string{{"uv.lock", "python/uv.lock"}, {"evidence/git.dsse.json", "evidence/python.dsse.json"}} { + data, err := os.ReadFile(filepath.Join(python.repo.Root, pair[0])) + if err != nil { + t.Fatal(err) + } + writeSupplyFile(t, cargo.repo.Root, pair[1], string(data)) + } + cargo.repo.Config.SupplyChain.GitEvidence.Attestations = append(cargo.repo.Config.SupplyChain.GitEvidence.Attestations, policy.GitEvidenceAttestation{Scope: "python/uv.lock", Provider: "ci", Path: "evidence/python.dsse.json"}) + state, err := ReadGitEvidenceState(cargo.repo, python.now) + if err != nil || len(state.Receipts) != 2 || state.Receipts[0].Scope != "Cargo.lock" || state.Receipts[1].Scope != "python/uv.lock" { + t.Fatalf("mixed ecosystem evidence state = %+v, %v", state, err) + } + writeSupplyFile(t, cargo.repo.Root, "evidence/python.dsse.json", "invalid") + changed, err := ReadGitEvidenceState(cargo.repo, python.now) + if err != nil || changed.IdentitySHA256 == state.IdentitySHA256 || len(changed.Receipts) != 1 || changed.Receipts[0].Scope != "Cargo.lock" { + t.Fatalf("invalid Python evidence discarded valid Cargo evidence: %+v, %v", changed, err) + } +} + +func TestGitEvidenceStateSharesTheInputBoundAcrossCargoAndPython(t *testing.T) { + t.Parallel() + fixture := newCargoGitEvidenceFixture(t, true) + writeSupplyFile(t, fixture.repo.Root, "python/uv.lock", "[[package]]\nname='local'\nversion='0.1.0'\nsource={editable='.'}\n") + for index := range 511 { + directory := fmt.Sprintf("workspace-%03d/", index) + writeSupplyFile(t, fixture.repo.Root, directory+"Cargo.lock", "version=4\n[[package]]\nname='local'\nversion='0.1.0'\n") + } + state, err := ReadGitEvidenceState(fixture.repo, fixture.now) + if err == nil || !strings.Contains(err.Error(), "input limit") || len(state.Receipts) != 0 { + t.Fatalf("mixed lock inventory exceeded its combined bound: %+v, %v", state, err) + } +} + +func TestCargoGitEvidenceRejectsUnsupportedInventoryButLeavesOptInOptional(t *testing.T) { + t.Parallel() + fixture := newCargoGitEvidenceFixture(t, true) + fixture.write(t) + data, err := os.ReadFile(filepath.Join(fixture.repo.Root, "Cargo.lock")) + if err != nil { + t.Fatal(err) + } + lock := strings.ReplaceAll(string(data), "registry+https://github.com/rust-lang/crates.io-index", "registry+https://private.example.test/index") + writeSupplyFile(t, fixture.repo.Root, "Cargo.lock", lock) + state, err := ReadGitEvidenceState(fixture.repo, fixture.now) + if err == nil || strings.Contains(err.Error(), "private.example.test") || len(state.Receipts) != 0 { + t.Fatalf("unsupported Cargo inventory satisfied complete evidence: %+v, %v", state, err) + } + fixture.repo.Config.SupplyChain.GitEvidence.Required = false + state, err = ReadGitEvidenceState(fixture.repo, fixture.now) + if err != nil || state.IdentitySHA256 != "" || len(state.Receipts) != 0 { + t.Fatalf("disabled Cargo evidence became required or credited: %+v, %v", state, err) + } +} diff --git a/internal/supplychain/cargo_release_age.go b/internal/supplychain/cargo_release_age.go index 619a8685..206d7939 100644 --- a/internal/supplychain/cargo_release_age.go +++ b/internal/supplychain/cargo_release_age.go @@ -25,7 +25,12 @@ func cargoReleaseAgeFindings(ctx context.Context, repo repository.Repository, fi locked, problems := resolvedCargoPackages(lock) findings = append(findings, problems...) if gitLockPackages(locked) { - findings = append(findings, cargoGitAgeFindings(repo, lock.Path, locked)...) + if len(problems) != 0 && repo.Config.SupplyChain.GitEvidence.Required { + findings = append(findings, gitEvidenceFinding(lock.Path, gitEvidenceFailure("coverage", "Cargo Git evidence requires a complete supported lock inventory"))) + } else { + assessment, _ := verifyGitLockEvidence(repo, lock.Path, locked, time.Now().UTC()) + findings = append(findings, assessment...) + } } packages = append(packages, registryReleasePackages(locked)...) } @@ -56,14 +61,6 @@ func cargoAgeLocks(repo repository.Repository, files []string, resolved []reposi return ordered, findings } -func cargoGitAgeFindings(repo repository.Repository, path string, packages []resolvedPackage) []policy.Finding { - if repo.Config.SupplyChain.GitEvidence.Required { - return []policy.Finding{{Check: "supplyChain.gitEvidence", Path: path, Subject: "cargo-source", Message: "Required Cargo Git assessments need Cargo lock inventory support; a Python lock attestation cannot establish evidence for this Cargo lockfile"}} - } - findings, _ := verifyGitLockEvidence(repo, path, packages, time.Now().UTC()) - return findings -} - func lookupCargoReleaseGroup(ctx context.Context, client *http.Client, group releaseGroup) (map[string]time.Time, error) { query := url.Values{} versions := []string{} diff --git a/internal/supplychain/git_evidence.go b/internal/supplychain/git_evidence.go index 7e7da60a..7ace8787 100644 --- a/internal/supplychain/git_evidence.go +++ b/internal/supplychain/git_evidence.go @@ -21,31 +21,55 @@ type gitEvidencePackage struct { Source string `json:"source"` } +type gitLockAssessment struct { + Findings []policy.Finding + Receipt *GitEvidenceReceipt + InventorySHA256 string + ArtifactSHA256 string + ArtifactStatus string +} + func verifyGitLockEvidence(repo repository.Repository, scope string, packages []resolvedPackage, now time.Time) ([]policy.Finding, *GitEvidenceReceipt) { if !repo.Config.SupplyChain.GitEvidence.Required { return []policy.Finding{{Check: "supplyChain.gitSourceCoverage", Path: scope, Severity: policy.FindingWarning, Message: "Git-source vulnerability, license, integrity, and authenticated release-age coverage is unverified; public registry checks do not assess Git contents", Remediation: policy.FindingRemediation{Summary: "Review Git sources with a suitable scanner; optionally require signed CI assessments with supplyChain.gitEvidence.required."}}}, nil } + assessment := assessRequiredGitLock(repo, scope, packages, now) + return assessment.Findings, assessment.Receipt +} + +func assessRequiredGitLock(repo repository.Repository, scope string, packages []resolvedPackage, now time.Time) gitLockAssessment { + result := gitLockAssessment{InventorySHA256: gitEvidenceInventoryDigest(packages), ArtifactStatus: "unavailable"} declaration, provider, err := configuredGitEvidence(repo.Config.SupplyChain.GitEvidence, scope) if err != nil { - return []policy.Finding{gitEvidenceFinding(scope, err)}, nil + result.Findings = []policy.Finding{gitEvidenceFinding(scope, err)} + return result } statement, digest, err := readSignedGitEvidence(repo, declaration, provider) + result.ArtifactSHA256 = digest + if digest != "" { + result.ArtifactStatus = "read" + } else if err != nil { + result.ArtifactStatus = gitEvidenceFinding(scope, err).Subject + } if err != nil { - return []policy.Finding{gitEvidenceFinding(scope, err)}, nil + result.Findings = []policy.Finding{gitEvidenceFinding(scope, err)} + return result } lock, err := readGitEvidenceFile(repo, scope, 16<<20) if err == nil { - err = validateGitEvidenceStatement(statement, provider, scope, gitEvidenceDigest(lock), packages, now) + err = validateGitEvidenceStatement(statement, provider, scope, gitEvidenceDigest(lock), result.InventorySHA256, packages, now) } if err != nil { - return []policy.Finding{gitEvidenceFinding(scope, err)}, nil + result.Findings = []policy.Finding{gitEvidenceFinding(scope, err)} + return result } - receipt := &GitEvidenceReceipt{ + result.Receipt = &GitEvidenceReceipt{ Scope: scope, Provider: provider.Name, Path: declaration.Path, SHA256: digest, PolicySHA256: provider.PolicySHA256, LockSHA256: statement.LockSHA256, InventorySHA256: statement.InventorySHA256, RetrievedAt: now.UTC(), ExpiresAt: statement.ExpiresAt.UTC(), } - return gitEvidencePolicyFindings(repo, statement, now), receipt + result.Findings = gitEvidencePolicyFindings(repo, statement, now) + return result } func configuredGitEvidence(configuration policy.GitEvidence, scope string) (policy.GitEvidenceAttestation, policy.GitEvidenceProvider, error) { diff --git a/internal/supplychain/git_evidence_state.go b/internal/supplychain/git_evidence_state.go index 41af37f6..dd197a33 100644 --- a/internal/supplychain/git_evidence_state.go +++ b/internal/supplychain/git_evidence_state.go @@ -26,7 +26,7 @@ func ReadGitEvidenceState(repo repository.Repository, now time.Time) (GitEvidenc if !repo.Config.SupplyChain.GitEvidence.Required { return GitEvidenceState{Receipts: []GitEvidenceReceipt{}}, nil } - inputs, err := onlineUVInputs(repo) + inputs, err := resolvedLockInputs(repo, map[string]func([]byte, string) ([]resolvedPackage, error){"uv.lock": parseUVLock, "Cargo.lock": osvCargoInventory}) if err != nil { return GitEvidenceState{}, err } @@ -36,12 +36,11 @@ func ReadGitEvidenceState(repo repository.Repository, now time.Time) (GitEvidenc if !gitLockPackages(input.Packages) { continue } - findings, receipt := verifyGitLockEvidence(repo, input.Scope, input.Packages, now) - binding := gitEvidenceBinding{Scope: input.Scope, InventorySHA256: gitEvidenceInventoryDigest(input.Packages), Findings: findings, Receipt: receipt} - binding.ArtifactSHA256, binding.ArtifactStatus = gitEvidenceArtifactIdentity(repo, input.Scope) - if receipt != nil { - state.Receipts = append(state.Receipts, *receipt) - identityReceipt := *receipt + assessment := assessRequiredGitLock(repo, input.Scope, input.Packages, now) + binding := gitEvidenceBinding{Scope: input.Scope, InventorySHA256: assessment.InventorySHA256, Findings: assessment.Findings, Receipt: assessment.Receipt, ArtifactSHA256: assessment.ArtifactSHA256, ArtifactStatus: assessment.ArtifactStatus} + if assessment.Receipt != nil { + state.Receipts = append(state.Receipts, *assessment.Receipt) + identityReceipt := *assessment.Receipt identityReceipt.RetrievedAt = time.Time{} binding.Receipt = &identityReceipt } @@ -57,15 +56,3 @@ func ReadGitEvidenceState(repo repository.Repository, now time.Time) (GitEvidenc state.IdentitySHA256 = gitEvidenceDigest(data) return state, nil } - -func gitEvidenceArtifactIdentity(repo repository.Repository, scope string) (string, string) { - declaration, _, err := configuredGitEvidence(repo.Config.SupplyChain.GitEvidence, scope) - if err != nil { - return "", "unavailable" - } - data, err := readGitEvidenceFile(repo, declaration.Path, maximumGitEvidenceBytes) - if err != nil { - return "", gitEvidenceFinding(scope, err).Subject - } - return gitEvidenceDigest(data), "read" -} diff --git a/internal/supplychain/git_evidence_validation.go b/internal/supplychain/git_evidence_validation.go index ee5c0819..c3a4f57d 100644 --- a/internal/supplychain/git_evidence_validation.go +++ b/internal/supplychain/git_evidence_validation.go @@ -13,8 +13,8 @@ import ( "github.com/riteofstring/code-polishy/internal/policy" ) -func validateGitEvidenceStatement(statement gitEvidenceStatement, provider policy.GitEvidenceProvider, scope, lockDigest string, packages []resolvedPackage, now time.Time) error { - if statement.Scope != scope || statement.LockSHA256 != lockDigest || statement.InventorySHA256 != gitEvidenceInventoryDigest(packages) { +func validateGitEvidenceStatement(statement gitEvidenceStatement, provider policy.GitEvidenceProvider, scope, lockDigest, inventoryDigest string, packages []resolvedPackage, now time.Time) error { + if statement.Scope != scope || statement.LockSHA256 != lockDigest || statement.InventorySHA256 != inventoryDigest { return gitEvidenceFailure("stale", "Git evidence does not bind the current lockfile and complete resolved dependency inventory") } if err := validateGitEvidenceTimes(statement, now); err != nil { diff --git a/internal/supplychain/osv_uv.go b/internal/supplychain/osv_uv.go index edee9fd3..c2e41b74 100644 --- a/internal/supplychain/osv_uv.go +++ b/internal/supplychain/osv_uv.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "path/filepath" + "slices" "strings" "github.com/riteofstring/code-polishy/internal/policy" @@ -30,18 +31,24 @@ func onlineUVInputs(repo repository.Repository) ([]osvLockInput, error) { } func osvLockInputs(repo repository.Repository, name string, parse func([]byte, string) ([]resolvedPackage, error)) ([]osvLockInput, error) { + return resolvedLockInputs(repo, map[string]func([]byte, string) ([]resolvedPackage, error){name: parse}) +} + +func resolvedLockInputs(repo repository.Repository, parsers map[string]func([]byte, string) ([]resolvedPackage, error)) ([]osvLockInput, error) { files, err := repo.RawFiles() if err != nil { - return nil, fmt.Errorf("enumerate resolved %s inventory: %w", name, err) + return nil, fmt.Errorf("enumerate resolved lock inventory: %w", err) } + slices.Sort(files) inputs := []osvLockInput{} remaining := 32 << 20 for _, scope := range files { - if filepath.Base(scope) != name || repo.IsExcluded(scope) { + parse, selected := parsers[filepath.Base(scope)] + if !selected || repo.IsExcluded(scope) { continue } if len(inputs) >= 512 || remaining <= 0 { - return nil, fmt.Errorf("resolved %s inventory exceeds its input limit", name) + return nil, fmt.Errorf("resolved lock inventory exceeds its input limit") } data, err := readGitEvidenceFile(repo, scope, int64(min(16<<20, remaining))) if err != nil { From d2fab6d646037943a4626744f2b9b08dc9874d31 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 05:50:13 +0000 Subject: [PATCH 25/49] fix: enforce Rust formatting exclusions before writing source --- docs/design/rust-inputs.md | 10 +++ internal/quality/rust_format.go | 43 +++++++--- internal/quality/rust_format_test.go | 113 ++++++++++++++++++++++++--- internal/quality/rust_source.go | 35 +++++++-- internal/quality/rust_source_test.go | 4 +- 5 files changed, 175 insertions(+), 30 deletions(-) diff --git a/docs/design/rust-inputs.md b/docs/design/rust-inputs.md index 720e2a85..805a82bc 100644 --- a/docs/design/rust-inputs.md +++ b/docs/design/rust-inputs.md @@ -219,6 +219,16 @@ Staged packages must pass a real Rust parse before replacing the previous exact distributions. Replacement retains recoverable backups until the installed parser passes that same check. Wheel metadata and licenses remain installed. +Native formatting parses the selected handwritten source before invoking rustfmt. +Direct and nested conditional `rustfmt::skip` attributes, including the +upstream [macro and attribute exclusion forms](https://github.com/rust-lang/rustfmt#tips), +fail at their original locations and their files remain untouched. Raw-string contents are data, not +attributes. The formatter consumes the same captured bytes that were parsed and +compares those bytes with the current file before writing; a concurrent edit +cannot introduce an unchecked exclusion or be overwritten. Invalid or missing +syntax evidence does not authorize a write. Generated and unselected inputs remain +outside this operation, and valid selected siblings can still be formatted. + Rust unit and integration execution uses the independent pinned cargo-nextest 0.9.138 release, published 2026-06-21. Its stable JUnit output supplies actual test-case outcomes on the stable compiler, whose built-in libtest JSON interface diff --git a/internal/quality/rust_format.go b/internal/quality/rust_format.go index d18475b3..838fd331 100644 --- a/internal/quality/rust_format.go +++ b/internal/quality/rust_format.go @@ -23,8 +23,24 @@ func rustFormatWrite(ctx context.Context, repo repository.Repository, selected [ plan := rustfacts.PlanDiscovery(repo, files, allFiles) discovery := rustfacts.Discover(ctx, repo, plan, commandRunner) findings := append([]policy.Finding{}, discovery.Findings...) - for _, path := range files { - findings = append(findings, rustFormatFile(ctx, repo, path, discovery.Workspaces, commandRunner, true)...) + sourcePlan := rustfacts.PlanSources(repo, files) + sources := rustfacts.ReadSources(ctx, repo, sourcePlan, commandRunner) + findings = append(findings, sources.Findings...) + parsed := map[string]rustfacts.Source{} + for _, source := range sources.Files { + parsed[source.Path] = source + } + for _, input := range sourcePlan.Inputs { + source, present := parsed[input.Path] + if !present { + continue + } + exclusions := rustFormatExclusionFindings(input.Path, source.Attributes) + if len(exclusions) != 0 { + findings = append(findings, exclusions...) + continue + } + findings = append(findings, rustFormatCaptured(ctx, repo, input, discovery.Workspaces, commandRunner, true)...) } return findings } @@ -33,28 +49,34 @@ func rustFormatFindings(ctx context.Context, repo repository.Repository, selecte files := editableFiles(repo, languageFiles(repo, repo.NativeAnalysisFiles(selected, "format", ""), "rust")) findings := []policy.Finding{} for _, path := range files { - findings = append(findings, rustFormatFile(ctx, repo, path, workspaces, runner.OSRunner{}, false)...) + findings = append(findings, rustFormatFile(ctx, repo, path, workspaces, runner.OSRunner{})...) } return findings } -func rustFormatFile(ctx context.Context, repo repository.Repository, path string, workspaces []rustfacts.Workspace, commandRunner runner.Runner, write bool) []policy.Finding { - edition, err := rustfacts.SourceEdition(workspaces, path) +func rustFormatFile(ctx context.Context, repo repository.Repository, path string, workspaces []rustfacts.Workspace, commandRunner runner.Runner) []policy.Finding { + input, err := repo.ReadContainedFile(path, 8<<20) if err != nil { return []policy.Finding{rustFormatFinding(path, err.Error())} } - input, err := repo.ReadContainedFile(path, 8<<20) + return rustFormatCaptured(ctx, repo, rustfacts.SourceInput{Path: path, Source: string(input)}, workspaces, commandRunner, false) +} + +func rustFormatCaptured(ctx context.Context, repo repository.Repository, source rustfacts.SourceInput, workspaces []rustfacts.Workspace, commandRunner runner.Runner, write bool) []policy.Finding { + path := source.Path + edition, err := rustfacts.SourceEdition(workspaces, path) if err != nil { return []policy.Finding{rustFormatFinding(path, err.Error())} } + input := []byte(source.Source) formatted, err := rustfacts.FormatSource(ctx, repo, path, edition, input, commandRunner) if err != nil { return []policy.Finding{rustFormatFinding(path, err.Error())} } - if bytes.Equal(input, formatted) { - return nil - } if !write { + if bytes.Equal(input, formatted) { + return nil + } return []policy.Finding{rustFormatFinding(path, "file is not rustfmt formatted")} } if err := writeRustFormat(repo, path, input, formatted); err != nil { @@ -71,6 +93,9 @@ func writeRustFormat(repo repository.Repository, path string, input, formatted [ if !bytes.Equal(current, input) { return errors.New("rust source changed during formatting; no output was written") } + if bytes.Equal(input, formatted) { + return nil + } return repo.WriteRegularFile(path, formatted) } diff --git a/internal/quality/rust_format_test.go b/internal/quality/rust_format_test.go index 1b779263..9053e80b 100644 --- a/internal/quality/rust_format_test.go +++ b/internal/quality/rust_format_test.go @@ -2,6 +2,7 @@ package quality import ( "context" + "errors" "os" "path/filepath" "slices" @@ -98,29 +99,117 @@ func TestRustFormatRejectsInvalidSourceAndWeakenedConfiguration(t *testing.T) { } func TestRustFormatPreservesConcurrentSourceEdit(t *testing.T) { + t.Parallel() + for _, before := range []string{"policy-rustfmt-", "policy-rust-source-facts"} { + t.Run(before, func(t *testing.T) { + for _, source := range []string{"pub fn value()->u32{42}\n", "pub fn value() -> u32 {\n 42\n}\n"} { + repo := rustFormatRepository(t) + writeQualityFile(t, repo.Root, "src/lib.rs", source) + concurrent := "#[rustfmt::skip]\npub fn value() -> u32 { 99 }\n" + boundary := rustEditingRunner{OSRunner: runner.OSRunner{}, before: before, edit: func() { writeQualityFile(t, repo.Root, "src/lib.rs", concurrent) }} + findings := rustFormatWrite(t.Context(), repo, []string{"src/lib.rs"}, boundary) + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return strings.Contains(finding.Message, "changed during formatting") + }) { + t.Fatalf("concurrent edit was not reported: %+v", findings) + } + if got := rustFormatFileText(t, repo, "src/lib.rs"); got != concurrent { + t.Fatalf("concurrent source edit was overwritten: %q", got) + } + } + }) + } +} + +func TestRustFormatRejectsParsedExclusionsWithoutRewritingThem(t *testing.T) { + t.Parallel() + for _, attribute := range []string{ + "#[rustfmt::skip]", "#[cfg_attr(any(), rustfmt::skip)]", "#[cfg_attr(all(), cfg_attr(any(), rustfmt::skip))]", + "#[rustfmt::skip::macros(println)]", "#![rustfmt::skip::attributes(custom)]", "#[r#rustfmt::r#skip]", + "#[cfg_attr(any(), rustfmt::skip::macros(println))]", + } { + t.Run(attribute, func(t *testing.T) { + repo := rustFormatRepository(t) + dirty := attribute + "\npub fn value()->u32{42}\n" + writeQualityFile(t, repo.Root, "src/lib.rs", dirty) + findings := Format(t.Context(), repo, repository.Selection{Files: []string{"src/lib.rs"}}, runner.OSRunner{}) + if len(findings) != 1 || findings[0].Check != "policy.rustAttribute" || findings[0].Path != "src/lib.rs" || findings[0].Line != 1 { + t.Fatalf("format exclusion did not fail at its original attribute: %+v", findings) + } + if got := rustFormatFileText(t, repo, "src/lib.rs"); got != dirty { + t.Fatalf("format exclusion was rewritten or removed: %q", got) + } + }) + } +} + +func TestRustFormatDoesNotTreatRawStringContentsAsAnExclusion(t *testing.T) { t.Parallel() repo := rustFormatRepository(t) - writeQualityFile(t, repo.Root, "src/lib.rs", "pub fn value()->u32{42}\n") - concurrent := "pub fn value() -> u32 { 99 }\n" - boundary := rustEditingRunner{OSRunner: runner.OSRunner{}, edit: func() { writeQualityFile(t, repo.Root, "src/lib.rs", concurrent) }} - findings := rustFormatWrite(t.Context(), repo, []string{"src/lib.rs"}, boundary) - if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { - return strings.Contains(finding.Message, "changed during formatting") - }) { - t.Fatalf("concurrent edit was not reported: %+v", findings) + writeQualityFile(t, repo.Root, "src/lib.rs", "pub fn text()->&'static str{r#\"#[rustfmt::skip]\"#}\n") + findings := Format(t.Context(), repo, repository.Selection{Files: []string{"src/lib.rs"}}, runner.OSRunner{}) + wanted := "pub fn text() -> &'static str {\n r#\"#[rustfmt::skip]\"#\n}\n" + if len(findings) != 0 || rustFormatFileText(t, repo, "src/lib.rs") != wanted { + t.Fatalf("ordinary string data blocked formatting: %+v", findings) + } +} + +func TestRustFormatPreservesExcludedSourceAndFormatsValidSelectedSiblings(t *testing.T) { + t.Parallel() + repo := rustFormatRepository(t) + excluded := "mod child;\n#[rustfmt::skip]\npub fn value()->u32{42}\n" + writeQualityFile(t, repo.Root, "src/lib.rs", excluded) + writeQualityFile(t, repo.Root, "src/child.rs", "pub fn value()->u32{99}\n") + findings := Format(t.Context(), repo, repository.Selection{Files: []string{"src/lib.rs", "src/child.rs"}}, runner.OSRunner{}) + if len(findings) != 1 || findings[0].Path != "src/lib.rs" || findings[0].Check != "policy.rustAttribute" { + t.Fatalf("format exclusion lost selected-file scope: %+v", findings) + } + if rustFormatFileText(t, repo, "src/lib.rs") != excluded || rustFormatFileText(t, repo, "src/child.rs") != "pub fn value() -> u32 {\n 99\n}\n" { + t.Fatal("rejected formatting changed excluded source or skipped a valid selected sibling") + } +} + +func TestRustFormatRequiresCompleteSyntaxEvidenceBeforeWriting(t *testing.T) { + t.Parallel() + for _, failure := range []error{errors.New("syntax tool unavailable"), nil} { + repo := rustFormatRepository(t) + dirty := "pub fn value()->u32{42}\n" + writeQualityFile(t, repo.Root, "src/lib.rs", dirty) + boundary := &rustSyntaxFailureRunner{failure: failure} + findings := Format(t.Context(), repo, repository.Selection{Files: []string{"src/lib.rs"}}, boundary) + if len(findings) != 1 || findings[0].Check != "policy.rustSource" || boundary.formatted != 0 { + t.Fatalf("missing or invalid syntax evidence authorized rustfmt: %+v, %d formatter calls", findings, boundary.formatted) + } + if rustFormatFileText(t, repo, "src/lib.rs") != dirty { + t.Fatal("missing syntax evidence authorized a source write") + } + } +} + +type rustSyntaxFailureRunner struct { + runner.OSRunner + failure error + formatted int +} + +func (boundary *rustSyntaxFailureRunner) RunStructured(ctx context.Context, root string, command policy.Command) (runner.Result, runner.Output, error) { + if command.Name == "policy-rust-source-facts" { + return runner.Result{}, runner.Output{Stdout: []byte("{}")}, boundary.failure } - if got := rustFormatFileText(t, repo, "src/lib.rs"); got != concurrent { - t.Fatalf("concurrent source edit was overwritten: %q", got) + if strings.HasPrefix(command.Name, "policy-rustfmt-") { + boundary.formatted++ } + return boundary.OSRunner.RunStructured(ctx, root, command) } type rustEditingRunner struct { runner.OSRunner - edit func() + before string + edit func() } func (boundary rustEditingRunner) RunStructured(ctx context.Context, root string, command policy.Command) (runner.Result, runner.Output, error) { - if strings.HasPrefix(command.Name, "policy-rustfmt-") { + if strings.HasPrefix(command.Name, boundary.before) { boundary.edit() } return boundary.OSRunner.RunStructured(ctx, root, command) diff --git a/internal/quality/rust_source.go b/internal/quality/rust_source.go index 46bffb87..36f23993 100644 --- a/internal/quality/rust_source.go +++ b/internal/quality/rust_source.go @@ -11,6 +11,8 @@ import ( "github.com/riteofstring/code-polishy/internal/rustfacts" ) +const rustFormatExclusionMessage = "Rust attributes must not exclude governed source from formatting" + func rustSourceFindings(ctx context.Context, repo repository.Repository, files []string, boundary runner.Runner) []policy.Finding { result := rustfacts.ReadSources(ctx, repo, rustfacts.PlanSources(repo, files), boundary) findings := result.Findings @@ -36,29 +38,48 @@ func rustAttributeFindings(repo repository.Repository, path string, attributes [ findings := []policy.Finding{} for _, attribute := range attributes { message := "" - switch attribute.Name { - case "allow", "expect": + switch { + case attribute.Name == "allow", attribute.Name == "expect": if repo.NativeAnalysis(path, "lint") { message = "inline Rust lint suppression must be replaced by an exact Code Polishy exception for the underlying finding" } - case "rustfmt::skip": + case rustFormatExclusion(attribute.Name): if repo.NativeAnalysis(path, "format") { - message = "Rust attributes must not exclude governed source from formatting" + message = rustFormatExclusionMessage } - case "doc": + case attribute.Name == "doc": if attribute.HasValue && repo.NativeAnalysis(path, "lint") && !repo.Config.Quality.CommentsAllowed() { findings = append(findings, rustCommentFinding(path, attribute.SourceLocation)) } } if message != "" { - subject := fmt.Sprintf("%s:%d:%d", attribute.Name, attribute.Line, attribute.Column) - findings = append(findings, rustLocatedFinding("policy.rustAttribute", path, subject, message, attribute.SourceLocation)) + findings = append(findings, rustAttributeFinding(path, attribute, message)) } findings = append(findings, rustAttributeFindings(repo, path, attribute.Children)...) } return findings } +func rustAttributeFinding(path string, attribute rustfacts.SourceAttribute, message string) policy.Finding { + subject := fmt.Sprintf("%s:%d:%d", attribute.Name, attribute.Line, attribute.Column) + return rustLocatedFinding("policy.rustAttribute", path, subject, message, attribute.SourceLocation) +} + +func rustFormatExclusionFindings(path string, attributes []rustfacts.SourceAttribute) []policy.Finding { + findings := []policy.Finding{} + for _, attribute := range attributes { + if rustFormatExclusion(attribute.Name) { + findings = append(findings, rustAttributeFinding(path, attribute, rustFormatExclusionMessage)) + } + findings = append(findings, rustFormatExclusionFindings(path, attribute.Children)...) + } + return findings +} + +func rustFormatExclusion(name string) bool { + return name == "rustfmt::skip" || name == "rustfmt::skip::macros" || name == "rustfmt::skip::attributes" +} + func rustComplexityFindings(repo repository.Repository, source rustfacts.Source) []policy.Finding { limits := policy.EffectiveQuality(repo.Config.Quality).Complexity findings := []policy.Finding{} diff --git a/internal/quality/rust_source_test.go b/internal/quality/rust_source_test.go index 37188b35..6dd21825 100644 --- a/internal/quality/rust_source_test.go +++ b/internal/quality/rust_source_test.go @@ -34,9 +34,9 @@ func TestRustSourcePolicyDistinguishesProseFromStringsAndDocMetadata(t *testing. func TestRustSourcePolicyRejectsConditionalLintAndFormatSuppression(t *testing.T) { t.Parallel() repo := cargoQualityRepository(t) - writeQualityFile(t, repo.Root, "src/lib.rs", "#[cfg_attr(feature = \"maybe\", r#allow(dead_code), rustfmt::skip)]\n#[expect(dead_code)]\nfn value() {}\n") + writeQualityFile(t, repo.Root, "src/lib.rs", "#[cfg_attr(feature = \"maybe\", r#allow(dead_code), rustfmt::skip, rustfmt::skip::macros(println))]\n#[expect(dead_code)]\n#[rustfmt::skip::attributes(custom)]\nfn value() {}\n") findings := rustSourceFindings(t.Context(), repo, []string{"src/lib.rs"}, runner.OSRunner{}) - for _, name := range []string{"allow", "expect", "rustfmt::skip"} { + for _, name := range []string{"allow", "expect", "rustfmt::skip", "rustfmt::skip::macros", "rustfmt::skip::attributes"} { if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { return finding.Check == "policy.rustAttribute" && strings.HasPrefix(finding.Subject, name+":") && finding.Path == "src/lib.rs" && finding.Line > 0 }) { From 3fafe17d95d81e5c923595b30089e8657f4e6234 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 06:19:33 +0000 Subject: [PATCH 26/49] feat: run native Rust doctests with governed evidence Add frozen Cargo doctest suites with a Rust 1.97.1 output adapter, complete case/count validation, distinct example semantics, and fresh structured artifacts. Reuse direct/gate execution, regression proofs and receipts. Keep output overflow operational even after a normal test-failure exit. Verified five boundary suites, targeted engine and regression-proof tests, source policy checks and a real public CLI run. The installed fixture now covers doctests; its updated native execution is next. General macro architecture, complete native release inventories and remaining native host evidence are still unfinished. --- .code-polishy.json | 6 + docs/ai-adoption.md | 7 +- docs/catalog.json | 2 +- docs/design/cargo-facts.md | 11 ++ docs/design/execution-observability.md | 4 + docs/design/installed-release-verification.md | 3 + docs/design/rust-inputs.md | 2 + docs/design/rust-tests.md | 45 +++++- docs/plans/code-polishy-rust-support-plan.md | 13 +- internal/behaviorreview/proof.go | 9 +- internal/behaviorreview/rust_proof_test.go | 33 +++- internal/engine/gate_command_runner.go | 15 +- internal/engine/rust_tests_test.go | 45 +++++- internal/engine/test_receipt_runner.go | 7 + internal/gaterun/identity.go | 2 +- internal/policy/model.go | 1 + internal/policy/rust_tests.go | 15 +- internal/policy/rust_tests_test.go | 44 ++++-- internal/runner/runner.go | 12 +- internal/runner/runner_test.go | 22 ++- internal/rustfacts/tests.go | 6 + internal/testartifact/rustdoc.go | 146 +++++++++++++++++ internal/testartifact/rustdoc_output.go | 143 +++++++++++++++++ internal/testartifact/rustdoc_test.go | 148 ++++++++++++++++++ internal/testartifact/xml.go | 3 + internal/testing/rust.go | 9 ++ internal/testing/rust_doctest_test.go | 120 ++++++++++++++ internal/testing/rust_output.go | 26 +++ internal/testing/testing.go | 6 + schema/code-polishy.schema.json | 1 + scripts/test-installed-release-rust.sh | 36 ++++- 31 files changed, 896 insertions(+), 46 deletions(-) create mode 100644 internal/testartifact/rustdoc.go create mode 100644 internal/testartifact/rustdoc_output.go create mode 100644 internal/testartifact/rustdoc_test.go create mode 100644 internal/testing/rust_doctest_test.go create mode 100644 internal/testing/rust_output.go diff --git a/.code-polishy.json b/.code-polishy.json index e3efb16d..e4863aec 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -35,7 +35,10 @@ "internal/rustfacts/tests.go", "internal/testing/rust.go", "internal/testing/rust_test.go", + "internal/testing/rust_output.go", + "internal/testing/rust_doctest_test.go", "internal/engine/test_receipts.go", + "internal/engine/test_receipt_runner.go", "internal/engine/test_execution_view.go", "internal/engine/rust_tests_test.go", "internal/behaviorreview/proof.go", @@ -44,6 +47,9 @@ "internal/testartifact/nextest.go", "internal/testartifact/nextest_config.go", "internal/testartifact/nextest_test.go", + "internal/testartifact/rustdoc.go", + "internal/testartifact/rustdoc_output.go", + "internal/testartifact/rustdoc_test.go", "internal/testartifact/xml.go" ] }, diff --git a/docs/ai-adoption.md b/docs/ai-adoption.md index 4ee21335..a691e50a 100644 --- a/docs/ai-adoption.md +++ b/docs/ai-adoption.md @@ -386,8 +386,11 @@ For Rust unit and integration suites, use the native `rust` selector described i [Rust test evidence](design/rust-tests.md). Preserve Cargo workspace default members and features unless the suite deliberately selects alternatives. Prepare the reviewed Cargo lock and dependency cache before frozen checks or tests. Native -unit/integration evidence does not establish doctest, benchmark or live-provider -coverage; those workflows need their own explicit evidence and authorization. +unit/integration evidence does not establish doctest coverage: declare a separate +suite with `rust.doctests: true` for compiler-owned documentation examples. It +preserves compile-fail, compile-only, expected-panic and ignored semantics. +Benchmark and live-provider workflows still need their own explicit evidence and +authorization. Use existing target tools where honest for genuinely project-specific providers. If such a provider requires a new or updated target dependency, diff --git a/docs/catalog.json b/docs/catalog.json index b19dae13..827cd5d2 100644 --- a/docs/catalog.json +++ b/docs/catalog.json @@ -314,7 +314,7 @@ "id": "rust-tests", "path": "docs/design/rust-tests.md", "title": "Native Rust Test Evidence", - "summary": "Understand native Cargo test selection, structured Nextest outcomes, artifact authority and doctest boundaries.", + "summary": "Understand native Cargo test selection, structured Nextest and doctest outcomes, artifact authority and compiler-specific boundaries.", "aliases": [], "public": true }, diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index 95349e75..b2722dc4 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -143,6 +143,17 @@ conditional/inline path overrides, absolute external-prelude paths and unresolve external glob bindings currently fail coverage explicitly. These are beta limitations, not optional exemptions. +A future compiler-semantic provider must retain generated references, hygiene +and original locations as well as written tokens. The Rust Analyzer revision +carried by Rust 1.97.1 builds its LSIF and SCIP indexes by iterating tokens in +physical source files and skipping unresolved classifications. Its +[static index](https://github.com/rust-lang/rust/blob/8bab26f4f68e0e26f0bb7960be334d5b520ea452/src/tools/rust-analyzer/crates/ide/src/static_index.rs) +can resolve written tokens through expansion, but does not enumerate every +reference introduced entirely by generated syntax. A successful navigation index +therefore cannot establish complete macro dependency coverage. General expansion +remains a required semantic-provider milestone; the written graph does not silently +admit those missing edges. + ## Compiler checks Compilation uses the matching pinned cargo-clippy executable directly, with an diff --git a/docs/design/execution-observability.md b/docs/design/execution-observability.md index e29f39f6..cf34b446 100644 --- a/docs/design/execution-observability.md +++ b/docs/design/execution-observability.md @@ -28,6 +28,10 @@ identifies both scope and cost. The observation wrapper forwards structured process results unchanged to suite execution. Recording an exit status while discarding it from the caller would make native test-artifact validation disagree with the command that produced it. +Bounded capture overflow is an operational failure even when the child exits with +a normal test-failure code. A partial stream cannot establish native outcome +evidence or a regression-proof red result. Cancellation and timeout retain their +more specific failure categories. Repository-wide Python dead-code analysis runs only for an explicit complete selection or merge gate. Focused Python checks run selected Ruff and ty work; diff --git a/docs/design/installed-release-verification.md b/docs/design/installed-release-verification.md index 3cf56d38..e201d572 100644 --- a/docs/design/installed-release-verification.md +++ b/docs/design/installed-release-verification.md @@ -107,6 +107,9 @@ The installed Rust adoption fixture uses a dependency-free Cargo library in a temporary repository. Every policy operation enters through the exact installed launcher and lock. It exercises selected formatting, compiler diagnostics in JSON and SARIF, a passing native suite, an assertion failure, and an empty selection. +Separate doctest cases cover runtime, expected-panic, compile-only, compile-fail +and ignored examples, assertion failure, empty evidence, crate compilation failure +and recovery through the installed compiler and result adapter. The fixture checks the retained structured test outcome rather than treating the CLI's nonzero exit as sufficient evidence. It never downloads target packages or changes the caller's repository authority. diff --git a/docs/design/rust-inputs.md b/docs/design/rust-inputs.md index 805a82bc..ea845082 100644 --- a/docs/design/rust-inputs.md +++ b/docs/design/rust-inputs.md @@ -238,3 +238,5 @@ the macOS archive is universal. Checked-in identities come from the exact Installation must authenticate archives and verify the staged executable before replacing a working tool. No Cargo source build, install script or self-update participates. Nextest does not run doctests; it cannot supply doctest coverage. +Separate native doctest suites use the same pinned Cargo and rustdoc, with the +version-bound evidence adapter defined in [Rust test evidence](rust-tests.md). diff --git a/docs/design/rust-tests.md b/docs/design/rust-tests.md index 34df3157..4561efa6 100644 --- a/docs/design/rust-tests.md +++ b/docs/design/rust-tests.md @@ -39,10 +39,42 @@ operational failures. Cancellation and timeouts never become report-bearing success. Required artifacts remain contained regular files, bounded and hashed once for reusable evidence by the existing artifact owner. -Doctests are not Nextest tests. Their compiler behavior, including compile-fail, -no-run and ignored examples, requires separate version-bound evidence before -doctest coverage can be claimed. This native suite contract does not turn a -successful build, a benchmark invocation or a live app probe into test evidence. +## Doctest evidence + +A separate suite with `rust.doctests: true` runs frozen `cargo test --doc` with +the same manifest, package and feature selection. It does not run Nextest or +import Nextest's project settings. Its `filter` field is forbidden because a +Nextest expression is not a rustdoc selector. Ignored examples remain ignored; +the runner never opts into them or into live-provider execution implicitly. + +The stable compiler's JSON and JUnit test formats still require unstable +options. Instead, the adapter is bound to Rust 1.97.1's Cargo JSON and captured, +uncolored libtest `pretty` output. It requires a successful final Cargo build, +every announced case, matching terminal counts and a complete test run. Compiler +upgrades must validate this exact output contract before doctests can execute; +no nightly flags, bootstrap override or deprecated logfile is used. + +The managed `rustdoc.json` artifact records each compiler-emitted batch and case. +Batch identity is execution-local, not an invented package identity. This retains +multiple workspace crates and Rust 2024's merged and standalone doctest batches, +including repeated display names in different batches. Cases distinguish runtime, +expected-panic, compile-only and compile-fail checks from ignored examples. +Compile-only checks do not claim runtime execution. A suite must check at least +one example; an empty or wholly ignored run fails. + +Ordinary example failures, including examples that fail to compile unexpectedly, +are doctest failures. A crate/build-script compilation failure has no successful +Cargo build and cannot establish a regression-proof red result. Truncated, +partial or inconsistent output, cancellation, timeout and artifact failures +remain operational outcomes. The original Cargo exit is retained. Direct tests, +gate execution, regression proofs and receipt reuse share the adapter and fresh +artifact boundary. Capturing output cannot replace a previous attempt's report. + +This contract follows the pinned +[libtest formatter](https://github.com/rust-lang/rust/blob/8bab26f4f68e0e26f0bb7960be334d5b520ea452/library/test/src/formatters/pretty.rs) +and [rustdoc execution](https://github.com/rust-lang/rust/blob/8bab26f4f68e0e26f0bb7960be334d5b520ea452/src/librustdoc/doctest.rs). +A successful build, benchmark invocation or live app probe alone is not test +evidence. ## Suite configuration @@ -75,3 +107,8 @@ Do not supply `argv`, `retryArgv`, a non-root `cwd` or a replacement `nextest.xm artifact for a native Rust suite. Native execution owns those fields and requires its result report. Frozen testing requires a reviewed lock and a prepared Cargo cache; it never installs dependencies or repairs resolution. + +Declare a distinct doctest suite using `"rust": {"manifest": "Cargo.toml", +"doctests": true}`. Its required `rustdoc.json` artifact is also native-owned. +Unit/integration and doctest results do not substitute for each other or schedule +each other; ordinary suite ownership, profiles and explicit selection still apply. diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 7ae6e652..18239114 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -129,7 +129,7 @@ changed modules; add a quick suite when a new module is introduced. function/closure measurements. Inline tests do not relax production limits. The pinned Tree-sitter Rust grammar supplies written-source facts; it does not pretend to resolve macro expansion or compiler configuration. -- Doctest evidence, general macro/module resolution, WGSL and +- General macro/module resolution, WGSL and installed-release integration remain unfinished. Project Clippy overrides and non-native targets currently fail explicitly rather than weakening the supported compiler configuration. @@ -159,7 +159,14 @@ changed modules; add a quick suite when a new module is introduced. Cargo/compiler/Nextest controls do not replace governed execution. Nextest state stays outside reusable read-only source views. Project Nextest settings require explicit support rather than silently dropping their default filters or scripts. - Doctests and ordinary macro-heavy project coverage remain unfinished. + Ordinary macro-heavy project coverage remains unfinished. +- Separate native doctest suites run the pinned stable Cargo/rustdoc without + unstable flags. Version-bound output becomes a required structured artifact + with complete case/count checks and distinct runtime, expected-panic, + compile-only, compile-fail and ignored outcomes. Real fixtures cover workspaces, + package/features, frozen locks, cancellation, empty/ignored runs, crate and + build-script failures, direct/gate execution, regression proofs and receipts. + A doctest result never substitutes for unit/integration evidence. Cargo dependency-input governance now parses ordinary, build, development, target-specific and workspace declaration tables, checks exact registry and Git @@ -429,7 +436,7 @@ copying the lock or substituting generic license text does not resolve it. Rust and Nextest SBOM entries retain explicit incomplete native compositions, host-specific authenticated download references and carried notice locations. -The next milestones extend ordinary macro/module and doctest coverage and +The next milestones extend ordinary macro/module coverage and complete installed-release integration. Existing coverage failures remain until their implementations and evidence exist. This beta does not yet establish first-class support for Masterbrainer's full project. diff --git a/internal/behaviorreview/proof.go b/internal/behaviorreview/proof.go index ddc6a246..4933d112 100644 --- a/internal/behaviorreview/proof.go +++ b/internal/behaviorreview/proof.go @@ -547,11 +547,16 @@ type proofSuiteRunner struct { } func (capture *proofSuiteRunner) RunWithResult(ctx context.Context, root string, command policy.Command) (runner.Result, error) { - result, output, err := capture.RunWithOutput(ctx, root, command) - capture.output = output + result, _, err := capture.RunWithOutput(ctx, root, command) return result, err } +func (capture *proofSuiteRunner) RunWithOutput(ctx context.Context, root string, command policy.Command) (runner.Result, runner.Output, error) { + result, output, err := capture.OutputRunner.RunWithOutput(ctx, root, command) + capture.output = output + return result, output, err +} + func cloneCommand(command policy.Command) policy.Command { return policy.Command{ Name: command.Name, Provides: slices.Clone(command.Provides), Argv: slices.Clone(command.Argv), Cwd: command.Cwd, diff --git a/internal/behaviorreview/rust_proof_test.go b/internal/behaviorreview/rust_proof_test.go index 1caffba6..00c08fd5 100644 --- a/internal/behaviorreview/rust_proof_test.go +++ b/internal/behaviorreview/rust_proof_test.go @@ -12,12 +12,22 @@ import ( func TestRustRegressionExecutionRequiresRealTestOutcomes(t *testing.T) { t.Parallel() + testRustRegressionExecution(t, false) +} + +func TestRustDoctestRegressionExecutionRequiresRealTestOutcomes(t *testing.T) { + t.Parallel() + testRustRegressionExecution(t, true) +} + +func testRustRegressionExecution(t *testing.T, doctests bool) { + t.Helper() policyRoot, err := filepath.Abs("../..") if err != nil { t.Fatal(err) } - suite := policy.TestSuite{Name: "rust-unit", Cwd: ".", TimeoutSeconds: 30, Rust: &policy.RustTestSuite{Manifest: "Cargo.toml"}} - for name, test := range map[string]struct { + suite := policy.TestSuite{Name: "rust-unit", Cwd: ".", TimeoutSeconds: 30, Rust: &policy.RustTestSuite{Manifest: "Cargo.toml", Doctests: doctests}} + tests := map[string]struct { source string redExit int operational bool @@ -25,7 +35,22 @@ func TestRustRegressionExecutionRequiresRealTestOutcomes(t *testing.T) { "assertion": {source: "#[test]\nfn behavior() { assert_eq!(2 + 2, 5); }\n", redExit: 100}, "compiler": {source: "#[test]\nfn behavior() { missing_function(); }\n", redExit: 101, operational: true}, "empty": {source: "pub fn answer() -> u32 { 42 }\n", redExit: 4, operational: true}, - } { + } + greenSource := "#[test]\nfn behavior() { assert_eq!(2 + 2, 4); }\n" + if doctests { + greenSource = "/// ```\n/// assert_eq!(2 + 2, 4);\n/// ```\npub fn answer() {}\n" + assertion := tests["assertion"] + assertion.source = "/// ```\n/// assert_eq!(2 + 2, 5);\n/// ```\npub fn answer() {}\n" + assertion.redExit = 101 + tests["assertion"] = assertion + compiler := tests["compiler"] + compiler.source = "pub fn answer() { missing_function(); }\n" + tests["compiler"] = compiler + empty := tests["empty"] + empty.redExit = 0 + tests["empty"] = empty + } + for name, test := range tests { t.Run(name, func(t *testing.T) { t.Parallel() repo := repository.Repository{Root: t.TempDir(), PolicyRoot: policyRoot} @@ -40,7 +65,7 @@ func TestRustRegressionExecutionRequiresRealTestOutcomes(t *testing.T) { if (err == nil) == test.operational { t.Fatalf("invalid regression red: %+v, %v", result, err) } - writeBehaviorFile(t, repo.Root, "src/lib.rs", "#[test]\nfn behavior() { assert_eq!(2 + 2, 4); }\n") + writeBehaviorFile(t, repo.Root, "src/lib.rs", greenSource) green, _, err := runSuite(t.Context(), repo, runner.OSRunner{}, repo.Root, suite) if err != nil || green.ExitStatus != 0 { t.Fatalf("valid green rejected: %+v, %v", green, err) diff --git a/internal/engine/gate_command_runner.go b/internal/engine/gate_command_runner.go index 54435458..d56231b3 100644 --- a/internal/engine/gate_command_runner.go +++ b/internal/engine/gate_command_runner.go @@ -245,7 +245,8 @@ func (commandRunner *gateArtifactRunner) execute(ctx context.Context, root strin commandRunner.err = err return runner.Result{ExitStatus: -1, FailureCategory: runner.FailureOperational}, runner.Output{}, err } - result, output, runErr := runGateCommand(ctx, commandRunner.delegate, root, actual, commandLog.Stdout(), commandLog.Stderr(), captureOutput) + result, output, runErr := runGateCommand(ctx, commandRunner.delegate, root, actual, commandLog.Stdout(), commandLog.Stderr(), captureOutput || testartifact.NeedsCommandOutput(actual)) + runErr = captureGateTestOutput(ctx, actual, &result, output, runErr) artifacts, artifactErr := commandRunner.validateTestArtifacts(index, actual, &result) runErr = errors.Join(runErr, artifactErr) logResult, logErr := commandLog.Close() @@ -278,6 +279,18 @@ func (commandRunner *gateArtifactRunner) execute(ctx context.Context, root strin return commandRunner.finishCommand(command, result, output, runErr, category, reportBearing, logResult, attempt.LogPath) } +func captureGateTestOutput(ctx context.Context, command policy.Command, result *runner.Result, output runner.Output, runErr error) error { + category := runner.FailureCategoryFor(ctx, *result, runErr) + if category != "" && category != runner.FailureCommandExit { + return runErr + } + if err := testartifact.CaptureCommandOutput(command, output.Stdout); err != nil { + result.FailureCategory = runner.FailureOperational + return errors.Join(runErr, err) + } + return runErr +} + func (commandRunner *gateArtifactRunner) validateTestArtifacts(index int, command policy.Command, result *runner.Result) ([]testartifact.Record, error) { if commandRunner.expected[index].Category != gaterun.OrdinaryTest { return []testartifact.Record{}, nil diff --git a/internal/engine/rust_tests_test.go b/internal/engine/rust_tests_test.go index b0467078..18a6dbcc 100644 --- a/internal/engine/rust_tests_test.go +++ b/internal/engine/rust_tests_test.go @@ -19,7 +19,22 @@ import ( func TestRustNativeGateRetainsRealValidatedEvidence(t *testing.T) { t.Parallel() + testRustNativeGate(t, false) +} + +func TestRustDoctestGateRetainsRealValidatedEvidence(t *testing.T) { + t.Parallel() + testRustNativeGate(t, true) +} + +func testRustNativeGate(t *testing.T, doctests bool) { + t.Helper() root := nativeRustEngineRepository(t) + artifactType := policy.NextestJUnitArtifact + if doctests { + enableRustDoctests(t, root) + artifactType = policy.RustdocArtifact + } installBehaviorReviewTestGuidance(t, root) initializeEngineGitRepository(t, root) writeEngineFile(t, root, "content/data.json", "{\"changed\":true}\n", 0o600) @@ -45,14 +60,27 @@ func TestRustNativeGateRetainsRealValidatedEvidence(t *testing.T) { if err := json.Unmarshal(data, &published); err != nil { t.Fatal(err) } - if len(published.TestEvidence) == 0 || len(published.TestEvidence[0].Artifacts) != 1 || published.TestEvidence[0].Artifacts[0].Type != policy.NextestJUnitArtifact { + if len(published.TestEvidence) == 0 || len(published.TestEvidence[0].Artifacts) != 1 || published.TestEvidence[0].Artifacts[0].Type != artifactType { t.Fatalf("missing native evidence: %+v", published.TestEvidence) } } func TestRustNativeReceiptsReuseOnlyTheSameSelectionAndInputs(t *testing.T) { t.Parallel() + testRustNativeReceipt(t, false) +} + +func TestRustDoctestReceiptsReuseOnlyTheSameSelectionAndInputs(t *testing.T) { + t.Parallel() + testRustNativeReceipt(t, true) +} + +func testRustNativeReceipt(t *testing.T, doctests bool) { + t.Helper() root := nativeRustEngineRepository(t) + if doctests { + enableRustDoctests(t, root) + } engine, err := Open(root, enginePolicyRoot(t), "") if err != nil { t.Fatal(err) @@ -75,7 +103,7 @@ func TestRustNativeReceiptsReuseOnlyTheSameSelectionAndInputs(t *testing.T) { if err != nil || !hit || !reused.Reused || len(reused.Artifacts) != 1 { t.Fatalf("native receipt reuse: %+v, %v, %v", reused, hit, err) } - suite.Rust.Filter = "test(=other)" + suite.Rust.Doctests = !doctests changed, err := newTestReceiptController(engine, []policy.TestSuite{suite}, true) if err != nil { t.Fatal(err) @@ -83,7 +111,7 @@ func TestRustNativeReceiptsReuseOnlyTheSameSelectionAndInputs(t *testing.T) { if _, hit, err := changed.ReuseSuite(suite, 1); err != nil || hit { t.Fatalf("changed selector reused evidence: hit=%v, %v", hit, err) } - suite.Rust.Filter = "" + suite.Rust.Doctests = doctests writeEngineFile(t, root, "content/lib.rs", "#[test]\nfn behavior() { assert_eq!(2 + 2, 5); }\n", 0o600) changed, err = newTestReceiptController(engine, []policy.TestSuite{suite}, true) if err != nil { @@ -103,6 +131,7 @@ func TestRustHistoricalSuiteComparisonKeepsTypedSelectors(t *testing.T) { func(value *policy.RustTestSuite) { value.Features[0] = "optional" }, func(value *policy.RustTestSuite) { value.Filter = "test(selected)" }, func(value *policy.RustTestSuite) { value.NoDefaultFeatures = true }, + func(value *policy.RustTestSuite) { value.Doctests = true }, } { changed := cloneReceiptSuite(original) alter(changed.Rust) @@ -112,6 +141,16 @@ func TestRustHistoricalSuiteComparisonKeepsTypedSelectors(t *testing.T) { } } +func enableRustDoctests(t *testing.T, root string) { + t.Helper() + data, err := os.ReadFile(filepath.Join(root, policy.ConfigFilename)) + if err != nil { + t.Fatal(err) + } + writeEngineFile(t, root, policy.ConfigFilename, strings.Replace(string(data), `"rust":{}`, `"rust":{"doctests":true}`, 1), 0o600) + writeEngineFile(t, root, "content/lib.rs", "/// ```\n/// assert_eq!(2 + 2, 4);\n/// ```\npub fn answer() {}\n", 0o600) +} + func TestRustDirectTestTelemetryPreservesNativeExitStatus(t *testing.T) { t.Parallel() for name, test := range map[string]struct { diff --git a/internal/engine/test_receipt_runner.go b/internal/engine/test_receipt_runner.go index 91039d12..a7287046 100644 --- a/internal/engine/test_receipt_runner.go +++ b/internal/engine/test_receipt_runner.go @@ -180,6 +180,13 @@ func (commandRunner *testReceiptRunner) RunWithResult(ctx context.Context, root return runner.Result{ExecutionDuration: time.Since(started)}, err } +func (commandRunner *testReceiptRunner) RunWithOutput(ctx context.Context, root string, command policy.Command) (runner.Result, runner.Output, error) { + if captured, ok := commandRunner.delegate.(runner.OutputRunner); ok { + return captured.RunWithOutput(ctx, root, command) + } + return runner.Result{ExitStatus: -1, FailureCategory: runner.FailureOperational}, runner.Output{}, errors.New("test receipt runner requires bounded native command output") +} + func (commandRunner *testReceiptRunner) ReuseSuite(suite policy.TestSuite, attempt int) (testpolicy.SuiteExecution, bool, error) { return commandRunner.controller.ReuseSuite(suite, attempt) } diff --git a/internal/gaterun/identity.go b/internal/gaterun/identity.go index 4bd7f9c9..a6216ef8 100644 --- a/internal/gaterun/identity.go +++ b/internal/gaterun/identity.go @@ -254,7 +254,7 @@ func validateArtifactSpecs(artifacts []ArtifactSpec) error { } func validArtifactType(value string) bool { - return value == "junit" || value == "cobertura" || value == policy.NextestJUnitArtifact + return value == "junit" || value == "cobertura" || value == policy.NextestJUnitArtifact || value == policy.RustdocArtifact } func validCommandIdentity(command CommandSpec) bool { diff --git a/internal/policy/model.go b/internal/policy/model.go index 8e328436..fd94a029 100644 --- a/internal/policy/model.go +++ b/internal/policy/model.go @@ -399,6 +399,7 @@ type TestArtifact struct { type RustTestSuite struct { Manifest string `json:"manifest,omitempty"` + Doctests bool `json:"doctests,omitempty"` Packages []string `json:"packages,omitempty"` Features []string `json:"features,omitempty"` NoDefaultFeatures bool `json:"noDefaultFeatures,omitempty"` diff --git a/internal/policy/rust_tests.go b/internal/policy/rust_tests.go index d2eec01d..0a3b4c9d 100644 --- a/internal/policy/rust_tests.go +++ b/internal/policy/rust_tests.go @@ -8,6 +8,8 @@ import ( ) const NextestJUnitArtifact = "nextest-junit" +const RustdocArtifact = "rustdoc" +const RustdocToolVersion = "1.97.1" func validateSuiteRunner(suite TestSuite, label string) error { if suite.Rust == nil { @@ -29,6 +31,9 @@ func validateSuiteRunner(suite TestSuite, label string) error { } func validateRustTestSelection(settings RustTestSuite, label string) error { + if settings.Doctests && settings.Filter != "" { + return fmt.Errorf("%s.rust.filter is a Nextest expression and cannot select doctests", label) + } if path.Base(settings.Manifest) != "Cargo.toml" { return fmt.Errorf("%s.rust.manifest must identify a Cargo.toml", label) } @@ -69,8 +74,8 @@ func rustExecutionVariable(name string) bool { func validateRustTestArtifacts(artifacts []TestArtifact, label string) error { for _, artifact := range artifacts { - if artifact.Path == "nextest.xml" || artifact.Path == ".code-polishy-nextest.toml" || artifact.Type == NextestJUnitArtifact { - return fmt.Errorf("%s.rust owns its required Nextest report and configuration", label) + if artifact.Path == "nextest.xml" || artifact.Path == "rustdoc.json" || artifact.Path == ".code-polishy-nextest.toml" || artifact.Type == NextestJUnitArtifact || artifact.Type == RustdocArtifact { + return fmt.Errorf("%s.rust owns its required native test reports and configuration", label) } } return nil @@ -78,7 +83,9 @@ func validateRustTestArtifacts(artifacts []TestArtifact, label string) error { func (suite TestSuite) ArtifactDeclarations() []TestArtifact { result := append([]TestArtifact{}, suite.Artifacts...) - if suite.Rust != nil { + if suite.Rust != nil && suite.Rust.Doctests { + result = append(result, TestArtifact{Path: "rustdoc.json", Type: RustdocArtifact, Required: true}) + } else if suite.Rust != nil { result = append(result, TestArtifact{Path: "nextest.xml", Type: NextestJUnitArtifact, Required: true}) } return result @@ -94,7 +101,7 @@ func (settings *RustTestSuite) Equal(other *RustTestSuite) bool { if settings == nil || other == nil { return settings == other } - return settings.Manifest == other.Manifest && settings.Filter == other.Filter && settings.NoDefaultFeatures == other.NoDefaultFeatures && + return settings.Manifest == other.Manifest && settings.Doctests == other.Doctests && settings.Filter == other.Filter && settings.NoDefaultFeatures == other.NoDefaultFeatures && slices.Equal(settings.Packages, other.Packages) && slices.Equal(settings.Features, other.Features) } diff --git a/internal/policy/rust_tests_test.go b/internal/policy/rust_tests_test.go index 6c875cfb..46b65631 100644 --- a/internal/policy/rust_tests_test.go +++ b/internal/policy/rust_tests_test.go @@ -31,18 +31,19 @@ func TestRustSuiteConfigurationPreservesCargoSelection(t *testing.T) { func TestRustSuiteConfigurationRejectsCommandAndEvidenceOverrides(t *testing.T) { t.Parallel() for name, selector := range map[string]string{ - "two runners": `"rust":{},"argv":["cargo","test"]`, - "no runner": `"environment":[]`, - "retry replacement": `"rust":{},"retryArgv":["true"]`, - "cwd replacement": `"rust":{},"cwd":"crate"`, - "escaped manifest": `"rust":{"manifest":"../Cargo.toml"}`, - "wrong manifest": `"rust":{"manifest":"Cargo.lock"}`, - "flags as package": `"rust":{"packages":["--workspace"]}`, - "joined features": `"rust":{"features":["one two"]}`, - "nul filter": `"rust":{"filter":"test(\u0000)"}`, - "ambient flags": `"rust":{},"environment":["RUSTFLAGS"]`, - "ambient retry": `"rust":{},"environment":["NEXTEST_RETRIES"]`, - "report override": `"rust":{},"artifacts":[{"path":"nextest.xml","type":"junit"}]`, + "two runners": `"rust":{},"argv":["cargo","test"]`, + "no runner": `"environment":[]`, + "retry replacement": `"rust":{},"retryArgv":["true"]`, + "cwd replacement": `"rust":{},"cwd":"crate"`, + "escaped manifest": `"rust":{"manifest":"../Cargo.toml"}`, + "wrong manifest": `"rust":{"manifest":"Cargo.lock"}`, + "flags as package": `"rust":{"packages":["--workspace"]}`, + "joined features": `"rust":{"features":["one two"]}`, + "nul filter": `"rust":{"filter":"test(\u0000)"}`, + "ambient flags": `"rust":{},"environment":["RUSTFLAGS"]`, + "ambient retry": `"rust":{},"environment":["NEXTEST_RETRIES"]`, + "report override": `"rust":{},"artifacts":[{"path":"nextest.xml","type":"junit"}]`, + "doctest Nextest filter": `"rust":{"doctests":true,"filter":"test(behavior)"}`, } { t.Run(name, func(t *testing.T) { input := strings.Replace(minimalConfig(), `"argv":["go","test","./..."]`, selector, 1) @@ -53,6 +54,25 @@ func TestRustSuiteConfigurationRejectsCommandAndEvidenceOverrides(t *testing.T) } } +func TestRustDoctestConfigurationSelectsDistinctEvidence(t *testing.T) { + t.Parallel() + input := strings.Replace(minimalConfig(), `"argv":["go","test","./..."]`, `"rust":{"doctests":true,"packages":["core"]}`, 1) + config, err := Parse([]byte(input), ConfigFilename) + if err != nil { + t.Fatal(err) + } + suite := config.Tests.Suites[0] + artifacts := suite.ArtifactDeclarations() + if !suite.Rust.Doctests || len(artifacts) != 1 || artifacts[0].Type != RustdocArtifact || !artifacts[0].Required { + t.Fatalf("doctest selection lost native evidence: %+v, %+v", suite, artifacts) + } + normal := suite.Rust.Clone() + normal.Doctests = false + if normal.Equal(suite.Rust) || !suite.Rust.Equal(suite.Rust.Clone()) { + t.Fatal("doctests and unit tests have indistinguishable selection") + } +} + func TestRustSuiteConfigurationRetainsCargoIdentifierConventions(t *testing.T) { t.Parallel() input := strings.Replace(minimalConfig(), `"argv":["go","test","./..."]`, `"rust":{"packages":["core_crate@1.2.3"],"features":["std+alloc","codec.v2","unicode_λ"]}`, 1) diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 9c346287..eb3a1d07 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -146,7 +146,7 @@ func (runner OSRunner) RunWithOutput(parent context.Context, root string, specif result, err := runner.run(parent, root, specification, appendOutputWriter(runner.Stdout, stdout), appendOutputWriter(runner.Stderr, stderr)) if stdout.truncated || stderr.truncated { err = errors.Join(err, fmt.Errorf("captured structured output for command %q exceeds its %d byte stream limit", specification.Name, captureLimit(stdout, stderr))) - result.FailureCategory = FailureCategoryFor(parent, result, err) + result.FailureCategory = captureFailureCategory(parent, result, err) } return result, Output{Stdout: stdout.buffer.Bytes(), Stderr: stderr.buffer.Bytes()}, err } @@ -157,7 +157,7 @@ func (runner OSRunner) RunStructured(parent context.Context, root string, specif result, err := runner.run(parent, root, specification, stdout, appendOutputWriter(runner.Stderr, stderr)) if stdout.truncated || stderr.truncated { err = errors.Join(err, fmt.Errorf("captured structured output for command %q exceeds its %d byte stream limit", specification.Name, captureLimit(stdout, stderr))) - result.FailureCategory = FailureCategoryFor(parent, result, err) + result.FailureCategory = captureFailureCategory(parent, result, err) } return result, Output{Stdout: stdout.buffer.Bytes(), Stderr: stderr.buffer.Bytes()}, err } @@ -169,6 +169,14 @@ func captureLimit(stdout, stderr *boundedOutput) int { return stderr.limit } +func captureFailureCategory(ctx context.Context, result Result, err error) FailureCategory { + category := FailureCategoryFor(ctx, result, err) + if category == FailureTimeout || category == FailureCanceled { + return category + } + return FailureOperational +} + func appendOutputWriter(configured io.Writer, captured io.Writer) io.Writer { if configured == nil { return captured diff --git a/internal/runner/runner_test.go b/internal/runner/runner_test.go index 5cd2ed71..3aea1174 100644 --- a/internal/runner/runner_test.go +++ b/internal/runner/runner_test.go @@ -104,19 +104,35 @@ func TestOSRunnerRunWithWritersUsesOnlyTheRequestedDestinations(t *testing.T) { } func TestOSRunnerBoundsStructuredOutputCapture(t *testing.T) { + t.Parallel() + for _, structured := range []bool{false, true} { + for _, exit := range []string{"0", "101"} { + t.Run("structured="+strconv.FormatBool(structured)+"/exit="+exit, func(t *testing.T) { + testBoundedOutputCapture(t, structured, exit) + }) + } + } +} + +func testBoundedOutputCapture(t *testing.T, structured bool, exit string) { + t.Helper() root := t.TempDir() script := filepath.Join(root, "large-report.sh") - if err := os.WriteFile(script, []byte("#!/bin/sh\nhead -c 8388609 /dev/zero\nhead -c 65537 /dev/zero >&2\n"), 0o700); err != nil { + if err := os.WriteFile(script, []byte("#!/bin/sh\nhead -c 8388609 /dev/zero\nhead -c 65537 /dev/zero >&2\nexit "+exit+"\n"), 0o700); err != nil { t.Fatal(err) } - result, output, err := (OSRunner{}).RunWithOutput(context.Background(), root, policy.Command{ + run := (OSRunner{}).RunWithOutput + if structured { + run = (OSRunner{}).RunStructured + } + result, output, err := run(context.Background(), root, policy.Command{ Name: "large-report", Argv: []string{"./large-report.sh"}, Cwd: ".", TimeoutSeconds: successfulCommandTimeoutSeconds, }) if err == nil || !strings.Contains(err.Error(), "captured structured output") { t.Fatalf("large structured output error = %v", err) } - if result.ExitStatus != 0 || len(output.Stdout) != maximumStructuredOutputBytes || len(output.Stderr) != maximumStructuredDiagnosticBytes { + if strconv.Itoa(result.ExitStatus) != exit || result.FailureCategory != FailureOperational || len(output.Stdout) != maximumStructuredOutputBytes || len(output.Stderr) != maximumStructuredDiagnosticBytes { t.Fatalf("large structured output result=%+v stdout=%d stderr=%d", result, len(output.Stdout), len(output.Stderr)) } } diff --git a/internal/rustfacts/tests.go b/internal/rustfacts/tests.go index 2609af2c..fe0f8d77 100644 --- a/internal/rustfacts/tests.go +++ b/internal/rustfacts/tests.go @@ -22,6 +22,12 @@ func TestProblem(repo repository.Repository, settings policy.RustTestSuite) erro if err := compilationTargetConfiguration(repo); err != nil { return err } + if settings.Doctests { + if repo.ToolPin("rust") != policy.RustdocToolVersion { + return errors.New("doctest evidence requires its matching pinned Rust compiler") + } + return nil + } info, err := os.Stat(repo.PolicyTool("cargo-nextest")) if err != nil || !info.Mode().IsRegular() || runtime.GOOS != "windows" && info.Mode()&0o111 == 0 || repo.ToolPin("cargo-nextest") == "" { return errors.New("pinned cargo-nextest is unavailable; install the policy-owned test runner") diff --git a/internal/testartifact/rustdoc.go b/internal/testartifact/rustdoc.go new file mode 100644 index 00000000..dd611bbc --- /dev/null +++ b/internal/testartifact/rustdoc.go @@ -0,0 +1,146 @@ +package testartifact + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/riteofstring/code-polishy/internal/policy" +) + +type rustdocCase struct { + Name string `json:"name"` + Kind string `json:"kind"` + Outcome string `json:"outcome"` +} + +type rustdocBatch struct { + Cases []rustdocCase `json:"cases"` + Passed int `json:"passed"` + Failed int `json:"failed"` + Ignored int `json:"ignored"` +} + +type rustdocReport struct { + Protocol string `json:"protocol"` + RustVersion string `json:"rustVersion"` + Suite string `json:"suite"` + BuildSucceeded bool `json:"buildSucceeded"` + Batches []rustdocBatch `json:"batches"` +} + +func NeedsCommandOutput(command policy.Command) bool { + for _, artifact := range command.TestArtifacts { + if artifact.Type == policy.RustdocArtifact { + return true + } + } + return false +} + +func CaptureCommandOutput(command policy.Command, stdout []byte) error { + if !NeedsCommandOutput(command) { + return nil + } + if command.TestArtifactDirectory == "" || command.TestArtifactSuite == "" { + return errors.New("doctest evidence has no managed artifact directory or suite") + } + report, err := parseRustdocOutput(stdout, command.TestArtifactSuite) + if err != nil { + return fmt.Errorf("invalid doctest output: %w", err) + } + data, err := json.Marshal(report) + if err != nil || len(data) > MaximumArtifactBytes { + return errors.New("doctest report exceeds its bounded JSON contract") + } + root, err := os.OpenRoot(command.TestArtifactDirectory) + if err != nil { + return err + } + defer root.Close() + file, err := root.OpenFile("rustdoc.json", os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) + if err != nil { + return err + } + _, writeErr := file.Write(data) + return errors.Join(writeErr, file.Close()) +} + +func validateRustdoc(data []byte, suite string, exitStatus int) error { + var report rustdocReport + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&report); err != nil { + return fmt.Errorf("invalid doctest evidence: %w", err) + } + if err := decoder.Decode(new(any)); !errors.Is(err, io.EOF) { + return errors.New("doctest evidence contains trailing data") + } + if !report.matches(suite) { + return errors.New("doctest evidence lacks the current suite, compiler or successful Cargo build") + } + passed, failed := 0, 0 + for _, batch := range report.Batches { + if err := validateRustdocBatch(batch); err != nil { + return err + } + passed += batch.Passed + failed += batch.Failed + } + if passed+failed == 0 { + return errors.New("doctest suite selected no checked examples") + } + if exitStatus == 0 && failed == 0 || exitStatus == 101 && failed > 0 { + return nil + } + return fmt.Errorf("cargo exit %d disagrees with doctest evidence or indicates an operational failure", exitStatus) +} + +func (report rustdocReport) matches(suite string) bool { + return report.Protocol == "rustdoc/v1" && report.RustVersion == policy.RustdocToolVersion && report.Suite == suite && report.BuildSucceeded +} + +func validateRustdocBatch(batch rustdocBatch) error { + seen := map[string]bool{} + counts := rustdocBatch{} + for _, test := range batch.Cases { + if strings.TrimSpace(test.Name) == "" || seen[test.Name] || test.Kind != rustdocCaseKind(test.Name) { + return errors.New("doctest report has missing, duplicate or inconsistent case identities") + } + seen[test.Name] = true + if err := countRustdocOutcome(&counts, test.Outcome); err != nil { + return err + } + } + if batch.Cases == nil || batch.Passed != counts.Passed || batch.Failed != counts.Failed || batch.Ignored != counts.Ignored { + return errors.New("doctest report counts disagree with its complete cases") + } + return nil +} + +func countRustdocOutcome(batch *rustdocBatch, outcome string) error { + switch outcome { + case "passed": + batch.Passed++ + case "failed": + batch.Failed++ + case "ignored": + batch.Ignored++ + default: + return errors.New("unknown doctest outcome") + } + return nil +} + +func rustdocCaseKind(name string) string { + for suffix, kind := range map[string]string{" - compile fail": "compile-fail", " - compile": "compile-only", " - should panic": "should-panic"} { + if strings.HasSuffix(name, suffix) { + return kind + } + } + return "run" +} diff --git a/internal/testartifact/rustdoc_output.go b/internal/testartifact/rustdoc_output.go new file mode 100644 index 00000000..8c775f52 --- /dev/null +++ b/internal/testartifact/rustdoc_output.go @@ -0,0 +1,143 @@ +package testartifact + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + "regexp" + "strconv" + "strings" + "unicode/utf8" + + "github.com/riteofstring/code-polishy/internal/policy" +) + +var rustdocRunStart = regexp.MustCompile(`^running ([0-9]+) (test|tests)$`) +var rustdocRunEnd = regexp.MustCompile(`^test result: (ok|FAILED)\. ([0-9]+) passed; ([0-9]+) failed; ([0-9]+) ignored; 0 measured; 0 filtered out; finished in [0-9]+\.[0-9]+s$`) +var rustdocRunTiming = regexp.MustCompile(`^all doctests ran in [0-9]+\.[0-9]+s; merged doctests compilation took [0-9]+\.[0-9]+s$`) + +type rustdocOutput struct { + report rustdocReport + active *rustdocBatch + expected int + seen map[string]bool +} + +func parseRustdocOutput(data []byte, suite string) (rustdocReport, error) { + state := rustdocOutput{report: rustdocReport{Protocol: "rustdoc/v1", RustVersion: policy.RustdocToolVersion, Suite: suite}} + if len(data) > 8<<20 || !utf8.Valid(data) { + return state.report, errors.New("doctest output exceeds 8388608 bytes or is not UTF-8") + } + scanner := bufio.NewScanner(bytes.NewReader(data)) + scanner.Buffer(make([]byte, 4096), 1<<20) + for scanner.Scan() { + if err := state.line(scanner.Text()); err != nil { + return state.report, err + } + } + if err := scanner.Err(); err != nil { + return state.report, err + } + if !state.report.BuildSucceeded || state.active != nil || len(state.report.Batches) == 0 { + return state.report, errors.New("doctest output has no complete Cargo build and test run") + } + return state.report, nil +} + +func (state *rustdocOutput) line(line string) error { + if line == "" { + return nil + } + if !state.report.BuildSucceeded { + return state.cargo(line) + } + if state.active == nil { + return state.start(line) + } + if len(state.active.Cases) < state.expected { + return state.test(line) + } + return state.finish(line) +} + +func (state *rustdocOutput) cargo(line string) error { + var message struct { + Reason string `json:"reason"` + Success *bool `json:"success"` + } + if err := json.Unmarshal([]byte(line), &message); err != nil { + return errors.New("doctest output precedes a complete Cargo JSON build") + } + switch message.Reason { + case "compiler-artifact", "compiler-message", "build-script-executed": + return nil + case "build-finished": + if message.Success != nil && *message.Success { + state.report.BuildSucceeded = true + return nil + } + } + return errors.New("doctest output contains a failed or unsupported Cargo build event") +} + +func (state *rustdocOutput) start(line string) error { + if len(state.report.Batches) > 0 && rustdocRunTiming.MatchString(line) { + return nil + } + match := rustdocRunStart.FindStringSubmatch(line) + if match == nil { + return errors.New("unexpected output outside a doctest run") + } + count, err := strconv.Atoi(match[1]) + if err != nil || count > 100000 || count == 1 && match[2] != "test" || count != 1 && match[2] != "tests" { + return errors.New("doctest run has an invalid case count") + } + state.active = &rustdocBatch{Cases: []rustdocCase{}} + state.expected = count + state.seen = map[string]bool{} + return nil +} + +func (state *rustdocOutput) test(line string) error { + name, outcome, found := strings.Cut(strings.TrimPrefix(line, "test "), " ... ") + if !strings.HasPrefix(line, "test ") || !found || name == "" || state.seen[name] { + return errors.New("doctest run has missing or duplicate case records") + } + status := "" + switch { + case outcome == "ok": + status = "passed" + case outcome == "FAILED": + status = "failed" + case outcome == "ignored" || strings.HasPrefix(outcome, "ignored, "): + status = "ignored" + default: + return errors.New("doctest case has an unsupported result") + } + state.seen[name] = true + state.active.Cases = append(state.active.Cases, rustdocCase{Name: name, Kind: rustdocCaseKind(name), Outcome: status}) + return countRustdocOutcome(state.active, status) +} + +func (state *rustdocOutput) finish(line string) error { + match := rustdocRunEnd.FindStringSubmatch(line) + if match == nil { + if state.active.Failed > 0 && !strings.HasPrefix(line, "test result:") { + return nil + } + return errors.New("doctest run omitted its complete result summary") + } + for index, count := range []int{state.active.Passed, state.active.Failed, state.active.Ignored} { + if match[index+2] != strconv.Itoa(count) { + return errors.New("doctest summary disagrees with its recorded cases") + } + } + if (match[1] == "ok") != (state.active.Failed == 0) { + return fmt.Errorf("doctest result %q disagrees with its failures", match[1]) + } + state.report.Batches = append(state.report.Batches, *state.active) + state.active = nil + return nil +} diff --git a/internal/testartifact/rustdoc_test.go b/internal/testartifact/rustdoc_test.go new file mode 100644 index 00000000..a42c1947 --- /dev/null +++ b/internal/testartifact/rustdoc_test.go @@ -0,0 +1,148 @@ +package testartifact + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" +) + +func TestRustdocOutputPreservesNativeCaseKinds(t *testing.T) { + t.Parallel() + input := rustdocBuild + "\nrunning 5 tests\n" + + "test src/lib.rs - answer (line 1) ... ok\n" + + "test src/lib.rs - answer (line 4) - compile fail ... ok\n" + + "test src/lib.rs - answer (line 7) - compile ... ok\n" + + "test src/lib.rs - answer (line 10) ... ignored, needs a live provider\n" + + "test src/lib.rs - answer (line 13) - should panic ... ok\n\n" + + "test result: ok. 4 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.04s\n\n" + + "all doctests ran in 0.50s; merged doctests compilation took 0.40s\n" + report, err := parseRustdocOutput([]byte(input), "docs") + if err != nil { + t.Fatal(err) + } + for index, want := range []string{"run", "compile-fail", "compile-only", "run", "should-panic"} { + if got := report.Batches[0].Cases[index].Kind; got != want { + t.Fatalf("case %d kind = %q, want %q", index, got, want) + } + } + data, err := json.Marshal(report) + if err != nil || validateRustdoc(data, "docs", 0) != nil { + t.Fatalf("complete doctest evidence rejected: %s, %v", data, err) + } +} + +func TestRustdocOutputRejectsPartialOrInconsistentEvidence(t *testing.T) { + t.Parallel() + pass := rustdocBuild + rustdocPassBatch + for name, input := range map[string]string{ + "empty": "", "build only": rustdocBuild, + "without build": rustdocPassBatch, + "failed build": strings.Replace(pass, `"success":true`, `"success":false`, 1), + "missing build outcome": strings.Replace(pass, `,"success":true`, "", 1), + "unknown Cargo event": strings.Replace(pass, "build-finished", "unknown", 1), + "partial test": strings.Split(pass, "test result:")[0], + "missing case": strings.Replace(pass, "running 1 test", "running 2 tests", 1), + "duplicate case": strings.Replace(strings.Replace(pass, "running 1 test", "running 2 tests", 1), "\ntest result:", "\ntest src/lib.rs - answer (line 1) ... ok\ntest result:", 1), + "wrong summary": strings.Replace(pass, "1 passed", "2 passed", 1), + "wrong outcome": strings.Replace(pass, "test result: ok", "test result: FAILED", 1), + "filtered tests": strings.Replace(pass, "0 filtered out", "1 filtered out", 1), + "benchmark": strings.Replace(pass, "0 measured", "1 measured", 1), + "surplus case": pass + "test src/lib.rs - extra (line 10) ... ok\n", + "surplus summary": pass + "test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s\n", + "too many tests": rustdocBuild + "running 100001 tests\n", + "oversized": strings.Repeat(" ", 8<<20+1), + "invalid UTF8": pass + "\xff", + } { + t.Run(name, func(t *testing.T) { + if _, err := parseRustdocOutput([]byte(input), "docs"); err == nil { + t.Fatal("invalid doctest output established evidence") + } + }) + } +} + +func TestRustdocEvidenceRequiresRealCasesAndMatchingExit(t *testing.T) { + t.Parallel() + for name, test := range map[string]struct { + batch string + exit int + valid bool + }{ + "pass": {rustdocPassBatch, 0, true}, + "failure": {rustdocFailedBatch, 101, true}, + "failed result exit zero": {rustdocFailedBatch, 0, false}, + "passing result failed exit": {rustdocPassBatch, 101, false}, + "operational exit": {rustdocFailedBatch, 1, false}, + "empty": {"running 0 tests\ntest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s\n", 0, false}, + "all ignored": {strings.ReplaceAll(strings.Replace(rustdocPassBatch, " ... ok", " ... ignored", 1), "1 passed; 0 failed; 0 ignored", "0 passed; 0 failed; 1 ignored"), 0, false}, + "multiple batches": {rustdocPassBatch + rustdocPassBatch, 0, true}, + } { + t.Run(name, func(t *testing.T) { + report, err := parseRustdocOutput([]byte(rustdocBuild+test.batch), "docs") + if err != nil { + t.Fatal(err) + } + data, err := json.Marshal(report) + if err != nil { + t.Fatal(err) + } + if err := validateRustdoc(data, "docs", test.exit); (err == nil) != test.valid { + t.Fatalf("validation = %v, want valid %v", err, test.valid) + } + }) + } +} + +func TestRustdocArtifactCannotReuseAnEarlierAttempt(t *testing.T) { + t.Parallel() + execution, err := Start(t.TempDir(), "") + if err != nil { + t.Fatal(err) + } + defer execution.Complete() + suite := policy.TestSuite{Name: "docs", Rust: &policy.RustTestSuite{Manifest: "Cargo.toml", Doctests: true}} + command, err := execution.PrepareCommand(suite, policy.Command{}) + if err != nil { + t.Fatal(err) + } + if err := CaptureCommandOutput(command, []byte(rustdocBuild+rustdocPassBatch)); err != nil { + t.Fatal(err) + } + if err := CaptureCommandOutput(command, []byte(rustdocBuild+rustdocFailedBatch)); err == nil { + t.Fatal("a later result replaced existing doctest evidence") + } + records, err := execution.ValidateCommand(command, 0) + if err != nil || len(records) != 1 || records[0].Type != policy.RustdocArtifact || records[0].SHA256 == "" { + t.Fatalf("doctest artifact = %+v, %v", records, err) + } + retry, err := execution.PrepareCommand(suite, policy.Command{}) + if err != nil { + t.Fatal(err) + } + if _, err := execution.ValidateCommand(retry, 0); err == nil { + t.Fatal("retry reused an earlier doctest report") + } + data, err := os.ReadFile(filepath.Join(command.TestArtifactDirectory, "rustdoc.json")) + if err != nil { + t.Fatal(err) + } + for _, invalid := range [][]byte{ + []byte(strings.Replace(string(data), `"passed":1`, `"passed":2`, 1)), + []byte(strings.Replace(string(data), `"suite":"docs"`, `"suite":"other"`, 1)), + []byte(strings.Replace(string(data), policy.RustdocToolVersion, "1.96.0", 1)), + []byte(strings.Replace(string(data), `"kind":"run"`, `"kind":"compile-only"`, 1)), + append(append([]byte{}, data...), []byte("{}")...), + } { + if err := validateRustdoc(invalid, "docs", 0); err == nil { + t.Fatal("changed doctest evidence was accepted") + } + } +} + +const rustdocBuild = "{\"reason\":\"build-finished\",\"success\":true}\n" +const rustdocPassBatch = "\nrunning 1 test\ntest src/lib.rs - answer (line 1) ... ok\n\ntest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s\n" +const rustdocFailedBatch = "\nrunning 1 test\ntest src/lib.rs - answer (line 1) ... FAILED\n\nfailures:\n---- src/lib.rs - answer (line 1) stdout ----\nassertion failed\nfailures:\n src/lib.rs - answer (line 1)\n\ntest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s\n" diff --git a/internal/testartifact/xml.go b/internal/testartifact/xml.go index 738a1d00..4977686a 100644 --- a/internal/testartifact/xml.go +++ b/internal/testartifact/xml.go @@ -55,6 +55,9 @@ func validateOne(directory, suite string, declaration policy.TestArtifact, exitS if declaration.Type == policy.NextestJUnitArtifact { return record, true, validateNextest(data, suite, exitStatus) } + if declaration.Type == policy.RustdocArtifact { + return record, true, validateRustdoc(data, suite, exitStatus) + } if err := validateXML(data, declaration.Type); err != nil { return Record{}, false, fmt.Errorf("test artifact %q: %w", declaration.Path, err) } diff --git a/internal/testing/rust.go b/internal/testing/rust.go index d2bd31ca..02a3a292 100644 --- a/internal/testing/rust.go +++ b/internal/testing/rust.go @@ -22,6 +22,9 @@ func SuiteCommand(repo repository.Repository, suite policy.TestSuite) policy.Com } native := rustfacts.Command(repo, suite.Name, "cargo") command.Argv = append([]string{repo.PolicyTool("cargo-nextest")}, rustTestArguments(*suite.Rust)...) + if suite.Rust.Doctests { + command.Argv[0] = repo.RustTool("cargo") + } command.Managed = true command.SealedEnvironment = true command.EnvironmentOverrides = append(native.EnvironmentOverrides, "CARGO="+repo.RustTool("cargo")) @@ -39,6 +42,9 @@ func rustTestArguments(settings policy.RustTestSuite) []string { "--config-file", testartifact.DirectoryToken + "/" + testartifact.NextestConfigName, "--user-config-file", "none", "--profile", "code-polishy", "--retries", "0", "--no-fail-fast", "--no-tests", "fail", } + if settings.Doctests { + arguments = []string{"test", "--doc", "--frozen", "--manifest-path", settings.Manifest, "--target", rustfacts.HostTarget(), "--message-format=json", "--no-fail-fast"} + } for _, name := range settings.Packages { arguments = append(arguments, "--package", name) } @@ -51,6 +57,9 @@ func rustTestArguments(settings policy.RustTestSuite) []string { if settings.Filter != "" { arguments = append(arguments, "--filter-expr", settings.Filter) } + if settings.Doctests { + arguments = append(arguments, "--", "--format=pretty", "--color=never", "--test-threads=1") + } return arguments } diff --git a/internal/testing/rust_doctest_test.go b/internal/testing/rust_doctest_test.go new file mode 100644 index 00000000..86be31c8 --- /dev/null +++ b/internal/testing/rust_doctest_test.go @@ -0,0 +1,120 @@ +package testing + +import ( + "bytes" + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestNativeRustDoctestsRespectExampleSemantics(t *testing.T) { + t.Parallel() + source := "/// ```\n/// assert_eq!(app::answer(), 42);\n/// ```\n" + + "/// ```compile_fail\n/// let value: u32 = \"wrong\";\n/// ```\n" + + "/// ```no_run\n/// panic!(\"must not execute\");\n/// ```\n" + + "/// ```ignore\n/// unavailable();\n/// ```\n" + + "/// ```should_panic\n/// panic!(\"expected\");\n/// ```\n" + + "pub fn answer() -> u32 { 42 }\n" + repo, suite := rustSuiteRepository(t, source) + suite.Rust.Doctests = true + writeRustSuiteFile(t, repo.Root, ".config/nextest.toml", "[profile.default]\ndefault-filter='none()'\n") + before, err := os.ReadFile(filepath.Join(repo.Root, "Cargo.lock")) + if err != nil { + t.Fatal(err) + } + execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + if execution.Failed() || len(execution.Artifacts) != 1 || execution.Artifacts[0].Type != policy.RustdocArtifact { + t.Fatalf("doctest execution: %+v", execution) + } + after, err := os.ReadFile(filepath.Join(repo.Root, "Cargo.lock")) + if err != nil || !bytes.Equal(before, after) { + t.Fatalf("doctesting changed the frozen lock: %v", err) + } +} + +func TestNativeRustDoctestsKeepFailuresDistinct(t *testing.T) { + t.Parallel() + for name, test := range map[string]struct { + source, build string + category runner.FailureCategory + artifact bool + }{ + "assertion": {source: "/// ```\n/// assert_eq!(2 + 2, 5);\n/// ```\npub fn answer() {}\n", category: runner.FailureCommandExit, artifact: true}, + "invalid example": {source: "/// ```\n/// let value: u8 = \"wrong\";\n/// ```\npub fn answer() {}\n", category: runner.FailureCommandExit, artifact: true}, + "compile fail unexpectedly passes": {source: "/// ```compile_fail\n/// let _value: u8 = 42;\n/// ```\npub fn answer() {}\n", category: runner.FailureCommandExit, artifact: true}, + "empty": {source: "pub fn answer() {}\n", category: runner.FailureOperational, artifact: true}, + "ignored": {source: "/// ```ignore\n/// panic!();\n/// ```\npub fn answer() {}\n", category: runner.FailureOperational, artifact: true}, + "compiler": {source: "/// ```\n/// assert_eq!(2 + 2, 4);\n/// ```\npub fn answer() -> u8 { \"wrong\" }\n", category: runner.FailureOperational}, + "build script": {source: "/// ```\n/// assert_eq!(2 + 2, 4);\n/// ```\npub fn answer() {}\n", build: "fn main() { panic!(\"cannot build\"); }\n", category: runner.FailureOperational}, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + repo, suite := rustSuiteRepository(t, test.source) + suite.Rust.Doctests = true + if test.build != "" { + writeRustSuiteFile(t, repo.Root, "build.rs", test.build) + } + execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + if !execution.Failed() || execution.FailureCategory != test.category || (len(execution.Artifacts) == 1) != test.artifact { + t.Fatalf("doctest classification: %+v", execution) + } + }) + } +} + +func TestNativeRustDoctestsHonorFeaturesAndCancellation(t *testing.T) { + t.Parallel() + repo, suite := rustSuiteRepository(t, "/// ```\n/// assert!(app::chosen());\n/// ```\npub fn chosen() -> bool { cfg!(feature = \"chosen\") && !cfg!(feature = \"ordinary\") }\n") + writeRustSuiteFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\nedition='2024'\n[workspace]\n[features]\ndefault=['ordinary']\nordinary=[]\nchosen=[]\n") + suite.Rust.Doctests = true + suite.Rust.Features = []string{"chosen"} + suite.Rust.NoDefaultFeatures = true + if execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1); execution.Failed() { + t.Fatalf("doctest features: %+v", execution) + } + ctx, cancel := context.WithCancel(t.Context()) + cancel() + if execution := ExecuteSuite(ctx, repo, runner.OSRunner{}, suite, 2); execution.FailureCategory != runner.FailureCanceled { + t.Fatalf("doctest cancellation: %+v", execution) + } + writeRustSuiteFile(t, repo.Root, "Cargo.lock", "version=4\n") + if execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 3); execution.FailureCategory != runner.FailureOperational { + t.Fatalf("doctest stale lock: %+v", execution) + } + data, err := os.ReadFile(filepath.Join(repo.Root, "Cargo.lock")) + if err != nil || string(data) != "version=4\n" { + t.Fatalf("stale lock changed: %s, %v", data, err) + } +} + +func TestNativeRustDoctestsKeepWorkspaceBatches(t *testing.T) { + t.Parallel() + repo, suite := rustSuiteRepository(t, "pub fn outside_workspace() {}\n") + writeRustSuiteFile(t, repo.Root, "Cargo.toml", "[workspace]\nmembers=['first','second']\nresolver='3'\n") + writeRustSuiteFile(t, repo.Root, "Cargo.lock", "version=4\n[[package]]\nname='first'\nversion='1.0.0'\n[[package]]\nname='second'\nversion='1.0.0'\n") + for _, name := range []string{"first", "second"} { + writeRustSuiteFile(t, repo.Root, name+"/Cargo.toml", "[package]\nname='"+name+"'\nversion='1.0.0'\nedition='2024'\n") + writeRustSuiteFile(t, repo.Root, name+"/src/lib.rs", "/// ```\n/// assert_eq!(2 + 2, 4);\n/// ```\npub fn answer() {}\n") + } + suite.Rust.Doctests = true + if execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1); execution.Failed() { + t.Fatalf("workspace doctests: %+v", execution) + } + writeRustSuiteFile(t, repo.Root, "second/src/lib.rs", "/// ```\n/// assert_eq!(2 + 2, 5);\n/// ```\npub fn answer() {}\n") + if execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 2); execution.FailureCategory != runner.FailureCommandExit { + t.Fatalf("workspace doctest failure: %+v", execution) + } + suite.Rust.Packages = []string{"first"} + if execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 3); execution.Failed() { + t.Fatalf("selected package doctests: %+v", execution) + } + plan := ExecutionPlanForDirectTest(Plan{Suites: []policy.TestSuite{suite}}) + if len(plan.Commands) != 1 || plan.Commands[0].Argv[0] != "cargo" || !strings.Contains(strings.Join(plan.Commands[0].Argv, " "), "test --doc --frozen") { + t.Fatalf("doctest execution plan: %+v", plan) + } +} diff --git a/internal/testing/rust_output.go b/internal/testing/rust_output.go new file mode 100644 index 00000000..549b82c8 --- /dev/null +++ b/internal/testing/rust_output.go @@ -0,0 +1,26 @@ +package testing + +import ( + "context" + "errors" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/runner" + "github.com/riteofstring/code-polishy/internal/testartifact" +) + +func runCapturedSuite(ctx context.Context, root string, boundary runner.Runner, command policy.Command) (runner.Result, bool, error) { + captured, ok := boundary.(runner.OutputRunner) + if !ok { + return runner.Result{ExitStatus: -1, FailureCategory: runner.FailureOperational}, true, errors.New("native doctests require bounded command output") + } + result, output, err := captured.RunWithOutput(ctx, root, command) + category := runner.FailureCategoryFor(ctx, result, err) + if category == "" || category == runner.FailureCommandExit { + if artifactErr := testartifact.CaptureCommandOutput(command, output.Stdout); artifactErr != nil { + result.FailureCategory = runner.FailureOperational + err = errors.Join(err, artifactErr) + } + } + return result, true, err +} diff --git a/internal/testing/testing.go b/internal/testing/testing.go index 14ace681..6e8a12f8 100644 --- a/internal/testing/testing.go +++ b/internal/testing/testing.go @@ -683,6 +683,9 @@ type resultRunner interface { } func runSuite(ctx context.Context, root string, commandRunner runner.Runner, command policy.Command) (runner.Result, bool, error) { + if testartifact.NeedsCommandOutput(command) && !runnerManagesTestArtifacts(commandRunner) { + return runCapturedSuite(ctx, root, commandRunner, command) + } if observed, ok := commandRunner.(resultRunner); ok { result, err := observed.RunWithResult(ctx, root, command) return result, true, err @@ -698,6 +701,9 @@ func ExecutionPlanForDirectTest(plan Plan) ExecutionPlan { argv := slices.Clone(suite.Argv) if suite.Rust != nil { argv = append([]string{"cargo-nextest"}, rustTestArguments(*suite.Rust)...) + if suite.Rust.Doctests { + argv[0] = "cargo" + } } commands = append(commands, ExecutionCommand{ Name: suite.Name, Kind: suite.Kind, Scope: suite.Scope, Cost: suite.Cost, diff --git a/schema/code-polishy.schema.json b/schema/code-polishy.schema.json index 17841a56..ccae069c 100644 --- a/schema/code-polishy.schema.json +++ b/schema/code-polishy.schema.json @@ -846,6 +846,7 @@ } }, "noDefaultFeatures": { "type": "boolean" }, + "doctests": { "type": "boolean", "default": false }, "filter": { "type": "string", "minLength": 1, "maxLength": 4096 } } }, diff --git a/scripts/test-installed-release-rust.sh b/scripts/test-installed-release-rust.sh index c176cc19..d5b7c1b8 100644 --- a/scripts/test-installed-release-rust.sh +++ b/scripts/test-installed-release-rust.sh @@ -66,16 +66,16 @@ mod tests {{ """ -def assert_native_outcome(report, exit_status, category=""): +def assert_native_outcome(report, exit_status, category="", suite="rust-unit", artifact_type="nextest-junit"): commands = report["testCommands"] assert len(commands) == 1, commands command = commands[0] - assert command["name"] == "rust-unit", command + assert command["name"] == suite, command assert command["result"]["exitStatus"] == exit_status, command assert command.get("failureCategory", "") == category, command if category != "operational": artifacts = command["artifacts"] - assert len(artifacts) == 1 and artifacts[0]["type"] == "nextest-junit", command + assert len(artifacts) == 1 and artifacts[0]["type"] == artifact_type, command assert artifacts[0]["size"] > 0 and len(artifacts[0]["sha256"]) == 64, command executed = report["execution"]["commands"] assert len(executed) == 1, executed @@ -88,6 +88,7 @@ config = { "modules": [{"name": "library", "paths": ["Cargo.toml", "src/**"]}], "tests": {"ownership": [], "suites": [ {"name": "rust-unit", "kind": "unit", "scope": "module", "modules": ["library"], "rust": {}}, + {"name": "rust-docs", "kind": "unit", "scope": "module", "modules": ["library"], "rust": {"doctests": True}}, {"name": "full", "kind": "unit", "scope": "repository", "rust": {}}, ]}, } @@ -123,7 +124,34 @@ sarif = policy("check", "--files", "src/lib.rs", status=1, output="sarif") assert any(result["ruleId"] == "quality.rustCompiler" for result in sarif["runs"][0]["results"]), sarif write("src/lib.rs", rust_source(42)) assert_native_outcome(policy("test", "--suite", "rust-unit"), 0) -print("Installed Rust formatting, compiler diagnostics, native evidence, failures, and recovery passed.") + +documentation = """/// ``` +/// assert_eq!(installed_rust::answer(), 42); +/// ``` +/// ```compile_fail +/// let _value: u8 = "wrong"; +/// ``` +/// ```no_run +/// panic!("must not execute"); +/// ``` +/// ```ignore +/// unavailable_live_provider(); +/// ``` +/// ```should_panic +/// panic!("expected"); +/// ``` +""" +write("src/lib.rs", documentation + rust_source(42)) +assert_native_outcome(policy("test", "--suite", "rust-docs"), 0, suite="rust-docs", artifact_type="rustdoc") +write("src/lib.rs", documentation.replace("answer(), 42", "answer(), 41") + rust_source(42)) +assert_native_outcome(policy("test", "--suite", "rust-docs", status=1), 101, "command-exit", "rust-docs", "rustdoc") +write("src/lib.rs", rust_source(42)) +assert_native_outcome(policy("test", "--suite", "rust-docs", status=1), 0, "operational", "rust-docs", "rustdoc") +write("src/lib.rs", 'pub fn answer() -> u32 { "not an integer" }\n') +assert_native_outcome(policy("test", "--suite", "rust-docs", status=1), 101, "operational", "rust-docs", "rustdoc") +write("src/lib.rs", documentation + rust_source(42)) +assert_native_outcome(policy("test", "--suite", "rust-docs"), 0, suite="rust-docs", artifact_type="rustdoc") +print("Installed Rust formatting, compiler diagnostics, native tests and doctests, failures, and recovery passed.") PY } From b2cd8d686faad3c2d4c8c7967db07b0e346c46d4 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 06:27:30 +0000 Subject: [PATCH 27/49] fix: preserve interrupted native test outcomes during capture Share the output-capture failure classifier between direct and gate runners. An overflow remains operational for a normal test exit without erasing timeout or cancellation; regression tests reproduce and verify both interrupted cases. Record the passing installed Linux Rust and doctest fixture at source 3fafe17 and its verified private release digest. Targeted engine/runner tests and scoped policy checks pass. Macro coverage and the remaining native host and inventory requirements are still open. --- docs/plans/code-polishy-rust-support-plan.md | 9 ++++-- internal/engine/gate_command_runner.go | 2 +- internal/engine/rust_tests_test.go | 31 ++++++++++++++++++++ internal/runner/runner.go | 6 ++-- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 18239114..73fe9b00 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -413,15 +413,18 @@ dependency governance now have runnable beta implementations. Source parsing use exact authenticated binary wheels in the existing governed Python runtime; it requires neither target package installation scripts nor a new Rust bootstrap. -Native unit/integration suites now retain nonempty structured results through +Native unit/integration and separate doctest suites now retain nonempty structured results through ordinary commands, gate artifacts, regression proofs and reusable receipts. The [Rust test contract](../design/rust-tests.md) documents selectors and boundaries. Native release builders now carry the Rust SDK, Nextest and parser dependencies. Manifest version 7 binds the executable pins; the SBOM includes both parser distributions and the top-level Rust tools. The installed Rust fixture passes on -Linux x64 against committed source `1e57580`: selected formatting, JSON/SARIF +Linux x64 against committed source `3fafe17`: selected formatting, JSON/SARIF compiler diagnostics, nonempty native tests, assertion failure, zero-test -rejection, and recovery through the verified launcher. All-host installed evidence +rejection, doctest example semantics and failures, and recovery through the +verified launcher. The private installed artifact has release digest +`5f4452b4fb01bdc396e948c761210c9e965f5246a377856213a89915c2fdc476`. +All-host installed evidence and complete bundled dependency notices/inventory remain publication requirements. No beta release has been published or adopted. diff --git a/internal/engine/gate_command_runner.go b/internal/engine/gate_command_runner.go index d56231b3..1dff8080 100644 --- a/internal/engine/gate_command_runner.go +++ b/internal/engine/gate_command_runner.go @@ -402,7 +402,7 @@ func runGateCommand(ctx context.Context, commandRunner runner.Runner, root strin result, err := streamed.RunWithWriters(ctx, root, command, io.MultiWriter(stdout, capturedStdout), io.MultiWriter(stderr, capturedStderr)) if capturedStdout.truncated || capturedStderr.truncated { err = errors.Join(err, fmt.Errorf("captured command output exceeds its structured output limit")) - result.FailureCategory = runner.FailureOperational + result.FailureCategory = runner.CaptureFailureCategory(ctx, result, err) } return result, runner.Output{Stdout: capturedStdout.Bytes(), Stderr: capturedStderr.Bytes()}, err } diff --git a/internal/engine/rust_tests_test.go b/internal/engine/rust_tests_test.go index 18a6dbcc..20918f44 100644 --- a/internal/engine/rust_tests_test.go +++ b/internal/engine/rust_tests_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "io" "os" "path/filepath" @@ -205,6 +206,36 @@ func nativeRustEngineRepository(t *testing.T) string { type nativeRustGateRunner struct{} +func TestRustDoctestGateTruncationPreservesInterruptedOutcomes(t *testing.T) { + t.Parallel() + for _, category := range []runner.FailureCategory{runner.FailureCommandExit, runner.FailureTimeout, runner.FailureCanceled} { + t.Run(string(category), func(t *testing.T) { + result, output, err := runGateCommand(t.Context(), rustDoctestOverflowRunner{category: category}, t.TempDir(), policy.Command{Name: "docs"}, io.Discard, io.Discard, true) + want := category + if category == runner.FailureCommandExit { + want = runner.FailureOperational + } + if err == nil || !strings.Contains(err.Error(), "captured command output") || result.FailureCategory != want || len(output.Stdout) != 8<<20 { + t.Fatalf("interrupted captured output: %+v, bytes=%d, %v", result, len(output.Stdout), err) + } + }) + } +} + +type rustDoctestOverflowRunner struct { + category runner.FailureCategory +} + +func (commandRunner rustDoctestOverflowRunner) Run(ctx context.Context, root string, command policy.Command) error { + _, err := commandRunner.RunWithWriters(ctx, root, command, io.Discard, io.Discard) + return err +} + +func (commandRunner rustDoctestOverflowRunner) RunWithWriters(_ context.Context, _ string, _ policy.Command, stdout, _ io.Writer) (runner.Result, error) { + _, err := stdout.Write(bytes.Repeat([]byte("x"), 8<<20+1)) + return runner.Result{ExitStatus: 101, FailureCategory: commandRunner.category}, errors.Join(errors.New("interrupted after output"), err) +} + func (nativeRustGateRunner) Run(ctx context.Context, root string, command policy.Command) error { _, err := (nativeRustGateRunner{}).RunWithWriters(ctx, root, command, io.Discard, io.Discard) return err diff --git a/internal/runner/runner.go b/internal/runner/runner.go index eb3a1d07..717233b9 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -146,7 +146,7 @@ func (runner OSRunner) RunWithOutput(parent context.Context, root string, specif result, err := runner.run(parent, root, specification, appendOutputWriter(runner.Stdout, stdout), appendOutputWriter(runner.Stderr, stderr)) if stdout.truncated || stderr.truncated { err = errors.Join(err, fmt.Errorf("captured structured output for command %q exceeds its %d byte stream limit", specification.Name, captureLimit(stdout, stderr))) - result.FailureCategory = captureFailureCategory(parent, result, err) + result.FailureCategory = CaptureFailureCategory(parent, result, err) } return result, Output{Stdout: stdout.buffer.Bytes(), Stderr: stderr.buffer.Bytes()}, err } @@ -157,7 +157,7 @@ func (runner OSRunner) RunStructured(parent context.Context, root string, specif result, err := runner.run(parent, root, specification, stdout, appendOutputWriter(runner.Stderr, stderr)) if stdout.truncated || stderr.truncated { err = errors.Join(err, fmt.Errorf("captured structured output for command %q exceeds its %d byte stream limit", specification.Name, captureLimit(stdout, stderr))) - result.FailureCategory = captureFailureCategory(parent, result, err) + result.FailureCategory = CaptureFailureCategory(parent, result, err) } return result, Output{Stdout: stdout.buffer.Bytes(), Stderr: stderr.buffer.Bytes()}, err } @@ -169,7 +169,7 @@ func captureLimit(stdout, stderr *boundedOutput) int { return stderr.limit } -func captureFailureCategory(ctx context.Context, result Result, err error) FailureCategory { +func CaptureFailureCategory(ctx context.Context, result Result, err error) FailureCategory { category := FailureCategoryFor(ctx, result, err) if category == FailureTimeout || category == FailureCanceled { return category From a07111496c225bdadb30d1405955f3dbd0a9489b Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 07:01:46 +0000 Subject: [PATCH 28/49] fix: accept unpackaged inactive Cargo dependency targets Retain absent remote test, bench, example and binary metadata without requiring files Cargo does not compile as dependency targets. Continue requiring local and dependency build sources, and reject symlinks, non-regular paths and escapes. Verification: observed the missing-target regression fail before the fix; all Rust-facts boundary tests pass. Go vet and staticcheck pass. The scoped policy report remains failed only on the separate unadmitted Rust semantic helper's unsupported coverage. --- internal/rustfacts/metadata.go | 15 +++-- internal/rustfacts/resolved_metadata_test.go | 66 ++++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/internal/rustfacts/metadata.go b/internal/rustfacts/metadata.go index 5e6bba68..26acf8bf 100644 --- a/internal/rustfacts/metadata.go +++ b/internal/rustfacts/metadata.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "io/fs" "path/filepath" "slices" "strings" @@ -178,25 +179,31 @@ func validatePackage(repo repository.Repository, pkg *Package) error { return fmt.Errorf("invalid minimum Rust version %q", pkg.RustVersion) } for index := range pkg.Targets { - if err := validateTarget(repo, &pkg.Targets[index]); err != nil { + if err := validateTarget(repo, &pkg.Targets[index], pkg.Source != nil); err != nil { return err } } return validateDependencies(repo, pkg.Dependencies) } -func validateTarget(repo repository.Repository, target *Target) error { +func validateTarget(repo repository.Repository, target *Target, remote bool) error { if target.Name == "" || len(target.Kinds) == 0 || len(target.CrateTypes) == 0 || !supportedEdition(target.Edition) { return fmt.Errorf("target %q has incomplete or unsupported facts", target.Name) } path, err := containedPath(repo, target.Path) - if err != nil { + if err != nil && !(remote && inactiveDependencyTarget(*target) && errors.Is(err, fs.ErrNotExist)) { return fmt.Errorf("target %q: %w", target.Name, err) } target.Path = path return nil } +func inactiveDependencyTarget(target Target) bool { + return len(target.Kinds) > 0 && !slices.ContainsFunc(target.Kinds, func(kind string) bool { + return !slices.Contains([]string{"bin", "example", "test", "bench"}, kind) + }) +} + func validateDependencies(repo repository.Repository, dependencies []Dependency) error { for index := range dependencies { dependency := &dependencies[index] @@ -228,7 +235,7 @@ func containedPath(repo repository.Repository, absolute string) (string, error) } path := filepath.ToSlash(relative) if _, err := repo.ReadContainedFile(path, 8<<20); err != nil { - return "", fmt.Errorf("cargo input %q: %w", path, err) + return path, fmt.Errorf("cargo input %q: %w", path, err) } return path, nil } diff --git a/internal/rustfacts/resolved_metadata_test.go b/internal/rustfacts/resolved_metadata_test.go index 6ee2a8e5..409e0b1d 100644 --- a/internal/rustfacts/resolved_metadata_test.go +++ b/internal/rustfacts/resolved_metadata_test.go @@ -2,7 +2,9 @@ package rustfacts import ( "encoding/json" + "os" "path/filepath" + "slices" "strings" "testing" @@ -80,6 +82,70 @@ func TestResolvedCargoMetadataRequiresExplicitResolution(t *testing.T) { } } +func TestResolvedCargoMetadataAllowsUnpackagedInactiveRemoteTargets(t *testing.T) { + t.Parallel() + for _, kind := range []string{"test", "bench", "example", "bin"} { + t.Run(kind, func(t *testing.T) { + repo, wire, graph := resolvedMetadataFixture(t) + pkg := &wire.Packages[1] + target := Target{Name: "unpackaged", Kinds: []string{kind}, CrateTypes: []string{"bin"}, Edition: "2021", Path: filepath.Join(filepath.Dir(pkg.Manifest), "missing", "target.rs")} + pkg.Targets = append(pkg.Targets, target) + resolution, err := ParseResolution(repo, "Cargo.toml", resolvedMetadataJSON(t, wire, graph)) + if err != nil { + t.Fatal(err) + } + retained := resolution.Packages[1].Targets[1] + if !slices.Equal(retained.Kinds, []string{kind}) || !strings.HasSuffix(retained.Path, "/missing/target.rs") { + t.Fatalf("inactive target facts were lost: %+v", retained) + } + }) + } +} + +func TestResolvedCargoMetadataStillRequiresDependencyBuildSources(t *testing.T) { + t.Parallel() + for _, kind := range []string{"lib", "rlib", "proc-macro", "custom-build"} { + t.Run(kind, func(t *testing.T) { + repo, wire, graph := resolvedMetadataFixture(t) + target := &wire.Packages[1].Targets[0] + target.Kinds, target.Path = []string{kind}, target.Path+".missing" + if _, err := ParseResolution(repo, "Cargo.toml", resolvedMetadataJSON(t, wire, graph)); err == nil { + t.Fatal("missing dependency build input passed") + } + }) + } +} + +func TestResolvedCargoMetadataRejectsUnsafeInactiveTargetsAndMissingLocalTests(t *testing.T) { + t.Parallel() + for _, mode := range []string{"local", "escape", "symlink", "directory"} { + t.Run(mode, func(t *testing.T) { + repo, wire, graph := resolvedMetadataFixture(t) + pkg := &wire.Packages[1] + if mode == "local" { + pkg = &wire.Packages[0] + } + path := filepath.Join(filepath.Dir(pkg.Manifest), "missing.rs") + switch mode { + case "escape": + path = filepath.Join(repo.Root, "..", "missing.rs") + case "symlink": + if err := os.Symlink("absent.rs", path); err != nil { + t.Fatal(err) + } + case "directory": + if err := os.Mkdir(path, 0o700); err != nil { + t.Fatal(err) + } + } + pkg.Targets = append(pkg.Targets, Target{Name: "test", Kinds: []string{"test"}, CrateTypes: []string{"bin"}, Edition: "2021", Path: path}) + if _, err := ParseResolution(repo, "Cargo.toml", resolvedMetadataJSON(t, wire, graph)); err == nil { + t.Fatal("unsafe or missing owned target passed") + } + }) + } +} + func resolvedMetadataFixture(t *testing.T) (repository.Repository, workspaceWire, ResolvedGraph) { t.Helper() repo, wire := metadataFixture(t) From e0c417a100ab9242dcdec74a9f966f09bacfd9d8 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 07:02:34 +0000 Subject: [PATCH 29/49] wip: checkpoint Rust semantic helper pending dependency admission Add an inactive Rust Analyzer HIR helper and real-workspace fixtures for generated macro references, dollar-crate hygiene, nesting and call-site mapping. Preserve the current architecture failure boundary; no Go consumer or shipped executable activates these facts. Candidate pins Rust Analyzer 0.0.346 with the Salsa 0.28.5 security fix. Cargo generated the lock without scripts. The rejected 0.27.2 Salsa candidate was not compiled. Locked 0.27.11 cannot represent crates.io security-fix age assessments or govern the new Rust source. A separately authorized local beta policy-lock upgrade is required before admission and compilation. Verification: Rust source parses through pinned rustfmt; Markdown format passes. Helper compilation and its fixtures are NOT RUN. Locked dependency-review fails the three unsupported Cargo provider requirements; scoped check fails only unsupported Rust coverage. Standard library, attribute/derive expansion, complete source/configuration ownership, runner integration and packaging remain unfinished. No lock upgrade, push or publication. --- docs/design/cargo-facts.md | 30 + docs/plans/code-polishy-rust-support-plan.md | 35 + internal/rustfacts/semantic/Cargo.lock | 1932 ++++++++++++++++++ internal/rustfacts/semantic/Cargo.toml | 23 + internal/rustfacts/semantic/src/graph.rs | 228 +++ internal/rustfacts/semantic/src/main.rs | 52 + internal/rustfacts/semantic/src/tests.rs | 153 ++ internal/rustfacts/semantic/src/workspace.rs | 77 + 8 files changed, 2530 insertions(+) create mode 100644 internal/rustfacts/semantic/Cargo.lock create mode 100644 internal/rustfacts/semantic/Cargo.toml create mode 100644 internal/rustfacts/semantic/src/graph.rs create mode 100644 internal/rustfacts/semantic/src/main.rs create mode 100644 internal/rustfacts/semantic/src/tests.rs create mode 100644 internal/rustfacts/semantic/src/workspace.rs diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index b2722dc4..dc03819c 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -44,6 +44,15 @@ in the reviewed lock inventory. Cargo owns resolution; this validation does not reimplement its resolver. Remote manifests and source targets must remain in the governed Cargo home. Local path packages retain contained checkout paths. +Published dependencies can retain Cargo metadata for tests, benches, examples or +binaries that their archives intentionally omit. These targets do not build when +the package is consumed as a library dependency. Their declared identities and +contained paths remain facts, but missing files do not invalidate dependency +resolution. Library, procedural-macro and build-script sources must still exist, +as must every local package target. Existing non-regular inputs, symlinks and +escaping paths remain invalid even for inactive remote targets. This distinction +does not authorize executing a dependency's absent development targets. + Metadata uses default features and retains target-specific edges for all platforms. It is not evidence that every feature or target was compiled. The lock may also include inactive optional dependencies absent from this resolved graph; @@ -154,6 +163,27 @@ therefore cannot establish complete macro dependency coverage. General expansion remains a required semantic-provider milestone; the written graph does not silently admit those missing edges. +The initial semantic helper source is in `internal/rustfacts/semantic/`. It uses +Rust Analyzer's HIR library to visit expanded syntax, resolve names with macro +hygiene and map references back to physical source. An explicit Cargo manifest, +native target and frozen build command establish its intended input boundary; +partial metadata and failed build-script collection are errors. Its draft +protocol is internal and has no active Go consumer or installed executable. +Standard-library loading, attribute/derive expansion, selected configuration +coverage, complete bounded source ownership, execution integration and distribution +remain unfinished. Its fixtures have not run: dependency admission currently +prevents building this helper. It supplies no architecture coverage yet. + +The candidate uses Rust Analyzer crates `0.0.346`, the first inspected published +line using Salsa `0.28`, with Salsa and its macro crates locked to `0.28.5`. +Earlier candidates depend on the affected Salsa series in +[RUSTSEC-2026-0308](https://rustsec.org/advisories/RUSTSEC-2026-0308.html). +The fixed release was published on 2026-09-24 and needs exact security-fix age +admission. The currently locked `0.27.11` schema does not support `crates.io` +release-age assessments. A separately authorized local beta policy upgrade is +required before admitting and compiling this candidate; no out-of-band age +waiver or alternate policy file replaces the locked assessment contract. + ## Compiler checks Compilation uses the matching pinned cargo-clippy executable directly, with an diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 73fe9b00..9bd8a951 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -167,6 +167,12 @@ changed modules; add a quick suite when a new module is introduced. package/features, frozen locks, cancellation, empty/ignored runs, crate and build-script failures, direct/gate execution, regression proofs and receipts. A doctest result never substitutes for unit/integration evidence. +- Real dependency preparation exposed published packages whose unused test, + example, bench or binary metadata points to files excluded from their archives. + Resolution now retains those remote target facts without requiring absent + inactive files. Local targets and dependency library/proc-macro/build sources + remain mandatory; escaping and linked paths still fail. The complete Rust-facts + suite passes this change, including the new red/green missing-target regression. Cargo dependency-input governance now parses ordinary, build, development, target-specific and workspace declaration tables, checks exact registry and Git @@ -220,6 +226,35 @@ dependency review defers installed-license checks, matching the Node workflow; normal verification uses prepared archives without downloading or executing code. Pinned Cargo packaging fixtures cover inherited workspace license metadata. +### Semantic helper admission checkpoint + +`internal/rustfacts/semantic/` contains an initial, uncompiled Rust Analyzer +helper and fixtures for generated-only dependencies, nested expansion, `$crate` +hygiene and original call-site locations. It is not connected to public commands, +shipped in releases or evidence of completed macro support. No helper build or +fixture execution has been authorized by successful dependency admission yet. + +The candidate pins Rust Analyzer `0.0.346` and locks Salsa, `salsa-macros` and +`salsa-macro-rules` to `0.28.5`. Earlier candidate Salsa releases fail +[RUSTSEC-2026-0308](https://rustsec.org/advisories/RUSTSEC-2026-0308.html), reported +2026-09-24. The fixed release is younger than the 30-day baseline. Registry lock +generation executed no build scripts, and the rejected vulnerable library was +not compiled. Review and data-only preparation do not grant executable admission. + +The outgoing `0.27.11` schema accepts release-age assessments only for +`artifact`, `go`, `npm`, `pnpm` and `pypi`, not `crates.io`. Its source checks also +lack Rust analysis. A local beta policy-lock upgrade needs separate caller +authorization before the native Cargo security-fix assessment and helper build +can proceed. Target providers cannot replace the locked assessment contract. +Keep the current lock unchanged meanwhile. An upgrade would neither authorize +publication nor constitute completed Rust support. + +After admission, compile and test the helper before integrating it. Complete +standard-library and attribute/derive expansion, source/configuration ownership, +the common command runner and release packaging; then replace the superseded +written graph at one coherent cutover. The full Rust objective and remaining +acceptance matrix are unchanged. + ## Phase 0: define the complete coverage contract - [x] Audit current Go/Python owners and Rust gaps against the recorded base; diff --git a/internal/rustfacts/semantic/Cargo.lock b/internal/rustfacts/semantic/Cargo.lock new file mode 100644 index 00000000..197b502d --- /dev/null +++ b/internal/rustfacts/semantic/Cargo.lock @@ -0,0 +1,1932 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "borsh" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" +dependencies = [ + "bytes", + "cfg_aliases", +] + +[[package]] +name = "boxcar" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "cargo_metadata" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "code-polishy-rust-semantic" +version = "0.1.0" +dependencies = [ + "ra_ap_hir", + "ra_ap_ide_db", + "ra_ap_load-cargo", + "ra_ap_project_model", + "ra_ap_syntax", + "ra_ap_vfs", + "serde", + "serde_json", +] + +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + +[[package]] +name = "cov-mark" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90863d8442510cddf7f46618c4f92413774635771a3e80830c8b30d183420b14" + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive-where" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dissimilar" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeda16ab4059c5fd2a83f2b9c9e9c981327b18aa8e3b313f7e6563799d4f093e" + +[[package]] +name = "drop_bomb" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "fst" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashlink" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inotify" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "153be1941a183ec9ccd095ddbe17a8b8d435ef6c76e9e02451b933c3999af2c8" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +dependencies = [ + "libc", +] + +[[package]] +name = "intrusive-collections" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4275b20e6057cd7733fd8df8a5a31701e4fe44497dad0f3fa0e1c4fb971506be" + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jod-thread" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a037eddb7d28de1d0fc42411f501b53b75838d313908078d6698d064f3029b24" + +[[package]] +name = "kqueue" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "la-arena" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3752f229dcc5a481d60f385fa479ff46818033d881d2d801aa27dffcfb5e8306" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "line-index" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e27e0ed5a392a7f5ba0b3808a2afccff16c64933312c84b57618b49d1209bd2" +dependencies = [ + "nohash-hasher", + "text-size", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "miow" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "perf-event" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4d6393d9238342159080d79b78cb59c67399a8e7ecfa5d410bd614169e4e823" +dependencies = [ + "libc", + "perf-event-open-sys", +] + +[[package]] +name = "perf-event-open-sys" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c44fb1c7651a45a3652c4afc6e754e40b3d6e6556f1487e2b230bfc4f33c2a8" +dependencies = [ + "libc", +] + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ra-ap-rustc_abi" +version = "0.166.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2cf1b1ffe31b6226c00b40cddfda65002b7729f9f4ed2d547b5856cdab0011c" +dependencies = [ + "bitflags", + "ra-ap-rustc_hashes", + "ra-ap-rustc_index", + "tracing", +] + +[[package]] +name = "ra-ap-rustc_ast_ir" +version = "0.166.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef42605e36e1305e815ccfc8830eb870f74d78534bca19a61629149536d8e98" + +[[package]] +name = "ra-ap-rustc_hashes" +version = "0.166.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f5542968215c17275920791b2fa13a43014287506ed0450777c79845102e86" +dependencies = [ + "rustc-stable-hash", +] + +[[package]] +name = "ra-ap-rustc_index" +version = "0.166.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9e47b9ca7d92cfb0d6653503adbabd41938b84474317397a664326b208d6c6" +dependencies = [ + "ra-ap-rustc_index_macros", +] + +[[package]] +name = "ra-ap-rustc_index_macros" +version = "0.166.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d744a7a2852a22f06210bcff9e4667ed0cacbfbe94894cc294044d25e876341" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ra-ap-rustc_lexer" +version = "0.166.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527c12b3731b7d0692498012810b85b2b8dfdb8b514321ed6afc434bd1c70191" +dependencies = [ + "memchr", + "unicode-ident", + "unicode-properties", +] + +[[package]] +name = "ra-ap-rustc_next_trait_solver" +version = "0.166.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a9663a8d7c369e934aac2b74a638537ad7eb4be75b4530d765384dc071c936" +dependencies = [ + "derive-where", + "ra-ap-rustc_index", + "ra-ap-rustc_type_ir", + "ra-ap-rustc_type_ir_macros", + "tracing", +] + +[[package]] +name = "ra-ap-rustc_parse_format" +version = "0.166.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c038b7a8b0f784d4e441ad8ab991fbbdaa5e0be482e59c639a846a1c8126951" +dependencies = [ + "ra-ap-rustc_lexer", + "rustc-literal-escaper", +] + +[[package]] +name = "ra-ap-rustc_pattern_analysis" +version = "0.166.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ca286f90e99bb97cd9274c088f3c874a05d1ee90cabf40a3928afedabe99fd" +dependencies = [ + "ra-ap-rustc_index", + "rustc-hash 2.1.3", + "rustc_apfloat", + "smallvec", + "tracing", +] + +[[package]] +name = "ra-ap-rustc_type_ir" +version = "0.166.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d6efb6008f665a9485e0afecf9f4950a6c4bedd8ddd330a9df8986a6c0160b" +dependencies = [ + "arrayvec", + "bitflags", + "derive-where", + "ena", + "indexmap", + "ra-ap-rustc_abi", + "ra-ap-rustc_ast_ir", + "ra-ap-rustc_index", + "ra-ap-rustc_type_ir_macros", + "rustc-hash 2.1.3", + "smallvec", + "thin-vec", + "tracing", +] + +[[package]] +name = "ra-ap-rustc_type_ir_macros" +version = "0.166.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4fd2355e2bbf1f343c730f623596efc6e465b5e3685b606a437567ebb75bf8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "ra_ap_base_db" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ff14d8633ba5644be8e10c056ca82d97b87ee0f1b3833f022fed3c906578db" +dependencies = [ + "dashmap", + "indexmap", + "la-arena", + "ra_ap_cfg", + "ra_ap_intern", + "ra_ap_span", + "ra_ap_syntax", + "ra_ap_vfs", + "rustc-hash 2.1.3", + "salsa", + "salsa-macros", + "semver", + "tracing", + "triomphe", +] + +[[package]] +name = "ra_ap_cfg" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc4b72dc03a6a5b56b1aa10ddf7ef70fe59c75ae90dfd3ca97636760a32be47e" +dependencies = [ + "ra_ap_intern", + "ra_ap_syntax", + "ra_ap_tt", + "rustc-hash 2.1.3", + "tracing", +] + +[[package]] +name = "ra_ap_edition" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9783aa966d3a19c3a407babeb331ee4370f0aaa4a23646ab9bb45ba8a3a9dc2" + +[[package]] +name = "ra_ap_hir" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "539df9a88e106897dac9b9b82f92aec0bc64c09d0925abaea642b123a0971c48" +dependencies = [ + "arrayvec", + "either", + "itertools", + "la-arena", + "ra-ap-rustc_type_ir", + "ra_ap_base_db", + "ra_ap_cfg", + "ra_ap_hir_def", + "ra_ap_hir_expand", + "ra_ap_hir_ty", + "ra_ap_intern", + "ra_ap_span", + "ra_ap_stdx", + "ra_ap_syntax", + "ra_ap_tt", + "rustc-hash 2.1.3", + "salsa", + "serde_json", + "smallvec", + "tracing", + "triomphe", +] + +[[package]] +name = "ra_ap_hir_def" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e12599394e6fff890ee1aeb60f04bd41e19f65f4153534eb40ee1cffa9fc7a" +dependencies = [ + "arrayvec", + "bitflags", + "cov-mark", + "drop_bomb", + "either", + "fst", + "indexmap", + "itertools", + "la-arena", + "ra-ap-rustc_abi", + "ra-ap-rustc_parse_format", + "ra_ap_base_db", + "ra_ap_cfg", + "ra_ap_hir_expand", + "ra_ap_intern", + "ra_ap_span", + "ra_ap_stdx", + "ra_ap_syntax", + "ra_ap_syntax-bridge", + "ra_ap_tt", + "rustc-hash 2.1.3", + "rustc_apfloat", + "salsa", + "smallvec", + "thin-vec", + "tracing", + "triomphe", +] + +[[package]] +name = "ra_ap_hir_expand" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "485825528c2726e9f4206cc9528772ccb064a643b2d2927a9757e933905c0c99" +dependencies = [ + "cov-mark", + "either", + "itertools", + "ra_ap_base_db", + "ra_ap_cfg", + "ra_ap_intern", + "ra_ap_mbe", + "ra_ap_parser", + "ra_ap_span", + "ra_ap_stdx", + "ra_ap_syntax", + "ra_ap_syntax-bridge", + "ra_ap_tt", + "rustc-hash 2.1.3", + "salsa", + "smallvec", + "thin-vec", + "tracing", + "triomphe", +] + +[[package]] +name = "ra_ap_hir_ty" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd596760659be29203c609eee9347a39d51c9f846498c7f285d94b6fb0e7059" +dependencies = [ + "arrayvec", + "bitflags", + "cov-mark", + "either", + "ena", + "indexmap", + "itertools", + "la-arena", + "oorandom", + "petgraph", + "ra-ap-rustc_abi", + "ra-ap-rustc_ast_ir", + "ra-ap-rustc_index", + "ra-ap-rustc_next_trait_solver", + "ra-ap-rustc_pattern_analysis", + "ra-ap-rustc_type_ir", + "ra_ap_base_db", + "ra_ap_hir_def", + "ra_ap_hir_expand", + "ra_ap_intern", + "ra_ap_macros", + "ra_ap_span", + "ra_ap_stdx", + "ra_ap_syntax", + "rustc-hash 2.1.3", + "rustc_apfloat", + "salsa", + "serde", + "serde_derive", + "smallvec", + "thin-vec", + "tracing", + "tracing-subscriber", + "tracing-tree", + "triomphe", + "typed-arena", +] + +[[package]] +name = "ra_ap_ide_db" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8785799a963ab225c300c02a247fcd4a062956e5ad4883970ec6764adfa8d7ad" +dependencies = [ + "arrayvec", + "bitflags", + "cov-mark", + "crossbeam-channel", + "either", + "fst", + "itertools", + "line-index", + "memchr", + "nohash-hasher", + "ra_ap_base_db", + "ra_ap_hir", + "ra_ap_macros", + "ra_ap_parser", + "ra_ap_profile", + "ra_ap_span", + "ra_ap_stdx", + "ra_ap_syntax", + "ra_ap_test_fixture", + "ra_ap_test_utils", + "ra_ap_vfs", + "rayon", + "rustc-hash 2.1.3", + "salsa", + "smallvec", + "tracing", + "triomphe", +] + +[[package]] +name = "ra_ap_intern" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a36d2110812d6ac9206742cc8013c0957474c0a74b2952b56e957c0b0aa4e263" +dependencies = [ + "arrayvec", + "dashmap", + "hashbrown 0.14.5", + "rayon", + "rustc-hash 2.1.3", + "triomphe", +] + +[[package]] +name = "ra_ap_load-cargo" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84fe4286ce3645b3f54024a653313c30a039f847a52eb82e74a3418ca4a62903" +dependencies = [ + "anyhow", + "crossbeam-channel", + "itertools", + "ra_ap_hir_expand", + "ra_ap_ide_db", + "ra_ap_intern", + "ra_ap_proc_macro_api", + "ra_ap_project_model", + "ra_ap_span", + "ra_ap_tt", + "ra_ap_vfs", + "ra_ap_vfs-notify", + "tracing", +] + +[[package]] +name = "ra_ap_macros" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6d76a9a3a51852b3776106df0dd39b163e9c5b2d2cd517fe35462920c3a1706" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "ra_ap_mbe" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3d1851d6f2b0a56eec1540adb2203746012a32dbbe4e7afa37f9f0393d0afe" +dependencies = [ + "arrayvec", + "bitflags", + "cov-mark", + "ra-ap-rustc_lexer", + "ra_ap_intern", + "ra_ap_parser", + "ra_ap_span", + "ra_ap_stdx", + "ra_ap_syntax-bridge", + "ra_ap_tt", + "rustc-hash 2.1.3", + "salsa", + "smallvec", +] + +[[package]] +name = "ra_ap_parser" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf11eb1b46bbba8aa1767436791234500562204c25654cc03900ffd043963a0" +dependencies = [ + "drop_bomb", + "ra-ap-rustc_lexer", + "ra_ap_edition", + "rustc-literal-escaper", + "tracing", + "winnow 0.7.15", +] + +[[package]] +name = "ra_ap_paths" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cbb13f567752c5478e2ddede6de942d508b5f0fa46bbc96b25651937c6790df" +dependencies = [ + "camino", +] + +[[package]] +name = "ra_ap_proc_macro_api" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91e2a4cc8104dd836becbafb722f1d8be42fb0a4d35a6d672b1a4e9a32984a8" +dependencies = [ + "indexmap", + "postcard", + "ra_ap_intern", + "ra_ap_paths", + "ra_ap_span", + "ra_ap_stdx", + "ra_ap_tt", + "rayon", + "rustc-hash 2.1.3", + "semver", + "serde", + "serde_derive", + "serde_json", + "tracing", +] + +[[package]] +name = "ra_ap_profile" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677e9d7e83cb38d48e7d94053035bbfad75b25b34089ae67406b1ea3300c0aed" +dependencies = [ + "libc", + "perf-event", + "windows-sys 0.61.2", +] + +[[package]] +name = "ra_ap_project_model" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a97de2c4deb93a2605dc81a666fef29591770280cfefccf5d306a073f0cbbf" +dependencies = [ + "anyhow", + "cargo_metadata", + "itertools", + "la-arena", + "ra_ap_base_db", + "ra_ap_cfg", + "ra_ap_intern", + "ra_ap_paths", + "ra_ap_span", + "ra_ap_stdx", + "ra_ap_toolchain", + "rustc-hash 2.1.3", + "semver", + "serde", + "serde_derive", + "serde_json", + "temp-dir", + "toml", + "tracing", + "triomphe", +] + +[[package]] +name = "ra_ap_span" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0f06ed71c2e392acf635f8130856da36e6ced7365b53fbdc1da569fa04092b" +dependencies = [ + "hashbrown 0.17.1", + "la-arena", + "ra_ap_stdx", + "ra_ap_syntax", + "ra_ap_vfs", + "rustc-hash 2.1.3", + "salsa", + "text-size", +] + +[[package]] +name = "ra_ap_stdx" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1462ef1865500b292afd582af8de4661fce3986a7c4e94cdd5ce46ae5252f9" +dependencies = [ + "crossbeam-channel", + "crossbeam-utils", + "itertools", + "jod-thread", + "libc", + "miow", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "ra_ap_syntax" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4b3e65ef77b752447610921a6d87d2a1e64ea3ab744b049467949302fa0f44" +dependencies = [ + "either", + "itertools", + "ra-ap-rustc_lexer", + "ra_ap_parser", + "ra_ap_stdx", + "rowan", + "rustc-hash 2.1.3", + "rustc-literal-escaper", + "smallvec", + "smol_str", + "tracing", + "triomphe", +] + +[[package]] +name = "ra_ap_syntax-bridge" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5fb8079020ea278c707e27974e7b47c502c65f0ff87feb27bddb9124b91b4f5" +dependencies = [ + "ra_ap_intern", + "ra_ap_parser", + "ra_ap_span", + "ra_ap_stdx", + "ra_ap_syntax", + "ra_ap_tt", + "rustc-hash 2.1.3", +] + +[[package]] +name = "ra_ap_test_fixture" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c244c8f3d21d89a14bd7a9cc1507b9f77b267c58da66e0081c533c949ee2b638" +dependencies = [ + "ra_ap_base_db", + "ra_ap_cfg", + "ra_ap_hir_expand", + "ra_ap_intern", + "ra_ap_paths", + "ra_ap_span", + "ra_ap_stdx", + "ra_ap_test_utils", + "ra_ap_tt", + "triomphe", +] + +[[package]] +name = "ra_ap_test_utils" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03eed74aed57120d73ad6db4fd7de371c791f6611b4cc7cd1998d77cf63da3fe" +dependencies = [ + "dissimilar", + "ra_ap_paths", + "ra_ap_profile", + "ra_ap_stdx", + "rustc-hash 2.1.3", + "text-size", +] + +[[package]] +name = "ra_ap_toolchain" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4ada3c3b061b51be63bfd736d967fb4651432ca8f74ed5c7dbfcd22e6f9a3c5" +dependencies = [ + "camino", + "semver", +] + +[[package]] +name = "ra_ap_tt" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88cf4ea21fbc05af1eaf2d2b0c4e94f41685a7700ef7a757f0c086e45a9bd350" +dependencies = [ + "arrayvec", + "indexmap", + "ra-ap-rustc_lexer", + "ra_ap_intern", + "ra_ap_span", + "ra_ap_stdx", + "rustc-hash 2.1.3", + "text-size", +] + +[[package]] +name = "ra_ap_vfs" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bdda57d4e2a11bc7a6aae94ab5e4593798903f665c8270e4d4472075edcb8c4" +dependencies = [ + "crossbeam-channel", + "fst", + "indexmap", + "nohash-hasher", + "ra_ap_paths", + "ra_ap_stdx", + "rustc-hash 2.1.3", + "tracing", +] + +[[package]] +name = "ra_ap_vfs-notify" +version = "0.0.346" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3efb5a0c22f810fbe2d50220df2be336e6cdc4852ca253fac108b0480a8d7d3" +dependencies = [ + "crossbeam-channel", + "notify", + "ra_ap_paths", + "ra_ap_stdx", + "ra_ap_vfs", + "rayon", + "rustc-hash 2.1.3", + "tracing", + "walkdir", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rowan" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b574c58582fa59fa43a2feb6608b8744184659f08a2e0117e4b8224d95ed61" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "memoffset", + "rustc-hash 1.1.0", + "text-size", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc-literal-escaper" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be87abb9e40db7466e0681dc8ecd9dcfd40360cb10b4c8fe24a7c4c3669b198" + +[[package]] +name = "rustc-stable-hash" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "781442f29170c5c93b7185ad559492601acdc71d5bb0706f5868094f45cfcd08" + +[[package]] +name = "rustc_apfloat" +version = "0.2.3+llvm-462a31f5a5ab" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486c2179b4796f65bfe2ee33679acf0927ac83ecf583ad6c91c3b4570911b9ad" +dependencies = [ + "bitflags", + "smallvec", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "salsa" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27d2e6e19e39591cd99c01ac4141544b4d012cfd0831348629b8187fa9fe6d34" +dependencies = [ + "boxcar", + "crossbeam-queue", + "crossbeam-utils", + "hashbrown 0.17.1", + "hashlink", + "indexmap", + "intrusive-collections", + "inventory", + "parking_lot", + "portable-atomic", + "rayon", + "rustc-hash 2.1.3", + "salsa-macro-rules", + "salsa-macros", + "smallvec", + "thin-vec", + "tracing", + "triomphe", + "typeid", +] + +[[package]] +name = "salsa-macro-rules" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf90a551c38f25ed8489111d3d5827aa18e1b85963f5e6a298505b1d1e2cb7e" + +[[package]] +name = "salsa-macros" +version = "0.28.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69dd77f86ec10e72e314f4ddf7c44c8d349e73cdff1aad524ba9668603fb0549" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[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 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "smol_str" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" +dependencies = [ + "borsh", + "serde_core", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "temp-dir" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016ef9739649996fcc983b9c588fe3d557cf216d4d98503ce1b057ab5a66d689" + +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + +[[package]] +name = "thin-vec" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79def32ffcd477db1ff26f76dab9e3a91f0bd42a85ca96577089b24623056f9d" + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "sharded-slab", + "thread_local", + "time", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-tree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac87aa03b6a4d5a7e4810d1a80c19601dbe0f8a837e9177f23af721c7ba7beec" +dependencies = [ + "nu-ansi-term", + "tracing-core", + "tracing-log", + "tracing-subscriber", +] + +[[package]] +name = "triomphe" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40688ea6389c8171614b25491f71d4a27946e0c7ce2da1c6de27e25abf1a0ae" +dependencies = [ + "serde", + "stable_deref_trait", +] + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/internal/rustfacts/semantic/Cargo.toml b/internal/rustfacts/semantic/Cargo.toml new file mode 100644 index 00000000..12ee15a4 --- /dev/null +++ b/internal/rustfacts/semantic/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "code-polishy-rust-semantic" +version = "0.1.0" +edition = "2024" +rust-version = "1.97.1" +license = "Apache-2.0" +publish = false + +[dependencies] +ra_ap_hir = "=0.0.346" +ra_ap_ide_db = "=0.0.346" +ra_ap_load-cargo = "=0.0.346" +ra_ap_project_model = "=0.0.346" +ra_ap_syntax = "=0.0.346" +ra_ap_vfs = "=0.0.346" +serde = { version = "=1.0.228", features = ["derive"] } +serde_json = "=1.0.150" + +[profile.dev] +debug = 0 + +[profile.release] +strip = "debuginfo" diff --git a/internal/rustfacts/semantic/src/graph.rs b/internal/rustfacts/semantic/src/graph.rs new file mode 100644 index 00000000..cef7b658 --- /dev/null +++ b/internal/rustfacts/semantic/src/graph.rs @@ -0,0 +1,228 @@ +use ra_ap_hir::{ModuleDef, PathResolution, Semantics}; +use ra_ap_ide_db::RootDatabase; +use ra_ap_syntax::{AstNode, SyntaxNode, ast}; +use ra_ap_vfs::{FileId, Vfs}; +use serde::Serialize; + +#[derive(Serialize)] +pub(crate) struct Graph { + protocol: &'static str, + edges: Vec, + problems: Vec, +} + +#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord)] +struct Edge { + source: Location, + target: Location, + expanded: bool, +} + +#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord)] +struct Location { + path: String, + start: u32, + end: u32, +} + +#[derive(Serialize)] +struct Problem { + location: Location, + message: String, +} + +pub(crate) fn collect( + database: &RootDatabase, + vfs: &Vfs, + files: &[String], +) -> Result> { + let semantics = Semantics::new(database); + let mut graph = Graph { + protocol: "rust-semantic/v1", + edges: Vec::new(), + problems: Vec::new(), + }; + let mut remaining = 1_000_000; + for path in files { + let (file, _) = vfs + .iter() + .find(|(_, candidate)| candidate.to_string() == *path) + .ok_or_else(|| { + format!("selected file is absent from the semantic workspace: {path}") + })?; + let module = semantics + .file_to_module_def(file) + .ok_or_else(|| format!("selected file has no compiled module: {path}"))?; + let edition = module + .as_source_file_id(database) + .ok_or("selected file has no source-file identity")?; + let root = semantics.parse(edition); + graph.walk(&semantics, vfs, root.syntax(), 0, &mut remaining)?; + } + graph.edges.sort(); + graph.edges.dedup(); + Ok(graph) +} + +impl Graph { + fn walk( + &mut self, + semantics: &Semantics<'_, RootDatabase>, + vfs: &Vfs, + node: &SyntaxNode, + depth: usize, + remaining: &mut usize, + ) -> Result<(), Box> { + if depth > 128 || *remaining == 0 { + return Err("semantic expansion exceeds its traversal limit".into()); + } + *remaining -= 1; + if let Some(call) = ast::MacroCall::cast(node.clone()) { + return self.macro_call(semantics, vfs, &call, depth, remaining); + } + if let Some(path) = ast::Path::cast(node.clone()) { + self.path(semantics, vfs, &path, depth > 0); + } + if let Some(call) = ast::MethodCallExpr::cast(node.clone()) { + match semantics.resolve_method_call(&call) { + Some(function) => self.reference( + semantics, + vfs, + node, + ModuleDef::Function(function), + depth > 0, + ), + None => self.problem(semantics, vfs, node, "method resolution is incomplete"), + } + } + for child in node.children() { + self.walk(semantics, vfs, &child, depth, remaining)?; + } + Ok(()) + } + + fn macro_call( + &mut self, + semantics: &Semantics<'_, RootDatabase>, + vfs: &Vfs, + call: &ast::MacroCall, + depth: usize, + remaining: &mut usize, + ) -> Result<(), Box> { + let Some(id) = semantics.to_def(call) else { + self.problem( + semantics, + vfs, + call.syntax(), + "macro resolution is incomplete", + ); + return Ok(()); + }; + if let Some(definition) = semantics.resolve_macro_call(call) { + self.reference( + semantics, + vfs, + call.syntax(), + ModuleDef::Macro(definition), + depth > 0, + ); + } + let expansion = semantics.expand(id); + if let Some(error) = expansion.err { + self.problem( + semantics, + vfs, + call.syntax(), + &format!("macro expansion failed: {error:?}"), + ); + return Ok(()); + } + self.walk(semantics, vfs, &expansion.value, depth + 1, remaining) + } + + fn path( + &mut self, + semantics: &Semantics<'_, RootDatabase>, + vfs: &Vfs, + path: &ast::Path, + expanded: bool, + ) { + if path + .syntax() + .parent() + .is_some_and(|node| ast::Path::can_cast(node.kind())) + || path + .syntax() + .ancestors() + .any(|node| ast::Attr::can_cast(node.kind())) + { + return; + } + match semantics.resolve_path(path) { + Some(PathResolution::Def(definition)) => { + self.reference(semantics, vfs, path.syntax(), definition, expanded) + } + Some(_) => {} + None => self.problem( + semantics, + vfs, + path.syntax(), + "path resolution is incomplete", + ), + } + } + + fn reference( + &mut self, + semantics: &Semantics<'_, RootDatabase>, + vfs: &Vfs, + node: &SyntaxNode, + definition: ModuleDef, + expanded: bool, + ) { + let target = match definition { + ModuleDef::Module(module) => Some(module), + other => other.module(semantics.db), + }; + let Some(module) = target else { + return; + }; + let source = location(semantics, vfs, node); + let module_source = module.definition_source(semantics.db); + let target_node = semantics.parse_or_expand(module_source.file_id); + let target = location(semantics, vfs, &target_node); + if source.path != target.path { + self.edges.push(Edge { + source, + target, + expanded, + }); + } + } + + fn problem( + &mut self, + semantics: &Semantics<'_, RootDatabase>, + vfs: &Vfs, + node: &SyntaxNode, + message: &str, + ) { + self.problems.push(Problem { + location: location(semantics, vfs, node), + message: message.into(), + }); + } +} + +fn location(semantics: &Semantics<'_, RootDatabase>, vfs: &Vfs, node: &SyntaxNode) -> Location { + let range = semantics.original_range(node); + Location { + path: path(vfs, range.file_id), + start: range.range.start().into(), + end: range.range.end().into(), + } +} + +fn path(vfs: &Vfs, file: FileId) -> String { + vfs.file_path(file).to_string() +} diff --git a/internal/rustfacts/semantic/src/main.rs b/internal/rustfacts/semantic/src/main.rs new file mode 100644 index 00000000..8e19a7d0 --- /dev/null +++ b/internal/rustfacts/semantic/src/main.rs @@ -0,0 +1,52 @@ +mod graph; +mod workspace; + +#[cfg(test)] +mod tests; + +use serde::Deserialize; +use std::io::{self, Read}; + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct Request { + protocol: String, + manifest: String, + target: String, + cargo: String, + proc_macro_server: String, + files: Vec, + #[serde(default)] + packages: Vec, + #[serde(default)] + features: Vec, + #[serde(default)] + no_default_features: bool, +} + +fn main() { + if let Err(error) = run() { + eprintln!("{error}"); + std::process::exit(1); + } +} + +fn run() -> Result<(), Box> { + let mut input = Vec::new(); + io::stdin() + .take(8 * 1024 * 1024 + 1) + .read_to_end(&mut input)?; + if input.len() > 8 * 1024 * 1024 { + return Err("semantic request exceeds 8 MiB".into()); + } + let request: Request = serde_json::from_slice(&input)?; + if request.protocol != "rust-semantic/v1" + || request.files.is_empty() + || request.files.len() > 4096 + { + return Err("unsupported semantic request protocol or file inventory".into()); + } + let result = workspace::analyze(&request)?; + serde_json::to_writer(io::stdout().lock(), &result)?; + Ok(()) +} diff --git a/internal/rustfacts/semantic/src/tests.rs b/internal/rustfacts/semantic/src/tests.rs new file mode 100644 index 00000000..6b8e4843 --- /dev/null +++ b/internal/rustfacts/semantic/src/tests.rs @@ -0,0 +1,153 @@ +use crate::{Request, workspace}; +use serde_json::Value; +use std::{ + fs, + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, +}; + +struct Fixture { + root: PathBuf, +} + +impl Fixture { + fn new() -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let root = std::env::temp_dir().join(format!( + "code-polishy-semantic-{}-{nonce}", + std::process::id() + )); + fs::create_dir(&root).unwrap(); + let fixture = Self { root }; + fixture.write( + "Cargo.toml", + "[package]\nname='semantic-fixture'\nversion='1.0.0'\nedition='2024'\n", + ); + fixture.write( + "Cargo.lock", + "version=4\n[[package]]\nname='semantic-fixture'\nversion='1.0.0'\n", + ); + fixture + } + + fn write(&self, path: &str, source: &str) { + let path = self.root.join(path); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, source).unwrap(); + } + + fn request(&self, files: &[&str]) -> Request { + let compiler = PathBuf::from(std::env::var("RUSTC").expect("pinned RUSTC is required")); + let sdk = compiler.parent().unwrap().parent().unwrap(); + Request { + protocol: "rust-semantic/v1".into(), + manifest: self.root.join("Cargo.toml").to_str().unwrap().into(), + target: std::env::var("CODE_POLISHY_RUST_HOST") + .expect("governed native host is required"), + cargo: std::env::var("CARGO").expect("pinned CARGO is required"), + proc_macro_server: sdk + .join("libexec") + .join(format!( + "rust-analyzer-proc-macro-srv{}", + std::env::consts::EXE_SUFFIX + )) + .to_str() + .unwrap() + .into(), + files: files + .iter() + .map(|path| self.root.join(path).to_str().unwrap().into()) + .collect(), + packages: Vec::new(), + features: Vec::new(), + no_default_features: false, + } + } + + fn graph(&self, files: &[&str]) -> Value { + serde_json::to_value(workspace::analyze(&self.request(files)).unwrap()).unwrap() + } +} + +impl Drop for Fixture { + fn drop(&mut self) { + fs::remove_dir_all(&self.root).unwrap(); + } +} + +#[test] +fn generated_references_retain_dollar_crate_hygiene_and_call_site() { + let fixture = Fixture::new(); + fixture.write( + "src/lib.rs", + "#![no_std]\n#[macro_use] mod macros;\nmod consumer;\nmod lower;\npub use consumer::run;\n", + ); + fixture.write( + "src/macros.rs", + "macro_rules! hidden_call { () => { $crate::lower::value() }; }\n", + ); + fixture.write("src/lower.rs", "pub fn value() -> u8 { 7 }\n"); + let source = + "mod lower { pub fn value() -> u8 { 9 } }\npub fn run() -> u8 { hidden_call!() }\n"; + fixture.write("src/consumer.rs", source); + let graph = fixture.graph(&["src/consumer.rs"]); + assert_eq!(graph["problems"], serde_json::json!([])); + let target = fixture + .root + .join("src/lower.rs") + .to_str() + .unwrap() + .to_owned(); + let edge = graph["edges"] + .as_array() + .unwrap() + .iter() + .find(|edge| edge["target"]["path"] == target) + .expect("generated dependency was omitted"); + assert_eq!(edge["expanded"], true); + assert_eq!( + edge["source"]["start"], + source.find("hidden_call!").unwrap() + ); + assert_eq!( + edge["source"]["path"], + fixture.root.join("src/consumer.rs").to_str().unwrap() + ); +} + +#[test] +fn nested_expansion_keeps_dependencies_introduced_without_input_tokens() { + let fixture = Fixture::new(); + fixture.write( + "src/lib.rs", + "#![no_std]\n#[macro_use] mod macros;\nmod lower;\nmod consumer;\npub use consumer::run;\n", + ); + fixture.write("src/macros.rs", "macro_rules! inner { () => { $crate::lower::value() }; }\nmacro_rules! outer { () => { inner!() }; }\n"); + fixture.write("src/lower.rs", "pub fn value() -> u8 { 7 }\n"); + fixture.write("src/consumer.rs", "pub fn run() -> u8 { outer!() }\n"); + let graph = fixture.graph(&["src/consumer.rs"]); + assert_eq!(graph["problems"], serde_json::json!([])); + assert!( + graph["edges"] + .as_array() + .unwrap() + .iter() + .any(|edge| edge["expanded"] == true + && edge["target"]["path"] == fixture.root.join("src/lower.rs").to_str().unwrap()) + ); +} + +#[test] +fn invalid_macro_expansion_is_a_failed_compilation_not_an_empty_graph() { + let fixture = Fixture::new(); + fixture.write("src/lib.rs", "#![no_std]\nmacro_rules! broken { () => { missing::value() }; }\npub fn run() { broken!(); }\n"); + let result = workspace::analyze(&fixture.request(&["src/lib.rs"])); + let message = result + .err() + .expect("invalid expansion was accepted") + .to_string(); + assert!(message.contains("Cargo compilation failed"), "{message}"); +} diff --git a/internal/rustfacts/semantic/src/workspace.rs b/internal/rustfacts/semantic/src/workspace.rs new file mode 100644 index 00000000..19871275 --- /dev/null +++ b/internal/rustfacts/semantic/src/workspace.rs @@ -0,0 +1,77 @@ +use crate::{Request, graph}; +use ra_ap_load_cargo::{LoadCargoConfig, ProcMacroServerChoice, load_workspace}; +use ra_ap_project_model::{ + CargoConfig, CargoFeatures, ProjectManifest, ProjectWorkspace, ProjectWorkspaceKind, +}; +use ra_ap_vfs::AbsPathBuf; + +pub(crate) fn analyze(request: &Request) -> Result> { + let manifest = ProjectManifest::from_manifest_file(absolute(&request.manifest)?)?; + let config = configuration(request); + let mut workspace = + ProjectWorkspace::load(manifest, &config, &|message| eprintln!("{message}"))?; + match &workspace.kind { + ProjectWorkspaceKind::Cargo { error: None, .. } => {} + _ => return Err("complete Cargo metadata is required".into()), + } + if workspace.target.is_err() || workspace.toolchain.is_none() { + return Err("compiler target or toolchain facts are unavailable".into()); + } + let scripts = workspace.run_build_scripts(&config, &|message| eprintln!("{message}"))?; + if let Some(error) = scripts.error() { + return Err(format!("Cargo compilation failed: {error}").into()); + } + workspace.set_build_scripts(scripts); + let load = LoadCargoConfig { + load_out_dirs_from_check: true, + with_proc_macro_server: ProcMacroServerChoice::Explicit(absolute( + &request.proc_macro_server, + )?), + prefill_caches: false, + num_worker_threads: 1, + proc_macro_processes: 1, + }; + let (database, vfs, server) = load_workspace(workspace, &config.extra_env, &load)?; + if server.is_none() { + return Err("the pinned procedural macro server is unavailable".into()); + } + graph::collect(&database, &vfs, &request.files) +} + +fn configuration(request: &Request) -> CargoConfig { + let mut command = vec![ + request.cargo.clone(), + "check".into(), + "--frozen".into(), + "--manifest-path".into(), + request.manifest.clone(), + "--message-format=json".into(), + "--target".into(), + request.target.clone(), + ]; + for package in &request.packages { + command.extend(["--package".into(), package.clone()]); + } + for feature in &request.features { + command.extend(["--features".into(), feature.clone()]); + } + if request.no_default_features { + command.push("--no-default-features".into()); + } + CargoConfig { + target: Some(request.target.clone()), + features: CargoFeatures::Selected { + features: request.features.clone(), + no_default_features: request.no_default_features, + }, + extra_args: vec!["--frozen".into()], + run_build_script_command: Some(command), + wrap_rustc_in_build_scripts: false, + set_test: false, + ..CargoConfig::default() + } +} + +fn absolute(path: &str) -> Result> { + Ok(AbsPathBuf::try_from(path.to_owned()).map_err(|_| "an absolute UTF-8 path is required")?) +} From d5cc3244f273c6d25330068555eddf59a2b4ba5e Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 07:20:15 +0000 Subject: [PATCH 30/49] fix: batch Rust syntax analysis and preserve large coordinates Partition source requests by encoded bytes and file count, retain exact per-batch inventory validation, preserve unaffected facts, and stop starting batches after cancellation. Use tuple coordinates to avoid the pinned Tree-sitter binding's borrowed-reference point getters. Verified rustfacts-unit, affected Rust/Cargo quality and architecture tests, formatting, and whitespace checks. Scoped policy check retains 55 existing semantic-helper coverage failures; the helper remains unbuilt pending authorized beta policy admission. No lock upgrade or publication. --- docs/design/cargo-facts.md | 16 ++ docs/plans/code-polishy-rust-support-plan.md | 9 ++ internal/rustfacts/source.go | 117 +++++++++++--- internal/rustfacts/source_batch_test.go | 157 +++++++++++++++++++ internal/rustfacts/source_parser.py | 12 +- internal/rustfacts/source_test.go | 15 ++ 6 files changed, 294 insertions(+), 32 deletions(-) create mode 100644 internal/rustfacts/source_batch_test.go diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index dc03819c..ebd202da 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -93,6 +93,22 @@ and macros. Error-recovered or incomplete syntax cannot establish complete source coverage. Parsed comments are distinct from comment-like raw strings; attributes and macro token trees retain their original source locations. +Source selection is partitioned in order into requests of at most 4096 files and +8388608 encoded bytes, including JSON escaping and envelope overhead. These are +per-command limits, not a repository-size ceiling. Each batch is part of the +ordinary command plan and validates its exact source inventory. An individually +oversized source or a failed batch produces coverage findings for the affected +paths without discarding valid siblings. Cancellation prevents later batches +from starting and leaves their files explicitly uncovered. + +Locations unpack Tree-sitter's point tuples and translate byte columns into +original Unicode coordinates. The pinned Python binding's +[point attribute getters](https://github.com/tree-sitter/py-tree-sitter/blob/v0.26.0/tree_sitter/binding/point.c) +return borrowed integer references, which can corrupt memory when rows or columns +exceed CPython's small-integer cache. Tuple unpacking uses the normal owned-reference +sequence interface; large-file and Unicode-coordinate fixtures exercise the real +parser without modifying its wheel or weakening source coverage. + Source complexity is measured on written syntax, not expanded macro output. Functions and closures have independent measurements. Conditions, loops, Boolean short circuits, guards, or-patterns, `?` and let-else add decisions; an exhaustive diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 9bd8a951..5bd1ded1 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -448,6 +448,15 @@ dependency governance now have runnable beta implementations. Source parsing use exact authenticated binary wheels in the existing governed Python runtime; it requires neither target package installation scripts nor a new Rust bootstrap. +Syntax requests are now batched by encoded bytes and source count, with each batch +visible in command planning. Valid siblings survive a failed or oversized input; +cancellation leaves unstarted sources explicitly uncovered. Large-input fixtures +also exposed unsafe coordinate-property access in the pinned Python binding. +Using its tuple interface preserves long-line and Unicode coordinates without a +dependency change. Actual workspace-lock containment before native test +compilation remains a separate test-runner milestone, including its command-plan +and reusable-view boundaries. + Native unit/integration and separate doctest suites now retain nonempty structured results through ordinary commands, gate artifacts, regression proofs and reusable receipts. The [Rust test contract](../design/rust-tests.md) documents selectors and boundaries. diff --git a/internal/rustfacts/source.go b/internal/rustfacts/source.go index 187680d8..f39339d9 100644 --- a/internal/rustfacts/source.go +++ b/internal/rustfacts/source.go @@ -1,9 +1,11 @@ package rustfacts import ( + "bytes" "context" _ "embed" "encoding/json" + "errors" "fmt" "slices" "unicode/utf8" @@ -15,6 +17,12 @@ import ( const SourceProtocol = "rust-source/v1" +const sourceRequestPrefix = `{"protocol":"` + SourceProtocol + `","sources":[` + +const maximumSourceRequest = 8 << 20 + +const sourceRequestOverhead = len(sourceRequestPrefix) + len("]}") + //go:embed source_parser.py var sourceParser string @@ -24,9 +32,10 @@ type SourceInput struct { } type SourcePlan struct { - Inputs []SourceInput - Commands []policy.Command - Findings []policy.Finding + Inputs []SourceInput + Commands []policy.Command + Findings []policy.Finding + batchEnds []int } type Sources struct { @@ -36,34 +45,68 @@ type Sources struct { func PlanSources(repo repository.Repository, paths []string) SourcePlan { plan := SourcePlan{} + encoded := [][]byte{} + requestSize := sourceRequestOverhead for _, path := range paths { if !sourceAnalysisSelected(repo, path) { continue } - data, err := repo.ReadContainedFile(path, 4<<20) + input, data, err := readSourceInput(repo, path) if err != nil { plan.Findings = append(plan.Findings, sourceFinding(path, err.Error())) continue } - if !utf8.Valid(data) { - plan.Findings = append(plan.Findings, sourceFinding(path, "Rust source is not UTF-8 text")) - continue + separator := 0 + if len(encoded) > 0 { + separator = 1 + } + if len(encoded) == 4096 || requestSize+separator+len(data) > maximumSourceRequest { + plan.appendBatch(repo, encoded, requestSize) + encoded = nil + requestSize = sourceRequestOverhead + separator = 0 } - plan.Inputs = append(plan.Inputs, SourceInput{Path: path, Source: string(data)}) + plan.Inputs = append(plan.Inputs, input) + encoded = append(encoded, data) + requestSize += separator + len(data) + } + plan.appendBatch(repo, encoded, requestSize) + return plan +} + +func readSourceInput(repo repository.Repository, path string) (SourceInput, []byte, error) { + data, err := repo.ReadContainedFile(path, 4<<20) + if err != nil { + return SourceInput{}, nil, err } - if len(plan.Inputs) == 0 { - return plan + if !utf8.Valid(data) { + return SourceInput{}, nil, errors.New("rust source is not UTF-8 text") } - request, err := json.Marshal(struct { - Protocol string `json:"protocol"` - Sources []SourceInput `json:"sources"` - }{SourceProtocol, plan.Inputs}) - if err != nil || len(request) > 8<<20 || len(plan.Inputs) > 4096 { - plan.Findings = append(plan.Findings, sourceFinding(plan.Inputs[0].Path, "Rust syntax request exceeds 8388608 bytes or 4096 sources; select a smaller source scope")) - return plan + input := SourceInput{Path: path, Source: string(data)} + encoded, err := json.Marshal(input) + if err != nil { + return SourceInput{}, nil, err } - plan.Commands = []policy.Command{{Name: "policy-rust-source-facts", Argv: repo.PythonCommand(sourceParser), Cwd: ".", Stdin: request, Managed: true, SealedEnvironment: true, TimeoutSeconds: 60}} - return plan + if len(encoded)+sourceRequestOverhead > maximumSourceRequest { + return SourceInput{}, nil, errors.New("encoded Rust source exceeds the 8388608-byte syntax request limit") + } + return input, encoded, nil +} + +func (plan *SourcePlan) appendBatch(repo repository.Repository, encoded [][]byte, size int) { + if len(encoded) == 0 { + return + } + request := make([]byte, 0, size) + request = append(request, sourceRequestPrefix...) + request = append(request, bytes.Join(encoded, []byte(","))...) + request = append(request, ']', '}') + name := "policy-rust-source-facts" + if len(plan.Commands) > 0 { + name += fmt.Sprintf("-%d", len(plan.Commands)+1) + } + plan.Commands = append(plan.Commands, policy.Command{Name: name, Argv: repo.PythonCommand(sourceParser), Cwd: ".", Stdin: request, Managed: true, SealedEnvironment: true, TimeoutSeconds: 60}) + plan.batchEnds = append(plan.batchEnds, len(plan.Inputs)) } func sourceAnalysisSelected(repo repository.Repository, path string) bool { @@ -80,19 +123,33 @@ func ReadSources(ctx context.Context, repo repository.Repository, plan SourcePla } structured, ok := boundary.(runner.StructuredRunner) if !ok { - result.Findings = append(result.Findings, sourceFinding(plan.Inputs[0].Path, "the runner cannot capture bounded Rust syntax facts")) + result.Findings = append(result.Findings, sourceBatchFindings(plan.Inputs, "the runner cannot capture bounded Rust syntax facts")...) return result } - _, output, err := structured.RunStructured(ctx, repo.Root, plan.Commands[0]) + start := 0 + for index, command := range plan.Commands { + end := plan.batchEnds[index] + batch := readSourceBatch(ctx, repo, plan.Inputs[start:end], command, structured) + result.Files = append(result.Files, batch.Files...) + result.Findings = append(result.Findings, batch.Findings...) + start = end + } + return result +} + +func readSourceBatch(ctx context.Context, repo repository.Repository, inputs []SourceInput, command policy.Command, boundary runner.StructuredRunner) Sources { + if err := ctx.Err(); err != nil { + return Sources{Findings: sourceBatchFindings(inputs, "Rust syntax analysis did not run: "+err.Error())} + } + _, output, err := boundary.RunStructured(ctx, repo.Root, command) if err != nil { - result.Findings = append(result.Findings, sourceFinding(plan.Inputs[0].Path, fmt.Sprintf("Rust syntax analysis failed: %v; %s", err, diagnosticOutput(output.Stderr)))) - return result + return Sources{Findings: sourceBatchFindings(inputs, fmt.Sprintf("Rust syntax analysis failed: %v; %s", err, diagnosticOutput(output.Stderr)))} } - sources, err := ParseSources(repo, plan.Inputs, output.Stdout) + sources, err := ParseSources(repo, inputs, output.Stdout) if err != nil { - result.Findings = append(result.Findings, sourceFinding(plan.Inputs[0].Path, "invalid Rust syntax evidence: "+err.Error())) - return result + return Sources{Findings: sourceBatchFindings(inputs, "invalid Rust syntax evidence: "+err.Error())} } + result := Sources{} for _, source := range sources { if source.Complete { result.Files = append(result.Files, source) @@ -103,6 +160,14 @@ func ReadSources(ctx context.Context, repo repository.Repository, plan SourcePla return result } +func sourceBatchFindings(inputs []SourceInput, message string) []policy.Finding { + findings := make([]policy.Finding, 0, len(inputs)) + for _, input := range inputs { + findings = append(findings, sourceFinding(input.Path, message)) + } + return findings +} + func sourceFinding(path, reason string) policy.Finding { return policy.Finding{Check: "policy.rustSource", Path: path, Subject: "syntax", Message: reason} } diff --git a/internal/rustfacts/source_batch_test.go b/internal/rustfacts/source_batch_test.go new file mode 100644 index 00000000..61ab19f7 --- /dev/null +++ b/internal/rustfacts/source_batch_test.go @@ -0,0 +1,157 @@ +package rustfacts + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "slices" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestRustSourceFactsBatchLargeScopes(t *testing.T) { + t.Parallel() + for _, scenario := range []struct { + name string + count int + source string + }{ + {"source count", 4097, "\n"}, + {"source bytes", 3, "/*" + strings.Repeat("x", 3<<20) + "*/\npub fn f() {}\n"}, + {"encoded bytes", 2, "pub const TEXT: &str = \"" + strings.Repeat("&", 1<<20) + "\";\n"}, + } { + t.Run(scenario.name, func(t *testing.T) { + repo := rustRepository(t) + paths := make([]string, scenario.count) + for index := range paths { + paths[index] = fmt.Sprintf("source_%04d.rs", index) + write(t, repo.Root, paths[index], scenario.source) + } + plan := PlanSources(repo, paths) + if len(plan.Findings) != 0 || len(plan.Commands) < 2 { + t.Fatalf("large source scope was not batched: commands=%d findings=%+v", len(plan.Commands), plan.Findings) + } + seen := []string{} + for _, command := range plan.Commands { + var request struct { + Protocol string `json:"protocol"` + Sources []SourceInput `json:"sources"` + } + if err := json.Unmarshal(command.Stdin, &request); err != nil { + t.Fatal(err) + } + if len(command.Stdin) > 8<<20 || len(request.Sources) > 4096 || len(request.Sources) == 0 || request.Protocol != SourceProtocol { + t.Fatalf("unbounded or empty parser request: bytes=%d sources=%d protocol=%s", len(command.Stdin), len(request.Sources), request.Protocol) + } + for _, source := range request.Sources { + seen = append(seen, source.Path) + } + } + if !slices.Equal(seen, paths) { + t.Fatal("batching omitted, repeated or reordered selected source") + } + result := ReadSources(t.Context(), repo, plan, runner.OSRunner{}) + if len(result.Findings) != 0 || len(result.Files) != len(paths) { + t.Fatalf("large source scope lost syntax facts: files=%d findings=%+v", len(result.Files), result.Findings) + } + }) + } +} + +func TestRustSourceFactsKeepSiblingsOfAnOversizedEncodedSource(t *testing.T) { + t.Parallel() + repo := rustRepository(t) + write(t, repo.Root, "before.rs", "pub fn before() {}\n") + write(t, repo.Root, "large.rs", "pub const TEXT: &str = \""+strings.Repeat("&", 2<<20)+"\";\n") + write(t, repo.Root, "after.rs", "pub fn after() {}\n") + plan := PlanSources(repo, []string{"before.rs", "large.rs", "after.rs"}) + result := ReadSources(t.Context(), repo, plan, runner.OSRunner{}) + if len(result.Findings) != 1 || result.Findings[0].Path != "large.rs" || !strings.Contains(result.Findings[0].Message, "8388608") { + t.Fatalf("oversized source lacks its own coverage failure: %+v", result.Findings) + } + if len(result.Files) != 2 || result.Files[0].Path != "before.rs" || result.Files[1].Path != "after.rs" { + t.Fatalf("oversized source discarded valid siblings: files=%d", len(result.Files)) + } +} + +func TestRustSourceFactsRetainValidBatchesAfterAParserFailure(t *testing.T) { + t.Parallel() + for _, failure := range []string{"command", "wrong inventory"} { + t.Run(failure, func(t *testing.T) { + repo := rustRepository(t) + paths := []string{"before.rs", "broken.rs", "after.rs"} + for _, path := range paths { + write(t, repo.Root, path, "pub const TEXT: &str = \""+strings.Repeat("&", 1<<20)+"\";\n") + } + plan := PlanSources(repo, paths) + var first runner.Output + boundary := &sourceBatchRunner{after: func(index int, output runner.Output) (runner.Output, error) { + if index == 1 { + first = output + } + if index != 2 { + return output, nil + } + if failure == "command" { + return output, errors.New("parser worker failed") + } + return first, nil + }} + result := ReadSources(t.Context(), repo, plan, boundary) + if len(result.Files) != 2 || result.Files[0].Path != "before.rs" || result.Files[1].Path != "after.rs" || len(result.Findings) != 1 || result.Findings[0].Path != "broken.rs" { + t.Fatalf("a failed batch discarded or fabricated sibling coverage: files=%d findings=%+v", len(result.Files), result.Findings) + } + if len(boundary.commands) != len(plan.Commands) { + t.Fatalf("execution omitted planned batches: ran=%d planned=%d", len(boundary.commands), len(plan.Commands)) + } + for index, command := range boundary.commands { + if command.Name != plan.Commands[index].Name { + t.Fatalf("batch execution departed from its command plan: %q != %q", command.Name, plan.Commands[index].Name) + } + } + }) + } +} + +func TestRustSourceFactsStopStartingBatchesAfterCancellation(t *testing.T) { + t.Parallel() + repo := rustRepository(t) + paths := []string{"first.rs", "second.rs", "third.rs"} + for _, path := range paths { + write(t, repo.Root, path, "pub const TEXT: &str = \""+strings.Repeat("&", 1<<20)+"\";\n") + } + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + boundary := &sourceBatchRunner{after: func(_ int, output runner.Output) (runner.Output, error) { + cancel() + return output, nil + }} + result := ReadSources(ctx, repo, PlanSources(repo, paths), boundary) + if len(boundary.commands) != 1 || len(result.Files) != 1 || result.Files[0].Path != "first.rs" || len(result.Findings) != 2 { + t.Fatalf("cancellation executed or lost unparsed batches: commands=%d files=%d findings=%+v", len(boundary.commands), len(result.Files), result.Findings) + } + for index, finding := range result.Findings { + if finding.Path != paths[index+1] || !strings.Contains(finding.Message, "canceled") { + t.Fatalf("canceled source lacks its own coverage failure: %+v", finding) + } + } +} + +type sourceBatchRunner struct { + runner.OSRunner + commands []policy.Command + after func(int, runner.Output) (runner.Output, error) +} + +func (boundary *sourceBatchRunner) RunStructured(ctx context.Context, root string, command policy.Command) (runner.Result, runner.Output, error) { + boundary.commands = append(boundary.commands, command) + result, output, err := boundary.OSRunner.RunStructured(ctx, root, command) + if err == nil { + output, err = boundary.after(len(boundary.commands), output) + } + return result, output, err +} diff --git a/internal/rustfacts/source_parser.py b/internal/rustfacts/source_parser.py index fc33ff2b..d7de78ef 100644 --- a/internal/rustfacts/source_parser.py +++ b/internal/rustfacts/source_parser.py @@ -115,16 +115,16 @@ def location(node, source): - start = node.start_point - end = node.end_point + start_row, start_column = node.start_point + end_row, end_column = node.end_point return { "startByte": node.start_byte, "endByte": node.end_byte, - "line": start.row + 1, - "column": len(source[node.start_byte - start.column : node.start_byte].decode()) + "line": start_row + 1, + "column": len(source[node.start_byte - start_column : node.start_byte].decode()) + 1, - "endLine": end.row + 1, - "endColumn": len(source[node.end_byte - end.column : node.end_byte].decode()) + "endLine": end_row + 1, + "endColumn": len(source[node.end_byte - end_column : node.end_byte].decode()) + 1, } diff --git a/internal/rustfacts/source_test.go b/internal/rustfacts/source_test.go index e67a9e8f..a6c69aa6 100644 --- a/internal/rustfacts/source_test.go +++ b/internal/rustfacts/source_test.go @@ -89,6 +89,21 @@ func TestRustSourceFactsKeepValidSiblingsAndProtectNonSourceInputs(t *testing.T) } } +func TestRustSourceFactsRetainLargeLineAndColumnCoordinates(t *testing.T) { + t.Parallel() + repo := rustRepository(t) + text := strings.Repeat("\n", 300) + strings.Repeat(" ", 300) + "pub fn café() { let _ = \"🦀\"; /* prose */ }\n" + write(t, repo.Root, "wide.rs", text) + result := ReadSources(t.Context(), repo, PlanSources(repo, []string{"wide.rs"}), runner.OSRunner{}) + if len(result.Findings) != 0 || len(result.Files) != 1 { + t.Fatalf("large original coordinates failed syntax analysis: %+v", result.Findings) + } + source := result.Files[0] + if len(source.Functions) != 1 || source.Functions[0].Line != 301 || source.Functions[0].Column != 301 || source.Functions[0].Name != "café" || len(source.Comments) != 1 { + t.Fatalf("original Unicode source coordinates were lost: %+v", source) + } +} + func TestRustSourceFactsRejectFabricatedOrIncompleteResponses(t *testing.T) { t.Parallel() repo := rustRepository(t) From 4d4cb8a2bd01a5fd638f2625de653fec1c01823a Mon Sep 17 00:00:00 2001 From: Jeff Musgrave Date: Fri, 25 Sep 2026 07:35:11 +0000 Subject: [PATCH 31/49] feat: validate native Rust test source evidence Separate production/test context from built-in test entry classification and reject empty test bodies through native lint. Conditional test attributes do not relax production complexity. Move syntax, graph evidence, and report schema together to rust-source/v2 without a legacy acceptance path. Verified rustfacts-unit, affected Rust/Cargo quality and architecture tests, ordinary compiled-test rejection and repair, report-schema validation, malformed-evidence regression, formatting, and whitespace checks. Scoped policy check still reports 55 existing semantic-helper coverage failures. The helper remains unbuilt pending authorization for a local Rust-aware policy lock; no push, publication, or dependency admission. --- docs/design/cargo-facts.md | 21 ++++- docs/design/rust-tests.md | 10 +++ docs/plans/code-polishy-rust-support-plan.md | 7 ++ docs/policies/code-quality.md | 7 +- docs/policies/verification.md | 4 +- internal/architecture/sourcegraph/facts.go | 2 +- .../architecture/sourcegraph/facts_test.go | 3 +- internal/engine/report_model_test.go | 2 +- internal/quality/rust_source.go | 17 +++- internal/quality/rust_source_test.go | 87 +++++++++++++++++++ internal/rustfacts/source.go | 2 +- internal/rustfacts/source_output.go | 55 +++++++++--- internal/rustfacts/source_parser.py | 41 ++++++++- internal/rustfacts/source_test.go | 26 +++++- schema/code-polishy-report.schema.json | 4 +- 15 files changed, 260 insertions(+), 28 deletions(-) diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index ebd202da..53d6a7c7 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -121,10 +121,29 @@ Rust owns separate `quality.complexity.rust` and `rustTest` limits, initially 12 and 20 with the same strict-below comparison as Go. Projects may tighten them. Built-in test functions and exact `cfg(test)` scopes receive the test limit; file-level test ownership alone cannot relax production functions co-located -with inline tests. Custom test attributes and exclusive Cargo test-target scope +with inline tests. A conditional test attribute does not establish test-only +ownership: its function still receives the production limit unless an enclosing +test-only scope already excludes production builds. Custom test attributes and exclusive Cargo test-target scope need further test-context evidence. Depth and parameter measurements are retained as facts; TypeScript's thresholds do not silently become Rust restrictions. +The `rust-source/v2` contract separates production/test context from built-in +test-entry classification. Helpers and closures inside a test scope retain test +complexity limits without becoming runnable test functions. Direct `test` +attributes and nested `cfg_attr` test declarations retain distinct entry facts; +compiler execution separately establishes which configuration actually ran. +Missing context, entry classification or body evidence invalidates syntax facts. +Graph validation and the public report schema require the same protocol. + +Native Rust lint rejects a declared built-in test whose body contains no +statements other than comments, attributes and empty semicolons. The finding uses +the existing `quality.testEvidence` rule and the original function location. +Ignored or conditionally enabled tests cannot conceal empty bodies. Ordinary +functions, helpers and closures are not test entries merely because their file +or enclosing scope contains tests. This is the same high-confidence vacuity +boundary as Go's empty-test checks, not proof of assertion strength, nontrivial +helper behavior, custom framework expansion or runtime execution. + Comment policy uses parsed line/block comments and value-bearing doc attributes, not text searches. Metadata such as `doc(hidden)` is not prose. Inline `allow`, `expect` and `rustfmt::skip`, including nested `cfg_attr` forms, cannot bypass diff --git a/docs/design/rust-tests.md b/docs/design/rust-tests.md index 4561efa6..475abd3a 100644 --- a/docs/design/rust-tests.md +++ b/docs/design/rust-tests.md @@ -76,6 +76,16 @@ and [rustdoc execution](https://github.com/rust-lang/rust/blob/8bab26f4f68e0e26f A successful build, benchmark invocation or live app probe alone is not test evidence. +## Source evidence + +Native source checks separately reject empty built-in Rust test bodies, including +ignored and conditionally declared tests. Parsed entry attributes distinguish +test functions from helpers and closures in a `cfg(test)` scope. Production code +co-located with tests retains production complexity limits; a conditional test +attribute alone cannot relax them. The [source-facts contract](cargo-facts.md#parsed-source-facts) +owns these checks. A nonempty body is not proof of meaningful assertions or a +substitute for the nonempty structured execution evidence above. + ## Suite configuration A module suite uses ordinary ownership and profile declarations, for example: diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 5bd1ded1..f089a903 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -457,6 +457,13 @@ dependency change. Actual workspace-lock containment before native test compilation remains a separate test-runner milestone, including its command-plan and reusable-view boundaries. +Native test-source checks now reject empty built-in Rust tests, including ignored +and conditional declarations, without classifying helpers or closures as test +entries. Conditional test attributes retain production complexity limits outside +test-only scopes. The version-2 syntax contract, graph validation and report schema +carry the corresponding context and entry evidence. Nonempty bodies alone still +do not establish assertion strength or custom test-framework coverage. + Native unit/integration and separate doctest suites now retain nonempty structured results through ordinary commands, gate artifacts, regression proofs and reusable receipts. The [Rust test contract](../design/rust-tests.md) documents selectors and boundaries. diff --git a/docs/policies/code-quality.md b/docs/policies/code-quality.md index c3d373eb..4551655f 100644 --- a/docs/policies/code-quality.md +++ b/docs/policies/code-quality.md @@ -865,8 +865,11 @@ formatted, linted, typechecked, and tested. ## Test source quality Ordinary code health rejects only high-confidence vacuity: obvious no-op or -pass-with-no-tests suite commands, empty Go test/subtest bodies, and Go tests -that unconditionally skip without other behavior. It intentionally does not +pass-with-no-tests suite commands, empty Go test/subtest bodies, empty built-in +Rust test bodies, and Go tests that unconditionally skip without other behavior. +Rust checks use parsed test attributes, including conditional declarations, rather +than treating every helper in a `cfg(test)` scope as a test. Comments and empty +semicolons do not make an empty test meaningful. These checks intentionally do not count assertions or infer meaning from test-framework keywords. Targets may address semantic weakness with an isolated supplemental mutation diff --git a/docs/policies/verification.md b/docs/policies/verification.md index 3fae3fd8..dc5a5d4c 100644 --- a/docs/policies/verification.md +++ b/docs/policies/verification.md @@ -749,8 +749,8 @@ merge workflow for a genuine merge or release candidate. invariants where input space is broad. The parser rejects obvious no-op and pass-with-no-tests commands, and ordinary -source checks reject empty or unconditional-skip Go tests. These intentionally -cheap checks are not semantic proof. Optional supplemental mutation testing can +source checks reject empty or unconditional-skip Go tests and empty built-in Rust +tests. These intentionally cheap checks are not semantic proof. Optional supplemental mutation testing can establish whether realistic production changes make the tests fail. See [Test Strength and Executable Specification](test-strength.md). diff --git a/internal/architecture/sourcegraph/facts.go b/internal/architecture/sourcegraph/facts.go index eb4f5fe8..c4bf256e 100644 --- a/internal/architecture/sourcegraph/facts.go +++ b/internal/architecture/sourcegraph/facts.go @@ -78,7 +78,7 @@ func validFactAnalyzer(analyzer, protocol string) bool { case "ruff": return protocol == "ruff-graph-facts/v2" case "rust-facts": - return protocol == "rust-source/v1" + return protocol == "rust-source/v2" default: return false } diff --git a/internal/architecture/sourcegraph/facts_test.go b/internal/architecture/sourcegraph/facts_test.go index a32aea55..f614bd6f 100644 --- a/internal/architecture/sourcegraph/facts_test.go +++ b/internal/architecture/sourcegraph/facts_test.go @@ -70,11 +70,12 @@ func TestRustGraphFactsCannotReplacePythonOrForeignProjectEvidence(t *testing.T) t.Parallel() node := Node{Path: "crate/src/lib.rs", Language: "rust", Root: "crate", Module: "app", Resolution: "file:crate/src/lib.rs"} input := graphTestFactInput("crate", node.Path) - input.Analyzer, input.Protocol, input.Project = "rust-facts", "rust-source/v1", "crate/Cargo.toml" + input.Analyzer, input.Protocol, input.Project = "rust-facts", "rust-source/v2", "crate/Cargo.toml" if graph, err := New([]Node{node}, nil, []FactInput{input}, nil); err != nil || Validate(graph) != nil { t.Fatalf("valid native Rust evidence failed: %+v, %v", graph, err) } for _, corrupt := range []func(*FactInput){ + func(input *FactInput) { input.Protocol = "rust-source/v1" }, func(input *FactInput) { input.Project = "crate/pyproject.toml" }, func(input *FactInput) { input.Protocol = "python-facts/v3" }, func(input *FactInput) { input.Paths = []string{"other/src/lib.rs"} }, diff --git a/internal/engine/report_model_test.go b/internal/engine/report_model_test.go index 0662051f..29d46cd4 100644 --- a/internal/engine/report_model_test.go +++ b/internal/engine/report_model_test.go @@ -276,7 +276,7 @@ func TestSourceGraphAndCycleEvidenceValidateAgainstPinnedReportSchema(t *testing func TestNativeGraphFactEvidenceValidatesAgainstPinnedReportSchema(t *testing.T) { for _, test := range []struct{ language, path, analyzer, protocol, manifest string }{ {"python", "app.py", "python-facts", "python-facts/v3", "pyproject.toml"}, - {"rust", "src/lib.rs", "rust-facts", "rust-source/v1", "Cargo.toml"}, + {"rust", "src/lib.rs", "rust-facts", "rust-source/v2", "Cargo.toml"}, } { t.Run(test.language, func(t *testing.T) { graph, err := sourcegraph.New([]sourcegraph.Node{{Path: test.path, Language: test.language, Root: ".", Module: "app", Resolution: "file:" + test.path}}, nil, diff --git a/internal/quality/rust_source.go b/internal/quality/rust_source.go index 36f23993..5195e674 100644 --- a/internal/quality/rust_source.go +++ b/internal/quality/rust_source.go @@ -22,6 +22,9 @@ func rustSourceFindings(ctx context.Context, repo repository.Repository, files [ findings = append(findings, rustCommentFinding(source.Path, comment)) } } + if repo.NativeAnalysis(source.Path, "lint") { + findings = append(findings, rustTestEvidenceFindings(source)...) + } findings = append(findings, rustAttributeFindings(repo, source.Path, source.Attributes)...) if repo.NativeAnalysis(source.Path, "complexity") { findings = append(findings, rustComplexityFindings(repo, source)...) @@ -85,7 +88,7 @@ func rustComplexityFindings(repo repository.Repository, source rustfacts.Source) findings := []policy.Finding{} for _, function := range source.Functions { limit := limits.Rust - if function.Test { + if function.Context == rustfacts.TestContext { limit = limits.RustTest } if function.Complexity < limit { @@ -100,6 +103,18 @@ func rustComplexityFindings(repo repository.Repository, source rustfacts.Source) return findings } +func rustTestEvidenceFindings(source rustfacts.Source) []policy.Finding { + findings := []policy.Finding{} + for _, function := range source.Functions { + if function.TestEntry == rustfacts.NotATest || function.Body != "empty" { + continue + } + subject := fmt.Sprintf("%s:%d:%d", function.Name, function.Line, function.Column) + findings = append(findings, rustLocatedFinding("quality.testEvidence", source.Path, subject, "test body is empty and provides no behavior evidence", function.SourceLocation)) + } + return findings +} + func rustLocatedFinding(check, path, subject, message string, location rustfacts.SourceLocation) policy.Finding { return policy.Finding{Check: check, Path: path, Subject: subject, Message: message, Line: location.Line, Column: location.Column, EndLine: location.EndLine, EndColumn: location.EndColumn} } diff --git a/internal/quality/rust_source_test.go b/internal/quality/rust_source_test.go index 6dd21825..20ca7e49 100644 --- a/internal/quality/rust_source_test.go +++ b/internal/quality/rust_source_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" "github.com/riteofstring/code-polishy/internal/runner" ) @@ -70,3 +71,89 @@ func TestRustSourcePolicyMissingParserDoesNotPassCommentCoverage(t *testing.T) { t.Fatalf("missing parser was accepted as comment coverage: %+v", findings) } } + +func TestRustSourcePolicyRejectsEmptyTestsWithoutClaimingHelpers(t *testing.T) { + t.Parallel() + for _, scenario := range []struct { + name string + source string + empty bool + }{ + {"empty test", "#[test]\nfn case() {}\n", true}, + {"comment only", "#[test]\nfn case() { /* no behavior */ }\n", true}, + {"empty statements", "#[test]\nfn case() { ; ; }\n", true}, + {"ignored empty test", "#[test]\n#[ignore]\nfn case() {}\n", true}, + {"raw test attribute", "#[r#test]\nfn case() {}\n", true}, + {"conditional test", "#[cfg_attr(test, test)]\nfn case() {}\n", true}, + {"nested conditional test", "#[cfg_attr(feature = \"checks\", cfg_attr(test, test))]\nfn case() {}\n", true}, + {"production function", "pub fn case() {}\n", false}, + {"test helper", "#[cfg(test)]\nmod checks { fn helper() {} }\n", false}, + {"attribute text", "pub const TEXT: &str = r#\"#[test] fn case() {}\"#;\n", false}, + {"actual behavior", "fn answer() -> u8 { 42 }\n#[test]\nfn case() { assert_eq!(answer(), 42); }\n", false}, + {"called helper", "fn answer() -> u8 { 42 }\nfn helper() { assert_eq!(answer(), 42); }\n#[test]\nfn case() { helper(); }\n", false}, + {"nested helper", "fn answer() -> u8 { 42 }\n#[test]\nfn case() { fn helper() { assert_eq!(answer(), 42); } helper(); }\n", false}, + {"test closure", "fn answer() -> u8 { 42 }\n#[test]\nfn case() { let helper = || {}; helper(); assert_eq!(answer(), 42); }\n", false}, + } { + t.Run(scenario.name, func(t *testing.T) { + repo := cargoQualityRepository(t) + allowed := true + repo.Config.Quality.AllowComments = &allowed + writeQualityFile(t, repo.Root, "src/lib.rs", scenario.source) + findings := rustSourceFindings(t.Context(), repo, []string{"src/lib.rs"}, runner.OSRunner{}) + if !scenario.empty { + if len(findings) != 0 { + t.Fatalf("source was mistaken for an empty test: %+v", findings) + } + return + } + if len(findings) != 1 || findings[0].Check != "quality.testEvidence" || findings[0].Path != "src/lib.rs" || !strings.HasPrefix(findings[0].Subject, "case:") || findings[0].Line < 2 || !strings.Contains(findings[0].Message, "empty") { + t.Fatalf("empty Rust test lacks original-location evidence: %+v", findings) + } + }) + } +} + +func TestRustConditionalTestsRetainProductionComplexityOutsideTestOnlyScopes(t *testing.T) { + t.Parallel() + for _, testOnly := range []bool{false, true} { + t.Run(map[bool]string{false: "production", true: "test only"}[testOnly], func(t *testing.T) { + repo := cargoQualityRepository(t) + repo.Config.Quality.Complexity.Rust = 3 + repo.Config.Quality.Complexity.RustTest = 5 + source := "#[cfg_attr(test, test)]\nfn conditionally_tested() { if true {} if true {} }\n" + if testOnly { + source = "#[cfg(test)]\nmod checks {\n" + source + "}\n" + } + writeQualityFile(t, repo.Root, "src/lib.rs", source) + findings := rustSourceFindings(t.Context(), repo, []string{"src/lib.rs"}, runner.OSRunner{}) + if testOnly { + if len(findings) != 0 { + t.Fatalf("test-only scope inherited production limits: %+v", findings) + } + return + } + if len(findings) != 1 || findings[0].Check != "quality.rustComplexity" || !strings.HasPrefix(findings[0].Subject, "conditionally_tested:") || findings[0].Fields["limit"] != "3" { + t.Fatalf("conditional test attribute relaxed production limits: %+v", findings) + } + }) + } +} + +func TestCargoCheckRejectsEmptyRustTestsAndAcceptsRepairedBehavior(t *testing.T) { + t.Parallel() + repo := cargoQualityRepository(t) + writeQualityFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\nedition='2024'\n") + writeQualityFile(t, repo.Root, "Cargo.lock", "version=4\n[[package]]\nname='app'\nversion='1.0.0'\n") + source := "pub fn answer() -> u8 {\n 42\n}\n\n#[test]\nfn answer_is_correct() {}\n" + writeQualityFile(t, repo.Root, "src/lib.rs", source) + selection := repository.Selection{Files: []string{"src/lib.rs"}} + findings := Check(t.Context(), repo, selection, runner.OSRunner{}, "check") + if len(findings) != 1 || findings[0].Check != "quality.testEvidence" || findings[0].Path != "src/lib.rs" || findings[0].Line != 6 { + t.Fatalf("ordinary Cargo checks accepted an empty compiled test: %+v", findings) + } + repaired := strings.Replace(source, "fn answer_is_correct() {}", "fn answer_is_correct() {\n assert_eq!(answer(), 42);\n}", 1) + writeQualityFile(t, repo.Root, "src/lib.rs", repaired) + if findings := Check(t.Context(), repo, selection, runner.OSRunner{}, "check"); len(findings) != 0 { + t.Fatalf("ordinary Cargo checks rejected repaired test behavior: %+v", findings) + } +} diff --git a/internal/rustfacts/source.go b/internal/rustfacts/source.go index f39339d9..abc5dfae 100644 --- a/internal/rustfacts/source.go +++ b/internal/rustfacts/source.go @@ -15,7 +15,7 @@ import ( "github.com/riteofstring/code-polishy/internal/runner" ) -const SourceProtocol = "rust-source/v1" +const SourceProtocol = "rust-source/v2" const sourceRequestPrefix = `{"protocol":"` + SourceProtocol + `","sources":[` diff --git a/internal/rustfacts/source_output.go b/internal/rustfacts/source_output.go index 66cf48b8..03cb47f4 100644 --- a/internal/rustfacts/source_output.go +++ b/internal/rustfacts/source_output.go @@ -32,13 +32,30 @@ type SourceAttribute struct { type SourceFunction struct { SourceLocation - Name string `json:"name"` - Test bool `json:"test"` - Parameters int `json:"parameters"` - Complexity int `json:"complexity"` - Depth int `json:"depth"` + Name string `json:"name"` + Context FunctionContext `json:"context"` + TestEntry TestEntryKind `json:"testEntry"` + Body string `json:"body"` + Parameters int `json:"parameters"` + Complexity int `json:"complexity"` + Depth int `json:"depth"` } +type FunctionContext string + +const ( + ProductionContext FunctionContext = "production" + TestContext FunctionContext = "test" +) + +type TestEntryKind string + +const ( + NotATest TestEntryKind = "none" + BuiltinTest TestEntryKind = "builtin" + ConditionalBuiltinTest TestEntryKind = "conditional-builtin" +) + type SourceMacro struct { SourceLocation Name string `json:"name"` @@ -122,9 +139,6 @@ func validateSourceInventory(inputs []SourceInput, sources []Source) error { } func validateSourceFacts(source Source, text string) error { - if source.Comments == nil || source.Attributes == nil || source.Functions == nil || source.Macros == nil || source.Graph == nil || source.Complete == (source.Error != "") { - return errors.New("rust source omitted complete facts or its coverage failure") - } locations, err := sourceFactLocations(source) if err != nil { return err @@ -145,10 +159,13 @@ func validateSourceFacts(source Source, text string) error { } func sourceFactLocations(source Source) ([]SourceLocation, error) { + if source.Comments == nil || source.Attributes == nil || source.Functions == nil || source.Macros == nil || source.Graph == nil || source.Complete == (source.Error != "") { + return nil, errors.New("rust source omitted complete facts or its coverage failure") + } locations := append([]SourceLocation{}, source.Comments...) for _, function := range source.Functions { - if function.Name == "" || function.Complexity < 1 || function.Parameters < 0 || function.Depth < 0 { - return nil, errors.New("rust function omitted its identity or structural measurements") + if err := validateSourceFunction(function); err != nil { + return nil, err } locations = append(locations, function.SourceLocation) } @@ -161,6 +178,24 @@ func sourceFactLocations(source Source) ([]SourceLocation, error) { return locations, nil } +func validateSourceFunction(function SourceFunction) error { + if function.Name == "" || function.Complexity < 1 || function.Parameters < 0 || function.Depth < 0 { + return errors.New("rust function omitted its identity or structural measurements") + } + if function.Context != ProductionContext && function.Context != TestContext { + return errors.New("rust function omitted its production or test context") + } + switch function.TestEntry { + case NotATest, BuiltinTest, ConditionalBuiltinTest: + default: + return errors.New("rust function omitted its test entry classification") + } + if function.Body != "empty" && function.Body != "nonempty" { + return errors.New("rust function omitted its body evidence") + } + return nil +} + func validateSourceAttributes(attributes []SourceAttribute, coordinates sourceCoordinates, depth int) error { if depth > 64 { return errors.New("rust attribute facts exceed 64 levels") diff --git a/internal/rustfacts/source_parser.py b/internal/rustfacts/source_parser.py index d7de78ef..1c661205 100644 --- a/internal/rustfacts/source_parser.py +++ b/internal/rustfacts/source_parser.py @@ -6,7 +6,7 @@ import tree_sitter_rust from tree_sitter import Language, Parser -PROTOCOL = "rust-source/v1" +PROTOCOL = "rust-source/v2" MAXIMUM_REQUEST = 8 * 1024 * 1024 MAXIMUM_NODES = 500_000 COMMENTS = {"line_comment", "block_comment"} @@ -217,10 +217,15 @@ def function_fact(node, source, test_context): name = node.child_by_field_name("name") parameters = node.child_by_field_name("parameters") attributes = item_attributes(node, source) + entry = ( + test_entry_attributes(attributes) if node.type == "function_item" else "none" + ) return { **location(node, source), "name": name.text.decode() if name is not None else "", - "test": test_context or any(value["name"] == "test" for value in attributes), + "context": "test" if test_context or entry == "builtin" else "production", + "testEntry": entry, + "body": function_body(node), "parameters": sum( child.type not in COMMENTS | {"attribute_item"} for child in parameters.named_children @@ -232,6 +237,36 @@ def function_fact(node, source, test_context): } +def test_entry_attributes(attributes): + if any(value["name"] == "test" for value in attributes): + return "builtin" + if any( + value["name"] == "cfg_attr" + and test_entry_attributes(value["children"]) != "none" + for value in attributes + ): + return "conditional-builtin" + return "none" + + +def function_body(node): + body = node.child_by_field_name("body") + if body is None: + raise ValueError("Rust function omitted its body") + if body.type != "block": + return "nonempty" + non_behavior = COMMENTS | { + "attribute_item", + "inner_attribute_item", + "empty_statement", + } + return ( + "nonempty" + if any(child.type not in non_behavior for child in body.named_children) + else "empty" + ) + + def decisions(node): if node.type in BRANCHES: return 1 @@ -673,7 +708,7 @@ def visit(self, node, function, test_context, depth, scope): body = node.child_by_field_name("body") function = function_fact(node, self.source, test_context) self.result["functions"].append(function) - test_context = function["test"] + test_context = function["context"] == "test" depth = 0 elif function is not None: function["complexity"] += decisions(node) diff --git a/internal/rustfacts/source_test.go b/internal/rustfacts/source_test.go index a6c69aa6..8a41023d 100644 --- a/internal/rustfacts/source_test.go +++ b/internal/rustfacts/source_test.go @@ -47,10 +47,11 @@ fn direct_test() { let check = || if true { 1 } else { 0 }; } for _, expected := range []struct { name string complexity, parameters int - test bool - }{{"choose", 3, 1, false}, {"", 3, 1, false}, {"test_it", 1, 0, true}, {"helper", 1, 0, true}, {"inner_helper", 1, 0, true}, {"direct_test", 1, 0, true}, {"", 2, 0, true}} { + context FunctionContext + entry TestEntryKind + }{{"choose", 3, 1, ProductionContext, NotATest}, {"", 3, 1, ProductionContext, NotATest}, {"test_it", 1, 0, TestContext, BuiltinTest}, {"helper", 1, 0, TestContext, NotATest}, {"inner_helper", 1, 0, TestContext, NotATest}, {"direct_test", 1, 0, TestContext, BuiltinTest}, {"", 2, 0, TestContext, NotATest}} { if !slices.ContainsFunc(source.Functions, func(function SourceFunction) bool { - return function.Name == expected.name && function.Complexity == expected.complexity && function.Parameters == expected.parameters && function.Test == expected.test + return function.Name == expected.name && function.Complexity == expected.complexity && function.Parameters == expected.parameters && function.Context == expected.context && function.TestEntry == expected.entry }) { t.Errorf("missing scoped function measurement %+v: %+v", expected, source.Functions) } @@ -114,11 +115,30 @@ func TestRustSourceFactsRejectFabricatedOrIncompleteResponses(t *testing.T) { t.Fatal(err) } for name, corrupt := range map[string]func(map[string]any){ + "old protocol": func(value map[string]any) { value["protocol"] = "rust-source/v1" }, "runtime": func(value map[string]any) { value["parserVersion"] = "0.0.0" }, "empty": func(value map[string]any) { value["sources"] = []any{} }, "foreign path": func(value map[string]any) { wireSource(value)["path"] = "outside.rs" }, "missing facts": func(value map[string]any) { delete(wireSource(value), "comments") }, "partial": func(value map[string]any) { wireSource(value)["complete"] = false }, + "missing test context": func(value map[string]any) { + delete(wireSource(value)["functions"].([]any)[0].(map[string]any), "context") + }, + "unknown test context": func(value map[string]any) { + wireSource(value)["functions"].([]any)[0].(map[string]any)["context"] = "unknown" + }, + "missing test entry": func(value map[string]any) { + delete(wireSource(value)["functions"].([]any)[0].(map[string]any), "testEntry") + }, + "unknown test entry": func(value map[string]any) { + wireSource(value)["functions"].([]any)[0].(map[string]any)["testEntry"] = "unknown" + }, + "missing body evidence": func(value map[string]any) { + delete(wireSource(value)["functions"].([]any)[0].(map[string]any), "body") + }, + "unknown body evidence": func(value map[string]any) { + wireSource(value)["functions"].([]any)[0].(map[string]any)["body"] = "unknown" + }, "coordinates": func(value map[string]any) { wireSource(value)["functions"].([]any)[0].(map[string]any)["column"] = 999 }, diff --git a/schema/code-polishy-report.schema.json b/schema/code-polishy-report.schema.json index f1cd65b3..83be2e37 100644 --- a/schema/code-polishy-report.schema.json +++ b/schema/code-polishy-report.schema.json @@ -1021,7 +1021,7 @@ { "properties": { "analyzer": { "const": "rust-facts" }, - "protocol": { "const": "rust-source/v1" }, + "protocol": { "const": "rust-source/v2" }, "provider": false } }, @@ -1051,7 +1051,7 @@ "enum": [ "python-facts/v3", "ruff-graph-facts/v2", - "rust-source/v1", + "rust-source/v2", "code-polishy-pack/v3" ] }, From a3b6f82be27b48a13dfb43e88397063c9eace531 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:14:58 +0000 Subject: [PATCH 32/49] Make Rust tools an authenticated optional installation Keep required Rust policy native while separating SDK and Nextest payloads from portable core archives. Validate explicit installation against the selected core inventory and block Rust execution with absent or altered tools. Preserve notices in the core and exercise recovery and rollback. Checkpoint verification: focused release, CLI, native Rust tests and installer contract pass. New-source quality findings are resolved. Locked verification remains blocked by unsupported semantic-helper coverage and expired existing dependency assessments; changed-scope suites are running. Semantic-helper admission/integration, workspace-test lock containment, complete native notices and all-host release evidence remain open. --- .code-polishy.json | 14 +- .github/workflows/release.yml | 9 +- cmd/code-polishy/help.go | 11 + cmd/code-polishy/help_test.go | 2 +- cmd/code-polishy/install_rust_meta.go | 35 ++++ cmd/code-polishy/install_rust_meta_test.go | 16 ++ cmd/code-polishy/main.go | 5 +- cmd/code-polishy/release_manifest_meta.go | 24 ++- docs/ai-adoption.md | 5 + docs/capabilities.json | 10 + docs/catalog.json | 8 + docs/design/installed-release-verification.md | 16 +- docs/design/optional-rust-tools.md | 37 ++++ docs/installation.md | 44 +++- docs/plans/code-polishy-rust-support-plan.md | 43 +++- docs/release-checklist.md | 7 +- internal/release/installed.go | 18 +- internal/release/publication.go | 2 +- internal/release/publication_test.go | 2 +- internal/release/rust_bundle.go | 169 ++++++++++++++++ internal/release/rust_bundle_install.go | 168 ++++++++++++++++ internal/release/rust_bundle_test.go | 188 ++++++++++++++++++ internal/release/rust_sbom.go | 2 +- internal/release/rust_sbom_test.go | 6 +- internal/rustfacts/execution.go | 4 + scripts/build-release.ps1 | 6 + scripts/install.sh | 3 + scripts/test-install.sh | 14 +- .../test-installed-release-python-adoption.sh | 2 +- scripts/test-installed-release-rust.sh | 23 ++- 30 files changed, 844 insertions(+), 49 deletions(-) create mode 100644 cmd/code-polishy/install_rust_meta.go create mode 100644 cmd/code-polishy/install_rust_meta_test.go create mode 100644 docs/design/optional-rust-tools.md create mode 100644 internal/release/rust_bundle.go create mode 100644 internal/release/rust_bundle_install.go create mode 100644 internal/release/rust_bundle_test.go diff --git a/.code-polishy.json b/.code-polishy.json index 2a5e8131..62c711d9 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -23,6 +23,18 @@ }, "documentation": { "design": [ + { + "path": "docs/design/optional-rust-tools.md", + "sourcePaths": [ + "internal/release/rust_bundle.go", + "internal/release/rust_bundle_test.go", + "internal/release/rust_bundle_install.go", + "internal/release/installed.go", + "internal/release/publication.go", + "internal/rustfacts/execution.go", + "cmd/code-polishy/install_rust_meta.go" + ] + }, { "path": "docs/design/javascript-lint-results.md", "sourcePaths": [ @@ -433,7 +445,7 @@ { "name": "rustfacts", "paths": ["internal/rustfacts/**"], - "dependsOn": ["policy", "repository", "runner"] + "dependsOn": ["policy", "repository", "runner", "release"] }, { "name": "runner", diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 286b6d99..17dc40df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -188,7 +188,8 @@ jobs: echo "The fresh installation did not report Code Polishy ${RELEASE_VERSION}." >&2 exit 1 fi - bash ./scripts/test-installed-release-rust.sh "${installed_root}" "${installation}/bin/code-polishy" + bash ./scripts/test-installed-release-rust.sh "${installed_root}" "${installation}/bin/code-polishy" \ + "${publication_dir}/code-polishy-${RELEASE_VERSION}-rust-${RELEASE_HOST}.zip" - name: Build and smoke-test native archive if: matrix.build == 'windows' @@ -238,7 +239,7 @@ jobs: } $bash = Join-Path $env:ProgramFiles 'Git\bin\bash.exe' if (-not (Test-Path -LiteralPath $bash -PathType Leaf)) { throw 'The native Rust fixture requires Git Bash.' } - & $bash ./scripts/test-installed-release-rust.sh $installedRoot (Join-Path $installation 'bin\code-polishy.exe') + & $bash ./scripts/test-installed-release-rust.sh $installedRoot (Join-Path $installation 'bin\code-polishy.exe') (Join-Path $publicationDir "code-polishy-$env:RELEASE_VERSION-rust-$env:RELEASE_HOST.zip") if ($LASTEXITCODE -ne 0) { throw 'The installed Rust fixture failed.' } - name: Retain native publication @@ -450,8 +451,8 @@ jobs: fi printf '%s %s\n' "${index_sha}" "$(basename "${index_path}")" >"${index_path}.sha256" asset_count="$(find "${release_root}" -maxdepth 1 -type f | wc -l | tr -d '[:space:]')" - if [[ "${asset_count}" -ne 27 ]]; then - echo "The complete publication must contain 27 assets, found ${asset_count}." >&2 + if [[ "${asset_count}" -ne 32 ]]; then + echo "The complete publication must contain 32 assets, found ${asset_count}." >&2 exit 1 fi printf 'engine=%s\n' "${engine}" >>"${GITHUB_OUTPUT}" diff --git a/cmd/code-polishy/help.go b/cmd/code-polishy/help.go index f76aa722..6b6b5157 100644 --- a/cmd/code-polishy/help.go +++ b/cmd/code-polishy/help.go @@ -144,6 +144,15 @@ var commandHelpPages = []commandHelpPage{ exits: []string{"0 plan recorded or upgrade applied", "2 invalid usage, stale evidence, unacknowledged findings, or operational failure"}, examples: []string{"code-polishy upgrade plan --index https://example.invalid/code-polishy-release-index.json --sha256 DIGEST", "code-polishy upgrade apply --plan .code-polishy-reports/capability-upgrades/DIGEST/plan.json"}, }, + { + name: "install-rust", + summary: "Install the selected release's authenticated optional Rust tools.", + syntax: []string{"code-polishy install-rust [--source ARCHIVE]"}, + selectors: []string{"--source ARCHIVE supplies a local companion ZIP; otherwise download the companion from the locked publication.", "The selected core release fixes the complete accepted tool inventory."}, + sideEffects: []string{"Stages and verifies the bundle, then installs or repairs the shared release's Rust tools. Required Rust checks remain mandatory when tools are absent."}, + exits: []string{"0 tools installed", "2 invalid usage, missing release authority, or installation failure"}, + examples: []string{"code-polishy install-rust", "code-polishy install-rust --source ./code-polishy-VERSION-rust-HOST.zip"}, + }, { name: "install-bundle", summary: "Install a verified native release archive.", @@ -166,6 +175,8 @@ var commandHelpPages = []commandHelpPage{ "code-polishy release-manifest satisfies-lock --root PATH", "code-polishy release-manifest materialize --source PATH --destination PATH", "code-polishy release-manifest archive --root PATH --output PATH", + "code-polishy release-manifest rust-archive --root PATH --output PATH", + "code-polishy release-manifest rust-notices --root PATH", "code-polishy release-manifest publish --archive PATH --destination PATH", "code-polishy release-manifest index --artifact-descriptor PATH... --output PATH", "code-polishy release-manifest oci-context --descriptor PATH --template PATH --destination PATH", diff --git a/cmd/code-polishy/help_test.go b/cmd/code-polishy/help_test.go index c92910fa..7d0dc5a7 100644 --- a/cmd/code-polishy/help_test.go +++ b/cmd/code-polishy/help_test.go @@ -16,7 +16,7 @@ import ( func TestEveryCommandHelpPageHasCompleteContract(t *testing.T) { publicCommands := []string{ - "version", "docs", "pack", "agents", "lock", "upgrade", "release-manifest", "change-boundary", "task-session", + "version", "docs", "install-rust", "pack", "agents", "lock", "upgrade", "release-manifest", "change-boundary", "task-session", "check", "gate", "checkpoint-gate", "merge-gate", "behavior-review", "regression-proof", "test", "test-plan", "test-levels", "test-receipts", "verify", "architecture", "supply-chain", "dependency-review", "artifact-security", "doctor", "design-context", "format", "fix", "list-files", diff --git a/cmd/code-polishy/install_rust_meta.go b/cmd/code-polishy/install_rust_meta.go new file mode 100644 index 00000000..be14fc23 --- /dev/null +++ b/cmd/code-polishy/install_rust_meta.go @@ -0,0 +1,35 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/riteofstring/code-polishy/internal/release" +) + +func handleInstallRustMeta(invocation invocation) int { + flags := flag.NewFlagSet("install-rust", flag.ContinueOnError) + flags.SetOutput(os.Stderr) + source := flags.String("source", "", "local Rust companion archive") + if err := flags.Parse(invocation.arguments); err != nil { + return commandUsageError("install-rust", err.Error()) + } + if flags.NArg() != 0 || invocation.configPath != "" { + return commandUsageError("install-rust", "install-rust accepts only --source PATH") + } + if *source != "" { + absolute, err := filepath.Abs(*source) + if err != nil { + return operationalError(err) + } + *source = absolute + } + if err := release.InstallRustBundle(context.Background(), invocation.repoRoot, invocation.policyRoot, *source); err != nil { + return operationalError(err) + } + fmt.Println("Installed the selected release's verified Rust tools.") + return 0 +} diff --git a/cmd/code-polishy/install_rust_meta_test.go b/cmd/code-polishy/install_rust_meta_test.go new file mode 100644 index 00000000..e18114b5 --- /dev/null +++ b/cmd/code-polishy/install_rust_meta_test.go @@ -0,0 +1,16 @@ +package main + +import ( + "strings" + "testing" +) + +func TestRustInstallerRequiresInstalledReleaseAuthority(t *testing.T) { + root := t.TempDir() + status, stdout, stderr := captureRunOutput(t, []string{ + "--repo-root", root, "--policy-root", root, "install-rust", + }) + if status != 2 || stdout != "" || !strings.Contains(stderr, "has no release-manifest.json") { + t.Fatalf("Rust setup did not enforce installed authority: status=%d stdout=%q stderr=%q", status, stdout, stderr) + } +} diff --git a/cmd/code-polishy/main.go b/cmd/code-polishy/main.go index 8f362f60..96fd1ee3 100644 --- a/cmd/code-polishy/main.go +++ b/cmd/code-polishy/main.go @@ -25,11 +25,12 @@ Commands: capabilities [--query TEXT] [--format human|json] task-start (--files PATH... | --module NAME...) [--intent-file PATH|-] [--feature NAME] [--situation NAME] [--format human|json] docs + install-rust [--source ARCHIVE] pack agents lock --index URL --sha256 DIGEST upgrade [options] - release-manifest [options] + release-manifest [options] change-boundary --base COMMIT --module NAME... [--allow-path PATH...] [--allow-new-path PATH...] task-session --module NAME... [options] -- COMMAND [ARG...] check [--git-changes|--staged|--all|--files PATH...|--module NAME...|--name NAME...] @@ -196,6 +197,8 @@ func handleCoreMetaCommand(invocation invocation) (int, bool) { case "install-bundle": return handleInstallBundleMeta(invocation), true + case "install-rust": + return handleInstallRustMeta(invocation), true case "release-manifest": return handleReleaseManifestMeta(invocation), true case "change-boundary": diff --git a/cmd/code-polishy/release_manifest_meta.go b/cmd/code-polishy/release_manifest_meta.go index 32b6d862..11df83d2 100644 --- a/cmd/code-polishy/release_manifest_meta.go +++ b/cmd/code-polishy/release_manifest_meta.go @@ -65,6 +65,16 @@ func handleReleasePublicationMeta(mode string, options releaseManifestOptions) i switch mode { case "archive": return archiveReleaseManifest(options) + case "rust-archive": + return archiveRustReleaseManifest(options) + case "rust-notices": + if options.root == "" || !releaseManifestOptionsEmpty(options, "root") { + return commandUsageError("release-manifest", "release-manifest rust-notices requires --root PATH") + } + if err := release.MaterializeRustNotices(options.root); err != nil { + return operationalError(err) + } + return 0 case "publish": return publishReleaseManifest(options) case "index": @@ -72,8 +82,20 @@ func handleReleasePublicationMeta(mode string, options releaseManifestOptions) i case "oci-context": return ociContextReleaseManifest(options) default: - return commandUsageError("release-manifest", "release-manifest requires write, verify, satisfies-lock, materialize, archive, publish, index, or oci-context") + return commandUsageError("release-manifest", "release-manifest requires write, verify, satisfies-lock, materialize, archive, rust-archive, rust-notices, publish, index, or oci-context") + } +} + +func archiveRustReleaseManifest(options releaseManifestOptions) int { + if options.root == "" || options.output == "" || !releaseManifestOptionsEmpty(options, "root", "output") { + return commandUsageError("release-manifest", "release-manifest rust-archive requires --root PATH and --output PATH") } + digest, err := release.WriteRustArchive(options.root, options.output) + if err != nil { + return operationalError(err) + } + fmt.Println(digest) + return 0 } func releaseManifestOptionsEmpty(options releaseManifestOptions, allowed ...string) bool { diff --git a/docs/ai-adoption.md b/docs/ai-adoption.md index c9f4738e..e725fcc8 100644 --- a/docs/ai-adoption.md +++ b/docs/ai-adoption.md @@ -382,6 +382,11 @@ ask a target dependency to satisfy shared coverage. A missing or corrupted shared tool means the locked release must be installed or repaired; it is not a target dependency gap. +For governed Rust inputs, run `./code-polishyw install-rust` to install the +exact optional tools selected by the existing core release. An absent installation +blocks required Rust checks; do not add exclusions, replacements, or relaxed +policy to avoid setup. + For Rust unit and integration suites, use the native `rust` selector described in [Rust test evidence](design/rust-tests.md). Preserve Cargo workspace default members and features unless the suite deliberately selects alternatives. Prepare diff --git a/docs/capabilities.json b/docs/capabilities.json index 68297699..ecd3c008 100644 --- a/docs/capabilities.json +++ b/docs/capabilities.json @@ -171,6 +171,16 @@ "enforcement": ["explicit"], "workflows": ["docs/agent-workflows.md"] }, + { + "name": "install-rust", + "kind": "command", + "description": "Install the exact optional Rust tools authenticated by the selected core release.", + "aliases": ["Rust tool setup"], + "paths": [], + "modules": [], + "enforcement": ["explicit"], + "workflows": ["docs/installation.md"] + }, { "name": "lock", "kind": "command", diff --git a/docs/catalog.json b/docs/catalog.json index 05607817..87a4a3f5 100644 --- a/docs/catalog.json +++ b/docs/catalog.json @@ -366,6 +366,14 @@ "aliases": [], "public": true }, + { + "id": "optional-rust-tools", + "path": "docs/design/optional-rust-tools.md", + "title": "Optional Rust Tool Installation", + "summary": "Keep the Rust SDK separately installed while preserving required analysis and exact release authority.", + "aliases": ["install-rust"], + "public": true + }, { "id": "rust-tests", "path": "docs/design/rust-tests.md", diff --git a/docs/design/installed-release-verification.md b/docs/design/installed-release-verification.md index e201d572..a7330ce2 100644 --- a/docs/design/installed-release-verification.md +++ b/docs/design/installed-release-verification.md @@ -74,10 +74,11 @@ installed wheel metadata and licenses. Release SBOM composition requires both parser distributions to match the manifest; missing, duplicate, wrong-version or undeclared Python distributions cannot be silently left out of publication. -Rust is a built-in release capability. Each native archive carries the matching -Rust SDK (Cargo, rustc, rustdoc, rustfmt, Clippy, and host standard library) and -the pinned Nextest executable. Builders probe these tools before sealing the -archive; installed commands never acquire a toolchain from rustup or the target +Rust is a built-in release capability. Each native core archive authenticates a separately +installed Rust SDK (Cargo, rustc, rustdoc, rustfmt, Clippy, and host standard +library) and the pinned Nextest executable. Its companion archive carries those +tools; the default core archive omits them. Builders probe these tools before +sealing their inventory; installed commands never acquire a toolchain from rustup or the target project. Manifest version 7 binds the Rust and Nextest pins into the cross-host release identity. The launcher's existing historical-manifest reader continues to interpret versions 2 through 6 with their original tool fields and identity @@ -120,3 +121,10 @@ the selected manifest, uses the installed Python and launcher, and keeps target state in a temporary directory. The same scenario runs on all five native hosts; it does not rebuild the archive or repeat the ordinary source gate. The fixture's URLs cannot acquire a release and never replace a consuming repository's lock. + +The [optional tools contract](optional-rust-tools.md) preserves required Rust +coverage across absent, damaged and repaired installations. The native archive +fixture first verifies rejection without tools, installs the companion against +the core's authority, then exercises Rust behavior and removal/recovery. Small +SDK notice copies remain in the core under `tools/rust-notices/HOST` so release +publication retains their evidence without requiring the optional SDK payload. diff --git a/docs/design/optional-rust-tools.md b/docs/design/optional-rust-tools.md new file mode 100644 index 00000000..29e3209a --- /dev/null +++ b/docs/design/optional-rust-tools.md @@ -0,0 +1,37 @@ +# Optional Rust Tool Installation + +Rust analysis and its policy remain native engine capabilities. The Rust SDK +and native test runner are distributed separately from the default installation +because repositories without Rust do not need their large executable payloads. +The existing provider protocol does not yet express Cargo workspace discovery +and native test evidence completely. A tool distribution boundary meets the +installation requirement without introducing an incomplete Rust provider. + +Installation optionality never changes required coverage. Governed Rust source, +Cargo inputs, and native Rust test declarations retain their ordinary checks. +An absent or damaged tool installation blocks the relevant operation and names +the setup command. Configuration cannot disable Rust recognition, select an +ambient compiler, change the approved tool versions, or substitute an arbitrary +successful command for native evidence. Unsupported semantics remain failures. + +The authenticated core release owns the exact Rust tool inventory, versions, +host, and file identities. Its default archive omits the corresponding tool +payload; a companion archive carries it. Installing that archive validates its +complete contents against the already selected core release. The archive cannot +declare its own authority, change the repository lock, or supply core files. +Transport location does not replace the release-owned file identities. + +Ordinary checks never download or repair tools. Explicit setup stages and +verifies the companion before replacing installed tools, preserving a working +installation on failure. Installed Rust execution requires complete tool +integrity, including libraries and supporting files, before any compiler, +formatter, test runner, or semantic helper runs. Removing tools cannot turn a +required check into a pass. An incomplete install can still enter setup to +repair itself without weakening subsequent analysis. + +Release verification covers a clean installation without Rust, required Rust +checks before installation, installation of the exact companion, real Rust +checks afterward, rejected foreign or modified payloads, interruption recovery, +and removal followed by a blocking required check. All supported hosts require +native execution evidence. Rust tool notices and dependency inventories retain +their publication obligations even though the payload is separately installed. diff --git a/docs/installation.md b/docs/installation.md index beeaea70..ef7bbe8f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -69,6 +69,37 @@ release through the stable launcher and performs no clone, download, build, fallback selection, or `PATH` mutation. The wrappers contain only bootstrap and dispatch logic; the complete release remains in the shared user store. +## Optional Rust tools + +The default portable installation contains Rust policy and its small syntax +parser, but omits the Rust SDK and Nextest binaries. A repository with governed +Rust inputs still requires the corresponding checks. Missing tools produce a +blocking setup instruction; optional installation cannot disable analysis. + +Install the companion selected by the repository's existing release lock: + +```sh +./code-polishyw install-rust +``` + +For an offline installation, use the matching published companion archive: + +```sh +./code-polishyw install-rust --source /path/to/code-polishy-VERSION-rust-HOST.zip +``` + +Setup verifies every companion file against the authenticated core release's +inventory. The companion cannot choose another version, alter policy, or change +the repository lock. Ordinary checks never download tools or use ambient rustup +installations. Modified, incomplete, or removed tools block Rust operations; +rerunning `install-rust` repairs the optional installation. Repositories sharing an exact +core installation also share its Rust tools. + +Source builds prepare the complete tool inventory for both archives. The native +source installer retains that developer installation; the portable release +archive is the default distribution without Rust executables. Both archives +retain their notice and dependency-inventory obligations. + ## Native source installers Source acquisition stays outside the installers. They never select a version, @@ -251,9 +282,9 @@ One release identity has one self-contained policy root per supported host: - the `code-polishy` binary built from the reviewed commit with the pinned Go toolchain; - the sealed Node runtime and JavaScript tool bundle; -- Rust `1.97.1` with Cargo, rustdoc, rustfmt, Clippy and the host standard library; - Nextest `0.9.138`; and Tree-sitter's pinned Rust syntax libraries in the carried - Python runtime, with the policy-owned formatter and Clippy settings; +- Tree-sitter's pinned Rust syntax libraries and policy-owned formatter/Clippy + settings; the optional companion supplies Rust `1.97.1`, Cargo, rustdoc, + rustfmt, Clippy, the host standard library and Nextest `0.9.138`; - every other pinned tool the engine runs: the Go toolchain, ShellCheck, staticcheck, govulncheck, OSV-Scanner, Ruff, PyPA `packaging` `26.3`, Vulture `2.16`, `ty`, and the carried CPython `3.12.13+20260728` runtime from @@ -349,14 +380,15 @@ installed release or a modified catalog cannot supply its capabilities. Older releases with no authenticated catalog report discovery evidence unavailable. Manifest version 7 adds `tools.rust` and `tools.cargo-nextest`. Each native release -carries the pinned Rust SDK, host standard library, and Nextest runner; target -repositories do not install rustup or a separate language pack. Rust parser +authenticates the pinned Rust SDK, host standard library, and Nextest runner +in its optional companion; target repositories install them with `install-rust`. Rust parser libraries reside in the carried Python runtime with their exact dependency manifest, lock, and distribution metadata. Changing either executable pin changes the cross-host release identity. `code-polishy release-manifest verify --root ` recomputes the -installed entry evidence. A release that was truncated, changed after +core entry evidence. Optional Rust tool integrity is required before Rust +execution. A core release that was truncated, changed after installation, or copied from another host fails verification and is reinstalled rather than executed. The launcher makes the same native judgment before every run on Linux, macOS, and Windows. diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index f089a903..06a04340 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -1,8 +1,9 @@ # First-class Rust support in Code Polishy Date: 2026-09-24. Status: beta implementation in progress. -Implementation branch: `beta-rust-support`, based on `main` at -`d6d14503ec561e54e61c36beb218f7ce684f75fe`, locked to Code Polishy `0.27.11`. +The original implementation branch was `beta-rust-support`, based on `main` at +`d6d14503ec561e54e61c36beb218f7ce684f75fe`. Integration continues on +`feature/rust-optional-install` with mainline safeguards and the `0.28.3` lock. Imported from Masterbrainer's `docs/plans/code-polishy-rust-support-plan.md` at planning commit `be593fb`. This copy owns the Code Polishy implementation. @@ -11,11 +12,13 @@ at planning commit `be593fb`. This copy owns the Code Polishy implementation. Rust receives the same product-level integration as Go and Python: automatic discovery, policy-owned tooling, source facts, architecture, quality, dependency governance, tests, reports, receipts, documentation, and verified releases. -Target repositories must receive complete Rust support through normal Code -Polishy setup and upgrade, without a separate pack-installation procedure or a -hand-maintained Cargo policy stack. Automatic installation of an authenticated, -release-owned Rust provider is compatible with this outcome; an optional pack -or a few Cargo commands alone are not. +Rust integration remains in the core engine. The large Rust SDK and native test +runner are an explicit, separately installed official bundle authenticated by +the exact core release. Repositories without Rust avoid that payload. Missing +or altered tools block required Rust operations; installation optionality cannot +disable source recognition or required policy coverage. The +[optional Rust tools contract](../design/optional-rust-tools.md) owns this +boundary. Full language-pack extraction is not required for this delivery. The Go policy engine remains the decision owner. A pinned Rust helper may supply compiler/parser facts through a bounded child-process protocol. Do not embed @@ -38,9 +41,27 @@ the capabilities already delivered through equivalent Go execution. ## Current baseline and architectural decisions -The implementation base includes the accepted changes through `0.27.11`. -The earlier handoff inspected a divergent `0.25.0` checkout. Its unpublished -behavior-review changes are not part of this base. +The original implementation base included the accepted changes through `0.27.11`. +The integration branch now includes mainline through `0.28.3`, preserving its +dependency-input and Windows filesystem safeguards. + +Portable releases separate the SDK and Nextest into an authenticated companion. +The core inventory remains authoritative and the native policy remains required. +Local contract tests cover absent tools, exact installation, foreign payload +rejection, altered and additional files, symlink rejection, cancellation, +partial-publication rollback, and repair. The Linux x64 tool payload is about +686 MB before compression. Release jobs exercise installation and removal of the +companion through the public CLI on every supported host; those jobs have not yet +supplied evidence for this candidate. + +Full Rust delivery remains incomplete. The locked `0.28.3` engine cannot govern +the candidate semantic helper's Rust source and Cargo dependencies. Its admission, +compilation and semantic integration remain required; disabling those checks is +not an acceptable bootstrap. Actual workspace-lock containment before native +tests, complete native dependency notices/inventory, and all-host execution also +remain open. The current dependency-review run additionally reports expired +JavaScript vulnerability assessments. None of these failures is waived by the +optional installation boundary. The current [first-party language-pack plan](installable-first-party-language-packs.md) and [universal discovery plan](universal-language-pack-capabilities.md) are @@ -416,7 +437,7 @@ the entire capability matrix through the public CLI and installed artifact. Publish an exact version/digest through separately authorized publication. Masterbrainer then adopts that release through its dependency workflow. -No target should need a Rust language-pack installation, hand-maintained import +Targets use the official tool bundle without hand-maintained import inventory, copied tool bootstrap, or duplicate lint/security gate to satisfy the baseline. Required capabilities with known gaps cannot be described as complete. Masterbrainer retains ownership of GPU baselines and real application workflows. diff --git a/docs/release-checklist.md b/docs/release-checklist.md index eb787e94..016d2778 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -74,16 +74,17 @@ manual dispatch authorizes the same operation for an existing tag. CI. Each host only verifies the generated manifest, installs its archive into a fresh prefix through the recorded SHA-256, and checks the installed version. It then runs the dependency-free installed Rust fixture against that - same native installation, covering formatter/compiler/parser execution, + same native installation, proving required Rust checks fail before companion + installation, then covering formatter/compiler/parser execution, structured test outcomes, zero-test rejection and recovery. The retained - publication contains the archive, internal manifest, + publication contains the core and Rust companion archives, internal manifest, CycloneDX SBOM, deterministic in-toto/SLSA metadata, and release descriptor. 7. After every host succeeds, the publish job restores the five publication directories and reuses the verified Linux x64 archive's engine to combine their descriptors into `code-polishy-release-index.json`. It records the canonical index SHA-256, creates an index checksum sidecar, and requires the - complete 27-file publication. + complete 32-file publication. 8. The same job gives Buildx an exact BuildKit image and the retained Linux x64 archive. It does not rebuild source, rerun the native archive contract, or diff --git a/internal/release/installed.go b/internal/release/installed.go index c44db77f..f3dbb5ff 100644 --- a/internal/release/installed.go +++ b/internal/release/installed.go @@ -19,14 +19,20 @@ func (manifest Manifest) Verify(releaseDir string) error { if err != nil { return fmt.Errorf("read the installed Code Polishy release: %w", err) } - if found.IsDir() { - return nil - } relative, err := filepath.Rel(releaseDir, installed) if err != nil { return fmt.Errorf("read the installed Code Polishy release: %w", err) } relative = filepath.ToSlash(relative) + if manifest.ManifestVersion >= 7 && (relative == ".tools/rust/"+manifest.Host || relative == rustNextestPath(manifest.Host)) { + if found.IsDir() { + return fs.SkipDir + } + return nil + } + if found.IsDir() { + return nil + } if relative == ManifestFilename { return nil @@ -36,6 +42,9 @@ func (manifest Manifest) Verify(releaseDir string) error { return fmt.Errorf("%s is installed under a Code Polishy release that does not record it", installed) } delete(pending, relative) + if rustToolEntry(manifest, relative) { + return nil + } return verifyEntry(installed, found, recorded) } if err := filepath.WalkDir(releaseDir, walk); err != nil { @@ -43,6 +52,9 @@ func (manifest Manifest) Verify(releaseDir string) error { } for _, entry := range manifest.Entries { + if rustToolEntry(manifest, entry.Path) { + continue + } if _, missing := pending[entry.Path]; missing { return fmt.Errorf( "the installed Code Polishy release is missing %s", diff --git a/internal/release/publication.go b/internal/release/publication.go index 49c8a45e..267ba523 100644 --- a/internal/release/publication.go +++ b/internal/release/publication.go @@ -75,7 +75,7 @@ func WriteArchive(releaseRoot, output string) (string, error) { if err != nil { return "", err } - return writeArchiveAtomically(root, destination, manifest) + return writeArchiveAtomically(root, destination, coreArchiveManifest(manifest)) } func verifiedReleaseRoot(releaseRoot string) (string, Manifest, error) { diff --git a/internal/release/publication_test.go b/internal/release/publication_test.go index 962b4045..90a4c184 100644 --- a/internal/release/publication_test.go +++ b/internal/release/publication_test.go @@ -324,7 +324,7 @@ func foreignRelease(t *testing.T, host string) (string, Manifest) { } } if host != manifest.Host { - before, after := ".tools/rust/"+manifest.Host, ".tools/rust/"+host + before, after := "tools/rust-notices/"+manifest.Host, "tools/rust-notices/"+host if err := os.Rename(filepath.Join(root, filepath.FromSlash(before)), filepath.Join(root, filepath.FromSlash(after))); err != nil { t.Fatal(err) } diff --git a/internal/release/rust_bundle.go b/internal/release/rust_bundle.go new file mode 100644 index 00000000..e6e26bb0 --- /dev/null +++ b/internal/release/rust_bundle.go @@ -0,0 +1,169 @@ +package release + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "slices" + "strings" +) + +func rustToolEntry(manifest Manifest, relative string) bool { + if manifest.ManifestVersion < 7 { + return false + } + return strings.HasPrefix(relative, ".tools/rust/"+manifest.Host+"/") || relative == rustNextestPath(manifest.Host) +} + +func rustNextestPath(host string) string { + name := ".tools/bin/cargo-nextest" + if host == "windows-x64" { + name += ".exe" + } + return name +} + +func rustToolManifest(manifest Manifest) (Manifest, error) { + entries := []Entry{} + for _, entry := range manifest.Entries { + if rustToolEntry(manifest, entry.Path) { + entries = append(entries, entry) + } + } + if len(entries) == 0 { + return Manifest{}, errors.New("the selected release has no authenticated Rust tool inventory") + } + for _, name := range []string{"rustc", "rustdoc", "cargo", "rustfmt", "cargo-fmt", "clippy-driver", "cargo-clippy"} { + suffix := "" + if manifest.Host == "windows-x64" { + suffix = ".exe" + } + path := ".tools/rust/" + manifest.Host + "/bin/" + name + suffix + if !slices.ContainsFunc(entries, func(entry Entry) bool { return entry.Path == path && entry.SHA256 != "" }) { + return Manifest{}, fmt.Errorf("the selected release has no authenticated %s", path) + } + } + if !slices.ContainsFunc(entries, func(entry Entry) bool { return entry.Path == rustNextestPath(manifest.Host) && entry.SHA256 != "" }) { + return Manifest{}, errors.New("the selected release has no authenticated Nextest executable") + } + return Manifest{Entries: entries}, nil +} + +func VerifyRustTools(root string) error { + manifest, present, err := ReadManifest(root) + if err != nil { + return err + } + if !present { + return nil + } + tools, err := rustToolManifest(manifest) + if err != nil { + return err + } + parents := map[string]bool{} + for _, entry := range tools.Entries { + if err := rustToolParents(root, entry.Path, parents); err != nil { + return err + } + installed := filepath.Join(root, filepath.FromSlash(entry.Path)) + info, err := os.Lstat(installed) + if err != nil { + return fmt.Errorf("rust tools are incomplete; run code-polishy install-rust: %w", err) + } + if err := verifyEntry(installed, fs.FileInfoToDirEntry(info), entry); err != nil { + return fmt.Errorf("rust tools do not match the selected release; run code-polishy install-rust: %w", err) + } + } + return verifyRustInventory(root, manifest, tools.Entries) +} + +func rustToolParents(root, relative string, checked map[string]bool) error { + for parent := filepath.Dir(relative); parent != "." && !checked[parent]; parent = filepath.Dir(parent) { + info, err := os.Lstat(filepath.Join(root, parent)) + if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("rust tool directory %s must be a contained regular directory; run code-polishy install-rust", parent) + } + checked[parent] = true + } + return nil +} + +func verifyRustInventory(root string, manifest Manifest, entries []Entry) error { + known := map[string]bool{} + for _, entry := range entries { + known[entry.Path] = true + } + return filepath.WalkDir(filepath.Join(root, ".tools", "rust", manifest.Host), func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + if entry.IsDir() { + return nil + } + relative, err := filepath.Rel(root, path) + if err != nil || !known[filepath.ToSlash(relative)] { + return fmt.Errorf("rust tool inventory contains an unrecorded file: %s", path) + } + return nil + }) +} + +func WriteRustArchive(releaseRoot, output string) (string, error) { + root, manifest, err := verifiedReleaseRoot(releaseRoot) + if err != nil { + return "", err + } + if err := VerifyRustTools(root); err != nil { + return "", err + } + tools, err := rustToolManifest(manifest) + if err != nil { + return "", err + } + destination, err := newOutputPath(output, root) + if err != nil { + return "", err + } + temporary, err := os.CreateTemp(filepath.Dir(destination), ".code-polishy-rust-*.zip") + if err != nil { + return "", err + } + defer os.Remove(temporary.Name()) + if err := writeReleaseZip(temporary, root, tools); err != nil { + return "", err + } + digest, err := fileSHA256(temporary.Name()) + if err != nil { + return "", err + } + return digest, os.Rename(temporary.Name(), destination) +} + +func coreArchiveManifest(manifest Manifest) Manifest { + manifest.Entries = slices.DeleteFunc(slices.Clone(manifest.Entries), func(entry Entry) bool { + return rustToolEntry(manifest, entry.Path) + }) + return manifest +} + +func MaterializeRustNotices(root string) error { + host, err := Host() + if err != nil { + return err + } + for _, destination := range rustReleaseNoticePaths(host, "rust") { + relative := strings.TrimPrefix(destination, "tools/rust-notices/"+host+"/") + source := filepath.Join(root, ".tools", "rust", host, "share", filepath.FromSlash(relative)) + target := filepath.Join(root, filepath.FromSlash(destination)) + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return err + } + if err := copyReleaseFile(source, target, 0o644); err != nil { + return err + } + } + return nil +} diff --git a/internal/release/rust_bundle_install.go b/internal/release/rust_bundle_install.go new file mode 100644 index 00000000..194c4056 --- /dev/null +++ b/internal/release/rust_bundle_install.go @@ -0,0 +1,168 @@ +package release + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" +) + +func RustArchiveName(manifest Manifest) string { + return "code-polishy-" + manifest.CodePolishyVersion + "-rust-" + manifest.Host + ".zip" +} + +func InstallRustBundle(ctx context.Context, repoRoot, policyRoot, source string) error { + manifest, err := VerifyLockedRelease(repoRoot, policyRoot) + if err != nil { + return err + } + tools, err := rustToolManifest(manifest) + if err != nil { + return err + } + root, err := filepath.EvalSymlinks(policyRoot) + if err != nil { + return err + } + staging, err := os.MkdirTemp(filepath.Dir(root), ".code-polishy-rust-install-") + if err != nil { + return err + } + keepStaging := false + defer func() { + if !keepStaging { + os.RemoveAll(staging) + } + }() + if err := stageRustBundle(ctx, repoRoot, manifest, tools, source, staging); err != nil { + return err + } + if err := publishRustTools(root, staging, manifest); err != nil { + keepStaging = true + return fmt.Errorf("rust installation failed; recovery files retained at %s: %w", staging, err) + } + return nil +} + +func stageRustBundle(ctx context.Context, repoRoot string, manifest, tools Manifest, source, staging string) error { + archive, err := acquireRustBundle(ctx, repoRoot, manifest, source, staging) + if err != nil { + return err + } + extracted := filepath.Join(staging, "payload") + if err := os.Mkdir(extracted, 0o755); err != nil { + return err + } + if err := extractReleaseZip(archive, extracted); err != nil { + return err + } + if err := tools.Verify(extracted); err != nil { + return fmt.Errorf("rust bundle does not match the selected core release: %w", err) + } + return ctx.Err() +} + +func acquireRustBundle(ctx context.Context, repoRoot string, manifest Manifest, source, staging string) (string, error) { + var reader io.ReadCloser + var err error + if source != "" { + reader, err = openLocalBundle(source) + } else { + reader, err = downloadRustBundle(ctx, repoRoot, manifest) + } + if err != nil { + return "", err + } + defer reader.Close() + archive := filepath.Join(staging, "rust.zip") + output, err := os.OpenFile(archive, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) + if err != nil { + return "", err + } + written, copyErr := io.Copy(output, io.LimitReader(reader, maximumReleaseBundleBytes+1)) + if err := errors.Join(copyErr, output.Close()); err != nil { + return "", err + } + if written > maximumReleaseBundleBytes { + return "", errors.New("rust bundle exceeds its byte bound") + } + return archive, ctx.Err() +} + +func downloadRustBundle(ctx context.Context, repoRoot string, manifest Manifest) (io.ReadCloser, error) { + lock, present, err := ReadLock(repoRoot) + if err != nil || !present || lock.Publication == nil { + return nil, errors.New("rust setup requires the selected release publication") + } + index, err := parseReleaseHTTPSURL(lock.Publication.IndexURL) + if err != nil { + return nil, err + } + source, err := resolvePublicationURL(index, RustArchiveName(manifest)) + if err != nil { + return nil, err + } + response, err := requestRelease(ctx, releaseHTTPClient(), source) + if err != nil { + return nil, err + } + if response.ContentLength > maximumReleaseBundleBytes { + response.Body.Close() + return nil, errors.New("rust bundle exceeds its byte bound") + } + return response.Body, nil +} + +func publishRustTools(root, staging string, manifest Manifest) error { + boundary, err := os.OpenRoot(filepath.Dir(root)) + if err != nil { + return err + } + defer boundary.Close() + target, stage := filepath.Base(root), filepath.Base(staging) + paths := []string{".tools/rust/" + manifest.Host, rustNextestPath(manifest.Host)} + installed, previous := []string{}, []string{} + rollback := func(cause error) error { + return restoreRustTools(boundary, target, stage, installed, previous, cause) + } + for _, path := range paths { + destination := filepath.Join(target, path) + if err := boundary.MkdirAll(filepath.Dir(destination), 0o755); err != nil { + return rollback(err) + } + if err := rustToolParents(root, path, map[string]bool{}); err != nil { + return rollback(err) + } + if _, err := boundary.Lstat(destination); err == nil { + backup := filepath.Join(stage, "previous", path) + if err := boundary.MkdirAll(filepath.Dir(backup), 0o755); err != nil { + return rollback(err) + } + if err := boundary.Rename(destination, backup); err != nil { + return rollback(err) + } + previous = append(previous, path) + } else if !errors.Is(err, os.ErrNotExist) { + return rollback(err) + } + if err := boundary.Rename(filepath.Join(stage, "payload", path), destination); err != nil { + return rollback(err) + } + installed = append(installed, path) + } + return nil +} + +func restoreRustTools(boundary *os.Root, target, stage string, installed, previous []string, cause error) error { + for index := len(installed) - 1; index >= 0; index-- { + path := installed[index] + cause = errors.Join(cause, boundary.Rename(filepath.Join(target, path), filepath.Join(stage, "payload", path))) + } + for index := len(previous) - 1; index >= 0; index-- { + path := previous[index] + cause = errors.Join(cause, boundary.Rename(filepath.Join(stage, "previous", path), filepath.Join(target, path))) + } + return cause +} diff --git a/internal/release/rust_bundle_test.go b/internal/release/rust_bundle_test.go new file mode 100644 index 00000000..607ced8f --- /dev/null +++ b/internal/release/rust_bundle_test.go @@ -0,0 +1,188 @@ +package release + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestRustBundleCancellationPreservesInstalledTools(t *testing.T) { + root, manifest := rustBundleFixture(t) + companion := filepath.Join(t.TempDir(), "rust.zip") + if _, err := WriteRustArchive(root, companion); err != nil { + t.Fatal(err) + } + repo := t.TempDir() + if err := WriteLock(repo, indexedManifestLock(manifest)); err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(t.Context()) + cancel() + if err := InstallRustBundle(ctx, repo, root, companion); err == nil { + t.Fatal("canceled installation succeeded") + } + if err := VerifyRustTools(root); err != nil { + t.Fatalf("cancellation damaged installed tools: %v", err) + } +} + +func TestRustBundlePublicationRollsBackEarlierTools(t *testing.T) { + root, manifest := rustBundleFixture(t) + stage, err := os.MkdirTemp(filepath.Dir(root), "rust-rollback-") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { os.RemoveAll(stage) }) + sdk := filepath.Join(".tools", "rust", manifest.Host) + write(t, stage, filepath.Join("payload", sdk, "replacement"), "replacement SDK") + if err := publishRustTools(root, stage, manifest); err == nil { + t.Fatal("publication succeeded with a missing staged runner") + } + if err := VerifyRustTools(root); err != nil { + t.Fatalf("partial publication damaged the original tools: %v", err) + } + if _, err := os.Stat(filepath.Join(stage, "payload", sdk, "replacement")); err != nil { + t.Fatalf("staged tools were lost during recovery: %v", err) + } +} + +func TestOptionalRustInstallationPreservesRequiredToolIntegrity(t *testing.T) { + root, manifest := rustBundleFixture(t) + core, companion := filepath.Join(t.TempDir(), "core.zip"), filepath.Join(t.TempDir(), "rust.zip") + if _, err := WriteArchive(root, core); err != nil { + t.Fatal(err) + } + if _, err := WriteRustArchive(root, companion); err != nil { + t.Fatal(err) + } + installed, repo := t.TempDir(), t.TempDir() + if err := extractReleaseZip(core, installed); err != nil { + t.Fatal(err) + } + if err := WriteLock(repo, indexedManifestLock(manifest)); err != nil { + t.Fatal(err) + } + if err := manifest.Verify(installed); err != nil { + t.Fatal(err) + } + if err := VerifyRustTools(installed); err == nil { + t.Fatal("a core-only installation supplied required Rust coverage") + } + if _, err := os.Stat(filepath.Join(installed, ".tools", "rust")); !os.IsNotExist(err) { + t.Fatalf("default archive carried the Rust SDK: %v", err) + } + if err := InstallRustBundle(t.Context(), repo, installed, companion); err != nil { + t.Fatal(err) + } + if err := VerifyRustTools(installed); err != nil { + t.Fatal(err) + } + tool := filepath.Join(installed, filepath.FromSlash(rustNextestPath(manifest.Host))) + if err := os.WriteFile(tool, []byte("replacement"), 0o755); err != nil { + t.Fatal(err) + } + if err := VerifyRustTools(installed); err == nil { + t.Fatal("a modified native test runner was accepted") + } + if err := InstallRustBundle(t.Context(), repo, installed, companion); err != nil { + t.Fatalf("repair of modified optional tools failed: %v", err) + } + if err := VerifyRustTools(installed); err != nil { + t.Fatal(err) + } + if err := os.Remove(tool); err != nil { + t.Fatal(err) + } + if err := VerifyRustTools(installed); err == nil { + t.Fatal("removing the optional test runner bypassed required integrity") + } +} + +func TestRustBundleCannotSupplyItsOwnAuthorityOrCoreFiles(t *testing.T) { + root, manifest := rustBundleFixture(t) + repo := t.TempDir() + if err := WriteLock(repo, indexedManifestLock(manifest)); err != nil { + t.Fatal(err) + } + foreign, _ := rustBundleFixture(t) + tool := filepath.Join(foreign, filepath.FromSlash(rustNextestPath(manifest.Host))) + if err := os.WriteFile(tool, []byte("foreign release"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.Remove(filepath.Join(foreign, ManifestFilename)); err != nil { + t.Fatal(err) + } + if _, err := WriteManifest(foreign, strings.Repeat("a", 40)); err != nil { + t.Fatal(err) + } + companion := filepath.Join(t.TempDir(), "foreign.zip") + if _, err := WriteRustArchive(foreign, companion); err != nil { + t.Fatal(err) + } + if err := InstallRustBundle(t.Context(), repo, root, companion); err == nil { + t.Fatal("foreign archive replaced the selected release's tool authority") + } + if err := VerifyRustTools(root); err != nil { + t.Fatalf("failed installation damaged working tools: %v", err) + } + core := filepath.Join(t.TempDir(), "core.zip") + if _, err := WriteArchive(root, core); err != nil { + t.Fatal(err) + } + if err := InstallRustBundle(t.Context(), repo, root, core); err == nil { + t.Fatal("Rust setup admitted core files") + } +} + +func TestRustIntegrityRejectsAdditionalFilesAndLinkedDirectories(t *testing.T) { + root, manifest := rustBundleFixture(t) + directory := filepath.Join(root, ".tools", "rust", manifest.Host) + extra := filepath.Join(directory, "lib", "injected-library") + if err := os.WriteFile(extra, []byte("injected"), 0o644); err != nil { + t.Fatal(err) + } + if err := VerifyRustTools(root); err == nil { + t.Fatal("unrecorded native library was accepted") + } + if err := os.Remove(extra); err != nil { + t.Fatal(err) + } + other := filepath.Join(t.TempDir(), "sdk") + if err := os.Rename(directory, other); err != nil { + t.Fatal(err) + } + if err := os.Symlink(other, directory); err != nil { + t.Skipf("symlinks unavailable: %v", err) + } + if err := VerifyRustTools(root); err == nil { + t.Fatal("linked SDK directory escaped the installed boundary") + } +} + +func rustBundleFixture(t *testing.T) (string, Manifest) { + t.Helper() + root := writableReleaseStage(t) + host, err := Host() + if err != nil { + t.Fatal(err) + } + files := map[string]string{BinaryPath: "engine", LauncherBinaryPath: "launcher", rustNextestPath(host): "nextest"} + for _, name := range []string{"rustc", "rustdoc", "cargo", "rustfmt", "cargo-fmt", "clippy-driver", "cargo-clippy"} { + suffix := "" + if host == "windows-x64" { + suffix = ".exe" + } + files[".tools/rust/"+host+"/bin/"+name+suffix] = name + } + files[".tools/rust/"+host+"/lib/compiler-library"] = "compiler library" + for path, value := range files { + write(t, root, path, value) + } + manifest, err := WriteManifest(root, exampleRevision) + if err != nil { + t.Fatal(err) + } + return root, manifest +} diff --git a/internal/release/rust_sbom.go b/internal/release/rust_sbom.go index 7fb1069b..e32accc5 100644 --- a/internal/release/rust_sbom.go +++ b/internal/release/rust_sbom.go @@ -132,7 +132,7 @@ func rustReleaseNoticePaths(host, name string) []string { if name == "cargo-nextest" { return []string{"tools/cargo-nextest-license.txt"} } - prefix := ".tools/rust/" + host + "/share/" + prefix := "tools/rust-notices/" + host + "/" paths := []string{prefix + "doc/rust/COPYRIGHT.html", prefix + "doc/rust/COPYRIGHT-library.html", prefix + "doc/cargo/LICENSE-THIRD-PARTY"} for _, component := range rustReleaseComponents { for _, license := range []string{"LICENSE-APACHE", "LICENSE-MIT"} { diff --git a/internal/release/rust_sbom_test.go b/internal/release/rust_sbom_test.go index 08a01279..b94ab7c6 100644 --- a/internal/release/rust_sbom_test.go +++ b/internal/release/rust_sbom_test.go @@ -112,7 +112,7 @@ func assertRustNoticeEvidence(t *testing.T, host string, component cdx.Component } wanted := "tools/cargo-nextest-license.txt" if component.Name == "rust" { - wanted = ".tools/rust/" + host + "/share/doc/rust/COPYRIGHT.html" + wanted = "tools/rust-notices/" + host + "/doc/rust/COPYRIGHT.html" } if !slices.Contains(*component.Properties, cdx.Property{Name: "code-polishy:notice-path", Value: wanted}) { t.Fatalf("native notice omitted: %+v", component) @@ -144,8 +144,8 @@ func TestReleaseSBOMRejectsMissingOrInconsistentNativeEvidence(t *testing.T) { {name: "mutable notice source", path: "tools/cargo-nextest-license.txt", change: func(value string) string { return strings.ReplaceAll(value, "fc97e97bbe0a3927482a694247da00c099f4269e", "main") }, problem: "immutable"}, - {name: "unsealed SDK copyright", path: ".tools/rust/HOST/share/doc/rust/COPYRIGHT.html", omit: true, problem: "notice is absent"}, - {name: "empty SDK license", path: ".tools/rust/HOST/share/licenses/rustc/LICENSE-MIT", change: func(string) string { return "" }, problem: "nonempty"}, + {name: "unsealed SDK copyright", path: "tools/rust-notices/HOST/doc/rust/COPYRIGHT.html", omit: true, problem: "notice is absent"}, + {name: "empty SDK license", path: "tools/rust-notices/HOST/licenses/rustc/LICENSE-MIT", change: func(string) string { return "" }, problem: "nonempty"}, } { t.Run(test.name, func(t *testing.T) { root, manifest := installedRelease(t, map[string]string{BinaryPath: "fixture"}, nil) diff --git a/internal/rustfacts/execution.go b/internal/rustfacts/execution.go index ae8be118..1d1fa398 100644 --- a/internal/rustfacts/execution.go +++ b/internal/rustfacts/execution.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/release" "github.com/riteofstring/code-polishy/internal/repository" ) @@ -34,6 +35,9 @@ func Command(repo repository.Repository, name, tool string, arguments ...string) } func ToolProblem(repo repository.Repository) error { + if err := release.VerifyRustTools(repo.PolicyRoot); err != nil { + return err + } pin := repo.ToolPin("rust") if pin == "" { return errors.New("pinned Rust version is unavailable") diff --git a/scripts/build-release.ps1 b/scripts/build-release.ps1 index 32bbfe04..fefabf09 100644 --- a/scripts/build-release.ps1 +++ b/scripts/build-release.ps1 @@ -210,6 +210,8 @@ try { if ($Provenance.protocolVersion -ne 3 -or $Provenance.operation -ne 'provenance' -or -not $Provenance.result.bundleDigest) { throw 'Portable JavaScript bundle returned incomplete provenance.' } + & $Engine --policy-root $Stage release-manifest rust-notices --root $Stage + if ($LASTEXITCODE -ne 0) { throw 'Rust notice materialization failed.' } $ReleaseDigest = (& $Engine --policy-root $Stage release-manifest write --root $Stage --source-revision $SourceRevision).Trim() if ($LASTEXITCODE -ne 0 -or $ReleaseDigest -notmatch '^[0-9a-f]{64}$') { throw 'Native release manifest creation failed.' } & $Engine --policy-root $Stage release-manifest verify --root $Stage @@ -220,6 +222,10 @@ try { if ($PublicationDirectory) { & $Engine --policy-root $Stage release-manifest publish --archive $Output --destination $PublicationDirectory if ($LASTEXITCODE -ne 0) { throw 'Native release publication metadata creation failed.' } + $Version = (Get-Content -Raw -LiteralPath (Join-Path $Stage 'VERSION')).Trim() + $RustArchive = Join-Path $PublicationDirectory "code-polishy-$Version-rust-windows-x64.zip" + & $Engine --policy-root $Stage release-manifest rust-archive --root $Stage --output $RustArchive + if ($LASTEXITCODE -ne 0) { throw 'Rust companion archive creation failed.' } } Write-Output "releaseDigest=$ReleaseDigest" Write-Output "archiveSHA256=$BundleDigest" diff --git a/scripts/install.sh b/scripts/install.sh index 5d430243..ff558629 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -785,6 +785,7 @@ for retired in "check_policy.sh" "git submodule"; do fi done +"${staging}/bin/code-polishy" --policy-root "${staging}" release-manifest rust-notices --root "${staging}" release_digest="$("${release_manifest}" write "${staging}" "${source_revision}")" "${release_manifest}" verify "${staging}" @@ -803,6 +804,8 @@ if [[ -n "${publication_dir}" ]]; then --root "${staging}" --output "${release_archive}" "${staging}/bin/code-polishy" --policy-root "${staging}" release-manifest publish \ --archive "${release_archive}" --destination "${publication_dir}" + "${staging}/bin/code-polishy" --policy-root "${staging}" release-manifest rust-archive \ + --root "${staging}" --output "${publication_dir}/code-polishy-${code_polishy_version}-rust-${javascript_platform_tag}.zip" fi if [[ "${build_only}" == true ]]; then echo "Built the portable Code Polishy publication at ${publication_dir}." diff --git a/scripts/test-install.sh b/scripts/test-install.sh index 24dda8a7..4c8279bf 100755 --- a/scripts/test-install.sh +++ b/scripts/test-install.sh @@ -261,7 +261,7 @@ EOF write_file "${source_root}/scripts/code-polishy-stub.sh" <"${source_root}/.tools/rust/${platform_tag}/.code-polishy-rust-release" + for notice in doc/rust/COPYRIGHT.html doc/rust/COPYRIGHT-library.html doc/cargo/LICENSE-THIRD-PARTY; do + write_file "${source_root}/.tools/rust/${platform_tag}/share/${notice}" <"${source_root}/.tools/python/${platform_tag}/.code-polishy-python-release" diff --git a/scripts/test-installed-release-python-adoption.sh b/scripts/test-installed-release-python-adoption.sh index f1551bc8..4ddf7c2d 100644 --- a/scripts/test-installed-release-python-adoption.sh +++ b/scripts/test-installed-release-python-adoption.sh @@ -485,7 +485,7 @@ EOF "python-adoption mutable GitLab include" "supplyChain.gitLabIncludePin" \ "ci/includes/deep.yml" "fixture/secure-templates" - if ! "${release}/scripts/release-manifest.sh" verify "${release}" >"${output}" 2>&1; then + if ! "${release}/bin/code-polishy" --policy-root "${release}" release-manifest verify --root "${release}" >"${output}" 2>&1; then fail "python-adoption: managed Python commands changed the installed release: $(excerpt)" fi } diff --git a/scripts/test-installed-release-rust.sh b/scripts/test-installed-release-rust.sh index d5b7c1b8..2cd23d2a 100644 --- a/scripts/test-installed-release-rust.sh +++ b/scripts/test-installed-release-rust.sh @@ -1,16 +1,16 @@ #!/usr/bin/env bash exercise_rust_adoption_fixture() { - local fixture_root="$1" real_git="$2" release="$3" launcher="$4" lock="$5" python + local fixture_root="$1" real_git="$2" release="$3" launcher="$4" lock="$5" companion="${6:-}" python python="$(installed_fixture_python "${release}")" - "${python}" -I -B - "${fixture_root}" "${real_git}" "${release}" "${launcher}" "${lock}" <<'PY' + "${python}" -I -B - "${fixture_root}" "${real_git}" "${release}" "${launcher}" "${lock}" "${companion}" <<'PY' import json import pathlib import shutil import subprocess import sys -fixture, git, release, launcher, lock = sys.argv[1:] +fixture, git, release, launcher, lock, companion = sys.argv[1:] root = pathlib.Path(fixture) / "rust-adoption" root.mkdir() @@ -100,6 +100,10 @@ write("src/lib.rs", rust_source(42)) install_fixture_lock() subprocess.run([git, "-C", str(root), "init", "--quiet"], check=True) subprocess.run([launcher, "--repo-root", str(root), "agents", "install"], check=True) +if companion: + missing = policy("check", "--files", "src/lib.rs", status=1) + assert any("install-rust" in finding["message"] for finding in missing["findings"]), missing + subprocess.run([launcher, "--repo-root", str(root), "install-rust", "--source", companion], check=True, timeout=300) policy("format", "--all") subprocess.run([git, "-C", str(root), "add", "-A"], check=True) subprocess.run([git, "-C", str(root), "-c", "user.name=Rust Fixture", "-c", "user.email=fixture@example.invalid", "commit", "--quiet", "-m", "fixture"], check=True) @@ -151,14 +155,21 @@ write("src/lib.rs", 'pub fn answer() -> u32 { "not an integer" }\n') assert_native_outcome(policy("test", "--suite", "rust-docs", status=1), 101, "operational", "rust-docs", "rustdoc") write("src/lib.rs", documentation + rust_source(42)) assert_native_outcome(policy("test", "--suite", "rust-docs"), 0, suite="rust-docs", artifact_type="rustdoc") +if companion: + manifest = json.loads((pathlib.Path(release) / "release-manifest.json").read_text()) + executable = "cargo-nextest.exe" if manifest["host"] == "windows-x64" else "cargo-nextest" + (pathlib.Path(release) / ".tools/bin" / executable).unlink() + missing = policy("test", "--suite", "rust-unit", status=1) + assert any("install-rust" in finding["message"] for finding in missing["findings"]), missing + subprocess.run([launcher, "--repo-root", str(root), "install-rust", "--source", companion], check=True, timeout=300) print("Installed Rust formatting, compiler diagnostics, native tests and doctests, failures, and recovery passed.") PY } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then set -euo pipefail - if [[ "$#" -ne 2 || ! -d "$1" || ! -x "$2" ]]; then - echo 'usage: test-installed-release-rust.sh RELEASE_ROOT LAUNCHER' >&2 + if [[ ( "$#" -ne 2 && "$#" -ne 3 ) || ! -d "$1" || ! -x "$2" ]]; then + echo 'usage: test-installed-release-rust.sh RELEASE_ROOT LAUNCHER [RUST_ARCHIVE]' >&2 exit 2 fi policy_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" @@ -166,5 +177,5 @@ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then fail() { echo "installed Rust fixture failure: $1" >&2; exit 1; } fixture_root="$(mktemp -d "${TMPDIR:-/tmp}/code-polishy-installed-rust.XXXXXX")" trap 'rm -rf -- "${fixture_root}"' EXIT - exercise_rust_adoption_fixture "${fixture_root}" "$(command -v git)" "$1" "$2" "" + exercise_rust_adoption_fixture "${fixture_root}" "$(command -v git)" "$1" "$2" "" "${3:-}" fi From 97278dff5788e3cf78c2be5325d53858b432a0ab Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:33:24 +0000 Subject: [PATCH 33/49] Own Rust bundle permissions and record installed verification Derive installed executable and shared-read permissions from authenticated tool paths instead of accepting archive permission metadata. Exercise modified archive modes alongside installation, integrity, cancellation, rollback and repair. Focused bundle tests pass and new-source quality findings are clear. Linux installed Rust lifecycle passed at a3b6f82. Changed-scope verification remains blocked by existing dependency assessments, JavaScript bundle-lock verification and checkout prerequisites; locked 0.28.3 still cannot govern the unfinished semantic helper. No release or lock cutover is claimed. --- docs/design/optional-rust-tools.md | 4 +++ docs/plans/code-polishy-rust-support-plan.md | 9 ++++++ internal/release/rust_bundle_install.go | 20 ++++++++++++ internal/release/rust_bundle_test.go | 33 ++++++++++++++++++++ 4 files changed, 66 insertions(+) diff --git a/docs/design/optional-rust-tools.md b/docs/design/optional-rust-tools.md index 29e3209a..4570fa15 100644 --- a/docs/design/optional-rust-tools.md +++ b/docs/design/optional-rust-tools.md @@ -20,6 +20,10 @@ payload; a companion archive carries it. Installing that archive validates its complete contents against the already selected core release. The archive cannot declare its own authority, change the repository lock, or supply core files. Transport location does not replace the release-owned file identities. +Installation assigns executable permissions to the authenticated `bin` and +`libexec` paths and read-only shared permissions to other files. Archive-supplied +permissions cannot make installed tools writable by other users or remove their +required execution bits. Ordinary checks never download or repair tools. Explicit setup stages and verifies the companion before replacing installed tools, preserving a working diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 06a04340..0a09b97e 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -54,6 +54,15 @@ partial-publication rollback, and repair. The Linux x64 tool payload is about companion through the public CLI on every supported host; those jobs have not yet supplied evidence for this candidate. +The Linux x64 installed fixture passed for committed source `a3b6f82`: required +Rust checks reject the absent payload, the exact companion installs through the +launcher, formatting/compiler diagnostics/native tests/doctests execute, and +removing Nextest blocks testing until repair. That private artifact has release +digest `7070c7937b5a01e3a3858a3b36499e6b08e55fe367a3148674ebd8c0d1d47360`. +Additional bundle tests cover release-owned permissions, preventing archive +metadata from adding shared write access or removing executable permissions. +This is local execution evidence, not a published release or all-host acceptance. + Full Rust delivery remains incomplete. The locked `0.28.3` engine cannot govern the candidate semantic helper's Rust source and Cargo dependencies. Its admission, compilation and semantic integration remain required; disabling those checks is diff --git a/internal/release/rust_bundle_install.go b/internal/release/rust_bundle_install.go index 194c4056..6b37a16e 100644 --- a/internal/release/rust_bundle_install.go +++ b/internal/release/rust_bundle_install.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "strings" ) func RustArchiveName(manifest Manifest) string { @@ -61,9 +62,28 @@ func stageRustBundle(ctx context.Context, repoRoot string, manifest, tools Manif if err := tools.Verify(extracted); err != nil { return fmt.Errorf("rust bundle does not match the selected core release: %w", err) } + if err := setRustToolPermissions(extracted, tools.Entries); err != nil { + return err + } return ctx.Err() } +func setRustToolPermissions(root string, entries []Entry) error { + for _, entry := range entries { + if entry.Symlink != "" { + continue + } + mode := os.FileMode(0o644) + if strings.Contains(entry.Path, "/bin/") || strings.Contains(entry.Path, "/libexec/") { + mode = 0o755 + } + if err := os.Chmod(filepath.Join(root, filepath.FromSlash(entry.Path)), mode); err != nil { + return err + } + } + return nil +} + func acquireRustBundle(ctx context.Context, repoRoot string, manifest Manifest, source, staging string) (string, error) { var reader io.ReadCloser var err error diff --git a/internal/release/rust_bundle_test.go b/internal/release/rust_bundle_test.go index 607ced8f..08d38da6 100644 --- a/internal/release/rust_bundle_test.go +++ b/internal/release/rust_bundle_test.go @@ -4,6 +4,7 @@ import ( "context" "os" "path/filepath" + "runtime" "strings" "testing" ) @@ -48,6 +49,38 @@ func TestRustBundlePublicationRollsBackEarlierTools(t *testing.T) { } } +func TestRustBundleNormalizesArchivePermissions(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("POSIX execution and shared-write permission bits") + } + root, manifest := rustBundleFixture(t) + paths := map[string]os.FileMode{ + rustNextestPath(manifest.Host): 0o755, + ".tools/rust/" + manifest.Host + "/lib/compiler-library": 0o644, + } + for path := range paths { + if err := os.Chmod(filepath.Join(root, filepath.FromSlash(path)), 0o777); err != nil { + t.Fatal(err) + } + } + archive, repo := filepath.Join(t.TempDir(), "rust.zip"), t.TempDir() + if _, err := WriteRustArchive(root, archive); err != nil { + t.Fatal(err) + } + if err := WriteLock(repo, indexedManifestLock(manifest)); err != nil { + t.Fatal(err) + } + if err := InstallRustBundle(t.Context(), repo, root, archive); err != nil { + t.Fatal(err) + } + for path, expected := range paths { + info, err := os.Stat(filepath.Join(root, filepath.FromSlash(path))) + if err != nil || info.Mode().Perm() != expected { + t.Fatalf("unsafe permissions on %s: %v %v", path, info, err) + } + } +} + func TestOptionalRustInstallationPreservesRequiredToolIntegrity(t *testing.T) { root, manifest := rustBundleFixture(t) core, companion := filepath.Join(t.TempDir(), "core.zip"), filepath.Join(t.TempDir(), "rust.zip") From d22adafc7bac19f22c7a2b970d77ee801c6e5c09 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:45:16 +0000 Subject: [PATCH 34/49] Validate actual Cargo workspaces before native tests and reuse Require a contained workspace lock before compilation, retries and receipt reuse. Validate source and sealed-view Cargo facts without dropping discovered targets. Record prerequisite commands in direct and gate plans and regression-proof preparation. Exercise optional Rust installation and recovery in the ordinary installed contract and select native Rust coverage on Windows. Focused native execution, gate retry, reuse, regression-proof and bundle tests pass. Locked source checks retain 67 existing semantic-helper coverage and expired-assessment errors. Changed-scope and clean installed-contract verification remain to be completed; no release or policy-lock cutover is included. --- .code-polishy.json | 4 + .github/workflows/ci.yml | 4 + cmd/code-polishy/main.go | 4 +- docs/design/installed-release-verification.md | 5 + docs/design/rust-tests.md | 18 +++ docs/plans/code-polishy-rust-support-plan.md | 15 ++- internal/behaviorreview/proof.go | 10 +- internal/behaviorreview/types.go | 9 +- internal/engine/gate_command_runner.go | 21 ++++ internal/engine/gate_run.go | 36 +++--- internal/engine/merge_gate.go | 1 + internal/engine/rust_tests_test.go | 8 +- internal/engine/rust_workspace_test.go | 103 ++++++++++++++++++ internal/engine/test_receipt_runner.go | 7 ++ internal/release/installed.go | 8 +- internal/release/rust_bundle.go | 4 + internal/rustfacts/metadata.go | 6 +- internal/rustfacts/test_workspace.go | 79 ++++++++++++++ internal/testing/rust_doctest_test.go | 2 +- internal/testing/rust_preflight.go | 71 ++++++++++++ internal/testing/rust_test.go | 2 +- internal/testing/rust_workspace_test.go | 80 ++++++++++++++ internal/testing/testing.go | 44 ++++++-- scripts/test-installed-release-contract.sh | 7 +- 24 files changed, 497 insertions(+), 51 deletions(-) create mode 100644 internal/engine/rust_workspace_test.go create mode 100644 internal/rustfacts/test_workspace.go create mode 100644 internal/testing/rust_preflight.go create mode 100644 internal/testing/rust_workspace_test.go diff --git a/.code-polishy.json b/.code-polishy.json index 62c711d9..e0ff9de0 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -65,6 +65,10 @@ "internal/policy/rust_tests.go", "internal/policy/rust_tests_test.go", "internal/rustfacts/tests.go", + "internal/rustfacts/test_workspace.go", + "internal/testing/rust_preflight.go", + "internal/testing/rust_workspace_test.go", + "internal/engine/rust_workspace_test.go", "internal/testing/rust.go", "internal/testing/rust_test.go", "internal/testing/rust_output.go", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4469a5d0..c1ce9a7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -213,6 +213,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'Native Windows process and lock contracts failed.' } & $go test -count=1 ./internal/release -run '^(TestReleaseHostMatrix|TestParseLock|TestRenderLock|TestReadLock)' if ($LASTEXITCODE -ne 0) { throw 'Native release identity contracts failed.' } + & $go test -count=1 ./internal/release -run '^(TestRustBundle|TestOptionalRustInstallation|TestRustIntegrity)' + if ($LASTEXITCODE -ne 0) { throw 'Native optional Rust bundle contracts failed.' } + & $go test -count=1 ./internal/testing -run '^TestNativeRust' + if ($LASTEXITCODE -ne 0) { throw 'Native Rust test execution contracts failed.' } & $go test -count=1 ./internal/javascript -run '^TestJavaScript(ExecutableUsesNativeHostContract|BundleContainsNativeTargetPaths)$' if ($LASTEXITCODE -ne 0) { throw 'Native sealed JavaScript platform contract failed.' } & $go test -count=1 ./internal/architecture -run '^TestRuffGraphFactsAreVersionedBoundedAndPathNormalized$' diff --git a/cmd/code-polishy/main.go b/cmd/code-polishy/main.go index 96fd1ee3..26dba99f 100644 --- a/cmd/code-polishy/main.go +++ b/cmd/code-polishy/main.go @@ -201,8 +201,6 @@ func handleCoreMetaCommand(invocation invocation) (int, bool) { return handleInstallRustMeta(invocation), true case "release-manifest": return handleReleaseManifestMeta(invocation), true - case "change-boundary": - return handleChangeBoundaryMeta(invocation), true default: return 0, false } @@ -210,6 +208,8 @@ func handleCoreMetaCommand(invocation invocation) (int, bool) { func handleWorkflowMetaCommand(invocation invocation) (int, bool) { switch invocation.command { + case "change-boundary": + return handleChangeBoundaryMeta(invocation), true case "task-session": return handleTaskSessionWorkflowMeta(invocation), true case "task-session-artifacts": diff --git a/docs/design/installed-release-verification.md b/docs/design/installed-release-verification.md index a7330ce2..30dcd915 100644 --- a/docs/design/installed-release-verification.md +++ b/docs/design/installed-release-verification.md @@ -17,6 +17,11 @@ keeps the quick boundary meaningful on clean CI hosts where no prior installation exists, and prevents a stale developer installation from testing different production code. The test requires a clean committed source because the installer binds its artifact to that source revision. +The same ordinary contract writes the Rust companion from the authenticated +installation, removes its optional tools, and passes that companion to the Rust +fixture. Missing-tool rejection, explicit installation, removal and repair +therefore execute before release publication as well as in native release jobs. +Windows CI separately selects the Rust bundle integrity and native test suites. The broader release harness accepts an explicit prefix and lock so final verification can exercise an already built native artifact without rebuilding diff --git a/docs/design/rust-tests.md b/docs/design/rust-tests.md index 475abd3a..239746b5 100644 --- a/docs/design/rust-tests.md +++ b/docs/design/rust-tests.md @@ -7,6 +7,24 @@ Nextest filter. Cargo default features remain the default. Native execution uses the pinned host compiler, frozen resolution and the governed Cargo cache. It does not enable every feature or claim another target was tested. +Before compilation, each attempt runs bounded, frozen workspace-only Cargo +metadata through the shared runner. The discovered workspace root, members, +manifest and source paths must be contained, and its actual `Cargo.lock` must +be a contained regular file. A nearer unrelated lock or `--frozen` alone cannot +establish containment. Discovery executes no build scripts or procedural macros. +The actual workspace's Nextest settings are checked before native tests run. + +Reusable suites validate the source workspace before creating their sealed view, +then validate Cargo's workspace inside that view before receipt lookup. Normalized +workspace facts must agree, including package targets, dependencies, features and +default members; copying a subset cannot silently remove an automatic build script +or test target. Both commands are declared in the gate plan. The view retains the original governed +writable Cargo cache and target directory; metadata must report that exact target. +Changing an external ancestor workspace cannot be hidden by an unchanged cached +receipt or by moving execution into a temporary directory. Failed preparation +cannot become an assertion failure or regression-proof red result. Candidate +retries revalidate the workspace and retain their prerequisite command logs. + The pinned Nextest runner supplies unit and integration outcomes. Each attempt receives a fresh managed artifact directory and release-owned Nextest settings. Nextest's state directory also lives there, never in the read-only source view. diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 0a09b97e..5e7dd2a3 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -66,9 +66,8 @@ This is local execution evidence, not a published release or all-host acceptance Full Rust delivery remains incomplete. The locked `0.28.3` engine cannot govern the candidate semantic helper's Rust source and Cargo dependencies. Its admission, compilation and semantic integration remain required; disabling those checks is -not an acceptable bootstrap. Actual workspace-lock containment before native -tests, complete native dependency notices/inventory, and all-host execution also -remain open. The current dependency-review run additionally reports expired +not an acceptable bootstrap. Complete native dependency notices/inventory and +all-host execution also remain open. The current dependency-review run additionally reports expired JavaScript vulnerability assessments. None of these failures is waived by the optional installation boundary. @@ -483,9 +482,13 @@ visible in command planning. Valid siblings survive a failed or oversized input; cancellation leaves unstarted sources explicitly uncovered. Large-input fixtures also exposed unsafe coordinate-property access in the pinned Python binding. Using its tuple interface preserves long-line and Unicode coordinates without a -dependency change. Actual workspace-lock containment before native test -compilation remains a separate test-runner milestone, including its command-plan -and reusable-view boundaries. +dependency change. Native tests now validate Cargo's actual workspace and its +contained regular lock before compilation, retries and receipt reuse. Reusable +execution validates both source and copied workspaces while retaining governed +writable build directories. Direct and gate plans include those metadata commands; +regression proofs retain the same preparation boundary. Fixtures reject escaping +ancestor workspaces and linked locks before a build script can run, and distinguish +an explicit workspace lock from unrelated root or member locks. Native test-source checks now reject empty built-in Rust tests, including ignored and conditional declarations, without classifying helpers or closures as test diff --git a/internal/behaviorreview/proof.go b/internal/behaviorreview/proof.go index 4933d112..2f9d23c5 100644 --- a/internal/behaviorreview/proof.go +++ b/internal/behaviorreview/proof.go @@ -557,6 +557,13 @@ func (capture *proofSuiteRunner) RunWithOutput(ctx context.Context, root string, return result, output, err } +func (capture *proofSuiteRunner) RunStructured(ctx context.Context, root string, command policy.Command) (runner.Result, runner.Output, error) { + if structured, ok := capture.OutputRunner.(runner.StructuredRunner); ok { + return structured.RunStructured(ctx, root, command) + } + return capture.OutputRunner.RunWithOutput(ctx, root, command) +} + func cloneCommand(command policy.Command) policy.Command { return policy.Command{ Name: command.Name, Provides: slices.Clone(command.Provides), Argv: slices.Clone(command.Argv), Cwd: command.Cwd, @@ -723,7 +730,8 @@ func replayProofPhases(repo repository.Repository, root *artifactHandle, review } digest := sha256Hex(data) command := testpolicy.SuiteCommand(repo, suite) - phases = append(phases, ReplayProofPhase{ProofID: id, ProofSHA256: digest, Baseline: cloneCommand(command), Candidate: cloneCommand(command)}) + suite.Reusable = false + phases = append(phases, ReplayProofPhase{ProofID: id, ProofSHA256: digest, Baseline: cloneCommand(command), Candidate: cloneCommand(command), Prerequisites: testpolicy.SuitePrerequisiteCommands(repo, suite)}) references = append(references, ProofReference{ID: id, SHA256: digest}) } sort.Slice(phases, func(left, right int) bool { return phases[left].ProofID < phases[right].ProofID }) diff --git a/internal/behaviorreview/types.go b/internal/behaviorreview/types.go index 56db6af3..f1aba683 100644 --- a/internal/behaviorreview/types.go +++ b/internal/behaviorreview/types.go @@ -210,10 +210,11 @@ type GateReplayPlan struct { } type ReplayProofPhase struct { - ProofID string `json:"proof_id"` - ProofSHA256 string `json:"proof_sha256"` - Baseline policy.Command `json:"baseline"` - Candidate policy.Command `json:"candidate"` + Prerequisites []policy.Command `json:"prerequisites,omitempty"` + ProofID string `json:"proof_id"` + ProofSHA256 string `json:"proof_sha256"` + Baseline policy.Command `json:"baseline"` + Candidate policy.Command `json:"candidate"` } const ( diff --git a/internal/engine/gate_command_runner.go b/internal/engine/gate_command_runner.go index 1dff8080..5a202bde 100644 --- a/internal/engine/gate_command_runner.go +++ b/internal/engine/gate_command_runner.go @@ -482,3 +482,24 @@ func conciseDuration(duration time.Duration) string { } return duration.Round(10 * time.Millisecond).String() } + +func (commandRunner *gateArtifactRunner) runTestPrerequisite(ctx context.Context, root string, command policy.Command) (runner.Result, runner.Output, error) { + suite := commandRunner.prerequisiteSuites[command.Name] + if _, failed := commandRunner.failedTests[suite]; failed { + for index, expected := range commandRunner.expected { + if expected.Category == gaterun.Check && samePolicyCommand(expected.Command, command) { + return commandRunner.execute(ctx, root, index, command, true, false, nil) + } + } + } + err := fmt.Errorf("test prerequisite %q does not match a failed planned suite", command.Name) + return runner.Result{ExitStatus: -1, FailureCategory: runner.FailureOperational}, runner.Output{}, err +} + +func (retry *gateRetryRunner) RunStructured(ctx context.Context, root string, command policy.Command) (runner.Result, runner.Output, error) { + return retry.parent.runTestPrerequisite(ctx, root, command) +} + +func (diagnostic *gateDiagnosticRunner) RunStructured(ctx context.Context, root string, command policy.Command) (runner.Result, runner.Output, error) { + return diagnostic.parent.runTestPrerequisite(ctx, root, command) +} diff --git a/internal/engine/gate_run.go b/internal/engine/gate_run.go index d659dc0b..89362224 100644 --- a/internal/engine/gate_run.go +++ b/internal/engine/gate_run.go @@ -43,19 +43,20 @@ type gateRunController struct { } type gateArtifactRunner struct { - delegate runner.Runner - run *gaterun.Run - expected []MergeGateExecutionCommand - reusable map[int]gaterun.ReusableReceipt - logPaths map[testLogKey]string - failedTests map[string]int - retryCommands map[string]policy.Command - artifacts map[testLogKey][]testartifact.Record - artifactExecution *testartifact.Execution - receipts *testReceiptController - progress io.Writer - next int - err error + delegate runner.Runner + run *gaterun.Run + expected []MergeGateExecutionCommand + reusable map[int]gaterun.ReusableReceipt + logPaths map[testLogKey]string + failedTests map[string]int + retryCommands map[string]policy.Command + prerequisiteSuites map[string]string + artifacts map[testLogKey][]testartifact.Record + artifactExecution *testartifact.Execution + receipts *testReceiptController + progress io.Writer + next int + err error } type testLogKey struct { @@ -166,12 +167,17 @@ func newGateArtifactRunner(engine *Engine, run *gaterun.Run, commands []MergeGat } receipts.RenderPlan() retryCommands := map[string]policy.Command{} + prerequisiteSuites := map[string]string{} for _, suite := range engine.Repository.Config.Tests.Suites { retryCommands[suite.Name] = testpolicy.SuiteCommand(engine.Repository, testpolicy.RetrySuite(suite)) + suite.Reusable = false + for _, prerequisite := range testpolicy.SuitePrerequisiteCommands(engine.Repository, suite) { + prerequisiteSuites[prerequisite.Name] = suite.Name + } } return &gateArtifactRunner{ delegate: engine.Runner, run: run, expected: commands, reusable: reusable, - logPaths: map[testLogKey]string{}, failedTests: map[string]int{}, retryCommands: retryCommands, + logPaths: map[testLogKey]string{}, failedTests: map[string]int{}, retryCommands: retryCommands, prerequisiteSuites: prerequisiteSuites, artifacts: map[testLogKey][]testartifact.Record{}, progress: progress, artifactExecution: artifacts, receipts: receipts, } @@ -350,7 +356,7 @@ func gateBehaviorProofCommands(plan behaviorreview.GateReplayPlan) ([]MergeGateE resources := []string{} timeout := 1 for _, proof := range plan.Proofs { - for _, command := range []policy.Command{proof.Baseline, proof.Candidate} { + for _, command := range append(append([]policy.Command{proof.Baseline, proof.Candidate}, proof.Prerequisites...), proof.Prerequisites...) { modules = append(modules, command.Modules...) paths = append(paths, command.Paths...) environment = append(environment, command.Environment...) diff --git a/internal/engine/merge_gate.go b/internal/engine/merge_gate.go index 37884172..c4679c8d 100644 --- a/internal/engine/merge_gate.go +++ b/internal/engine/merge_gate.go @@ -379,6 +379,7 @@ func mergeGateSuiteCommands(repo repository.Repository, suites []policy.TestSuit continue } seen[suite.Name] = true + result = append(result, mergeGateCheckCommands(gaterun.Check, testpolicy.SuitePrerequisiteCommands(repo, suite))...) result = append(result, MergeGateExecutionCommand{Category: gaterun.OrdinaryTest, Kind: suite.Kind, Scope: suite.Scope, Cost: suite.Cost, Command: testpolicy.SuiteCommand(repo, suite)}) } return result diff --git a/internal/engine/rust_tests_test.go b/internal/engine/rust_tests_test.go index 20918f44..594b5fe4 100644 --- a/internal/engine/rust_tests_test.go +++ b/internal/engine/rust_tests_test.go @@ -100,9 +100,9 @@ func testRustNativeReceipt(t *testing.T, doctests bool) { t.Fatalf("native receipt publication: %+v, reasons=%v; %s", run, controller.reasons, output) } controller.allowReuse = true - reused, hit, err := controller.ReuseSuite(suite, 1) - if err != nil || !hit || !reused.Reused || len(reused.Artifacts) != 1 { - t.Fatalf("native receipt reuse: %+v, %v, %v", reused, hit, err) + reusedRun := testpolicy.RunWithEvidence(t.Context(), engine.Repository, commandRunner, testpolicy.Plan{Suites: []policy.TestSuite{suite}}, nil) + if len(reusedRun.Findings) != 0 || len(reusedRun.Executions) != 1 || !reusedRun.Executions[0].Reused || reusedRun.Executions[0].ReceiptPath == "" || len(reusedRun.Executions[0].Artifacts) != 1 { + t.Fatalf("native receipt reuse: %+v", reusedRun) } suite.Rust.Doctests = !doctests changed, err := newTestReceiptController(engine, []policy.TestSuite{suite}, true) @@ -182,7 +182,7 @@ func TestRustDirectTestTelemetryPreservesNativeExitStatus(t *testing.T) { t.Fatalf("instrumentation lost native outcome: %+v", command) } telemetry := engine.AttachExecution(report).Execution - if len(telemetry.Commands) != 1 || telemetry.Commands[0].ExitStatus != test.exit { + if len(telemetry.Commands) != 2 || telemetry.Commands[0].Name != "focused-workspace" || telemetry.Commands[1].ExitStatus != test.exit { t.Fatalf("native telemetry is inconsistent: %+v", telemetry) } }) diff --git a/internal/engine/rust_workspace_test.go b/internal/engine/rust_workspace_test.go new file mode 100644 index 00000000..c58f3856 --- /dev/null +++ b/internal/engine/rust_workspace_test.go @@ -0,0 +1,103 @@ +package engine + +import ( + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/gaterun" + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/runner" + testpolicy "github.com/riteofstring/code-polishy/internal/testing" +) + +func TestRustReceiptCannotHideNewExternalWorkspace(t *testing.T) { + t.Parallel() + root := nativeRustEngineRepository(t) + manifest, err := os.ReadFile(filepath.Join(root, "Cargo.toml")) + if err != nil { + t.Fatal(err) + } + writeEngineFile(t, root, "Cargo.toml", strings.Replace(string(manifest), "[workspace]\n", "", 1), 0o600) + engine, err := Open(root, enginePolicyRoot(t), "") + if err != nil { + t.Fatal(err) + } + suite := engine.Repository.Config.Tests.Suites[0] + suite.Reusable = true + controller, err := newTestReceiptController(engine, []policy.TestSuite{suite}, true) + if err != nil { + t.Fatal(err) + } + commandRunner := &testReceiptRunner{delegate: runner.OSRunner{}, controller: controller} + plan := testpolicy.Plan{Suites: []policy.TestSuite{suite}} + initial := testpolicy.RunWithEvidence(t.Context(), engine.Repository, commandRunner, plan, nil) + if len(initial.Findings) != 0 || initial.Executions[0].ReceiptPath == "" { + t.Fatalf("initial native receipt: %+v", initial) + } + parent := filepath.Dir(root) + writeEngineFile(t, parent, "Cargo.toml", "[workspace]\nmembers=['"+filepath.Base(root)+"']\nresolver='3'\n", 0o600) + writeEngineFile(t, parent, "Cargo.lock", "version=4\n[[package]]\nname='native-tests'\nversion='1.0.0'\n", 0o600) + changed := testpolicy.RunWithEvidence(t.Context(), engine.Repository, commandRunner, plan, nil) + if len(changed.Findings) == 0 || changed.Executions[0].Reused || !strings.Contains(changed.Executions[0].FailureMessage, "workspace root") { + t.Fatalf("receipt concealed an external workspace without changed repository inputs: %+v", changed) + } +} + +func TestRustGateRetryRecordsWorkspaceRevalidation(t *testing.T) { + t.Parallel() + root := nativeRustEngineRepository(t) + writeEngineFile(t, root, "content/lib.rs", "#[test]\nfn retry_behavior() { let marker = std::path::Path::new(env!(\"CARGO_MANIFEST_DIR\")).join(\".tools/attempt\"); let previous = marker.exists(); std::fs::write(marker, b\"attempted\").unwrap(); assert!(previous); }\n", 0o600) + installBehaviorReviewTestGuidance(t, root) + initializeEngineGitRepository(t, root) + writeEngineFile(t, root, "content/data.json", "{\"changed\":true}\n", 0o600) + commitEngineCandidate(t, root, "change content") + engine, err := Open(root, enginePolicyRoot(t), "") + if err != nil { + t.Fatal(err) + } + engine.Output = io.Discard + engine.Runner = nativeRustGateRunner{} + report, err := engine.MergeGate(t.Context(), "main") + if err != nil || HasFindings(report) || report.GateRunPolicy == nil || report.GateRunPolicy.Status != "passed" { + t.Fatalf("native retry gate: %+v, %v", report, err) + } + workspace := readGateCommandOutcome(t, root, report.GateRunPolicy.ReportPath, "focused-workspace") + suite := readGateCommandOutcome(t, root, report.GateRunPolicy.ReportPath, "focused") + if len(workspace.Attempts) != 2 || workspace.Status != gaterun.Passed || len(suite.Attempts) != 2 || suite.Status != gaterun.Passed { + t.Fatalf("retry skipped workspace discovery: workspace=%+v suite=%+v", workspace, suite) + } +} + +func TestRustReusableViewCannotDropDiscoveredBuildTarget(t *testing.T) { + t.Parallel() + root := nativeRustEngineRepository(t) + writeEngineFile(t, root, "build.rs", "fn main() { panic!(\"required build script\"); }\n", 0o600) + engine, err := Open(root, enginePolicyRoot(t), "") + if err != nil { + t.Fatal(err) + } + suite := engine.Repository.Config.Tests.Suites[0] + suite.Reusable = true + controller, err := newTestReceiptController(engine, []policy.TestSuite{suite}, true) + if err != nil { + t.Fatal(err) + } + commandRunner := &testReceiptRunner{delegate: runner.OSRunner{}, controller: controller} + result := testpolicy.RunWithEvidence(t.Context(), engine.Repository, commandRunner, testpolicy.Plan{Suites: []policy.TestSuite{suite}}, nil) + if len(result.Findings) == 0 || !strings.Contains(result.Executions[0].FailureMessage, "workspace differs") || result.Executions[0].ReceiptPath != "" { + t.Fatalf("view silently dropped the build target: %+v", result) + } + suite.ExtraInputs = []string{"build.rs"} + controller, err = newTestReceiptController(engine, []policy.TestSuite{suite}, false) + if err != nil { + t.Fatal(err) + } + commandRunner.controller = controller + declared := testpolicy.RunWithEvidence(t.Context(), engine.Repository, commandRunner, testpolicy.Plan{Suites: []policy.TestSuite{suite}}, nil) + if len(declared.Findings) == 0 || strings.Contains(declared.Executions[0].FailureMessage, "workspace differs") { + t.Fatalf("declared build target did not reach compilation: %+v", declared) + } +} diff --git a/internal/engine/test_receipt_runner.go b/internal/engine/test_receipt_runner.go index a7287046..c9a88eaa 100644 --- a/internal/engine/test_receipt_runner.go +++ b/internal/engine/test_receipt_runner.go @@ -202,3 +202,10 @@ func (commandRunner *testReceiptRunner) ReceiptNotes() []string { func (commandRunner *testReceiptRunner) PrepareSuiteView(suite policy.TestSuite) (string, func() error, error) { return commandRunner.controller.PrepareSuiteView(suite) } + +func (commandRunner *testReceiptRunner) RunStructured(ctx context.Context, root string, command policy.Command) (runner.Result, runner.Output, error) { + if structured, ok := commandRunner.delegate.(runner.StructuredRunner); ok { + return structured.RunStructured(ctx, root, command) + } + return commandRunner.RunWithOutput(ctx, root, command) +} diff --git a/internal/release/installed.go b/internal/release/installed.go index f3dbb5ff..1c0e997a 100644 --- a/internal/release/installed.go +++ b/internal/release/installed.go @@ -24,7 +24,7 @@ func (manifest Manifest) Verify(releaseDir string) error { return fmt.Errorf("read the installed Code Polishy release: %w", err) } relative = filepath.ToSlash(relative) - if manifest.ManifestVersion >= 7 && (relative == ".tools/rust/"+manifest.Host || relative == rustNextestPath(manifest.Host)) { + if rustToolRoot(manifest, relative) { if found.IsDir() { return fs.SkipDir } @@ -42,15 +42,15 @@ func (manifest Manifest) Verify(releaseDir string) error { return fmt.Errorf("%s is installed under a Code Polishy release that does not record it", installed) } delete(pending, relative) - if rustToolEntry(manifest, relative) { - return nil - } return verifyEntry(installed, found, recorded) } if err := filepath.WalkDir(releaseDir, walk); err != nil { return err } + return manifest.verifyMissingEntries(releaseDir, pending) +} +func (manifest Manifest) verifyMissingEntries(releaseDir string, pending map[string]Entry) error { for _, entry := range manifest.Entries { if rustToolEntry(manifest, entry.Path) { continue diff --git a/internal/release/rust_bundle.go b/internal/release/rust_bundle.go index e6e26bb0..a7710d36 100644 --- a/internal/release/rust_bundle.go +++ b/internal/release/rust_bundle.go @@ -10,6 +10,10 @@ import ( "strings" ) +func rustToolRoot(manifest Manifest, relative string) bool { + return manifest.ManifestVersion >= 7 && (relative == ".tools/rust/"+manifest.Host || relative == rustNextestPath(manifest.Host)) +} + func rustToolEntry(manifest Manifest, relative string) bool { if manifest.ManifestVersion < 7 { return false diff --git a/internal/rustfacts/metadata.go b/internal/rustfacts/metadata.go index 26acf8bf..12de7c7a 100644 --- a/internal/rustfacts/metadata.go +++ b/internal/rustfacts/metadata.go @@ -79,11 +79,15 @@ func ParseWorkspace(repo repository.Repository, manifest string, data []byte) (W } func workspaceFromWire(repo repository.Repository, manifest string, wire workspaceWire) (Workspace, error) { + return workspaceFromWireWithTarget(repo, manifest, wire, TargetDirectory(repo)) +} + +func workspaceFromWireWithTarget(repo repository.Repository, manifest string, wire workspaceWire, target string) (Workspace, error) { root, err := containedPath(repo, filepath.Join(wire.Root, "Cargo.toml")) if err != nil { return Workspace{}, fmt.Errorf("cargo workspace root: %w", err) } - if wire.TargetDir != TargetDirectory(repo) { + if wire.TargetDir != target { return Workspace{}, errors.New("cargo metadata did not retain the governed target directory") } workspace := Workspace{Root: filepath.ToSlash(filepath.Dir(root)), TargetDir: wire.TargetDir, Packages: wire.Packages, Members: wire.Members, DefaultMembers: wire.DefaultMembers} diff --git a/internal/rustfacts/test_workspace.go b/internal/rustfacts/test_workspace.go new file mode 100644 index 00000000..0ea6340f --- /dev/null +++ b/internal/rustfacts/test_workspace.go @@ -0,0 +1,79 @@ +package rustfacts + +import ( + "context" + "errors" + "fmt" + "path/filepath" + "reflect" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestWorkspaceCommand(repo repository.Repository, name, manifest string) policy.Command { + return Command(repo, name, "cargo", "metadata", "--format-version", "1", "--no-deps", "--frozen", "--manifest-path", manifest) +} + +func ValidateTestWorkspace(ctx context.Context, repo repository.Repository, root string, settings policy.RustTestSuite, command policy.Command, commandRunner runner.Runner) (Workspace, runner.Result, error) { + view := repo + view.Root = root + if err := configurationProblem(view); err != nil { + return Workspace{}, runner.Result{ExitStatus: -1}, err + } + structured, ok := commandRunner.(runner.StructuredRunner) + if !ok { + return Workspace{}, runner.Result{ExitStatus: -1}, errors.New("native Rust tests require bounded Cargo workspace discovery") + } + result, output, err := structured.RunStructured(ctx, root, command) + if err != nil { + return Workspace{}, result, fmt.Errorf("native Rust workspace discovery failed: %w: %s", err, diagnosticOutput(output.Stderr)) + } + workspace, err := validateTestWorkspaceOutput(view, settings, output.Stdout, TargetDirectory(repo)) + return workspace, result, err +} + +func validateTestWorkspaceOutput(repo repository.Repository, settings policy.RustTestSuite, data []byte, target string) (Workspace, error) { + wire, err := decodeWorkspace(data) + if err != nil { + return Workspace{}, err + } + workspace, err := workspaceFromWireWithTarget(repo, settings.Manifest, wire, target) + if err != nil { + return Workspace{}, err + } + lock := filepath.ToSlash(filepath.Join(workspace.Root, "Cargo.lock")) + if _, err := repo.ReadContainedFile(lock, 8<<20); err != nil { + return Workspace{}, fmt.Errorf("native Rust tests require a contained Cargo.lock at the discovered workspace root: %w", err) + } + if !settings.Doctests { + return workspace, nextestAncestorSettings(repo, filepath.ToSlash(filepath.Join(workspace.Root, "Cargo.toml"))) + } + return workspace, nil +} + +func SameTestWorkspace(source, view Workspace) bool { + return reflect.DeepEqual(testWorkspaceIdentity(source), testWorkspaceIdentity(view)) +} + +type testWorkspaceFacts struct { + Root string + TargetDir string + Packages map[string]Package + DefaultMembers map[string]bool +} + +func testWorkspaceIdentity(workspace Workspace) testWorkspaceFacts { + facts := testWorkspaceFacts{Root: workspace.Root, TargetDir: workspace.TargetDir, Packages: map[string]Package{}, DefaultMembers: map[string]bool{}} + manifests := map[string]string{} + for _, pkg := range workspace.Packages { + manifests[pkg.ID] = pkg.Manifest + pkg.ID = "" + facts.Packages[pkg.Manifest] = pkg + } + for _, member := range workspace.DefaultMembers { + facts.DefaultMembers[manifests[member]] = true + } + return facts +} diff --git a/internal/testing/rust_doctest_test.go b/internal/testing/rust_doctest_test.go index 86be31c8..9845594b 100644 --- a/internal/testing/rust_doctest_test.go +++ b/internal/testing/rust_doctest_test.go @@ -114,7 +114,7 @@ func TestNativeRustDoctestsKeepWorkspaceBatches(t *testing.T) { t.Fatalf("selected package doctests: %+v", execution) } plan := ExecutionPlanForDirectTest(Plan{Suites: []policy.TestSuite{suite}}) - if len(plan.Commands) != 1 || plan.Commands[0].Argv[0] != "cargo" || !strings.Contains(strings.Join(plan.Commands[0].Argv, " "), "test --doc --frozen") { + if len(plan.Commands) != 2 || plan.Commands[1].Argv[0] != "cargo" || !strings.Contains(strings.Join(plan.Commands[1].Argv, " "), "test --doc --frozen") { t.Fatalf("doctest execution plan: %+v", plan) } } diff --git a/internal/testing/rust_preflight.go b/internal/testing/rust_preflight.go new file mode 100644 index 00000000..45d22688 --- /dev/null +++ b/internal/testing/rust_preflight.go @@ -0,0 +1,71 @@ +package testing + +import ( + "context" + "errors" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" + "github.com/riteofstring/code-polishy/internal/rustfacts" +) + +func SuitePrerequisiteCommands(repo repository.Repository, suite policy.TestSuite) []policy.Command { + if suite.Rust == nil { + return nil + } + commands := []policy.Command{rustfacts.TestWorkspaceCommand(repo, suite.Name+"-workspace", suite.Rust.Manifest)} + if suite.Reusable { + commands = append(commands, rustfacts.TestWorkspaceCommand(repo, suite.Name+"-view-workspace", suite.Rust.Manifest)) + } + return commands +} + +func prepareRustSuite(ctx context.Context, repo repository.Repository, commandRunner runner.Runner, suite policy.TestSuite, reporter *ExecutionReporter, offset int) (string, func() error, runner.Result, error) { + root, cleanup := repo.Root, func() error { return nil } + result := runner.Result{ExitStatus: -1} + if suite.Rust == nil { + return root, cleanup, result, nil + } + var source rustfacts.Workspace + for index, command := range SuitePrerequisiteCommands(repo, suite) { + if index > 0 { + var err error + root, cleanup, err = suiteExecutionRoot(repo.Root, commandRunner, suite) + if err != nil { + return "", nil, result, err + } + } + var err error + runContext := suiteRunContext(ctx, reporter, offset+index) + var workspace rustfacts.Workspace + workspace, result, err = rustfacts.ValidateTestWorkspace(runContext, repo, root, *suite.Rust, command, commandRunner) + if err == nil && index > 0 && !rustfacts.SameTestWorkspace(source, workspace) { + err = errors.New("reusable Rust view workspace differs from its source workspace; declare every Cargo input") + } + source = workspace + reportPreparation(reporter, offset+index, result, err) + if err != nil { + return "", nil, result, errors.Join(err, cleanup()) + } + } + return root, cleanup, result, nil +} + +func failedSuitePreparation(ctx context.Context, suite policy.TestSuite, attempt int, result runner.Result, err error) SuiteExecution { + execution := failedSuiteExecution(suite, attempt, err) + category := runner.FailureCategoryFor(ctx, result, err) + if category == runner.FailureCanceled || category == runner.FailureTimeout { + execution.FailureCategory = category + execution.Result.FailureCategory = category + } + return execution +} + +func reportPreparation(reporter *ExecutionReporter, index int, result runner.Result, err error) { + execution := SuiteExecution{Result: result, ResultKnown: true} + if err != nil { + execution.FailureMessage = err.Error() + } + reportSuiteExecution(reporter, index, execution) +} diff --git a/internal/testing/rust_test.go b/internal/testing/rust_test.go index e7215ce9..88c14779 100644 --- a/internal/testing/rust_test.go +++ b/internal/testing/rust_test.go @@ -149,7 +149,7 @@ func TestNativeRustCommandAndCopiesRetainGovernedSelection(t *testing.T) { t.Fatal("retry changed the selected original package") } plan := ExecutionPlanForDirectTest(Plan{Suites: []policy.TestSuite{suite}}) - if len(plan.Commands) != 1 || !slices.Contains(plan.Commands[0].Argv, "--package") { + if len(plan.Commands) != 2 || !slices.Contains(plan.Commands[1].Argv, "--package") { t.Fatalf("native command missing from execution plan: %+v", plan) } ctx, cancel := context.WithCancel(t.Context()) diff --git a/internal/testing/rust_workspace_test.go b/internal/testing/rust_workspace_test.go new file mode 100644 index 00000000..bc849db9 --- /dev/null +++ b/internal/testing/rust_workspace_test.go @@ -0,0 +1,80 @@ +package testing + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestNativeRustRejectsExternalWorkspaceBeforeCompilation(t *testing.T) { + t.Parallel() + for _, doctests := range []bool{false, true} { + repo, suite := rustSuiteRepository(t, "#[test]\nfn behavior() { assert_eq!(2 + 2, 4); }\n") + parent := t.TempDir() + root := filepath.Join(parent, "member") + if err := os.Rename(repo.Root, root); err != nil { + t.Fatal(err) + } + repo.Root = root + suite.Rust.Doctests = doctests + writeRustSuiteFile(t, root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\nedition='2024'\n") + writeRustSuiteFile(t, parent, "Cargo.toml", "[workspace]\nmembers=['member']\nresolver='3'\n") + writeRustSuiteFile(t, parent, "Cargo.lock", "version=4\n[[package]]\nname='app'\nversion='1.0.0'\n") + writeRustSuiteFile(t, root, "build.rs", "fn main() { std::fs::write(\"build-started\", b\"ran\").unwrap(); }\n") + for attempt := 1; attempt <= 2; attempt++ { + execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, attempt) + if !execution.Failed() || !strings.Contains(execution.FailureMessage, "workspace root") { + t.Fatalf("external workspace accepted for doctests=%v attempt=%d: %+v", doctests, attempt, execution) + } + if _, err := os.Stat(filepath.Join(root, "build-started")); !os.IsNotExist(err) { + t.Fatalf("build script executed before containment validation: %v", err) + } + } + } +} + +func TestNativeRustRejectsLinkedWorkspaceLockBeforeCompilation(t *testing.T) { + t.Parallel() + for _, doctests := range []bool{false, true} { + repo, suite := rustSuiteRepository(t, "#[test]\nfn behavior() { assert_eq!(2 + 2, 4); }\n") + suite.Rust.Doctests = doctests + lock := filepath.Join(repo.Root, "Cargo.lock") + outside := filepath.Join(t.TempDir(), "Cargo.lock") + if err := os.Rename(lock, outside); err != nil { + t.Fatal(err) + } + if err := os.Symlink(outside, lock); err != nil { + t.Skipf("symlink creation unavailable: %v", err) + } + writeRustSuiteFile(t, repo.Root, "build.rs", "fn main() { std::fs::write(\"build-started\", b\"ran\").unwrap(); }\n") + execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 2) + if !execution.Failed() || !strings.Contains(execution.FailureMessage, "contained Cargo.lock") { + t.Fatalf("linked lock accepted for doctests=%v: %+v", doctests, execution) + } + if _, err := os.Stat(filepath.Join(repo.Root, "build-started")); !os.IsNotExist(err) { + t.Fatalf("build script executed with a linked lock: %v", err) + } + } +} + +func TestNativeRustUsesActualExplicitWorkspaceLock(t *testing.T) { + t.Parallel() + repo, suite := rustSuiteRepository(t, "pub fn unrelated() {}\n") + suite.Rust.Manifest = "crates/app/Cargo.toml" + writeRustSuiteFile(t, repo.Root, suite.Rust.Manifest, "[package]\nname='app'\nversion='1.0.0'\nedition='2024'\nworkspace='../../workspace'\n") + writeRustSuiteFile(t, repo.Root, "workspace/Cargo.toml", "[workspace]\nmembers=['../crates/app']\nresolver='3'\n") + writeRustSuiteFile(t, repo.Root, "crates/app/src/lib.rs", "#[test]\nfn behavior() { assert_eq!(2 + 2, 4); }\n") + lock := "version=4\n[[package]]\nname='app'\nversion='1.0.0'\n" + writeRustSuiteFile(t, repo.Root, "crates/app/Cargo.lock", lock) + missing := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + if !missing.Failed() || !strings.Contains(missing.FailureMessage, "contained Cargo.lock") { + t.Fatalf("unrelated root or member lock replaced the workspace lock: %+v", missing) + } + writeRustSuiteFile(t, repo.Root, "workspace/Cargo.lock", lock) + if recovered := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 2); recovered.Failed() { + t.Fatalf("valid explicit workspace failed: %+v", recovered) + } +} diff --git a/internal/testing/testing.go b/internal/testing/testing.go index 6e8a12f8..1b757040 100644 --- a/internal/testing/testing.go +++ b/internal/testing/testing.go @@ -448,11 +448,13 @@ func run(ctx context.Context, repo repository.Repository, commandRunner runner.R runResult := RunResult{} var artifactExecution *testartifact.Execution reuse := suiteReuseController(commandRunner) - for index, suite := range plan.Suites { - runContext := suiteRunContext(ctx, reporter, index) - execution := runOneSuite(runContext, repo, commandRunner, suite, reuse, &artifactExecution) + index := 0 + for _, suite := range plan.Suites { + execution := runOneSuite(ctx, repo, commandRunner, suite, reuse, &artifactExecution, reporter, index) + index += len(SuitePrerequisiteCommands(repo, suite)) runResult.Executions = append(runResult.Executions, execution) reportSuiteExecution(reporter, index, execution) + index++ if execution.Failed() { runResult.Findings = append(runResult.Findings, policy.Finding{Check: "test." + suite.Kind, Path: "repository", Subject: suite.Name, Message: execution.FailureMessage}) if stopAfterFailure { @@ -478,10 +480,16 @@ func suiteRunContext(ctx context.Context, reporter *ExecutionReporter, index int }) } -func runOneSuite(ctx context.Context, repo repository.Repository, commandRunner runner.Runner, suite policy.TestSuite, reuse SuiteReuseController, artifacts **testartifact.Execution) SuiteExecution { +func runOneSuite(ctx context.Context, repo repository.Repository, commandRunner runner.Runner, suite policy.TestSuite, reuse SuiteReuseController, artifacts **testartifact.Execution, reporter *ExecutionReporter, index int) (execution SuiteExecution) { if err := suiteProblem(repo, suite); err != nil { return failedSuiteExecution(suite, 1, err) } + executionRoot, cleanup, preparation, err := prepareRustSuite(ctx, repo, commandRunner, suite, reporter, index) + if err != nil { + return failedSuitePreparation(ctx, suite, 1, preparation, err) + } + defer func() { finishSuiteExecution(&execution, cleanup(), reuse) }() + ctx = suiteRunContext(ctx, reporter, index+len(SuitePrerequisiteCommands(repo, suite))) execution, reused, err := reusableSuiteExecution(reuse, suite, 1) if err != nil { return failedSuiteExecution(suite, 1, err) @@ -492,13 +500,15 @@ func runOneSuite(ctx context.Context, repo repository.Repository, commandRunner if err := ensureRunArtifacts(repo.Root, commandRunner, artifacts); err != nil { return failedSuiteExecution(suite, 1, err) } - executionRoot, cleanup, err := suiteExecutionRoot(repo.Root, commandRunner, suite) - if err != nil { - return failedSuiteExecution(suite, 1, err) + if suite.Rust == nil { + var viewCleanup func() error + executionRoot, viewCleanup, err = suiteExecutionRoot(repo.Root, commandRunner, suite) + if err != nil { + return failedSuiteExecution(suite, 1, err) + } + cleanup = viewCleanup } - execution = executeSuite(ctx, repo, executionRoot, commandRunner, suite, 1, *artifacts) - finishSuiteExecution(&execution, cleanup(), reuse) - return execution + return executeSuite(ctx, repo, executionRoot, commandRunner, suite, 1, *artifacts) } func ensureRunArtifacts(root string, commandRunner runner.Runner, artifacts **testartifact.Execution) error { @@ -526,7 +536,7 @@ func finishSuiteExecution(execution *SuiteExecution, cleanupErr error, reuse Sui execution.FailureCategory = runner.FailureOperational execution.FailureMessage = errors.Join(errorFromMessage(execution.FailureMessage), cleanupErr).Error() } - if execution.Suite.Reusable && !execution.Failed() && reuse != nil { + if execution.Suite.Reusable && !execution.Reused && !execution.Failed() && reuse != nil { receipt, err := reuse.RecordSuite(*execution) if err != nil { execution.FailureCategory = runner.FailureOperational @@ -582,6 +592,12 @@ func ExecuteSuite(ctx context.Context, repo repository.Repository, commandRunner if err := suiteProblem(repo, suite); err != nil { return failedSuiteExecution(suite, attempt, err) } + preflight := suite + preflight.Reusable = false + _, _, preparation, prepareErr := prepareRustSuite(ctx, repo, commandRunner, preflight, nil, 0) + if prepareErr != nil { + return failedSuitePreparation(ctx, suite, attempt, preparation, prepareErr) + } if runnerManagesTestArtifacts(commandRunner) { return executeSuite(ctx, repo, repo.Root, commandRunner, suite, attempt, nil) } @@ -698,6 +714,12 @@ func runSuite(ctx context.Context, root string, commandRunner runner.Runner, com func ExecutionPlanForDirectTest(plan Plan) ExecutionPlan { commands := make([]ExecutionCommand, 0, len(plan.Suites)) for _, suite := range plan.Suites { + for _, prerequisite := range SuitePrerequisiteCommands(repository.Repository{}, suite) { + commands = append(commands, ExecutionCommand{ + Name: prerequisite.Name, Kind: "check", Scope: suite.Scope, Cost: "quick", + Argv: prerequisite.Argv, Cwd: prerequisite.Cwd, TimeoutSeconds: prerequisite.TimeoutSeconds, + }) + } argv := slices.Clone(suite.Argv) if suite.Rust != nil { argv = append([]string{"cargo-nextest"}, rustTestArguments(*suite.Rust)...) diff --git a/scripts/test-installed-release-contract.sh b/scripts/test-installed-release-contract.sh index 0da649eb..d9e3fd14 100755 --- a/scripts/test-installed-release-contract.sh +++ b/scripts/test-installed-release-contract.sh @@ -43,5 +43,10 @@ EOF --prefix "${fixture_root}/prefix" \ --lock "${fixture_root}/target/.code-polishy.lock.json" \ --fixture first-adoption +release_root="$(dirname "${manifest}")" +"${releases[0]}" release-manifest rust-archive --root "${release_root}" --output "${fixture_root}/rust.zip" +host="$(awk -F'"' '/"host"/ { print $4; exit }' "${manifest}")" +rm -rf "${release_root}/.tools/rust/${host}" +rm "${release_root}/.tools/bin/cargo-nextest" bash "${policy_root}/scripts/test-installed-release-rust.sh" \ - "$(dirname "${manifest}")" "${fixture_root}/prefix/bin/code-polishy" + "${release_root}" "${fixture_root}/prefix/bin/code-polishy" "${fixture_root}/rust.zip" From d66647ecd519c383f171d95bad2fc8b8b0dcc22f Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:10:59 +0000 Subject: [PATCH 35/49] Assert workspace preflight in installed Rust execution evidence --- scripts/test-installed-release-rust.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/test-installed-release-rust.sh b/scripts/test-installed-release-rust.sh index 2cd23d2a..3ebd501a 100644 --- a/scripts/test-installed-release-rust.sh +++ b/scripts/test-installed-release-rust.sh @@ -78,8 +78,10 @@ def assert_native_outcome(report, exit_status, category="", suite="rust-unit", a assert len(artifacts) == 1 and artifacts[0]["type"] == artifact_type, command assert artifacts[0]["size"] > 0 and len(artifacts[0]["sha256"]) == 64, command executed = report["execution"]["commands"] - assert len(executed) == 1, executed - pathlib.Path(executed[0]["argv"][0]).resolve().relative_to(pathlib.Path(release).resolve()) + assert [entry["name"] for entry in executed] == [suite + "-workspace", suite], executed + assert executed[0]["argv"][1] == "metadata" and executed[0]["exitStatus"] == 0, executed + for entry in executed: + pathlib.Path(entry["argv"][0]).resolve().relative_to(pathlib.Path(release).resolve()) config = { From 02ff240e460064945a2d45e84d92fca18f4aeaa5 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:18:57 +0000 Subject: [PATCH 36/49] Record Rust containment and installed lifecycle verification --- docs/plans/code-polishy-rust-support-plan.md | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 5e7dd2a3..7c1b07cd 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -63,6 +63,33 @@ Additional bundle tests cover release-owned permissions, preventing archive metadata from adding shared write access or removing executable permissions. This is local execution evidence, not a published release or all-host acceptance. +Workspace containment at `d22adaf` passed the focused native, receipt, retry, +regression-proof and release suites. The changed-scope run completed 38 suites: +36 passed, the installed fixture exposed an outdated single-command assertion, +and the JavaScript bundle fixture could not complete an offline installation. +The installed assertion now requires successful workspace discovery before native +execution. The focused installed-release rerun passed at `d66647e`, covering +first adoption, pack verification, absent Rust tools, exact installation, +formatting, diagnostics, native tests, doctests, failure outcomes and repair. +Its private Linux x64 artifact has release digest +`424f856fa177a57d978660f0c28181262d22caa5fc8d137dc5a747a680d6d953`. +Across the original run and focused correction, 37 of the 38 selected suites +passed. The verification command still fails its 12 expired vulnerability +assessments. A diagnostic rerun identified the JavaScript fixture's missing cached +`@types/node@24.13.3` archive; this run does not establish a source defect. +Checkpoint validation still reports unsupported semantic-helper coverage, +Cargo dependency coverage, architecture imports and expired JavaScript +assessments. A clean verification checkout also lacked two module Python +environments. No policy failure was waived. + +The ordinary installed-release contract now exercises the optional Rust +installation lifecycle, and Windows CI selects optional-bundle and native Rust +contract tests. Windows execution remains unverified locally. Authenticating +the 171-entry, 686,461,770-byte Rust payload took 3.33 seconds in a local +measurement, excluding core verification and the inventory walk. Integrity +checks remain active; verification is not cached across untrusted build-script +or test execution. + Full Rust delivery remains incomplete. The locked `0.28.3` engine cannot govern the candidate semantic helper's Rust source and Cargo dependencies. Its admission, compilation and semantic integration remain required; disabling those checks is From 89b1ed9f5d16c4100e1d1302474e3c4a9984830d Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:37:33 +0000 Subject: [PATCH 37/49] Replace expired brace assessments with aged fixes Update only brace-expansion to 1.1.21 and 5.0.12 in both frozen JavaScript graphs. Remove the 12 obsolete assessments and regenerate the installed inventory. Frozen script-disabled bundle installation and the bundle contract pass; offline supply-chain verification retains three existing Cargo coverage failures. Full changed-scope verification remains pending. Record data-only inspection of all 188 semantic-helper registry packages and the publication-backed self-hosting upgrade requirement. No Rust helper compilation, policy-lock change or publication is performed. --- .code-polishy.json | 204 ------------------- docs/design/cargo-facts.md | 20 +- docs/plans/code-polishy-rust-support-plan.md | 25 ++- docs/security/dependency-assessments.md | 87 ++++---- providers/javascript/pnpm-lock.yaml | 16 +- tools/javascript/pnpm-lock.yaml | 16 +- tools/javascript_bundle_inventory.txt | 4 +- 7 files changed, 85 insertions(+), 287 deletions(-) diff --git a/.code-polishy.json b/.code-polishy.json index e0ff9de0..f2fb94ca 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -1867,210 +1867,6 @@ } ], "vulnerabilityAssessments": [ - { - "id": "tooling-brace-1-parse-comma-unreachable", - "ecosystem": "pnpm", - "advisory": "GHSA-6j4f-fj2g-mc7p", - "package": "brace-expansion", - "affectedVersion": "1.1.18", - "scope": "tools/javascript/pnpm-lock.yaml", - "severity": "high", - "status": "not-affected", - "basis": "unreachable", - "reason": "The tools linter passes only policy-owned extension patterns and fixed JSX control names to minimatch; repository paths and source are matcher candidates, never brace patterns.", - "impact": "Repository-controlled input cannot reach brace-expansion as a pattern, so the advisory payload cannot trigger.", - "evidence": "https://github.com/riteofstring/code-polishy/blob/main/docs/security/dependency-assessments.md", - "owner": "Codex", - "reviewed": "2026-10-01", - "expires": "2026-10-14" - }, - { - "id": "tooling-brace-1-nested-groups-unreachable", - "ecosystem": "pnpm", - "advisory": "GHSA-qhr7-859c-m2p7", - "package": "brace-expansion", - "affectedVersion": "1.1.18", - "scope": "tools/javascript/pnpm-lock.yaml", - "severity": "high", - "status": "not-affected", - "basis": "unreachable", - "reason": "The tools linter passes only policy-owned extension patterns and fixed JSX control names to minimatch; repository paths and source are matcher candidates, never brace patterns.", - "impact": "Repository-controlled input cannot reach brace-expansion as a pattern, so the advisory payload cannot trigger.", - "evidence": "https://github.com/riteofstring/code-polishy/blob/main/docs/security/dependency-assessments.md", - "owner": "Codex", - "reviewed": "2026-10-01", - "expires": "2026-10-14" - }, - { - "id": "tooling-brace-1-rewrite-complexity-unreachable", - "ecosystem": "pnpm", - "advisory": "GHSA-q2hr-2g5m-vwhr", - "package": "brace-expansion", - "affectedVersion": "1.1.18", - "scope": "tools/javascript/pnpm-lock.yaml", - "severity": "moderate", - "status": "not-affected", - "basis": "unreachable", - "reason": "The tools linter passes only policy-owned extension patterns and fixed JSX control names to minimatch; repository paths and source are matcher candidates, never brace patterns.", - "impact": "Repository-controlled input cannot reach brace-expansion as a pattern, so the advisory payload cannot trigger.", - "evidence": "https://github.com/riteofstring/code-polishy/blob/main/docs/security/dependency-assessments.md", - "owner": "Codex", - "reviewed": "2026-10-01", - "expires": "2026-10-14" - }, - { - "id": "tooling-brace-5-parse-comma-unreachable", - "ecosystem": "pnpm", - "advisory": "GHSA-6j4f-fj2g-mc7p", - "package": "brace-expansion", - "affectedVersion": "5.0.9", - "scope": "tools/javascript/pnpm-lock.yaml", - "severity": "high", - "status": "not-affected", - "basis": "unreachable", - "reason": "typescript-estree calls minimatch 10 only for parserOptions.projectService.allowDefaultProject; the tools parser configuration never enables projectService or supplies allowDefaultProject.", - "impact": "Repository-controlled input cannot reach brace-expansion as a pattern, so the advisory payload cannot trigger.", - "evidence": "https://github.com/riteofstring/code-polishy/blob/main/docs/security/dependency-assessments.md", - "owner": "Codex", - "reviewed": "2026-10-01", - "expires": "2026-10-14" - }, - { - "id": "tooling-brace-5-nested-groups-unreachable", - "ecosystem": "pnpm", - "advisory": "GHSA-qhr7-859c-m2p7", - "package": "brace-expansion", - "affectedVersion": "5.0.9", - "scope": "tools/javascript/pnpm-lock.yaml", - "severity": "high", - "status": "not-affected", - "basis": "unreachable", - "reason": "typescript-estree calls minimatch 10 only for parserOptions.projectService.allowDefaultProject; the tools parser configuration never enables projectService or supplies allowDefaultProject.", - "impact": "Repository-controlled input cannot reach brace-expansion as a pattern, so the advisory payload cannot trigger.", - "evidence": "https://github.com/riteofstring/code-polishy/blob/main/docs/security/dependency-assessments.md", - "owner": "Codex", - "reviewed": "2026-10-01", - "expires": "2026-10-14" - }, - { - "id": "tooling-brace-5-rewrite-complexity-unreachable", - "ecosystem": "pnpm", - "advisory": "GHSA-q2hr-2g5m-vwhr", - "package": "brace-expansion", - "affectedVersion": "5.0.9", - "scope": "tools/javascript/pnpm-lock.yaml", - "severity": "moderate", - "status": "not-affected", - "basis": "unreachable", - "reason": "typescript-estree calls minimatch 10 only for parserOptions.projectService.allowDefaultProject; the tools parser configuration never enables projectService or supplies allowDefaultProject.", - "impact": "Repository-controlled input cannot reach brace-expansion as a pattern, so the advisory payload cannot trigger.", - "evidence": "https://github.com/riteofstring/code-polishy/blob/main/docs/security/dependency-assessments.md", - "owner": "Codex", - "reviewed": "2026-10-01", - "expires": "2026-10-14" - }, - { - "id": "provider-brace-1-parse-comma-unreachable", - "ecosystem": "pnpm", - "advisory": "GHSA-6j4f-fj2g-mc7p", - "package": "brace-expansion", - "affectedVersion": "1.1.18", - "scope": "providers/javascript/pnpm-lock.yaml", - "severity": "high", - "status": "not-affected", - "basis": "unreachable", - "reason": "The provider linter passes only the fixed policy pattern **/*.* and fixed JSX control names to minimatch; repository paths and source are matcher candidates, never brace patterns.", - "impact": "Repository-controlled input cannot reach brace-expansion as a pattern, so the advisory payload cannot trigger.", - "evidence": "https://github.com/riteofstring/code-polishy/blob/main/docs/security/dependency-assessments.md", - "owner": "Codex", - "reviewed": "2026-10-01", - "expires": "2026-10-14" - }, - { - "id": "provider-brace-1-nested-groups-unreachable", - "ecosystem": "pnpm", - "advisory": "GHSA-qhr7-859c-m2p7", - "package": "brace-expansion", - "affectedVersion": "1.1.18", - "scope": "providers/javascript/pnpm-lock.yaml", - "severity": "high", - "status": "not-affected", - "basis": "unreachable", - "reason": "The provider linter passes only the fixed policy pattern **/*.* and fixed JSX control names to minimatch; repository paths and source are matcher candidates, never brace patterns.", - "impact": "Repository-controlled input cannot reach brace-expansion as a pattern, so the advisory payload cannot trigger.", - "evidence": "https://github.com/riteofstring/code-polishy/blob/main/docs/security/dependency-assessments.md", - "owner": "Codex", - "reviewed": "2026-10-01", - "expires": "2026-10-14" - }, - { - "id": "provider-brace-1-rewrite-complexity-unreachable", - "ecosystem": "pnpm", - "advisory": "GHSA-q2hr-2g5m-vwhr", - "package": "brace-expansion", - "affectedVersion": "1.1.18", - "scope": "providers/javascript/pnpm-lock.yaml", - "severity": "moderate", - "status": "not-affected", - "basis": "unreachable", - "reason": "The provider linter passes only the fixed policy pattern **/*.* and fixed JSX control names to minimatch; repository paths and source are matcher candidates, never brace patterns.", - "impact": "Repository-controlled input cannot reach brace-expansion as a pattern, so the advisory payload cannot trigger.", - "evidence": "https://github.com/riteofstring/code-polishy/blob/main/docs/security/dependency-assessments.md", - "owner": "Codex", - "reviewed": "2026-10-01", - "expires": "2026-10-14" - }, - { - "id": "provider-brace-5-parse-comma-unreachable", - "ecosystem": "pnpm", - "advisory": "GHSA-6j4f-fj2g-mc7p", - "package": "brace-expansion", - "affectedVersion": "5.0.9", - "scope": "providers/javascript/pnpm-lock.yaml", - "severity": "high", - "status": "not-affected", - "basis": "unreachable", - "reason": "typescript-estree calls minimatch 10 only for parserOptions.projectService.allowDefaultProject; the provider parser configuration never enables projectService or supplies allowDefaultProject.", - "impact": "Repository-controlled input cannot reach brace-expansion as a pattern, so the advisory payload cannot trigger.", - "evidence": "https://github.com/riteofstring/code-polishy/blob/main/docs/security/dependency-assessments.md", - "owner": "Codex", - "reviewed": "2026-10-01", - "expires": "2026-10-14" - }, - { - "id": "provider-brace-5-nested-groups-unreachable", - "ecosystem": "pnpm", - "advisory": "GHSA-qhr7-859c-m2p7", - "package": "brace-expansion", - "affectedVersion": "5.0.9", - "scope": "providers/javascript/pnpm-lock.yaml", - "severity": "high", - "status": "not-affected", - "basis": "unreachable", - "reason": "typescript-estree calls minimatch 10 only for parserOptions.projectService.allowDefaultProject; the provider parser configuration never enables projectService or supplies allowDefaultProject.", - "impact": "Repository-controlled input cannot reach brace-expansion as a pattern, so the advisory payload cannot trigger.", - "evidence": "https://github.com/riteofstring/code-polishy/blob/main/docs/security/dependency-assessments.md", - "owner": "Codex", - "reviewed": "2026-10-01", - "expires": "2026-10-14" - }, - { - "id": "provider-brace-5-rewrite-complexity-unreachable", - "ecosystem": "pnpm", - "advisory": "GHSA-q2hr-2g5m-vwhr", - "package": "brace-expansion", - "affectedVersion": "5.0.9", - "scope": "providers/javascript/pnpm-lock.yaml", - "severity": "moderate", - "status": "not-affected", - "basis": "unreachable", - "reason": "typescript-estree calls minimatch 10 only for parserOptions.projectService.allowDefaultProject; the provider parser configuration never enables projectService or supplies allowDefaultProject.", - "impact": "Repository-controlled input cannot reach brace-expansion as a pattern, so the advisory payload cannot trigger.", - "evidence": "https://github.com/riteofstring/code-polishy/blob/main/docs/security/dependency-assessments.md", - "owner": "Codex", - "reviewed": "2026-10-01", - "expires": "2026-10-14" - }, { "id": "provider-fast-uri-host-case-unreachable", "ecosystem": "pnpm", diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index 53d6a7c7..036a615e 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -213,11 +213,21 @@ The candidate uses Rust Analyzer crates `0.0.346`, the first inspected published line using Salsa `0.28`, with Salsa and its macro crates locked to `0.28.5`. Earlier candidates depend on the affected Salsa series in [RUSTSEC-2026-0308](https://rustsec.org/advisories/RUSTSEC-2026-0308.html). -The fixed release was published on 2026-09-24 and needs exact security-fix age -admission. The currently locked `0.27.11` schema does not support `crates.io` -release-age assessments. A separately authorized local beta policy upgrade is -required before admitting and compiling this candidate; no out-of-band age -waiver or alternate policy file replaces the locked assessment contract. +Registry inspection on 2026-10-15 found 188 registry packages in the candidate +lock, with matching published checksums and no yanked releases. Only Salsa and +its two macro crates remain younger than 30 days. All three become ordinarily +eligible by 2026-10-24 at 09:56:31 UTC. This data-only review neither compiles +the helper nor establishes complete dependency admission. + +The currently locked `0.28.3` engine lacks native Cargo governance and +`crates.io` release-age assessments. Its lock command requires an authenticated +HTTPS publication index and archive-backed release identity; a private source +installation alone cannot activate new governing policy. A published engine +with native Cargo enforcement and a separately authorized self-hosting lock +upgrade are required before admitting and compiling this candidate. Waiting for +the fixed dependencies to age avoids an early-admission exception; no demonstrated +deployed-helper exposure currently justifies one. An alternate policy file, +handwritten lock or disabled source check cannot replace that boundary. ## Compiler checks diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 7c1b07cd..785f242c 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -94,8 +94,9 @@ Full Rust delivery remains incomplete. The locked `0.28.3` engine cannot govern the candidate semantic helper's Rust source and Cargo dependencies. Its admission, compilation and semantic integration remain required; disabling those checks is not an acceptable bootstrap. Complete native dependency notices/inventory and -all-host execution also remain open. The current dependency-review run additionally reports expired -JavaScript vulnerability assessments. None of these failures is waived by the +all-host execution also remain open. The 2026-10-15 JavaScript dependency update +removes the expired brace-expansion assessments by adopting the now-aged fixes. +None of the remaining failures is waived by the optional installation boundary. The current [first-party language-pack plan](installable-first-party-language-packs.md) @@ -297,13 +298,21 @@ The candidate pins Rust Analyzer `0.0.346` and locks Salsa, `salsa-macros` and generation executed no build scripts, and the rejected vulnerable library was not compiled. Review and data-only preparation do not grant executable admission. -The outgoing `0.27.11` schema accepts release-age assessments only for +The outgoing `0.28.3` schema accepts release-age assessments only for `artifact`, `go`, `npm`, `pnpm` and `pypi`, not `crates.io`. Its source checks also -lack Rust analysis. A local beta policy-lock upgrade needs separate caller -authorization before the native Cargo security-fix assessment and helper build -can proceed. Target providers cannot replace the locked assessment contract. -Keep the current lock unchanged meanwhile. An upgrade would neither authorize -publication nor constitute completed Rust support. +lack Rust analysis. A private source install cannot provide the authenticated +HTTPS publication index and archive-backed identity required by the supported +lock upgrade. A published native-Cargo engine and a separately authorized +self-hosting lock upgrade must precede the helper build. Target providers cannot +replace the locked assessment contract. Keep the current lock unchanged meanwhile. + +The 2026-10-15 registry inspection covered all 188 locked registry packages: +checksums matched and none was yanked. Only the three Salsa packages remained +under 30 days; all are ordinarily eligible by 2026-10-24 at 09:56:31 UTC. +Waiting removes the age exception requirement. The helper is not deployed, so +the current evidence does not establish that an early security-fix admission +is safer than waiting. This metadata inspection is not executable admission, +successful compilation or a replacement for Cargo security checks. After admission, compile and test the helper before integrating it. Complete standard-library and attribute/derive expansion, source/configuration ownership, diff --git a/docs/security/dependency-assessments.md b/docs/security/dependency-assessments.md index c012b234..45ae2d77 100644 --- a/docs/security/dependency-assessments.md +++ b/docs/security/dependency-assessments.md @@ -1,6 +1,6 @@ # Dependency vulnerability assessments -Brace expansion reviewed on 2026-10-01; other JavaScript and Go module findings +Brace expansion resolved with aged fixes on 2026-10-15; other JavaScript and Go module findings reviewed on 2026-10-08; Go toolchain admission reviewed on 2026-10-10. Assessment owner: Codex. @@ -106,57 +106,40 @@ version or weaken vulnerability scanning. ## Brace expansion -Retain `brace-expansion` 1.1.18 and 5.0.9 in both graphs. The advisories require -an attacker-controlled string to reach the brace expansion pattern argument: - -| Advisory | Vulnerable behavior | Fixed 1.x / 5.x releases | -| ------------------------------------------------------------------------ | ---------------------------------------------------------------------- | ------------------------ | -| [GHSA-6j4f-fj2g-mc7p](https://github.com/advisories/GHSA-6j4f-fj2g-mc7p) | `parseCommaParts()` recursion or argument expansion exhausts the stack | 1.1.19 / 5.0.10 | -| [GHSA-qhr7-859c-m2p7](https://github.com/advisories/GHSA-qhr7-859c-m2p7) | Deeply nested brace groups exhaust `expand_()`'s stack | 1.1.20 / 5.0.11 | -| [GHSA-q2hr-2g5m-vwhr](https://github.com/advisories/GHSA-q2hr-2g5m-vwhr) | The `{a},b}` rewrite consumes quadratic CPU time | 1.1.21 / 5.0.12 | - -All three exploit the pattern, not the filename being matched. Existing -expansion-result limits do not prevent these parsing attacks; the assessment -rests on input authority and disabled call paths instead. - -### Version 1.1.18 - -ESLint 9.39.5 and `@eslint/config-array` 0.21.2 reach `minimatch` 3.1.5, -which depends on this version. The tooling's `lintConfiguration` in -[`runner.mjs`](../../tools/javascript/runner.mjs) generates its file pattern -from a policy-owned extension allowlist. The provider's `lintMessages` in -[`analysis.mjs`](../../providers/javascript/analysis.mjs) uses `**/*.*`. -Repository-controlled source and paths are candidate text and filenames, -not configuration patterns. Both configurations disable inline configuration. - -`eslint-plugin-jsx-a11y` 6.10.2 also uses `minimatch` through -`mayContainChildComponent`. The enabled label rule supplies fixed component -names; [`policy.mjs`](../../tools/javascript/policy.mjs) does not accept -repository-selected component patterns or rule options. A JSX component name -from source is the match candidate, not the pattern. - -Knip 5.55.1 accepts workspace patterns but its `ConfigurationChief` and glob -utilities use `picomatch` and `fast-glob`. That path does not establish a call -to `brace-expansion`. Neither sealed analyzer loads target ESLint or Knip -configuration. - -### Version 5.0.9 - -`@typescript-eslint/typescript-estree` 8.64.0 depends on `minimatch` 10.2.x, -which resolves this brace-expansion version. Its -`useProgramFromProjectService.js` calls `minimatch` from -`filePathMatchedBy` only for `allowDefaultProject` patterns. Both analyzer -configurations above omit `projectService` and `allowDefaultProject`. -Importing the parser can load the matcher; it does not enable this call path. - -### Remediation - -Update to 1.1.21 and 5.0.12 on or after 2026-10-14 at 21:59:29 UTC, when -both exact releases have aged for 30 days. The assessments expire at the end -of that UTC date. Reassess immediately if analyzer configuration becomes -repository-controlled, a rule starts accepting component patterns, or a -parser project service is enabled. Remove the exact assessments when their -affected versions leave the locks. +Both JavaScript graphs now resolve `brace-expansion` 1.1.21 and 5.0.12. +These are the smallest releases in the existing major lines that address all +three assessed parsing advisories: + +| Advisory | Vulnerable behavior | Fixed 1.x / 5.x releases | +| ------------------------------------------------------------------------ | ---------------------------------------------------------------- | ------------------------ | +| [GHSA-6j4f-fj2g-mc7p](https://github.com/advisories/GHSA-6j4f-fj2g-mc7p) | Recursive comma parsing or argument expansion exhausts the stack | 1.1.19 / 5.0.10 | +| [GHSA-qhr7-859c-m2p7](https://github.com/advisories/GHSA-qhr7-859c-m2p7) | Nested brace expansion exhausts the stack | 1.1.20 / 5.0.11 | +| [GHSA-q2hr-2g5m-vwhr](https://github.com/advisories/GHSA-q2hr-2g5m-vwhr) | Brace rewrite repeatedly rescans growing input | 1.1.21 / 5.0.12 | + +The [npm registry](https://registry.npmjs.org/brace-expansion) records 1.1.21 +at 2026-09-14 21:59:28.332 UTC and 5.0.12 at 21:59:00.288 UTC. Both meet +the ordinary 30-day minimum. No release-age admission is needed. The 12 exact +assessments for 1.1.18 and 5.0.9 have been removed with those versions from +`tools/javascript/pnpm-lock.yaml` and `providers/javascript/pnpm-lock.yaml`. + +The prior non-exposure conclusion still matches the inspected callers: native +ESLint uses policy-owned extension patterns, the provider uses `**/*.*`, JSX +rules receive fixed component patterns, and neither parser enables +`projectService.allowDefaultProject`. Repository filenames are match candidates, +not brace-expansion patterns. Knip's repository-derived glob patterns use the +separate fast-glob/picomatch path and retain their existing bounded-input checks. +The update closes the dependency findings now that an aged fix is available; +it does not remove these input boundaries. + +Registry-integrity-verified old and new archives show iterative comma parsing, +element-wise appends, and bounded nesting/rewrite counts. Existing dependency +requirements remain unchanged; neither release adds an install lifecycle script. +The 5.x maintainer-only script list adds `format:check`. Candidate locks change +only the two brace-expansion resolutions, without overrides, new dependencies, +or direct-pin changes. Frozen installation keeps lifecycle execution disabled. +Official provenance and age reduce admission uncertainty without proving the +absence of upstream compromise or regressions. Ordinary analyzer and provider +contracts remain the compatibility evidence. ## URI processing diff --git a/providers/javascript/pnpm-lock.yaml b/providers/javascript/pnpm-lock.yaml index fccd52e0..102e9844 100644 --- a/providers/javascript/pnpm-lock.yaml +++ b/providers/javascript/pnpm-lock.yaml @@ -362,11 +362,11 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - brace-expansion@1.1.18: - resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} + brace-expansion@1.1.21: + resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==} - brace-expansion@5.0.9: - resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} engines: {node: 20 || >=22} braces@3.0.3: @@ -1748,12 +1748,12 @@ snapshots: balanced-match@4.0.4: {} - brace-expansion@1.1.18: + brace-expansion@1.1.21: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.9: + brace-expansion@5.0.12: dependencies: balanced-match: 4.0.4 @@ -2407,11 +2407,11 @@ snapshots: minimatch@10.2.6: dependencies: - brace-expansion: 5.0.9 + brace-expansion: 5.0.12 minimatch@3.1.5: dependencies: - brace-expansion: 1.1.18 + brace-expansion: 1.1.21 minimist@1.2.8: {} diff --git a/tools/javascript/pnpm-lock.yaml b/tools/javascript/pnpm-lock.yaml index 253d4f5e..2c73cde4 100644 --- a/tools/javascript/pnpm-lock.yaml +++ b/tools/javascript/pnpm-lock.yaml @@ -224,11 +224,11 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - brace-expansion@1.1.18: - resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} + brace-expansion@1.1.21: + resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==} - brace-expansion@5.0.9: - resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} engines: {node: 20 || >=22} braces@3.0.3: @@ -1237,12 +1237,12 @@ snapshots: balanced-match@4.0.4: {} - brace-expansion@1.1.18: + brace-expansion@1.1.21: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.9: + brace-expansion@5.0.12: dependencies: balanced-match: 4.0.4 @@ -1859,11 +1859,11 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.9 + brace-expansion: 5.0.12 minimatch@3.1.5: dependencies: - brace-expansion: 1.1.18 + brace-expansion: 1.1.21 minimist@1.2.8: {} diff --git a/tools/javascript_bundle_inventory.txt b/tools/javascript_bundle_inventory.txt index f3c78d3a..65cd33fc 100644 --- a/tools/javascript_bundle_inventory.txt +++ b/tools/javascript_bundle_inventory.txt @@ -50,8 +50,8 @@ axe-core@4.12.1 MPL-2.0 axobject-query@4.1.0 Apache-2.0 balanced-match@1.0.2 MIT balanced-match@4.0.4 MIT -brace-expansion@1.1.18 MIT -brace-expansion@5.0.9 MIT +brace-expansion@1.1.21 MIT +brace-expansion@5.0.12 MIT braces@3.0.3 MIT call-bind-apply-helpers@1.0.2 MIT call-bind@1.0.9 MIT From 8dede4b6a7711287511218e537f77ce584144ac4 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:57:23 +0000 Subject: [PATCH 38/49] Adopt aged fast-uri fix before assessment expiry Resolve fast-uri 3.1.8 in the provider lock and remove the 3.1.7 host-case assessment. Registry and integrity-verified source review confirm ordinary age eligibility, no new dependencies and no install lifecycle scripts. Frozen installation passes; online and offline reviews retain only the existing three Rust-helper coverage errors. Focused provider verification is running. --- .code-polishy.json | 17 -------------- docs/security/dependency-assessments.md | 31 +++++++++++-------------- providers/javascript/pnpm-lock.yaml | 8 +++---- 3 files changed, 18 insertions(+), 38 deletions(-) diff --git a/.code-polishy.json b/.code-polishy.json index f2fb94ca..f5cf222c 100644 --- a/.code-polishy.json +++ b/.code-polishy.json @@ -1867,23 +1867,6 @@ } ], "vulnerabilityAssessments": [ - { - "id": "provider-fast-uri-host-case-unreachable", - "ecosystem": "pnpm", - "advisory": "GHSA-hrr3-gc8f-f4qj", - "package": "fast-uri", - "affectedVersion": "3.1.7", - "scope": "providers/javascript/pnpm-lock.yaml", - "severity": "moderate", - "status": "not-affected", - "basis": "unreachable", - "reason": "The provider imports Astro compiler and TypeScript services directly; it never imports the YAML language-server plugin that reaches Ajv and fast-uri.", - "impact": "No repository-controlled URI reaches fast-uri.", - "evidence": "https://github.com/riteofstring/code-polishy/blob/main/docs/security/dependency-assessments.md", - "owner": "Codex", - "reviewed": "2026-10-08", - "expires": "2026-10-15" - }, { "id": "tooling-glob-depth-unreachable", "ecosystem": "pnpm", diff --git a/docs/security/dependency-assessments.md b/docs/security/dependency-assessments.md index 45ae2d77..ec945afa 100644 --- a/docs/security/dependency-assessments.md +++ b/docs/security/dependency-assessments.md @@ -1,6 +1,6 @@ # Dependency vulnerability assessments -Brace expansion resolved with aged fixes on 2026-10-15; other JavaScript and Go module findings +Brace expansion and URI processing resolved with aged fixes on 2026-10-15; other JavaScript and Go module findings reviewed on 2026-10-08; Go toolchain admission reviewed on 2026-10-10. Assessment owner: Codex. @@ -143,13 +143,13 @@ contracts remain the compatibility evidence. ## URI processing -The JavaScript provider graph now resolves `fast-uri` 3.1.7. This official -release fixes the malformed-authority and port-injection advisories below; -their exact 3.1.6 assessments have been removed. The +The JavaScript provider graph now resolves `fast-uri` 3.1.8. This official +release retains the malformed-authority and port-injection fixes and corrects +the remaining host-case advisory. All exact fast-uri assessments have been +removed. The [npm registry](https://registry.npmjs.org/fast-uri) records publication at -2026-09-02 11:06:41.962 UTC, so it meets the 30-day minimum. The lock changes +2026-09-15 07:36:25.444 UTC, so it meets the 30-day minimum. The lock changes only this transitive dependency, without adding lifecycle scripts or overrides. -The remaining host-case advisory is assessed against 3.1.7. | Advisory | Required application behavior | Fixed release | | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | ------------- | @@ -172,17 +172,14 @@ service, which reaches Ajv through `yamlSchemaService.js`; Ajv's `dist/runtime/uri.js` imports `fast-uri`. None of those services is enabled by the provider's checker construction. -### Remediation - -Retain the 3.1.7 host-case assessment with its existing 2026-10-15 expiry. -The official 3.1.8 fix was published on 2026-09-15 at 07:36:25.444 UTC and is -still under 30 days old at this review. With no enabled call to the affected -API or host authorization decision, waiting does not expose this provider to -the advisory; admitting the fresh release would add avoidable supply-chain -risk. Update to 3.1.8 on or after 2026-10-15 at 07:36:26 UTC and remove the -remaining assessment, which expires at the end of that UTC date. Reassess -immediately if the YAML service, full Astro language server, or a new URI -consumer is enabled. +The integrity-verified archive delta normalizes percent-encoded host octets +before folding the decoded reg-name to lowercase, then restores uppercase +escape digits. It also moves host normalization outside scheme-specific path +normalization. Neither release declares dependencies or install lifecycle +scripts. The update follows the original dated remediation plan now that the +fix is aged; no early-admission or replacement vulnerability assessment is +needed. Enabling the YAML service, full Astro language server or another URI +consumer still requires a new applicability review of any future advisories. ## Aged security updates diff --git a/providers/javascript/pnpm-lock.yaml b/providers/javascript/pnpm-lock.yaml index 102e9844..0262e78f 100644 --- a/providers/javascript/pnpm-lock.yaml +++ b/providers/javascript/pnpm-lock.yaml @@ -585,8 +585,8 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-uri@3.1.7: - resolution: {integrity: sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==} + fast-uri@3.1.8: + resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1658,7 +1658,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.7 + fast-uri: 3.1.8 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -2079,7 +2079,7 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-uri@3.1.7: {} + fast-uri@3.1.8: {} fastq@1.20.1: dependencies: From 48b4668f92f7ba26435826247a605a6b1a7d5f02 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:13:55 +0000 Subject: [PATCH 39/49] Record dependency recovery and remaining Rust governance blockers --- docs/plans/code-polishy-rust-support-plan.md | 28 +++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 785f242c..41411ea5 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -74,8 +74,8 @@ formatting, diagnostics, native tests, doctests, failure outcomes and repair. Its private Linux x64 artifact has release digest `424f856fa177a57d978660f0c28181262d22caa5fc8d137dc5a747a680d6d953`. Across the original run and focused correction, 37 of the 38 selected suites -passed. The verification command still fails its 12 expired vulnerability -assessments. A diagnostic rerun identified the JavaScript fixture's missing cached +passed. At that checkpoint, the verification command also failed 12 expired +vulnerability assessments. A diagnostic rerun identified the JavaScript fixture's missing cached `@types/node@24.13.3` archive; this run does not establish a source defect. Checkpoint validation still reports unsupported semantic-helper coverage, Cargo dependency coverage, architecture imports and expired JavaScript @@ -90,12 +90,32 @@ measurement, excluding core verification and the inventory walk. Integrity checks remain active; verification is not cached across untrusted build-script or test execution. +JavaScript dependency verification recovered on 2026-10-15. Commit `89b1ed9` updates both +JavaScript graphs to aged brace-expansion 1.1.21 and 5.0.12, removes the 12 +expired assessments, regenerates the installed inventory and restores the +frozen offline cache. All 38 changed-scope suites passed at that commit, +including the installed optional Rust lifecycle. The private Linux x64 artifact +has release digest +`9b9516ab1ee818a8763f87a978f236e4bd2fd3f7bfa6c992da786172ae76434b`. +Commit `8dede4b` additionally updates only the provider's fast-uri resolution to +aged 3.1.8 and removes its assessment before expiry. All 39 focused provider +cases passed against that final lock. The other suites remain evidence for +`89b1ed9`; the provider-only follow-up did not repeat the full run. +Online dependency review and installed offline supply-chain verification at the +final dependency state report only the three existing Cargo coverage failures. +Formatting passes. No assessment was extended and no release-age exemption was +introduced. See the [dependency record](../security/dependency-assessments.md). +The final clean-checkout checkpoint at `8dede4b` remains rejected: 59 errors +all concern the semantic helper's unsupported Rust source, imports or Cargo +governance. It also retains 12 existing file-length warnings. The dependency +cleanup does not establish checkpoint acceptance or authorize a release. + Full Rust delivery remains incomplete. The locked `0.28.3` engine cannot govern the candidate semantic helper's Rust source and Cargo dependencies. Its admission, compilation and semantic integration remain required; disabling those checks is not an acceptable bootstrap. Complete native dependency notices/inventory and -all-host execution also remain open. The 2026-10-15 JavaScript dependency update -removes the expired brace-expansion assessments by adopting the now-aged fixes. +all-host execution also remain open. The 2026-10-15 JavaScript dependency updates +remove the brace-expansion and fast-uri assessments by adopting the now-aged fixes. None of the remaining failures is waived by the optional installation boundary. From 3b229f67bed156111f4171791e9355a81966df37 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:44:21 +0000 Subject: [PATCH 40/49] Carry authenticated Nextest source notices with explicit inventory gaps --- docs/design/installed-release-verification.md | 18 +- docs/plans/code-polishy-rust-support-plan.md | 39 +- internal/release/rust_sbom.go | 2 +- internal/release/rust_sbom_test.go | 1 + tools/cargo-nextest-license.txt | 14805 ++++++++++++++++ 5 files changed, 14854 insertions(+), 11 deletions(-) diff --git a/docs/design/installed-release-verification.md b/docs/design/installed-release-verification.md index 30dcd915..ac1e20cb 100644 --- a/docs/design/installed-release-verification.md +++ b/docs/design/installed-release-verification.md @@ -91,10 +91,20 @@ rules. An older engine does not execute a version 7 release. The SDK retains upstream component licenses and copyright inventories; parser wheels retain their distribution license files. Nextest's binary-only archive -requires a separately carried upstream license. Its pinned MIT text is recorded -with the corresponding source revision. That top-level notice does not establish -complete third-party notices or an exact linked-dependency inventory for Nextest; -those remain a publication requirement alongside native installed-Rust evidence. +requires separately carried upstream notices. The existing Nextest license file +retains its pinned MIT text and a deduplicated source notice inventory for the +362 registry packages in the union of the five supported targets' default-feature +normal and build dependency closures. Registry archive checksums come from the +exact upstream source lock; supplemental notice URLs name the immutable revisions +recorded in the crates' VCS metadata. Numbered shared texts preserve each package's +source location without repeating identical license bodies. The file is bounded +at 2 MiB and remains in the authenticated core alongside SDK notices. + +The inventory explicitly identifies five packages with missing notice evidence. +It is not an exact linked-binary graph: the upstream build recipe does not request +locked resolution, and neither its release assets nor the inspected Linux binary +provide a dependency attestation. Complete native notice and linked-dependency +evidence remain publication requirements alongside native installed-Rust evidence. The SBOM links each Rust tool to the selected host's reviewed upstream archive URLs and SHA-256 identities, reusing the installer's checksum inventory. Archive diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 41411ea5..9aa1fc04 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -570,15 +570,42 @@ No beta release has been published or adopted. The Nextest source audit at `fc97e97bbe0a3927482a694247da00c099f4269e` found 580 locked packages: 571 crates.io archives and 9 workspace packages, -including upstream test fixtures. All 571 archive checksums matched on -2026-09-25. A recursive notice-filename inventory found 979 files, but 45 crates -had no packaged license/notice/copyright files under those names. This is a -source-inventory observation, not proof of the linked binary graph or license -compliance. Completing the notice set requires exact upstream source evidence; -copying the lock or substituting generic license text does not resolve it. +including upstream test fixtures. All 571 archive checksums were reverified on +2026-10-15. Data-only, frozen, offline Cargo resolution selected the binary's +default-feature normal and build dependencies for all five supported targets. +Their union contains 362 registry packages and five Nextest workspace packages. +The per-target registry counts are 334 for Linux x64, 329 for Linux arm64, +338 for Windows x64, 329 for macOS x64 and 333 for macOS arm64. These commands +executed no dependency build scripts or procedural macros. + +`tools/cargo-nextest-license.txt` now carries 642 packaged notice files from +that source closure and 18 supplemental upstream files covering 14 packages. +Supplemental URLs use the immutable revisions in the authenticated crates' VCS +metadata. Deduplication and whitespace normalization retain 226 distinct texts +and every package's source reference. The complete file is 749,595 bytes, about +87 KB with gzip; the existing installers already carry and authenticate it. +The SBOM reader accepts this bounded inventory up to 2 MiB. + +Five packages still lack sufficient notice evidence: `async-scoped@0.9.0`, +`backtrace-ext@0.2.1`, `guppy-workspace-hack@0.1.0`, `recursion@0.5.4` and +`sigchld@0.2.4`. Their declarations are retained as metadata, never substituted +for missing notices. The source inventory is not proof of the linked binary +graph or license compliance. The [upstream release workflow](https://github.com/nextest-rs/nextest/blob/fc97e97bbe0a3927482a694247da00c099f4269e/.github/workflows/release.yml) +does not request locked resolution from its pinned upload action; that +[action defaults `locked` to false](https://github.com/taiki-e/upload-rust-binary-action/blob/f0d45ae91ee7b8ee928de7a9d04d893a08bcbec6/action.yml). +The release has no dependency SBOM asset, and the inspected Linux x64 binary +has no cargo-auditable dependency section. Thus even this frozen source closure +cannot authenticate the actual binary's dependency versions. Rust and Nextest SBOM entries retain explicit incomplete native compositions, host-specific authenticated download references and carried notice locations. +Bootstrap sequencing remains unresolved at the publication boundary: the native +Cargo engine must be published and adopted before the draft semantic helper can +be admitted under it. This does not authorize excluding required source from +governance, changing the lock by hand, or describing the draft as complete. +The helper remains in the branch with its coverage failures visible. No native +publication or self-hosting upgrade has occurred. + The next milestones extend ordinary macro/module coverage and complete installed-release integration. Existing coverage failures remain until their implementations and evidence exist. This beta does not yet establish diff --git a/internal/release/rust_sbom.go b/internal/release/rust_sbom.go index e32accc5..4afc5b16 100644 --- a/internal/release/rust_sbom.go +++ b/internal/release/rust_sbom.go @@ -158,7 +158,7 @@ func rustReleaseNotices(root *os.Root, manifest Manifest, name string) ([]cdx.Pr } func nextestReleaseSource(root *os.Root, version string) (string, error) { - data, err := readCapabilityFile(root, "tools/cargo-nextest-license.txt", 32<<10) + data, err := readCapabilityFile(root, "tools/cargo-nextest-license.txt", 2<<20) if err != nil { return "", err } diff --git a/internal/release/rust_sbom_test.go b/internal/release/rust_sbom_test.go index b94ab7c6..e1d4409d 100644 --- a/internal/release/rust_sbom_test.go +++ b/internal/release/rust_sbom_test.go @@ -140,6 +140,7 @@ func TestReleaseSBOMRejectsMissingOrInconsistentNativeEvidence(t *testing.T) { {name: "oversized inventory", path: "tools/rust_checksums.txt", change: func(string) string { return strings.Repeat(" ", 33<<10) }, problem: "exceeds"}, {name: "missing nextest notice", path: "tools/cargo-nextest-license.txt", omit: true, problem: "notice is absent"}, {name: "empty nextest notice", path: "tools/cargo-nextest-license.txt", change: func(string) string { return "" }, problem: "nonempty"}, + {name: "oversized nextest notice", path: "tools/cargo-nextest-license.txt", change: func(string) string { return strings.Repeat(" ", (2<<20)+1) }, problem: "exceeds"}, {name: "wrong notice version", path: "tools/cargo-nextest-license.txt", change: func(value string) string { return strings.ReplaceAll(value, "0.9.138", "0.9.137") }, problem: "exact release"}, {name: "mutable notice source", path: "tools/cargo-nextest-license.txt", change: func(value string) string { return strings.ReplaceAll(value, "fc97e97bbe0a3927482a694247da00c099f4269e", "main") diff --git a/tools/cargo-nextest-license.txt b/tools/cargo-nextest-license.txt index 6699e0fe..4eb7349d 100644 --- a/tools/cargo-nextest-license.txt +++ b/tools/cargo-nextest-license.txt @@ -26,3 +26,14808 @@ 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. + +NEXTEST SOURCE NOTICE INVENTORY (INCOMPLETE) + +Source: https://github.com/nextest-rs/nextest/blob/fc97e97bbe0a3927482a694247da00c099f4269e/Cargo.lock + +Scope: the union of cargo-nextest's default-feature normal and build dependency +closures resolved from this source lock for x86_64-unknown-linux-gnu, +aarch64-unknown-linux-gnu, x86_64-pc-windows-msvc, x86_64-apple-darwin and +aarch64-apple-darwin. This inventory covers 362 registry packages. The five +Nextest workspace packages use the upstream license above. + +This is a source dependency inventory, not an attestation of the dependencies +linked into the downloaded binaries. It excludes development-only and inactive +locked packages. It does not establish complete native-tool notices or license +compliance. The native SBOM composition remains incomplete. Five packages lack +notice evidence: async-scoped 0.9.0, backtrace-ext 0.2.1, guppy-workspace-hack +0.1.0, recursion 0.5.4 and sigchld 0.2.4. + +Archive notices were read from the registry archives authenticated against the +source lock. Supplemental files come from immutable upstream revisions recorded +in each crate's .cargo_vcs_info.json. Exact duplicate texts share one numbered +notice; every package retains its source location. Line endings and trailing +whitespace are normalized. Declared license expressions +are package metadata, not replacement license texts or copyright notices. + +PACKAGE INDEX + +Package: addr2line 0.24.2 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/addr2line/addr2line-0.24.2.crate +Archive SHA-256: dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1 + Notice 001: archive file LICENSE-APACHE + Notice 002: archive file LICENSE-MIT + +Package: adler2 2.0.1 +Declared license: 0BSD OR MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/adler2/adler2-2.0.1.crate +Archive SHA-256: 320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa + Notice 003: archive file LICENSE-0BSD + Notice 004: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: ahash 0.8.12 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/ahash/ahash-0.8.12.crate +Archive SHA-256: 5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75 + Notice 001: archive file LICENSE-APACHE + Notice 006: archive file LICENSE-MIT + +Package: aho-corasick 1.1.4 +Declared license: Unlicense OR MIT +Archive: https://static.crates.io/crates/aho-corasick/aho-corasick-1.1.4.crate +Archive SHA-256: ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301 + Notice 007: archive file COPYING + Notice 008: archive file LICENSE-MIT + Notice 009: archive file UNLICENSE + +Package: allocator-api2 0.2.21 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/allocator-api2/allocator-api2-0.2.21.crate +Archive SHA-256: 683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: anstream 1.0.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/anstream/anstream-1.0.0.crate +Archive SHA-256: 824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: anstyle 1.0.14 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/anstyle/anstyle-1.0.14.crate +Archive SHA-256: 940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000 + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: anstyle-parse 1.0.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/anstyle-parse/anstyle-parse-1.0.0.crate +Archive SHA-256: 52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: anstyle-query 1.1.2 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/anstyle-query/anstyle-query-1.1.2.crate +Archive SHA-256: 79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: anstyle-wincon 3.0.8 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/anstyle-wincon/anstyle-wincon-3.0.8.crate +Archive SHA-256: 6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa + Notice 011: archive file LICENSE-APACHE + Notice 013: archive file LICENSE-MIT + +Package: anyhow 1.0.98 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/anyhow/anyhow-1.0.98.crate +Archive SHA-256: e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: async-scoped 0.9.0 +Declared license: Apache-2.0/MIT +Archive: https://static.crates.io/crates/async-scoped/async-scoped-0.9.0.crate +Archive SHA-256: 4042078ea593edffc452eef14e99fdb2b120caa4ad9618bcdeabc4a023b98740 + Notice evidence: unavailable; declared license is not a substitute. + +Package: atomic 0.6.1 +Declared license: Apache-2.0/MIT +Archive: https://static.crates.io/crates/atomic/atomic-0.6.1.crate +Archive SHA-256: a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340 + Notice 001: archive file LICENSE-APACHE + Notice 014: archive file LICENSE-MIT + +Package: atomicwrites 0.4.4 +Declared license: MIT +Archive: https://static.crates.io/crates/atomicwrites/atomicwrites-0.4.4.crate +Archive SHA-256: 3ef1bb8d1b645fe38d51dfc331d720fb5fc2c94b440c76cc79c80ff265ca33e3 + Notice 015: archive file LICENSE + +Package: autocfg 1.5.0 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/autocfg/autocfg-1.5.0.crate +Archive SHA-256: c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8 + Notice 001: archive file LICENSE-APACHE + Notice 016: archive file LICENSE-MIT + +Package: aws-lc-rs 1.16.2 +Declared license: ISC AND (Apache-2.0 OR ISC) +Archive: https://static.crates.io/crates/aws-lc-rs/aws-lc-rs-1.16.2.crate +Archive SHA-256: a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc + Notice 017: archive file LICENSE + +Package: aws-lc-sys 0.39.0 +Declared license: ISC AND (Apache-2.0 OR ISC) AND Apache-2.0 AND MIT AND BSD-3-Clause AND (Apache-2.0 OR ISC OR MIT) AND (Apache-2.0 OR ISC OR MIT-0) +Archive: https://static.crates.io/crates/aws-lc-sys/aws-lc-sys-0.39.0.crate +Archive SHA-256: 1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a + Notice 018: archive file LICENSE + Notice 018: archive file aws-lc/LICENSE + Notice 019: archive file aws-lc/third_party/fiat/LICENSE + +Package: backtrace 0.3.75 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/backtrace/backtrace-0.3.75.crate +Archive SHA-256: 6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002 + Notice 001: archive file LICENSE-APACHE + Notice 020: archive file LICENSE-MIT + +Package: backtrace-ext 0.2.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/backtrace-ext/backtrace-ext-0.2.1.crate +Archive SHA-256: 537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50 + Notice evidence: unavailable; declared license is not a substitute. + +Package: base64 0.22.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/base64/base64-0.22.1.crate +Archive SHA-256: 72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6 + Notice 001: archive file LICENSE-APACHE + Notice 021: archive file LICENSE-MIT + +Package: bit-set 0.5.3 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/bit-set/bit-set-0.5.3.crate +Archive SHA-256: 0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1 + Notice 022: archive file LICENSE-APACHE + Notice 014: archive file LICENSE-MIT + +Package: bit-set 0.8.0 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/bit-set/bit-set-0.8.0.crate +Archive SHA-256: 08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3 + Notice 022: archive file LICENSE-APACHE + Notice 023: archive file LICENSE-MIT + +Package: bit-vec 0.6.3 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/bit-vec/bit-vec-0.6.3.crate +Archive SHA-256: 349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb + Notice 022: archive file LICENSE-APACHE + Notice 024: archive file LICENSE-MIT + +Package: bit-vec 0.8.0 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/bit-vec/bit-vec-0.8.0.crate +Archive SHA-256: 5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7 + Notice 022: archive file LICENSE-APACHE + Notice 023: archive file LICENSE-MIT + +Package: bitflags 1.3.2 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/bitflags/bitflags-1.3.2.crate +Archive SHA-256: bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a + Notice 001: archive file LICENSE-APACHE + Notice 025: archive file LICENSE-MIT + +Package: bitflags 2.11.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/bitflags/bitflags-2.11.1.crate +Archive SHA-256: c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3 + Notice 001: archive file LICENSE-APACHE + Notice 025: archive file LICENSE-MIT + +Package: block-buffer 0.10.4 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/block-buffer/block-buffer-0.10.4.crate +Archive SHA-256: 3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71 + Notice 026: archive file LICENSE-APACHE + Notice 027: archive file LICENSE-MIT + +Package: bstr 1.12.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/bstr/bstr-1.12.1.crate +Archive SHA-256: 63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab + Notice 028: archive file COPYING + Notice 001: archive file LICENSE-APACHE + Notice 029: archive file LICENSE-MIT + +Package: bytemuck 1.24.0 +Declared license: Zlib OR Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/bytemuck/bytemuck-1.24.0.crate +Archive SHA-256: 1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4 + Notice 030: archive file LICENSE-APACHE + Notice 031: archive file LICENSE-MIT + Notice 032: archive file LICENSE-ZLIB + +Package: byteorder 1.5.0 +Declared license: Unlicense OR MIT +Archive: https://static.crates.io/crates/byteorder/byteorder-1.5.0.crate +Archive SHA-256: 1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b + Notice 007: archive file COPYING + Notice 008: archive file LICENSE-MIT + Notice 009: archive file UNLICENSE + +Package: bytes 1.11.1 +Declared license: MIT +Archive: https://static.crates.io/crates/bytes/bytes-1.11.1.crate +Archive SHA-256: 1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33 + Notice 033: archive file LICENSE + +Package: bytesize 2.3.1 +Declared license: Apache-2.0 +Archive: https://static.crates.io/crates/bytesize/bytesize-2.3.1.crate +Archive SHA-256: 6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3 + Notice 011: archive file LICENSE + +Package: camino 1.2.2 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/camino/camino-1.2.2.crate +Archive SHA-256: e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48 + Notice 001: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: camino-tempfile 1.4.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/camino-tempfile/camino-tempfile-1.4.1.crate +Archive SHA-256: 64308c4c82a5c38679945ddf88738dc1483dcc563bbb5780755ae9f8497d2b20 + Notice 001: archive file LICENSE-APACHE + Notice 034: archive file LICENSE-MIT + +Package: cargo-platform 0.3.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/cargo-platform/cargo-platform-0.3.0.crate +Archive SHA-256: 8abf5d501fd757c2d2ee78d0cc40f606e92e3a63544420316565556ed28485e2 + Notice 004: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: cargo_metadata 0.23.1 +Declared license: MIT +Archive: https://static.crates.io/crates/cargo_metadata/cargo_metadata-0.23.1.crate +Archive SHA-256: ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9 + Notice 005: archive file LICENSE-MIT + +Package: cc 1.2.30 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/cc/cc-1.2.30.crate +Archive SHA-256: deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7 + Notice 001: archive file LICENSE-APACHE + Notice 020: archive file LICENSE-MIT + +Package: cfg-expr 0.20.6 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/cfg-expr/cfg-expr-0.20.6.crate +Archive SHA-256: 78cef5b5a1a6827c7322ae2a636368a573006b27cfa76c7ebd53e834daeaab6a + Notice 022: archive file LICENSE-APACHE + Notice 035: archive file LICENSE-MIT + +Package: cfg-if 1.0.4 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/cfg-if/cfg-if-1.0.4.crate +Archive SHA-256: 9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801 + Notice 001: archive file LICENSE-APACHE + Notice 020: archive file LICENSE-MIT + +Package: cfg_aliases 0.2.1 +Declared license: MIT +Archive: https://static.crates.io/crates/cfg_aliases/cfg_aliases-0.2.1.crate +Archive SHA-256: 613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724 + Notice 036: archive file LICENSE + +Package: chacha20 0.10.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/chacha20/chacha20-0.10.0.crate +Archive SHA-256: 6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601 + Notice 026: archive file LICENSE-APACHE + Notice 037: archive file LICENSE-MIT + +Package: chrono 0.4.45 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/chrono/chrono-0.4.45.crate +Archive SHA-256: 1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327 + Notice 038: archive file LICENSE.txt + +Package: clap 4.6.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/clap/clap-4.6.1.crate +Archive SHA-256: 1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51 + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: clap_builder 4.6.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/clap_builder/clap_builder-4.6.0.crate +Archive SHA-256: 714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: clap_derive 4.6.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/clap_derive/clap_derive-4.6.1.crate +Archive SHA-256: f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9 + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: clap_lex 1.0.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/clap_lex/clap_lex-1.0.0.crate +Archive SHA-256: 3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831 + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: cmake 0.1.54 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/cmake/cmake-0.1.54.crate +Archive SHA-256: e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0 + Notice 001: archive file LICENSE-APACHE + Notice 020: archive file LICENSE-MIT + +Package: color-eyre 0.6.5 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/color-eyre/color-eyre-0.6.5.crate +Archive SHA-256: e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d + Notice 001: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: color-spantrace 0.3.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/color-spantrace/color-spantrace-0.3.0.crate +Archive SHA-256: b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427 + Notice 001: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: colorchoice 1.0.3 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/colorchoice/colorchoice-1.0.3.crate +Archive SHA-256: 5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990 + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: config 0.15.23 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/config/config-0.15.23.crate +Archive SHA-256: f316c6237b2d38be61949ecd15268a4c6ca32570079394a2444d9ce2c72a72d8 + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: console 0.16.3 +Declared license: MIT +Archive: https://static.crates.io/crates/console/console-0.16.3.crate +Archive SHA-256: d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87 + Notice 039: archive file LICENSE + +Package: convert_case 0.7.1 +Declared license: MIT +Archive: https://static.crates.io/crates/convert_case/convert_case-0.7.1.crate +Archive SHA-256: bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7 + Notice 040: archive file LICENSE + +Package: core-foundation-sys 0.8.7 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/core-foundation-sys/core-foundation-sys-0.8.7.crate +Archive SHA-256: 773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b + Notice 001: archive file LICENSE-APACHE + Notice 041: archive file LICENSE-MIT + +Package: countio 0.2.19 +Declared license: MIT +Archive: https://static.crates.io/crates/countio/countio-0.2.19.crate +Archive SHA-256: cdbee2fbff35a44b492c859b1b2e32e94b631f1d3ea2831dd8393861564180e3 + Notice 042: archive file LICENSE.txt + +Package: cpufeatures 0.2.17 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/cpufeatures/cpufeatures-0.2.17.crate +Archive SHA-256: 59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280 + Notice 026: archive file LICENSE-APACHE + Notice 043: archive file LICENSE-MIT + +Package: cpufeatures 0.3.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/cpufeatures/cpufeatures-0.3.0.crate +Archive SHA-256: 8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201 + Notice 026: archive file LICENSE-APACHE + Notice 043: archive file LICENSE-MIT + +Package: crc32fast 1.5.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/crc32fast/crc32fast-1.5.0.crate +Archive SHA-256: 9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511 + Notice 011: archive file LICENSE-APACHE + Notice 044: archive file LICENSE-MIT + +Package: crossterm 0.29.0 +Declared license: MIT +Archive: https://static.crates.io/crates/crossterm/crossterm-0.29.0.crate +Archive SHA-256: d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b + Notice 045: archive file LICENSE + +Package: crossterm_winapi 0.9.1 +Declared license: MIT +Archive: https://static.crates.io/crates/crossterm_winapi/crossterm_winapi-0.9.1.crate +Archive SHA-256: acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b + Notice 045: archive file LICENSE + +Package: crypto-common 0.1.6 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/crypto-common/crypto-common-0.1.6.crate +Archive SHA-256: 1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3 + Notice 026: archive file LICENSE-APACHE + Notice 046: archive file LICENSE-MIT + +Package: csscolorparser 0.6.2 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/csscolorparser/csscolorparser-0.6.2.crate +Archive SHA-256: eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf + Notice 047: archive file LICENSE-APACHE + Notice 048: archive file LICENSE-MIT + +Package: debug-ignore 1.0.5 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/debug-ignore/debug-ignore-1.0.5.crate +Archive SHA-256: ffe7ed1d93f4553003e20b629abe9085e1e81b1429520f897f8f8860bc6dfc21 + Notice 001: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: deltae 0.3.2 +Declared license: MIT +Archive: https://static.crates.io/crates/deltae/deltae-0.3.2.crate +Archive SHA-256: 5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4 + Notice 049: archive file LICENSE + Notice 050: archive file docs/doc/COPYRIGHT.txt + Notice 001: archive file docs/doc/LICENSE-APACHE.txt + Notice 005: archive file docs/doc/LICENSE-MIT.txt + +Package: derive-where 1.6.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/derive-where/derive-where-1.6.1.crate +Archive SHA-256: d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534 + Notice 051: archive file LICENSE-APACHE + Notice 052: archive file LICENSE-MIT + +Package: derive_more 2.0.1 +Declared license: MIT +Archive: https://static.crates.io/crates/derive_more/derive_more-2.0.1.crate +Archive SHA-256: 093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678 + Notice 053: archive file LICENSE + +Package: derive_more-impl 2.0.1 +Declared license: MIT +Archive: https://static.crates.io/crates/derive_more-impl/derive_more-impl-2.0.1.crate +Archive SHA-256: bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3 + Notice 053: archive file LICENSE + +Package: dialoguer 0.12.0 +Declared license: MIT +Archive: https://static.crates.io/crates/dialoguer/dialoguer-0.12.0.crate +Archive SHA-256: 25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96 + Notice 039: archive file LICENSE + +Package: digest 0.10.7 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/digest/digest-0.10.7.crate +Archive SHA-256: 9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292 + Notice 026: archive file LICENSE-APACHE + Notice 054: archive file LICENSE-MIT + +Package: dirs 6.0.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/dirs/dirs-6.0.0.crate +Archive SHA-256: c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e + Notice 055: archive file LICENSE-APACHE + Notice 056: archive file LICENSE-MIT + +Package: dirs-sys 0.5.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/dirs-sys/dirs-sys-0.5.0.crate +Archive SHA-256: e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab + Notice 055: archive file LICENSE-APACHE + Notice 056: archive file LICENSE-MIT + +Package: document-features 0.2.11 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/document-features/document-features-0.2.11.crate +Archive SHA-256: 95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d + Notice 057: archive file LICENSE-APACHE + Notice 058: archive file LICENSE-MIT + +Package: dof 0.4.0 +Declared license: Apache-2.0 +Archive: https://static.crates.io/crates/dof/dof-0.4.0.crate +Archive SHA-256: 0ed9b77e3c2a83995eedff2fbf992eef44c9f319b8016254f68108e27a4d06e7 + Notice 059: https://raw.githubusercontent.com/oxidecomputer/usdt/86420e653a67a7f14106f5124c8abaacb5be1511/LICENSE + +Package: dtrace-parser 0.3.0 +Declared license: Apache-2.0 +Archive: https://static.crates.io/crates/dtrace-parser/dtrace-parser-0.3.0.crate +Archive SHA-256: dc09b90bda5770641457f1c0a42c8203c48f5a3d9799dcf1bafbd84e30ccf080 + Notice 059: https://raw.githubusercontent.com/oxidecomputer/usdt/86420e653a67a7f14106f5124c8abaacb5be1511/LICENSE + +Package: duct 1.1.1 +Declared license: MIT +Archive: https://static.crates.io/crates/duct/duct-1.1.1.crate +Archive SHA-256: 7e66e9c0c03d094e1a0ba1be130b849034aa80c3a2ab8ee94316bc809f3fa684 + Notice 060: archive file LICENSE + +Package: dunce 1.0.5 +Declared license: CC0-1.0 OR MIT-0 OR Apache-2.0 +Archive: https://static.crates.io/crates/dunce/dunce-1.0.5.crate +Archive SHA-256: 92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813 + Notice 061: archive file LICENSE + +Package: eazip 0.2.4 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/eazip/eazip-0.2.4.crate +Archive SHA-256: fa96d97482d56b4c30764798a9b129904d7e1493671e5ef7ff95b35f0e1aecb0 + Notice 062: archive file LICENSE-APACHE + Notice 063: archive file LICENSE-MIT + +Package: either 1.15.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/either/either-1.15.0.crate +Archive SHA-256: 48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719 + Notice 001: archive file LICENSE-APACHE + Notice 064: archive file LICENSE-MIT + +Package: enable-ansi-support 0.3.1 +Declared license: MIT +Archive: https://static.crates.io/crates/enable-ansi-support/enable-ansi-support-0.3.1.crate +Archive SHA-256: ea7457668b3da8a4b702f3d79e131aa3e81cd7e81cc95fb2d54fce9f182ecc77 + Notice 005: archive file LICENSE + +Package: encode_unicode 1.0.0 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/encode_unicode/encode_unicode-1.0.0.crate +Archive SHA-256: 34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0 + Notice 051: archive file LICENSE-APACHE + Notice 065: archive file LICENSE-MIT + +Package: encoding_rs 0.8.35 +Declared license: (Apache-2.0 OR MIT) AND BSD-3-Clause +Archive: https://static.crates.io/crates/encoding_rs/encoding_rs-0.8.35.crate +Archive SHA-256: 75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3 + Notice 066: archive file COPYRIGHT + Notice 051: archive file LICENSE-APACHE + Notice 067: archive file LICENSE-MIT + Notice 068: archive file LICENSE-WHATWG + +Package: enum_dispatch 0.3.13 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/enum_dispatch/enum_dispatch-0.3.13.crate +Archive SHA-256: aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd + Notice 069: archive file LICENSE + +Package: equivalent 1.0.2 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/equivalent/equivalent-1.0.2.crate +Archive SHA-256: 877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f + Notice 001: archive file LICENSE-APACHE + Notice 070: archive file LICENSE-MIT + +Package: errno 0.3.12 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/errno/errno-0.3.12.crate +Archive SHA-256: cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18 + Notice 001: archive file LICENSE-APACHE + Notice 071: archive file LICENSE-MIT + +Package: etcetera 0.11.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/etcetera/etcetera-0.11.0.crate +Archive SHA-256: de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: euclid 0.22.11 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/euclid/euclid-0.22.11.crate +Archive SHA-256: ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48 + Notice 072: archive file COPYRIGHT + Notice 001: archive file LICENSE-APACHE + Notice 041: archive file LICENSE-MIT + +Package: eyre 0.6.12 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/eyre/eyre-0.6.12.crate +Archive SHA-256: 7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec + Notice 001: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: fancy-regex 0.11.0 +Declared license: MIT +Archive: https://static.crates.io/crates/fancy-regex/fancy-regex-0.11.0.crate +Archive SHA-256: b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2 + Notice 073: archive file LICENSE + +Package: fastrand 2.3.0 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/fastrand/fastrand-2.3.0.crate +Archive SHA-256: 37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be + Notice 001: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: filedescriptor 0.8.3 +Declared license: MIT +Archive: https://static.crates.io/crates/filedescriptor/filedescriptor-0.8.3.crate +Archive SHA-256: e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d + Notice 074: archive file LICENSE.md + +Package: filetime 0.2.25 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/filetime/filetime-0.2.25.crate +Archive SHA-256: 35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586 + Notice 001: archive file LICENSE-APACHE + Notice 020: archive file LICENSE-MIT + +Package: finl_unicode 1.4.0 +Declared license: (MIT OR Apache-2.0) AND Unicode-DFS-2016 +Archive: https://static.crates.io/crates/finl_unicode/finl_unicode-1.4.0.crate +Archive SHA-256: 9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + Notice 075: archive file LICENSE-UNICODE + +Package: fixedbitset 0.4.2 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/fixedbitset/fixedbitset-0.4.2.crate +Archive SHA-256: 0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80 + Notice 001: archive file LICENSE-APACHE + Notice 076: archive file LICENSE-MIT + +Package: fixedbitset 0.5.7 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/fixedbitset/fixedbitset-0.5.7.crate +Archive SHA-256: 1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99 + Notice 001: archive file LICENSE-APACHE + Notice 076: archive file LICENSE-MIT + +Package: flate2 1.1.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/flate2/flate2-1.1.1.crate +Archive SHA-256: 7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece + Notice 001: archive file LICENSE-APACHE + Notice 077: archive file LICENSE-MIT + +Package: fnv 1.0.7 +Declared license: Apache-2.0 / MIT +Archive: https://static.crates.io/crates/fnv/fnv-1.0.7.crate +Archive SHA-256: 3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1 + Notice 001: archive file LICENSE-APACHE + Notice 078: archive file LICENSE-MIT + +Package: foldhash 0.1.5 +Declared license: Zlib +Archive: https://static.crates.io/crates/foldhash/foldhash-0.1.5.crate +Archive SHA-256: d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2 + Notice 079: archive file LICENSE + +Package: foldhash 0.2.0 +Declared license: Zlib +Archive: https://static.crates.io/crates/foldhash/foldhash-0.2.0.crate +Archive SHA-256: 77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb + Notice 079: archive file LICENSE + +Package: fs_extra 1.3.0 +Declared license: MIT +Archive: https://static.crates.io/crates/fs_extra/fs_extra-1.3.0.crate +Archive SHA-256: 42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c + Notice 080: archive file LICENSE + +Package: future-queue 0.4.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/future-queue/future-queue-0.4.0.crate +Archive SHA-256: 47cdf4a7eef4808ffa1e5c47dbf37124dfe33a7acc34e8568c5d5359b365a8cb + Notice 001: archive file LICENSE-APACHE + Notice 081: archive file LICENSE-MIT + +Package: futures 0.3.32 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/futures/futures-0.3.32.crate +Archive SHA-256: 8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d + Notice 082: archive file LICENSE-APACHE + Notice 083: archive file LICENSE-MIT + +Package: futures-channel 0.3.32 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/futures-channel/futures-channel-0.3.32.crate +Archive SHA-256: 07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d + Notice 082: archive file LICENSE-APACHE + Notice 083: archive file LICENSE-MIT + +Package: futures-core 0.3.32 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/futures-core/futures-core-0.3.32.crate +Archive SHA-256: 7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d + Notice 082: archive file LICENSE-APACHE + Notice 083: archive file LICENSE-MIT + +Package: futures-executor 0.3.32 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/futures-executor/futures-executor-0.3.32.crate +Archive SHA-256: baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d + Notice 082: archive file LICENSE-APACHE + Notice 083: archive file LICENSE-MIT + +Package: futures-io 0.3.32 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/futures-io/futures-io-0.3.32.crate +Archive SHA-256: cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718 + Notice 082: archive file LICENSE-APACHE + Notice 083: archive file LICENSE-MIT + +Package: futures-macro 0.3.32 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/futures-macro/futures-macro-0.3.32.crate +Archive SHA-256: e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b + Notice 082: archive file LICENSE-APACHE + Notice 083: archive file LICENSE-MIT + +Package: futures-sink 0.3.32 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/futures-sink/futures-sink-0.3.32.crate +Archive SHA-256: c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893 + Notice 082: archive file LICENSE-APACHE + Notice 083: archive file LICENSE-MIT + +Package: futures-task 0.3.32 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/futures-task/futures-task-0.3.32.crate +Archive SHA-256: 037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393 + Notice 082: archive file LICENSE-APACHE + Notice 083: archive file LICENSE-MIT + +Package: futures-util 0.3.32 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/futures-util/futures-util-0.3.32.crate +Archive SHA-256: 389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6 + Notice 082: archive file LICENSE-APACHE + Notice 083: archive file LICENSE-MIT + +Package: generic-array 0.14.7 +Declared license: MIT +Archive: https://static.crates.io/crates/generic-array/generic-array-0.14.7.crate +Archive SHA-256: 85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a + Notice 084: archive file LICENSE + +Package: getrandom 0.3.4 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/getrandom/getrandom-0.3.4.crate +Archive SHA-256: 899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd + Notice 085: archive file LICENSE-APACHE + Notice 086: archive file LICENSE-MIT + +Package: getrandom 0.4.2 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/getrandom/getrandom-0.4.2.crate +Archive SHA-256: 0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555 + Notice 085: archive file LICENSE-APACHE + Notice 087: archive file LICENSE-MIT + +Package: gimli 0.31.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/gimli/gimli-0.31.1.crate +Archive SHA-256: 07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f + Notice 001: archive file LICENSE-APACHE + Notice 024: archive file LICENSE-MIT + +Package: globset 0.4.18 +Declared license: Unlicense OR MIT +Archive: https://static.crates.io/crates/globset/globset-0.4.18.crate +Archive SHA-256: 52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3 + Notice 007: archive file COPYING + Notice 008: archive file LICENSE-MIT + Notice 009: archive file UNLICENSE + +Package: goblin 0.10.3 +Declared license: MIT +Archive: https://static.crates.io/crates/goblin/goblin-0.10.3.crate +Archive SHA-256: 51876e3748c4a347fe65b906f2b1ae46a1e55a497b22c94c1f4f2c469ff7673a + Notice 088: archive file LICENSE + +Package: guppy 0.17.26 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/guppy/guppy-0.17.26.crate +Archive SHA-256: eb611f63088c20f3f7252d896f0beaa176a9628da8c21cdf258fe9ae179bebda + Notice 089: https://raw.githubusercontent.com/guppy-rs/guppy/3f7c2ddd3362d43d5cd3ff948f4e6fbda8727d60/LICENSE-MIT + Notice 001: https://raw.githubusercontent.com/guppy-rs/guppy/3f7c2ddd3362d43d5cd3ff948f4e6fbda8727d60/LICENSE-APACHE + +Package: guppy-workspace-hack 0.1.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/guppy-workspace-hack/guppy-workspace-hack-0.1.0.crate +Archive SHA-256: 92620684d99f750bae383ecb3be3748142d6095760afd5cbcf2261e9a279d780 + Notice evidence: unavailable; declared license is not a substitute. + +Package: hashbrown 0.15.5 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/hashbrown/hashbrown-0.15.5.crate +Archive SHA-256: 9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1 + Notice 001: archive file LICENSE-APACHE + Notice 090: archive file LICENSE-MIT + +Package: hashbrown 0.16.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/hashbrown/hashbrown-0.16.1.crate +Archive SHA-256: 841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100 + Notice 001: archive file LICENSE-APACHE + Notice 090: archive file LICENSE-MIT + +Package: hashbrown 0.17.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/hashbrown/hashbrown-0.17.0.crate +Archive SHA-256: 4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51 + Notice 001: archive file LICENSE-APACHE + Notice 090: archive file LICENSE-MIT + +Package: heck 0.5.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/heck/heck-0.5.0.crate +Archive SHA-256: 2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea + Notice 001: archive file LICENSE-APACHE + Notice 024: archive file LICENSE-MIT + +Package: hex 0.4.3 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/hex/hex-0.4.3.crate +Archive SHA-256: 7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70 + Notice 011: archive file LICENSE-APACHE + Notice 091: archive file LICENSE-MIT + +Package: home 0.5.12 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/home/home-0.5.12.crate +Archive SHA-256: cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d + Notice 004: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: http 1.4.2 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/http/http-1.4.2.crate +Archive SHA-256: 6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425 + Notice 092: archive file LICENSE-APACHE + Notice 093: archive file LICENSE-MIT + +Package: httparse 1.10.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/httparse/httparse-1.10.1.crate +Archive SHA-256: 6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87 + Notice 001: archive file LICENSE-APACHE + Notice 094: archive file LICENSE-MIT + +Package: humantime 2.3.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/humantime/humantime-2.3.0.crate +Archive SHA-256: 135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424 + Notice 011: archive file LICENSE-APACHE + Notice 095: archive file LICENSE-MIT + +Package: humantime-serde 1.1.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/humantime-serde/humantime-serde-1.1.1.crate +Archive SHA-256: 57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c + Notice 001: archive file LICENSE-APACHE + Notice 096: archive file LICENSE-MIT + +Package: iana-time-zone 0.1.63 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/iana-time-zone/iana-time-zone-0.1.63.crate +Archive SHA-256: b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8 + Notice 097: archive file LICENSE-APACHE + Notice 098: archive file LICENSE-MIT + +Package: iddqd 0.4.4 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/iddqd/iddqd-0.4.4.crate +Archive SHA-256: 5f48f166155928aa2e7c88cc1695bd7301c2f0fc105a136414d9c5b39e450bf5 + Notice 082: archive file LICENSE-APACHE + Notice 099: archive file LICENSE-MIT + +Package: idna_adapter 1.0.0 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/idna_adapter/idna_adapter-1.0.0.crate +Archive SHA-256: cfdf4f5d937a025381f5ab13624b1c5f51414bfe5c9885663226eae8d6d39560 + Notice 001: archive file LICENSE-APACHE + Notice 100: archive file LICENSE-MIT + +Package: include_dir 0.7.4 +Declared license: MIT +Archive: https://static.crates.io/crates/include_dir/include_dir-0.7.4.crate +Archive SHA-256: 923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd + Notice 101: https://raw.githubusercontent.com/Michael-F-Bryan/include_dir/d742c6fffce99ee89da91b934e7ce6fb2a82680c/LICENSE + +Package: include_dir_macros 0.7.4 +Declared license: MIT +Archive: https://static.crates.io/crates/include_dir_macros/include_dir_macros-0.7.4.crate +Archive SHA-256: 7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75 + Notice 101: https://raw.githubusercontent.com/Michael-F-Bryan/include_dir/d742c6fffce99ee89da91b934e7ce6fb2a82680c/LICENSE + +Package: indenter 0.3.4 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/indenter/indenter-0.3.4.crate +Archive SHA-256: 964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: indexmap 2.14.0 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/indexmap/indexmap-2.14.0.crate +Archive SHA-256: d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9 + Notice 001: archive file LICENSE-APACHE + Notice 102: archive file LICENSE-MIT + +Package: indicatif 0.18.4 +Declared license: MIT +Archive: https://static.crates.io/crates/indicatif/indicatif-0.18.4.crate +Archive SHA-256: 25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb + Notice 039: archive file LICENSE + +Package: is_ci 1.2.0 +Declared license: ISC +Archive: https://static.crates.io/crates/is_ci/is_ci-1.2.0.crate +Archive SHA-256: 7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45 + Notice 103: archive file LICENSE + +Package: is_terminal_polyfill 1.70.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/is_terminal_polyfill/is_terminal_polyfill-1.70.1.crate +Archive SHA-256: 7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: itertools 0.14.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/itertools/itertools-0.14.0.crate +Archive SHA-256: 2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285 + Notice 001: archive file LICENSE-APACHE + Notice 064: archive file LICENSE-MIT + +Package: itoa 1.0.17 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/itoa/itoa-1.0.17.crate +Archive SHA-256: 92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: jobserver 0.1.33 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/jobserver/jobserver-0.1.33.crate +Archive SHA-256: 38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a + Notice 001: archive file LICENSE-APACHE + Notice 020: archive file LICENSE-MIT + +Package: lab 0.11.0 +Declared license: MIT +Archive: https://static.crates.io/crates/lab/lab-0.11.0.crate +Archive SHA-256: bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f + Notice 104: archive file LICENSE + +Package: lazy_static 1.5.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/lazy_static/lazy_static-1.5.0.crate +Archive SHA-256: bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe + Notice 001: archive file LICENSE-APACHE + Notice 105: archive file LICENSE-MIT + +Package: libc 0.2.186 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/libc/libc-0.2.186.crate +Archive SHA-256: 68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66 + Notice 010: archive file LICENSE-APACHE + Notice 106: archive file LICENSE-MIT + +Package: linux-raw-sys 0.4.15 +Declared license: Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/linux-raw-sys/linux-raw-sys-0.4.15.crate +Archive SHA-256: d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab + Notice 107: archive file COPYRIGHT + Notice 001: archive file LICENSE-APACHE + Notice 108: archive file LICENSE-Apache-2.0_WITH_LLVM-exception + Notice 005: archive file LICENSE-MIT + +Package: linux-raw-sys 0.9.4 +Declared license: Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/linux-raw-sys/linux-raw-sys-0.9.4.crate +Archive SHA-256: cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12 + Notice 107: archive file COPYRIGHT + Notice 001: archive file LICENSE-APACHE + Notice 108: archive file LICENSE-Apache-2.0_WITH_LLVM-exception + Notice 005: archive file LICENSE-MIT + +Package: litrs 0.4.1 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/litrs/litrs-0.4.1.crate +Archive SHA-256: b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5 + Notice 010: archive file LICENSE-APACHE + Notice 109: archive file LICENSE-MIT + +Package: lock_api 0.4.13 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/lock_api/lock_api-0.4.13.crate +Archive SHA-256: 96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765 + Notice 001: archive file LICENSE-APACHE + Notice 014: archive file LICENSE-MIT + +Package: locked-tripwire 0.1.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/locked-tripwire/locked-tripwire-0.1.0.crate +Archive SHA-256: 2a05236a0d3f2adf28c97ae240f3b037e79a44f77da737c0e4249529e8c5c40c + Notice 001: archive file LICENSE-APACHE + Notice 110: archive file LICENSE-MIT + +Package: log 0.4.29 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/log/log-0.4.29.crate +Archive SHA-256: 5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897 + Notice 001: archive file LICENSE-APACHE + Notice 025: archive file LICENSE-MIT + +Package: lru 0.16.3 +Declared license: MIT +Archive: https://static.crates.io/crates/lru/lru-0.16.3.crate +Archive SHA-256: a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593 + Notice 111: archive file LICENSE + +Package: mac_address 1.1.8 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/mac_address/mac_address-1.1.8.crate +Archive SHA-256: c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303 + Notice 112: archive file LICENSE-APACHE2.0 + Notice 113: archive file LICENSE-MIT + +Package: memchr 2.7.6 +Declared license: Unlicense OR MIT +Archive: https://static.crates.io/crates/memchr/memchr-2.7.6.crate +Archive SHA-256: f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273 + Notice 007: archive file COPYING + Notice 008: archive file LICENSE-MIT + Notice 009: archive file UNLICENSE + +Package: memmap2 0.9.8 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/memmap2/memmap2-0.9.8.crate +Archive SHA-256: 843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7 + Notice 114: archive file LICENSE-APACHE + Notice 115: archive file LICENSE-MIT + +Package: memmem 0.1.1 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/memmem/memmem-0.1.1.crate +Archive SHA-256: a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15 + Notice 001: archive file LICENSE-APACHE + Notice 025: archive file LICENSE-MIT + +Package: memoffset 0.9.1 +Declared license: MIT +Archive: https://static.crates.io/crates/memoffset/memoffset-0.9.1.crate +Archive SHA-256: 488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a + Notice 116: archive file LICENSE + +Package: miette 7.6.0 +Declared license: Apache-2.0 +Archive: https://static.crates.io/crates/miette/miette-7.6.0.crate +Archive SHA-256: 5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7 + Notice 051: archive file LICENSE + +Package: miette-derive 7.6.0 +Declared license: Apache-2.0 +Archive: https://static.crates.io/crates/miette-derive/miette-derive-7.6.0.crate +Archive SHA-256: db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b + Notice 117: archive file LICENSE + +Package: minimal-lexical 0.2.1 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/minimal-lexical/minimal-lexical-0.2.1.crate +Archive SHA-256: 68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a + Notice 022: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + Notice 118: archive file LICENSE.md + +Package: miniz_oxide 0.8.9 +Declared license: MIT OR Zlib OR Apache-2.0 +Archive: https://static.crates.io/crates/miniz_oxide/miniz_oxide-0.8.9.crate +Archive SHA-256: 1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316 + Notice 119: archive file LICENSE + Notice 059: archive file LICENSE-APACHE.md + Notice 120: archive file LICENSE-MIT.md + Notice 121: archive file LICENSE-ZLIB.md + +Package: mio 1.2.0 +Declared license: MIT +Archive: https://static.crates.io/crates/mio/mio-1.2.0.crate +Archive SHA-256: 50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1 + Notice 122: archive file LICENSE + +Package: mukti-metadata 0.3.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/mukti-metadata/mukti-metadata-0.3.0.crate +Archive SHA-256: 26fa66de8d0e39780ff7cbb1409149f8423418339230f520240e5eb08576e1e8 + Notice 110: https://raw.githubusercontent.com/nextest-rs/mukti/e146664895ad148e1f6981d70852e4e6cb799397/LICENSE-MIT + Notice 001: https://raw.githubusercontent.com/nextest-rs/mukti/e146664895ad148e1f6981d70852e4e6cb799397/LICENSE-APACHE + +Package: nested 0.1.1 +Declared license: MIT +Archive: https://static.crates.io/crates/nested/nested-0.1.1.crate +Archive SHA-256: ca2b420f638f07fe83056b55ea190bb815f609ec5a35e7017884a10f78839c9e + Notice 123: archive file LICENSE-MIT.md + +Package: newtype-uuid 1.3.2 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/newtype-uuid/newtype-uuid-1.3.2.crate +Archive SHA-256: 5c012d14ef788ab066a347d19e3dda699916c92293b05b85ba2c76b8c82d2830 + Notice 082: archive file LICENSE-APACHE + Notice 124: archive file LICENSE-MIT + +Package: nix 0.29.0 +Declared license: MIT +Archive: https://static.crates.io/crates/nix/nix-0.29.0.crate +Archive SHA-256: 71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46 + Notice 125: archive file LICENSE + +Package: nix 0.31.3 +Declared license: MIT +Archive: https://static.crates.io/crates/nix/nix-0.31.3.crate +Archive SHA-256: cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d + Notice 125: archive file LICENSE + +Package: nom 7.1.3 +Declared license: MIT +Archive: https://static.crates.io/crates/nom/nom-7.1.3.crate +Archive SHA-256: d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a + Notice 126: archive file LICENSE + +Package: num-derive 0.4.2 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/num-derive/num-derive-0.4.2.crate +Archive SHA-256: ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202 + Notice 001: archive file LICENSE-APACHE + Notice 025: archive file LICENSE-MIT + +Package: num-traits 0.2.19 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/num-traits/num-traits-0.2.19.crate +Archive SHA-256: 071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841 + Notice 001: archive file LICENSE-APACHE + Notice 025: archive file LICENSE-MIT + +Package: object 0.36.7 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/object/object-0.36.7.crate +Archive SHA-256: 62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87 + Notice 001: archive file LICENSE-APACHE + Notice 127: archive file LICENSE-MIT + +Package: once_cell 1.21.3 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/once_cell/once_cell-1.21.3.crate +Archive SHA-256: 42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d + Notice 001: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: once_cell_polyfill 1.70.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/once_cell_polyfill/once_cell_polyfill-1.70.1.crate +Archive SHA-256: a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: option-ext 0.2.0 +Declared license: MPL-2.0 +Archive: https://static.crates.io/crates/option-ext/option-ext-0.2.0.crate +Archive SHA-256: 04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d + Notice 128: archive file LICENSE.txt + +Package: ordered-float 4.6.0 +Declared license: MIT +Archive: https://static.crates.io/crates/ordered-float/ordered-float-4.6.0.crate +Archive SHA-256: 7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951 + Notice 129: archive file LICENSE-MIT + +Package: os_pipe 1.2.2 +Declared license: MIT +Archive: https://static.crates.io/crates/os_pipe/os_pipe-1.2.2.crate +Archive SHA-256: db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224 + Notice 060: archive file LICENSE + +Package: owo-colors 4.3.0 +Declared license: MIT +Archive: https://static.crates.io/crates/owo-colors/owo-colors-4.3.0.crate +Archive SHA-256: d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d + Notice 130: archive file LICENSE + +Package: parking_lot 0.12.4 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/parking_lot/parking_lot-0.12.4.crate +Archive SHA-256: 70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13 + Notice 001: archive file LICENSE-APACHE + Notice 014: archive file LICENSE-MIT + +Package: parking_lot_core 0.9.11 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/parking_lot_core/parking_lot_core-0.9.11.crate +Archive SHA-256: bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5 + Notice 001: archive file LICENSE-APACHE + Notice 014: archive file LICENSE-MIT + +Package: pathdiff 0.2.3 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/pathdiff/pathdiff-0.2.3.crate +Archive SHA-256: df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3 + Notice 131: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: percent-encoding 2.3.2 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/percent-encoding/percent-encoding-2.3.2.crate +Archive SHA-256: 9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220 + Notice 001: archive file LICENSE-APACHE + Notice 132: archive file LICENSE-MIT + +Package: pest 2.8.3 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/pest/pest-2.8.3.crate +Archive SHA-256: 989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4 + Notice 001: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: pest_derive 2.8.3 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/pest_derive/pest_derive-2.8.3.crate +Archive SHA-256: 187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de + Notice 001: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: pest_generator 2.8.3 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/pest_generator/pest_generator-2.8.3.crate +Archive SHA-256: 49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843 + Notice 001: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: pest_meta 2.8.3 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/pest_meta/pest_meta-2.8.3.crate +Archive SHA-256: 72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a + Notice 001: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: petgraph 0.8.3 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/petgraph/petgraph-0.8.3.crate +Archive SHA-256: 8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455 + Notice 001: archive file LICENSE-APACHE + Notice 064: archive file LICENSE-MIT + Notice 133: archive file assets/images/LICENSE.md + +Package: phf 0.11.3 +Declared license: MIT +Archive: https://static.crates.io/crates/phf/phf-0.11.3.crate +Archive SHA-256: 1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078 + Notice 134: archive file LICENSE + +Package: phf_codegen 0.11.3 +Declared license: MIT +Archive: https://static.crates.io/crates/phf_codegen/phf_codegen-0.11.3.crate +Archive SHA-256: aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a + Notice 134: archive file LICENSE + +Package: phf_generator 0.11.3 +Declared license: MIT +Archive: https://static.crates.io/crates/phf_generator/phf_generator-0.11.3.crate +Archive SHA-256: 3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d + Notice 134: archive file LICENSE + +Package: phf_macros 0.11.3 +Declared license: MIT +Archive: https://static.crates.io/crates/phf_macros/phf_macros-0.11.3.crate +Archive SHA-256: f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216 + Notice 134: archive file LICENSE + +Package: phf_shared 0.11.3 +Declared license: MIT +Archive: https://static.crates.io/crates/phf_shared/phf_shared-0.11.3.crate +Archive SHA-256: 67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5 + Notice 134: archive file LICENSE + +Package: pin-project 1.1.10 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/pin-project/pin-project-1.1.10.crate +Archive SHA-256: 677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a + Notice 059: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: pin-project-internal 1.1.10 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/pin-project-internal/pin-project-internal-1.1.10.crate +Archive SHA-256: 6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861 + Notice 059: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: pin-project-lite 0.2.17 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/pin-project-lite/pin-project-lite-0.2.17.crate +Archive SHA-256: a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd + Notice 059: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: pkg-config 0.3.32 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/pkg-config/pkg-config-0.3.32.crate +Archive SHA-256: 7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c + Notice 001: archive file LICENSE-APACHE + Notice 020: archive file LICENSE-MIT + +Package: plain 0.2.3 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/plain/plain-0.2.3.crate +Archive SHA-256: b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6 + Notice 001: archive file LICENSE-APACHE + Notice 135: archive file LICENSE-MIT + +Package: portable-atomic 1.13.1 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/portable-atomic/portable-atomic-1.13.1.crate +Archive SHA-256: c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49 + Notice 059: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: ppv-lite86 0.2.21 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/ppv-lite86/ppv-lite86-0.2.21.crate +Archive SHA-256: 85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9 + Notice 136: archive file LICENSE-APACHE + Notice 137: archive file LICENSE-MIT + +Package: pretty-hex 0.4.1 +Declared license: MIT +Archive: https://static.crates.io/crates/pretty-hex/pretty-hex-0.4.1.crate +Archive SHA-256: bbc83ee4a840062f368f9096d80077a9841ec117e17e7f700df81958f1451254 + Notice 138: archive file LICENSE + +Package: proc-macro2 1.0.106 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/proc-macro2/proc-macro2-1.0.106.crate +Archive SHA-256: 8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: proptest 1.11.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/proptest/proptest-1.11.0.crate +Archive SHA-256: 4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744 + Notice 001: archive file LICENSE-APACHE + Notice 139: archive file LICENSE-MIT + +Package: pulldown-cmark 0.13.3 +Declared license: MIT +Archive: https://static.crates.io/crates/pulldown-cmark/pulldown-cmark-0.13.3.crate +Archive SHA-256: 7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad + Notice 140: archive file LICENSE + +Package: quick-error 1.2.3 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/quick-error/quick-error-1.2.3.crate +Archive SHA-256: a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0 + Notice 011: archive file LICENSE-APACHE + Notice 141: archive file LICENSE-MIT + +Package: quick-junit 0.6.0 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/quick-junit/quick-junit-0.6.0.crate +Archive SHA-256: e3e64c58c4c88fc1045e8fe98a1b7cec3643187e3dd678f9bbcdd8f12a6933d6 + Notice 001: archive file LICENSE-APACHE + Notice 110: archive file LICENSE-MIT + +Package: quick-xml 0.38.4 +Declared license: MIT +Archive: https://static.crates.io/crates/quick-xml/quick-xml-0.38.4.crate +Archive SHA-256: b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c + Notice 123: archive file LICENSE-MIT.md + +Package: quote 1.0.45 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/quote/quote-1.0.45.crate +Archive SHA-256: 41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: rand 0.10.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/rand/rand-0.10.1.crate +Archive SHA-256: d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207 + Notice 142: archive file COPYRIGHT + Notice 143: archive file LICENSE-APACHE + Notice 144: archive file LICENSE-MIT + +Package: rand 0.8.6 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/rand/rand-0.8.6.crate +Archive SHA-256: 5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a + Notice 142: archive file COPYRIGHT + Notice 143: archive file LICENSE-APACHE + Notice 144: archive file LICENSE-MIT + +Package: rand 0.9.4 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/rand/rand-0.9.4.crate +Archive SHA-256: 44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea + Notice 142: archive file COPYRIGHT + Notice 143: archive file LICENSE-APACHE + Notice 144: archive file LICENSE-MIT + +Package: rand_chacha 0.9.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/rand_chacha/rand_chacha-0.9.0.crate +Archive SHA-256: d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb + Notice 142: archive file COPYRIGHT + Notice 143: archive file LICENSE-APACHE + Notice 144: archive file LICENSE-MIT + +Package: rand_core 0.10.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/rand_core/rand_core-0.10.0.crate +Archive SHA-256: 0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba + Notice 145: archive file COPYRIGHT + Notice 146: archive file LICENSE-APACHE + Notice 147: archive file LICENSE-MIT + +Package: rand_core 0.6.4 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/rand_core/rand_core-0.6.4.crate +Archive SHA-256: ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c + Notice 142: archive file COPYRIGHT + Notice 146: archive file LICENSE-APACHE + Notice 144: archive file LICENSE-MIT + +Package: rand_core 0.9.3 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/rand_core/rand_core-0.9.3.crate +Archive SHA-256: 99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38 + Notice 142: archive file COPYRIGHT + Notice 146: archive file LICENSE-APACHE + Notice 144: archive file LICENSE-MIT + +Package: rand_xorshift 0.4.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/rand_xorshift/rand_xorshift-0.4.0.crate +Archive SHA-256: 513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a + Notice 142: archive file COPYRIGHT + Notice 143: archive file LICENSE-APACHE + Notice 144: archive file LICENSE-MIT + +Package: recursion 0.5.4 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/recursion/recursion-0.5.4.crate +Archive SHA-256: 9dba2197bf7b1d87b4dd460c195f4edeb45a94e82e8054f8d5f317c1f0e93ca1 + Notice evidence: unavailable; declared license is not a substitute. + +Package: regex 1.12.4 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/regex/regex-1.12.4.crate +Archive SHA-256: f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba + Notice 001: archive file LICENSE-APACHE + Notice 025: archive file LICENSE-MIT + +Package: regex-automata 0.4.14 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/regex-automata/regex-automata-0.4.14.crate +Archive SHA-256: 6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f + Notice 001: archive file LICENSE-APACHE + Notice 025: archive file LICENSE-MIT + +Package: regex-syntax 0.8.11 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/regex-syntax/regex-syntax-0.8.11.crate +Archive SHA-256: d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4 + Notice 001: archive file LICENSE-APACHE + Notice 025: archive file LICENSE-MIT + Notice 148: archive file src/unicode_tables/LICENSE-UNICODE + +Package: rustc-demangle 0.1.25 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/rustc-demangle/rustc-demangle-0.1.25.crate +Archive SHA-256: 989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f + Notice 001: archive file LICENSE-APACHE + Notice 020: archive file LICENSE-MIT + +Package: rustix 0.38.44 +Declared license: Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/rustix/rustix-0.38.44.crate +Archive SHA-256: fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154 + Notice 149: archive file COPYRIGHT + Notice 001: archive file LICENSE-APACHE + Notice 108: archive file LICENSE-Apache-2.0_WITH_LLVM-exception + Notice 005: archive file LICENSE-MIT + +Package: rustix 1.0.7 +Declared license: Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/rustix/rustix-1.0.7.crate +Archive SHA-256: c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266 + Notice 149: archive file COPYRIGHT + Notice 001: archive file LICENSE-APACHE + Notice 108: archive file LICENSE-Apache-2.0_WITH_LLVM-exception + Notice 005: archive file LICENSE-MIT + +Package: rustls 0.23.40 +Declared license: Apache-2.0 OR ISC OR MIT +Archive: https://static.crates.io/crates/rustls/rustls-0.23.40.crate +Archive SHA-256: ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b + Notice 001: archive file LICENSE-APACHE + Notice 150: archive file LICENSE-ISC + Notice 151: archive file LICENSE-MIT + +Package: rustls-pki-types 1.12.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/rustls-pki-types/rustls-pki-types-1.12.0.crate +Archive SHA-256: 229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79 + Notice 152: archive file LICENSE-APACHE + Notice 153: archive file LICENSE-MIT + +Package: rustls-webpki 0.103.13 +Declared license: ISC +Archive: https://static.crates.io/crates/rustls-webpki/rustls-webpki-0.103.13.crate +Archive SHA-256: 61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e + Notice 154: archive file LICENSE + +Package: rusty-fork 0.3.0 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/rusty-fork/rusty-fork-0.3.0.crate +Archive SHA-256: cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f + Notice 001: archive file LICENSE-APACHE + Notice 139: archive file LICENSE-MIT + +Package: sapling-streampager 0.12.0 +Declared license: MIT +Archive: https://static.crates.io/crates/sapling-streampager/sapling-streampager-0.12.0.crate +Archive SHA-256: 17810e56620c313aa7ee8b1ffcd6b148e84061a9f80fbc609df0495c4b4a6172 + Notice 155: archive file LICENSE.md + +Package: scopeguard 1.2.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/scopeguard/scopeguard-1.2.0.crate +Archive SHA-256: 94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49 + Notice 001: archive file LICENSE-APACHE + Notice 156: archive file LICENSE-MIT + +Package: scroll 0.13.0 +Declared license: MIT +Archive: https://static.crates.io/crates/scroll/scroll-0.13.0.crate +Archive SHA-256: c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add + Notice 157: archive file LICENSE + +Package: scroll_derive 0.13.1 +Declared license: MIT +Archive: https://static.crates.io/crates/scroll_derive/scroll_derive-0.13.1.crate +Archive SHA-256: ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d + Notice 158: archive file LICENSE + +Package: self-replace 1.5.0 +Declared license: Apache-2.0 +Archive: https://static.crates.io/crates/self-replace/self-replace-1.5.0.crate +Archive SHA-256: 03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7 + Notice 001: archive file LICENSE + +Package: self_update 0.44.0 +Declared license: MIT +Archive: https://static.crates.io/crates/self_update/self_update-0.44.0.crate +Archive SHA-256: 2e79722b5a505d4ddc77527455a97244e9e8c4c07533ff44cf4421cce7bb6d17 + Notice 159: archive file LICENSE + +Package: semver 1.0.28 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/semver/semver-1.0.28.crate +Archive SHA-256: 8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: serde 1.0.228 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/serde/serde-1.0.228.crate +Archive SHA-256: 9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: serde_core 1.0.228 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/serde_core/serde_core-1.0.228.crate +Archive SHA-256: 41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: serde_derive 1.0.228 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/serde_derive/serde_derive-1.0.228.crate +Archive SHA-256: d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: serde_ignored 0.1.14 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/serde_ignored/serde_ignored-0.1.14.crate +Archive SHA-256: 115dffd5f3853e06e746965a20dcbae6ee747ae30b543d91b0e089668bb07798 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: serde_json 1.0.150 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/serde_json/serde_json-1.0.150.crate +Archive SHA-256: e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: serde_path_to_error 0.1.20 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/serde_path_to_error/serde_path_to_error-0.1.20.crate +Archive SHA-256: 10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: serde_spanned 1.1.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/serde_spanned/serde_spanned-1.1.1.crate +Archive SHA-256: 6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26 + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: serde_tokenstream 0.2.2 +Declared license: Apache-2.0 +Archive: https://static.crates.io/crates/serde_tokenstream/serde_tokenstream-0.2.2.crate +Archive SHA-256: 64060d864397305347a78851c51588fd283767e7e7589829e8121d65512340f1 + Notice 051: archive file LICENSE + +Package: sha2 0.10.9 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/sha2/sha2-0.10.9.crate +Archive SHA-256: a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283 + Notice 026: archive file LICENSE-APACHE + Notice 160: archive file LICENSE-MIT + +Package: sharded-slab 0.1.7 +Declared license: MIT +Archive: https://static.crates.io/crates/sharded-slab/sharded-slab-0.1.7.crate +Archive SHA-256: f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6 + Notice 161: archive file LICENSE + +Package: shared_child 1.1.1 +Declared license: MIT +Archive: https://static.crates.io/crates/shared_child/shared_child-1.1.1.crate +Archive SHA-256: 1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7 + Notice 060: archive file LICENSE + +Package: shared_thread 0.2.0 +Declared license: MIT +Archive: https://static.crates.io/crates/shared_thread/shared_thread-0.2.0.crate +Archive SHA-256: 52b86057fcb5423f5018e331ac04623e32d6b5ce85e33300f92c79a1973928b0 + Notice 060: archive file LICENSE + +Package: shell-words 1.1.1 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/shell-words/shell-words-1.1.1.crate +Archive SHA-256: dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77 + Notice 162: archive file LICENSE-APACHE + Notice 163: archive file LICENSE-MIT + +Package: shlex 1.3.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/shlex/shlex-1.3.0.crate +Archive SHA-256: 0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64 + Notice 164: archive file LICENSE-APACHE + Notice 165: archive file LICENSE-MIT + +Package: sigchld 0.2.4 +Declared license: MIT +Archive: https://static.crates.io/crates/sigchld/sigchld-0.2.4.crate +Archive SHA-256: 47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1 + Notice evidence: unavailable; declared license is not a substitute. + +Package: signal-hook 0.3.18 +Declared license: Apache-2.0/MIT +Archive: https://static.crates.io/crates/signal-hook/signal-hook-0.3.18.crate +Archive SHA-256: d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2 + Notice 001: archive file LICENSE-APACHE + Notice 166: archive file LICENSE-MIT + +Package: signal-hook-mio 0.2.5 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/signal-hook-mio/signal-hook-mio-0.2.5.crate +Archive SHA-256: b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc + Notice 001: archive file LICENSE-APACHE + Notice 166: archive file LICENSE-MIT + +Package: signal-hook-registry 1.4.5 +Declared license: Apache-2.0/MIT +Archive: https://static.crates.io/crates/signal-hook-registry/signal-hook-registry-1.4.5.crate +Archive SHA-256: 9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410 + Notice 001: archive file LICENSE-APACHE + Notice 166: archive file LICENSE-MIT + +Package: siphasher 1.0.1 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/siphasher/siphasher-1.0.1.crate +Archive SHA-256: 56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d + Notice 167: archive file COPYING + +Package: slab 0.4.11 +Declared license: MIT +Archive: https://static.crates.io/crates/slab/slab-0.4.11.crate +Archive SHA-256: 7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589 + Notice 168: archive file LICENSE + +Package: smallvec 1.15.2 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/smallvec/smallvec-1.15.2.crate +Archive SHA-256: 8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90 + Notice 001: archive file LICENSE-APACHE + Notice 169: archive file LICENSE-MIT + +Package: smol_str 0.3.2 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/smol_str/smol_str-0.3.2.crate +Archive SHA-256: 9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d + Notice 001: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: socket2 0.6.3 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/socket2/socket2-0.6.3.crate +Archive SHA-256: 3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e + Notice 001: archive file LICENSE-APACHE + Notice 020: archive file LICENSE-MIT + +Package: socks 0.3.4 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/socks/socks-0.3.4.crate +Archive SHA-256: f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b + Notice 011: archive file LICENSE-APACHE + Notice 170: archive file LICENSE-MIT + +Package: static_assertions 1.1.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/static_assertions/static_assertions-1.1.0.crate +Archive SHA-256: a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f + Notice 051: archive file LICENSE-APACHE + Notice 171: archive file LICENSE-MIT + +Package: strip-ansi-escapes 0.2.1 +Declared license: Apache-2.0/MIT +Archive: https://static.crates.io/crates/strip-ansi-escapes/strip-ansi-escapes-0.2.1.crate +Archive SHA-256: 2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025 + Notice 001: archive file LICENSE-APACHE + Notice 172: archive file LICENSE-MIT + +Package: strsim 0.11.1 +Declared license: MIT +Archive: https://static.crates.io/crates/strsim/strsim-0.11.1.crate +Archive SHA-256: 7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f + Notice 173: archive file LICENSE + +Package: subtle 2.6.1 +Declared license: BSD-3-Clause +Archive: https://static.crates.io/crates/subtle/subtle-2.6.1.crate +Archive SHA-256: 13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292 + Notice 174: archive file LICENSE + +Package: supports-color 3.0.2 +Declared license: Apache-2.0 +Archive: https://static.crates.io/crates/supports-color/supports-color-3.0.2.crate +Archive SHA-256: c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6 + Notice 117: archive file LICENSE + +Package: supports-hyperlinks 3.1.0 +Declared license: Apache-2.0 +Archive: https://static.crates.io/crates/supports-hyperlinks/supports-hyperlinks-3.1.0.crate +Archive SHA-256: 804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b + Notice 117: archive file LICENSE + +Package: supports-unicode 3.0.0 +Declared license: Apache-2.0 +Archive: https://static.crates.io/crates/supports-unicode/supports-unicode-3.0.0.crate +Archive SHA-256: b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2 + Notice 117: archive file LICENSE + +Package: swrite 0.1.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/swrite/swrite-0.1.0.crate +Archive SHA-256: 7f3fece30b2dc06d65ecbca97b602db15bf75f932711d60cc604534f1f8b7a03 + Notice 059: archive file LICENSE-Apache + Notice 175: archive file LICENSE-MIT + +Package: syn 1.0.109 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/syn/syn-1.0.109.crate +Archive SHA-256: 72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237 + Notice 001: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: syn 2.0.117 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/syn/syn-2.0.117.crate +Archive SHA-256: e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: tar 0.4.46 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/tar/tar-0.4.46.crate +Archive SHA-256: 3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840 + Notice 001: archive file LICENSE-APACHE + Notice 176: archive file LICENSE-MIT + +Package: target-lexicon 0.13.3 +Declared license: Apache-2.0 WITH LLVM-exception +Archive: https://static.crates.io/crates/target-lexicon/target-lexicon-0.13.3.crate +Archive SHA-256: df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c + Notice 108: archive file LICENSE + +Package: target-spec 3.6.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/target-spec/target-spec-3.6.0.crate +Archive SHA-256: b00e973676af5497c2a69cc9787e2205c00f3b6f4f70e7d7b0112e28aa84b501 + Notice 089: https://raw.githubusercontent.com/guppy-rs/guppy/3545e8baab03ac8ae075342b45ebd0422a021a93/LICENSE-MIT + Notice 001: https://raw.githubusercontent.com/guppy-rs/guppy/3545e8baab03ac8ae075342b45ebd0422a021a93/LICENSE-APACHE + +Package: target-spec-miette 0.4.8 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/target-spec-miette/target-spec-miette-0.4.8.crate +Archive SHA-256: c6259294bbf45237355d68da4e7ed1d4bd1fd0b51ff063d242e14fe6aa6773ef + Notice 089: https://raw.githubusercontent.com/guppy-rs/guppy/bc1c1511b89efc0c8e18b59e473c7d8a1a621e94/LICENSE-MIT + Notice 001: https://raw.githubusercontent.com/guppy-rs/guppy/bc1c1511b89efc0c8e18b59e473c7d8a1a621e94/LICENSE-APACHE + +Package: tempfile 3.20.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/tempfile/tempfile-3.20.0.crate +Archive SHA-256: e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1 + Notice 001: archive file LICENSE-APACHE + Notice 177: archive file LICENSE-MIT + +Package: terminal_size 0.4.3 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/terminal_size/terminal_size-0.4.3.crate +Archive SHA-256: 60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0 + Notice 011: archive file LICENSE-APACHE + Notice 178: archive file LICENSE-MIT + +Package: terminfo 0.9.0 +Declared license: WTFPL +Archive: https://static.crates.io/crates/terminfo/terminfo-0.9.0.crate +Archive SHA-256: d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662 + Notice 179: archive file LICENSE + +Package: termios 0.3.3 +Declared license: MIT +Archive: https://static.crates.io/crates/termios/termios-0.3.3.crate +Archive SHA-256: 411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b + Notice 180: archive file LICENSE + +Package: termwiz 0.23.3 +Declared license: MIT +Archive: https://static.crates.io/crates/termwiz/termwiz-0.23.3.crate +Archive SHA-256: 4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7 + Notice 074: archive file LICENSE.md + +Package: textwrap 0.16.2 +Declared license: MIT +Archive: https://static.crates.io/crates/textwrap/textwrap-0.16.2.crate +Archive SHA-256: c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057 + Notice 181: archive file LICENSE + +Package: thiserror 1.0.69 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/thiserror/thiserror-1.0.69.crate +Archive SHA-256: b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: thiserror 2.0.18 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/thiserror/thiserror-2.0.18.crate +Archive SHA-256: 4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: thiserror-impl 1.0.69 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/thiserror-impl/thiserror-impl-1.0.69.crate +Archive SHA-256: 4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: thiserror-impl 2.0.18 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/thiserror-impl/thiserror-impl-2.0.18.crate +Archive SHA-256: ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: thread-id 5.0.0 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/thread-id/thread-id-5.0.0.crate +Archive SHA-256: 99043e46c5a15af379c06add30d9c93a6c0e8849de00d244c4a2c417da128d80 + Notice 051: archive file license-apache + Notice 182: archive file license-mit + +Package: thread_local 1.1.8 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/thread_local/thread_local-1.1.8.crate +Archive SHA-256: 8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c + Notice 001: archive file LICENSE-APACHE + Notice 014: archive file LICENSE-MIT + +Package: tinyvec 1.9.0 +Declared license: Zlib OR Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/tinyvec/tinyvec-1.9.0.crate +Archive SHA-256: 09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71 + Notice 051: archive file LICENSE-APACHE.md + Notice 183: archive file LICENSE-MIT.md + Notice 032: archive file LICENSE-ZLIB.md + +Package: tinyvec_macros 0.1.1 +Declared license: MIT OR Apache-2.0 OR Zlib +Archive: https://static.crates.io/crates/tinyvec_macros/tinyvec_macros-0.1.1.crate +Archive SHA-256: 1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20 + Notice 184: archive file LICENSE-APACHE.md + Notice 185: archive file LICENSE-MIT.md + Notice 186: archive file LICENSE-ZLIB.md + +Package: tokio 1.52.3 +Declared license: MIT +Archive: https://static.crates.io/crates/tokio/tokio-1.52.3.crate +Archive SHA-256: 8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe + Notice 187: archive file LICENSE + +Package: tokio-macros 2.7.0 +Declared license: MIT +Archive: https://static.crates.io/crates/tokio-macros/tokio-macros-2.7.0.crate +Archive SHA-256: 385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496 + Notice 188: archive file LICENSE + +Package: tokio-stream 0.1.18 +Declared license: MIT +Archive: https://static.crates.io/crates/tokio-stream/tokio-stream-0.1.18.crate +Archive SHA-256: 32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70 + Notice 187: archive file LICENSE + +Package: toml 1.1.2+spec-1.1.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/toml/toml-1.1.2+spec-1.1.0.crate +Archive SHA-256: 81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: toml_datetime 1.1.1+spec-1.1.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/toml_datetime/toml_datetime-1.1.1+spec-1.1.0.crate +Archive SHA-256: 3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7 + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: toml_edit 0.25.12+spec-1.1.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/toml_edit/toml_edit-0.25.12+spec-1.1.0.crate +Archive SHA-256: d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7 + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: toml_parser 1.1.2+spec-1.1.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/toml_parser/toml_parser-1.1.2+spec-1.1.0.crate +Archive SHA-256: a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526 + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: toml_writer 1.1.1+spec-1.1.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/toml_writer/toml_writer-1.1.1+spec-1.1.0.crate +Archive SHA-256: 756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db + Notice 011: archive file LICENSE-APACHE + Notice 012: archive file LICENSE-MIT + +Package: tracing 0.1.44 +Declared license: MIT +Archive: https://static.crates.io/crates/tracing/tracing-0.1.44.crate +Archive SHA-256: 63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100 + Notice 189: archive file LICENSE + +Package: tracing-attributes 0.1.31 +Declared license: MIT +Archive: https://static.crates.io/crates/tracing-attributes/tracing-attributes-0.1.31.crate +Archive SHA-256: 7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da + Notice 189: archive file LICENSE + +Package: tracing-core 0.1.36 +Declared license: MIT +Archive: https://static.crates.io/crates/tracing-core/tracing-core-0.1.36.crate +Archive SHA-256: db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a + Notice 189: archive file LICENSE + Notice 190: archive file src/spin/LICENSE + +Package: tracing-error 0.2.1 +Declared license: MIT +Archive: https://static.crates.io/crates/tracing-error/tracing-error-0.2.1.crate +Archive SHA-256: 8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db + Notice 189: archive file LICENSE + +Package: tracing-log 0.2.0 +Declared license: MIT +Archive: https://static.crates.io/crates/tracing-log/tracing-log-0.2.0.crate +Archive SHA-256: ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3 + Notice 189: archive file LICENSE + +Package: tracing-subscriber 0.3.23 +Declared license: MIT +Archive: https://static.crates.io/crates/tracing-subscriber/tracing-subscriber-0.3.23.crate +Archive SHA-256: cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319 + Notice 189: archive file LICENSE + +Package: typenum 1.18.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/typenum/typenum-1.18.0.crate +Archive SHA-256: 1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f + Notice 191: archive file LICENSE + Notice 192: archive file LICENSE-APACHE + Notice 193: archive file LICENSE-MIT + +Package: ucd-trie 0.1.7 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/ucd-trie/ucd-trie-0.1.7.crate +Archive SHA-256: 2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971 + Notice 001: archive file LICENSE-APACHE + Notice 008: archive file LICENSE-MIT + +Package: unarray 0.1.4 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/unarray/unarray-0.1.4.crate +Archive SHA-256: eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94 + Notice 194: archive file LICENSE-APACHE + Notice 195: archive file LICENSE-MIT + +Package: unicase 2.9.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/unicase/unicase-2.9.0.crate +Archive SHA-256: dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142 + Notice 001: archive file LICENSE-APACHE + Notice 196: archive file LICENSE-MIT + +Package: unicode-ident 1.0.24 +Declared license: (MIT OR Apache-2.0) AND Unicode-3.0 +Archive: https://static.crates.io/crates/unicode-ident/unicode-ident-1.0.24.crate +Archive SHA-256: e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75 + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + Notice 075: archive file LICENSE-UNICODE + +Package: unicode-linebreak 0.1.5 +Declared license: Apache-2.0 +Archive: https://static.crates.io/crates/unicode-linebreak/unicode-linebreak-0.1.5.crate +Archive SHA-256: 3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f + Notice 194: archive file LICENSE + +Package: unicode-normalization 0.1.25 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/unicode-normalization/unicode-normalization-0.1.25.crate +Archive SHA-256: 5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8 + Notice 197: archive file COPYRIGHT + Notice 001: archive file LICENSE-APACHE + Notice 024: archive file LICENSE-MIT + +Package: unicode-segmentation 1.12.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/unicode-segmentation/unicode-segmentation-1.12.0.crate +Archive SHA-256: f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493 + Notice 197: archive file COPYRIGHT + Notice 001: archive file LICENSE-APACHE + Notice 024: archive file LICENSE-MIT + +Package: unicode-width 0.1.14 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/unicode-width/unicode-width-0.1.14.crate +Archive SHA-256: 7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af + Notice 197: archive file COPYRIGHT + Notice 001: archive file LICENSE-APACHE + Notice 024: archive file LICENSE-MIT + +Package: unicode-width 0.2.2 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/unicode-width/unicode-width-0.2.2.crate +Archive SHA-256: b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254 + Notice 197: archive file COPYRIGHT + Notice 001: archive file LICENSE-APACHE + Notice 024: archive file LICENSE-MIT + +Package: unit-prefix 0.5.1 +Declared license: MIT +Archive: https://static.crates.io/crates/unit-prefix/unit-prefix-0.5.1.crate +Archive SHA-256: 323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817 + Notice 198: archive file LICENSE + +Package: untrusted 0.9.0 +Declared license: ISC +Archive: https://static.crates.io/crates/untrusted/untrusted-0.9.0.crate +Archive SHA-256: 8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1 + Notice 199: archive file LICENSE.txt + +Package: ureq 3.3.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/ureq/ureq-3.3.0.crate +Archive SHA-256: dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0 + Notice 194: archive file LICENSE-APACHE + Notice 200: archive file LICENSE-MIT + +Package: ureq-proto 0.6.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/ureq-proto/ureq-proto-0.6.0.crate +Archive SHA-256: e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c + Notice 201: archive file LICENSE-MIT.txt + +Package: urlencoding 2.1.3 +Declared license: MIT +Archive: https://static.crates.io/crates/urlencoding/urlencoding-2.1.3.crate +Archive SHA-256: daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da + Notice 202: archive file LICENSE + +Package: usdt 0.6.0 +Declared license: Apache-2.0 +Archive: https://static.crates.io/crates/usdt/usdt-0.6.0.crate +Archive SHA-256: 1953f8d8a98ac7883c230963291acb65c7ed6ae3e2e4c99d5c65f4e65cc9db38 + Notice 059: https://raw.githubusercontent.com/oxidecomputer/usdt/86420e653a67a7f14106f5124c8abaacb5be1511/LICENSE + +Package: usdt-attr-macro 0.6.0 +Declared license: Apache-2.0 +Archive: https://static.crates.io/crates/usdt-attr-macro/usdt-attr-macro-0.6.0.crate +Archive SHA-256: 55d0d673848744c556fcfe8479f87b6974459106e4c41f38375f6d559bb0ee28 + Notice 059: https://raw.githubusercontent.com/oxidecomputer/usdt/86420e653a67a7f14106f5124c8abaacb5be1511/LICENSE + +Package: usdt-impl 0.6.0 +Declared license: Apache-2.0 +Archive: https://static.crates.io/crates/usdt-impl/usdt-impl-0.6.0.crate +Archive SHA-256: cf0085a93af1ca095d8b1dc8672cc4620fcd1db5dff8d165486067badce05555 + Notice 059: https://raw.githubusercontent.com/oxidecomputer/usdt/86420e653a67a7f14106f5124c8abaacb5be1511/LICENSE + +Package: usdt-macro 0.6.0 +Declared license: Apache-2.0 +Archive: https://static.crates.io/crates/usdt-macro/usdt-macro-0.6.0.crate +Archive SHA-256: c9bf594f86b676f7e2fd3d523d50f9d0cffecff6c19729ff5dbebe86c4cb8cb2 + Notice 059: https://raw.githubusercontent.com/oxidecomputer/usdt/86420e653a67a7f14106f5124c8abaacb5be1511/LICENSE + +Package: utf8-zero 0.8.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/utf8-zero/utf8-zero-0.8.1.crate +Archive SHA-256: b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e + Notice 010: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: utf8parse 0.2.2 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/utf8parse/utf8parse-0.2.2.crate +Archive SHA-256: 06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821 + Notice 010: archive file LICENSE-APACHE + Notice 203: archive file LICENSE-MIT + +Package: uuid 1.18.1 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/uuid/uuid-1.18.1.crate +Archive SHA-256: 2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2 + Notice 001: archive file LICENSE-APACHE + Notice 204: archive file LICENSE-MIT + +Package: vec_map 0.8.2 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/vec_map/vec_map-0.8.2.crate +Archive SHA-256: f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191 + Notice 022: archive file LICENSE-APACHE + Notice 024: archive file LICENSE-MIT + +Package: version_check 0.9.5 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/version_check/version_check-0.9.5.crate +Archive SHA-256: 0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a + Notice 001: archive file LICENSE-APACHE + Notice 205: archive file LICENSE-MIT + +Package: vte 0.14.1 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/vte/vte-0.14.1.crate +Archive SHA-256: 231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077 + Notice 010: archive file LICENSE-APACHE + Notice 203: archive file LICENSE-MIT + +Package: vtparse 0.6.2 +Declared license: MIT +Archive: https://static.crates.io/crates/vtparse/vtparse-0.6.2.crate +Archive SHA-256: 6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0 + Notice 206: https://raw.githubusercontent.com/wez/wezterm/edeae72b5fc55c7fa4aa1d08bbefd08c5493f757/LICENSE.md + +Package: wait-timeout 0.2.1 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/wait-timeout/wait-timeout-0.2.1.crate +Archive SHA-256: 09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11 + Notice 001: archive file LICENSE-APACHE + Notice 020: archive file LICENSE-MIT + +Package: webpki-roots 1.0.0 +Declared license: CDLA-Permissive-2.0 +Archive: https://static.crates.io/crates/webpki-roots/webpki-roots-1.0.0.crate +Archive SHA-256: 2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb + Notice 207: archive file LICENSE + +Package: wezterm-bidi 0.2.3 +Declared license: MIT AND Unicode-DFS-2016 +Archive: https://static.crates.io/crates/wezterm-bidi/wezterm-bidi-0.2.3.crate +Archive SHA-256: 0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec + Notice 208: archive file LICENSE.md + +Package: wezterm-blob-leases 0.1.1 +Declared license: MIT +Archive: https://static.crates.io/crates/wezterm-blob-leases/wezterm-blob-leases-0.1.1.crate +Archive SHA-256: 692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7 + Notice 209: archive file LICENSE.md + +Package: wezterm-color-types 0.3.0 +Declared license: MIT +Archive: https://static.crates.io/crates/wezterm-color-types/wezterm-color-types-0.3.0.crate +Archive SHA-256: 7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296 + Notice 210: archive file LICENSE.md + +Package: wezterm-dynamic 0.2.1 +Declared license: MIT +Archive: https://static.crates.io/crates/wezterm-dynamic/wezterm-dynamic-0.2.1.crate +Archive SHA-256: 5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac + Notice 211: archive file LICENSE.md + +Package: wezterm-dynamic-derive 0.1.1 +Declared license: MIT +Archive: https://static.crates.io/crates/wezterm-dynamic-derive/wezterm-dynamic-derive-0.1.1.crate +Archive SHA-256: 46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b + Notice 074: archive file LICENSE.md + +Package: wezterm-input-types 0.1.0 +Declared license: MIT +Archive: https://static.crates.io/crates/wezterm-input-types/wezterm-input-types-0.1.0.crate +Archive SHA-256: 7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e + Notice 206: https://raw.githubusercontent.com/wez/wezterm/d5ca5509b95edc31291ba29bd1fcbf3f0b12c92d/LICENSE.md + +Package: win32job 2.0.3 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/win32job/win32job-2.0.3.crate +Archive SHA-256: 8c6a6724ccfbf34154a8691bd868b0fcd2be2ca3f7b47b32614654f1a01b191c + Notice 001: archive file LICENSE-APACHE + Notice 005: archive file LICENSE-MIT + +Package: winapi 0.3.9 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/winapi/winapi-0.3.9.crate +Archive SHA-256: 5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419 + Notice 212: archive file LICENSE-APACHE + Notice 213: archive file LICENSE-MIT + +Package: windows 0.61.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows/windows-0.61.1.crate +Archive SHA-256: c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419 + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows-collections 0.2.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows-collections/windows-collections-0.2.0.crate +Archive SHA-256: 3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8 + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows-core 0.61.2 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows-core/windows-core-0.61.2.crate +Archive SHA-256: c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3 + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows-future 0.2.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows-future/windows-future-0.2.1.crate +Archive SHA-256: fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows-implement 0.60.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows-implement/windows-implement-0.60.0.crate +Archive SHA-256: a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836 + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows-interface 0.59.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows-interface/windows-interface-0.59.1.crate +Archive SHA-256: bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8 + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows-link 0.1.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows-link/windows-link-0.1.1.crate +Archive SHA-256: 76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38 + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows-link 0.2.1 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows-link/windows-link-0.2.1.crate +Archive SHA-256: f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5 + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows-numerics 0.2.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows-numerics/windows-numerics-0.2.0.crate +Archive SHA-256: 9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1 + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows-result 0.3.4 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows-result/windows-result-0.3.4.crate +Archive SHA-256: 56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6 + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows-strings 0.4.2 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows-strings/windows-strings-0.4.2.crate +Archive SHA-256: 56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57 + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows-sys 0.52.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows-sys/windows-sys-0.52.0.crate +Archive SHA-256: 282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows-sys 0.59.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows-sys/windows-sys-0.59.0.crate +Archive SHA-256: 1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows-sys 0.60.2 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows-sys/windows-sys-0.60.2.crate +Archive SHA-256: f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows-sys 0.61.2 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows-sys/windows-sys-0.61.2.crate +Archive SHA-256: ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows-targets 0.52.6 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows-targets/windows-targets-0.52.6.crate +Archive SHA-256: 9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973 + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows-targets 0.53.2 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows-targets/windows-targets-0.53.2.crate +Archive SHA-256: c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows-threading 0.1.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows-threading/windows-threading-0.1.0.crate +Archive SHA-256: b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6 + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows_x86_64_msvc 0.52.6 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows_x86_64_msvc/windows_x86_64_msvc-0.52.6.crate +Archive SHA-256: 589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: windows_x86_64_msvc 0.53.0 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/windows_x86_64_msvc/windows_x86_64_msvc-0.53.0.crate +Archive SHA-256: 271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486 + Notice 214: archive file license-apache-2.0 + Notice 215: archive file license-mit + +Package: winnow 1.0.3 +Declared license: MIT +Archive: https://static.crates.io/crates/winnow/winnow-1.0.3.crate +Archive SHA-256: 0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1 + Notice 216: archive file LICENSE-MIT + +Package: xattr 1.5.0 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/xattr/xattr-1.5.0.crate +Archive SHA-256: 0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e + Notice 001: archive file LICENSE-APACHE + Notice 177: archive file LICENSE-MIT + +Package: xxhash-rust 0.8.15 +Declared license: BSL-1.0 +Archive: https://static.crates.io/crates/xxhash-rust/xxhash-rust-0.8.15.crate +Archive SHA-256: fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3 + Notice 217: archive file LICENSE + +Package: zerocopy 0.8.27 +Declared license: BSD-2-Clause OR Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/zerocopy/zerocopy-0.8.27.crate +Archive SHA-256: 0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c + Notice 218: archive file LICENSE-APACHE + Notice 219: archive file LICENSE-BSD + Notice 220: archive file LICENSE-MIT + +Package: zerocopy-derive 0.8.27 +Declared license: BSD-2-Clause OR Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/zerocopy-derive/zerocopy-derive-0.8.27.crate +Archive SHA-256: 88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831 + Notice 218: archive file LICENSE-APACHE + Notice 219: archive file LICENSE-BSD + Notice 220: archive file LICENSE-MIT + +Package: zeroize 1.8.1 +Declared license: Apache-2.0 OR MIT +Archive: https://static.crates.io/crates/zeroize/zeroize-1.8.1.crate +Archive SHA-256: ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde + Notice 051: archive file LICENSE-APACHE + Notice 221: archive file LICENSE-MIT + +Package: zmij 1.0.18 +Declared license: MIT +Archive: https://static.crates.io/crates/zmij/zmij-1.0.18.crate +Archive SHA-256: 1966f8ac2c1f76987d69a74d0e0f929241c10e78136434e3be70ff7f58f64214 + Notice 005: archive file LICENSE-MIT + +Package: zstd 0.13.3 +Declared license: MIT +Archive: https://static.crates.io/crates/zstd/zstd-0.13.3.crate +Archive SHA-256: e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a + Notice 222: archive file LICENSE + +Package: zstd-safe 7.2.4 +Declared license: MIT OR Apache-2.0 +Archive: https://static.crates.io/crates/zstd-safe/zstd-safe-7.2.4.crate +Archive SHA-256: 8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d + Notice 223: archive file LICENSE + Notice 059: archive file LICENSE.Apache-2.0 + Notice 222: archive file LICENSE.Mit + +Package: zstd-sys 2.0.15+zstd.1.5.7 +Declared license: MIT/Apache-2.0 +Archive: https://static.crates.io/crates/zstd-sys/zstd-sys-2.0.15+zstd.1.5.7.crate +Archive SHA-256: eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237 + Notice 223: archive file LICENSE + Notice 059: archive file LICENSE.Apache-2.0 + Notice 224: archive file LICENSE.BSD-3-Clause + Notice 222: archive file LICENSE.Mit + Notice 225: archive file zstd/COPYING + Notice 226: archive file zstd/LICENSE + +NOTICE TEXTS + +===== NOTICE 001 ===== + 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 [yyyy] [name of copyright owner] + +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. + +===== NOTICE 002 ===== +Copyright (c) 2016-2018 The gimli 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. + +===== NOTICE 003 ===== +Copyright (C) Jonas Schievink + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +===== NOTICE 004 ===== + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/LICENSE-2.0 + +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 [yyyy] [name of copyright owner] + +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 + + https://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. + +===== NOTICE 005 ===== +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. + +===== NOTICE 006 ===== +Copyright (c) 2018 Tom Kaitchuck + +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. + +===== NOTICE 007 ===== +This project is dual-licensed under the Unlicense and MIT licenses. + +You may use this code under the terms of either license. + +===== NOTICE 008 ===== +The MIT License (MIT) + +Copyright (c) 2015 Andrew Gallant + +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. + +===== NOTICE 009 ===== +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +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 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. + +For more information, please refer to + +===== NOTICE 010 ===== + 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 + +===== NOTICE 011 ===== + 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 {yyyy} {name of copyright owner} + + 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. + + +===== NOTICE 012 ===== +Copyright (c) Individual contributors + +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. + +===== NOTICE 013 ===== +Copyright (c) 2015 Josh Triplett, 2022 The rust-cli 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. + +===== NOTICE 014 ===== +Copyright (c) 2016 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. + +===== NOTICE 015 ===== +Copyright (c) 2015 Markus Unterwaditzer + +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. + +===== NOTICE 016 ===== +Copyright (c) 2018 Josh Stone + +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. + +===== NOTICE 017 ===== +SPDX-License-Identifier: ISC AND (Apache-2.0 OR ISC) + + +Apache 2.0 license +------------------------------------- + + + 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 + + +ISC license +------------------------------------- + + +Copyright Amazon.com, Inc. or its affiliates. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +===== NOTICE 018 ===== +AWS Libcrypto (AWS-LC) + +AWS-LC is a fork of BoringSSL, which is itself a fork of OpenSSL. +Content from these and other sources retains their original licensing, +as described below. New files from AWS-LC are made available under the Apache-2.0 license OR the ISC +license. These licenses are reproduced at the bottom of this file. + +``` +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 OR ISC +``` + + +================================================================================ +BoringSSL +================================================================================ + +BoringSSL is a Google-maintained fork of OpenSSL. Historically, code +authored by Google for BoringSSL was licensed under the ISC License. +BoringSSL has since relicensed upstream to Apache License 2.0. Existing +AWS-LC code originating from BoringSSL retains its ISC license, while +newer code taken from BoringSSL is licensed under Apache License 2.0. + +``` +Copyright (c) 2014-2024 Google Inc. +SPDX-License-Identifier: ISC +``` + +Additional individual contributions to BoringSSL-derived code are +covered under the ISC license: + + - Brian Smith (Copyright 2016) + - Robert Nagy (Copyright 2022) + - Arm Ltd (Copyright 2020) + +``` +Copyright (c) 2025-2026 Google Inc. +SPDX-License-Identifier: Apache-2.0 +``` + +================================================================================ +OpenSSL +================================================================================ + +Code derived from the OpenSSL project is licensed under the +Apache License, Version 2.0. + +``` +Copyright (c) 1998-2011 The OpenSSL Project. All rights reserved. +SPDX-License-Identifier: Apache-2.0 +``` + +Some OpenSSL-derived files also carry the original SSLeay copyright: + +``` +Copyright (c) 1995-1998 Eric Young (eay@cryptsoft.com). All rights reserved. +SPDX-License-Identifier: Apache-2.0 +``` + +Portions of OpenSSL-derived code include contributions from: + + - Sun Microsystems, Inc. (Copyright 2002) + - Nokia (Copyright 2005) + - Intel Corporation (Copyright 2012-2021) + +These contributions are covered under the Apache-2.0 license. + +================================================================================ +mlkem-native +================================================================================ + +Code from the mlkem-native project is licensed under Apache License 2.0 or MIT or ISC. + +``` +Copyright (c) The mlkem-native project authors. +SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT +``` + +================================================================================ +mldsa-native +================================================================================ + +Code from the mldsa-native project is licensed under Apache License 2.0 or MIT or ISC + +``` +Copyright (c) The mldsa-native project authors. +SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT +``` + +================================================================================ +Third-Party Libraries (compiled into libcrypto/libssl) +================================================================================ + +Fiat Cryptography +----------------- +Synthesizing Correct-by-Construction Code for Cryptographic Primitives. +See third_party/fiat/LICENSE. + +``` +Copyright (c) 2015-2020 the fiat-crypto authors. +SPDX-License-Identifier: MIT +``` + +s2n-bignum +---------- +Integer arithmetic routines for cryptography. +See third_party/s2n-bignum/s2n-bignum-imported/LICENSE. + +``` +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT-0 +``` + +Note: ML-KEM/SHA3 code within s2n-bignum is licensed as +Apache-2.0 OR ISC OR MIT (with attribution), sourced from the +mlkem-native project. + +Jitter Entropy RNG +------------------- +CPU Jitter Random Number Generator Library. +See third_party/jitterentropy/jitterentropy-library/LICENSE. + +The Jitter Entropy library is dual-licensed under a BSD-style license +and the GNU General Public License Version 2. Amazon expressly elects +to distribute the package under the 3-Clause BSD License and NOT under +GNU General Public License Version 2. + +``` +Copyright (C) 2017 - 2025, Stephan Mueller . +SPDX-License-Identifier: BSD-3-Clause +``` + +Keccak / AES Reference Implementations +--------------------------------------- +Public domain (CC0) contributions. +https://creativecommons.org/public-domain/cc0 + +Code from sources and by authors listed in comments on top of the respective files. + + +================================================================================ +Third-Party Libraries (NOT compiled into libcrypto/libssl) +================================================================================ + +The following are used for testing and build tooling only. Distributing +code linked against AWS-LC (libcrypto/libssl) does NOT trigger these +license obligations. + +Google Test +----------- +See third_party/googletest/LICENSE. + +``` +Copyright 2008 Google Inc. +SPDX-License-Identifier: BSD-3-Clause +``` + +Go Standard Library +------------------- +Code in ssl/test/runner/ is derived from the Go standard library. + +``` +Copyright (c) The Go Authors. All rights reserved. +SPDX-License-Identifier: BSD-3-Clause +``` + +Wycheproof Test Vectors +------------------------ +Project Wycheproof is a community managed repository of test vectors that can be used by cryptography library +developers to test against known attacks, specification inconsistencies, and other various implementation bugs. + +See third_party/wycheproof_testvectors/LICENSE. + +``` +SPDX-License-Identifier: Apache-2.0 +``` + + +================================================================================ +Full License Texts +================================================================================ + +Apache License 2.0 +------------------ +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 [yyyy] [name of copyright owner] + +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. + +ISC License +----------- +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted, provided that the +above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +MIT License +----------- +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. + +BSD 3-Clause License +-------------------- +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +MIT No Attribution (MIT-0) +-------------------------- +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. + +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. + +===== NOTICE 019 ===== +The MIT License (MIT) + +Copyright (c) 2015-2020 the fiat-crypto authors (see +https://github.com/mit-plv/fiat-crypto/blob/master/AUTHORS). + +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. + +===== NOTICE 020 ===== +Copyright (c) 2014 Alex Crichton + +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. + +===== NOTICE 021 ===== +The MIT License (MIT) + +Copyright (c) 2015 Alice Maz + +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. + +===== NOTICE 022 ===== + 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 [yyyy] [name of copyright owner] + +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. + +===== NOTICE 023 ===== +Copyright (c) 2023 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. + +===== NOTICE 024 ===== +Copyright (c) 2015 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. + +===== NOTICE 025 ===== +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. + +===== NOTICE 026 ===== + 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 [yyyy] [name of copyright owner] + +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. + +===== NOTICE 027 ===== +Copyright (c) 2018-2019 The RustCrypto 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. + +===== NOTICE 028 ===== +This project is licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or + https://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or + https://opensource.org/licenses/MIT) + +at your option. + +===== NOTICE 029 ===== +The MIT License (MIT) + +Copyright (c) 2018-2019 Andrew Gallant + +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. + +===== NOTICE 030 ===== +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 [yyyy] [name of copyright owner] + +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. + +===== NOTICE 031 ===== +MIT License + +Copyright (c) 2019 Daniel "Lokathor" Gee. + +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 (including the next paragraph) 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. + +===== NOTICE 032 ===== +Copyright (c) 2019 Daniel "Lokathor" Gee. + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + +===== NOTICE 033 ===== +Copyright (c) 2018 Carl Lerche + +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. + +===== NOTICE 034 ===== +Copyright (c) 2015 Steven Allen +Copyright (c) The assert_fs Contributors +Copyright (c) The camino-tempfile Contributors + +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. + +===== NOTICE 035 ===== +Copyright (c) 2019 Embark Studios + +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. + +===== NOTICE 036 ===== +MIT License + +Copyright (c) 2020 Katharos Technology + +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. + +===== NOTICE 037 ===== +Copyright (c) 2019-2026 The RustCrypto 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. + +===== NOTICE 038 ===== +Rust-chrono is dual-licensed under The MIT License [1] and +Apache 2.0 License [2]. Copyright (c) 2014--2026, Kang Seonghoon and +contributors. + +Nota Bene: This is same as the Rust Project's own license. + + +[1]: , which is reproduced below: + +~~~~ +The MIT License (MIT) + +Copyright (c) 2014, Kang Seonghoon. + +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. +~~~~ + + +[2]: , which is reproduced below: + +~~~~ + 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 [yyyy] [name of copyright owner] + +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. +~~~~ + + +===== NOTICE 039 ===== +The MIT License (MIT) + +Copyright (c) 2017 Armin Ronacher + +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. + + +===== NOTICE 040 ===== +MIT License + +Copyright (c) 2025 rutrum + +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. + +===== NOTICE 041 ===== +Copyright (c) 2012-2013 Mozilla Foundation + +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. + +===== NOTICE 042 ===== +MIT License + +Copyright (c) 2024 Spire + +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. + +===== NOTICE 043 ===== +Copyright (c) 2020-2025 The RustCrypto 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. + +===== NOTICE 044 ===== +MIT License + +Copyright (c) 2018 Sam Rijs, Alex Crichton and contributors + +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. + +===== NOTICE 045 ===== +MIT License + +Copyright (c) 2019 Timon + +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. + +===== NOTICE 046 ===== +Copyright (c) 2021 RustCrypto 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. + +===== NOTICE 047 ===== + 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 2020 Nor Khasyatillah + + 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. + +===== NOTICE 048 ===== +MIT License + +Copyright (c) 2020 Nor Khasyatillah + +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. + +===== NOTICE 049 ===== +MIT License + +Copyright 2019 Ryan O'Beirne + +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. + +===== NOTICE 050 ===== +These documentation pages include resources by third parties. This copyright +file applies only to those resources. The following third party resources are +included, and carry their own copyright notices and license terms: + +* Fira Sans (FiraSans-Regular.woff2, FiraSans-Medium.woff2, + FiraSans-Regular.woff, FiraSans-Medium.woff): + + Copyright (c) 2014, Mozilla Foundation https://mozilla.org/ + with Reserved Font Name Fira Sans. + + Copyright (c) 2014, Telefonica S.A. + + Licensed under the SIL Open Font License, Version 1.1. + See FiraSans-LICENSE.txt. + +* rustdoc.css, main.js, and playpen.js: + + Copyright 2015 The Rust Developers. + Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE.txt) or + the MIT license (LICENSE-MIT.txt) at your option. + +* normalize.css: + + Copyright (c) Nicolas Gallagher and Jonathan Neal. + Licensed under the MIT license (see LICENSE-MIT.txt). + +* Source Code Pro (SourceCodePro-Regular.ttf.woff2, + SourceCodePro-Semibold.ttf.woff2, SourceCodePro-It.ttf.woff2, + SourceCodePro-Regular.ttf.woff, SourceCodePro-Semibold.ttf.woff, + SourceCodePro-It.ttf.woff): + + Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), + with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark + of Adobe Systems Incorporated in the United States and/or other countries. + + Licensed under the SIL Open Font License, Version 1.1. + See SourceCodePro-LICENSE.txt. + +* Source Serif 4 (SourceSerif4-Regular.ttf.woff2, SourceSerif4-Bold.ttf.woff2, + SourceSerif4-It.ttf.woff2, SourceSerif4-Regular.ttf.woff, + SourceSerif4-Bold.ttf.woff, SourceSerif4-It.ttf.woff): + + Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name + 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United + States and/or other countries. + + Licensed under the SIL Open Font License, Version 1.1. + See SourceSerif4-LICENSE.md. + +This copyright file is intended to be distributed with rustdoc output. + +===== NOTICE 051 ===== + + 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 [yyyy] [name of copyright owner] + + 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. + +===== NOTICE 052 ===== +MIT License + +Copyright (c) 2021 Roland Fredenhagen + +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. + +===== NOTICE 053 ===== +The MIT License (MIT) + +Copyright (c) 2016 Jelte Fennema + +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. + +===== NOTICE 054 ===== +Copyright (c) 2017 Artyom Pavlov + +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. + +===== NOTICE 055 ===== + 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. + +===== NOTICE 056 ===== +Copyright (c) 2018-2019 dirs-rs contributors + +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. + +===== NOTICE 057 ===== +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 [yyyy] [name of copyright owner] + +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. + +===== NOTICE 058 ===== +Copyright (c) 2020 Olivier Goffart + +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. + +===== NOTICE 059 ===== + + 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 + +===== NOTICE 060 ===== +The MIT License (MIT) + +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. + +===== NOTICE 061 ===== +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. + +===== NOTICE 062 ===== +Copyright 2025 Benoît du Garreau + +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. + +===== NOTICE 063 ===== +Copyright 2025 Benoît du Garreau + +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. + +===== NOTICE 064 ===== +Copyright (c) 2015 + +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. + +===== NOTICE 065 ===== +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 + +===== NOTICE 066 ===== +encoding_rs is copyright Mozilla Foundation. + +Licensed under the Apache License, Version 2.0 + or the MIT +license , +at your option. All files in the project carrying such +notice may not be copied, modified, or distributed except +according to those terms. + +This crate includes data derived from the data files supplied +with the WHATWG Encoding Standard, which, when incorporated into +source code, are licensed under the BSD 3-Clause License +. + +Test code within encoding_rs is dedicated to the Public Domain when so +designated (see the individual files for PD/CC0-dedicated sections). + +===== NOTICE 067 ===== +Copyright Mozilla Foundation + +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. + +===== NOTICE 068 ===== +Copyright © WHATWG (Apple, Google, Mozilla, Microsoft). + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +===== NOTICE 069 ===== +MIT License + +Copyright (c) 2019 Anton Lazarev + +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. + +===== NOTICE 070 ===== +Copyright (c) 2016--2023 + +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. + +===== NOTICE 071 ===== +Copyright (c) 2014 Chris Wong + +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. + +===== NOTICE 072 ===== +Licensed under the Apache License, Version 2.0 or the MIT license +, at your +option. All files in the project carrying such notice may not be +copied, modified, or distributed except according to those terms. + +===== NOTICE 073 ===== +The MIT License + +Copyright 2015 The Fancy Regex Authors. + +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. + +===== NOTICE 074 ===== +MIT License + +Copyright (c) 2018 Wez Furlong + +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. + +===== NOTICE 075 ===== +UNICODE LICENSE V3 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 1991-2023 Unicode, Inc. + +NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of data files and any associated documentation (the "Data Files") or +software and any associated documentation (the "Software") to deal in the +Data Files or Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, and/or sell +copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that either (a) +this copyright and permission notice appear with all copies of the Data +Files or Software, or (b) this copyright and permission notice appear in +associated Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF +THIRD PARTY RIGHTS. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written +authorization of the copyright holder. + +===== NOTICE 076 ===== +Copyright (c) 2015-2017 + +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. + +===== NOTICE 077 ===== +Copyright (c) 2014-2025 Alex Crichton + +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. + +===== NOTICE 078 ===== +Copyright (c) 2017 Contributors + +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. + +===== NOTICE 079 ===== +Copyright (c) 2024 Orson Peters + +This software is provided 'as-is', without any express or implied warranty. In +no event will the authors be held liable for any damages arising from the use of +this software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim + that you wrote the original software. If you use this software in a product, + an acknowledgment in the product documentation would be appreciated but is + not required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + +===== NOTICE 080 ===== +MIT License + +Copyright (c) 2017 Denis Kurilenko + +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. + +===== NOTICE 081 ===== +Copyright (c) The future-queue Contributors + +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. + +--- + +The code is derived from futures-rs, and is used under +the Apache 2.0 and MIT licenses. + +> Copyright (c) 2016 Alex Crichton +> Copyright (c) 2017 The Tokio Authors +> +> 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. + +===== NOTICE 082 ===== + 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 (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +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. + +===== NOTICE 083 ===== +Copyright (c) 2016 Alex Crichton +Copyright (c) 2017 The Tokio Authors + +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. + +===== NOTICE 084 ===== +The MIT License (MIT) + +Copyright (c) 2015 Bartłomiej Kamiński + +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. + +===== NOTICE 085 ===== + Apache License + Version 2.0, January 2004 + https://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 [yyyy] [name of copyright owner] + +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 + + https://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. + +===== NOTICE 086 ===== +Copyright (c) 2018-2025 The rust-random Project Developers +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. + +===== NOTICE 087 ===== +Copyright (c) 2018-2026 The rust-random Project Developers +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. + +===== NOTICE 088 ===== +The MIT License (MIT) + +Copyright (c) m4b 2016-2024 + +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. + +===== NOTICE 089 ===== +Copyright (c) The cargo-guppy Contributors + +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. + +===== NOTICE 090 ===== +Copyright (c) 2016 Amanieu d'Antras + +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. + +===== NOTICE 091 ===== +Copyright (c) 2013-2014 The Rust Project Developers. +Copyright (c) 2015-2020 The rust-hex 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. + +===== NOTICE 092 ===== + 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 2017 http-rs authors + +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. + +===== NOTICE 093 ===== +Copyright (c) 2017 http-rs authors + +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. + +===== NOTICE 094 ===== +Copyright (c) 2015-2025 Sean McArthur + +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. + + +===== NOTICE 095 ===== +Copyright (c) 2016 The humantime Developers + +Includes parts of http date with the following copyright: +Copyright (c) 2016 Pyfisch + +Includes portions of musl libc with the following copyright: +Copyright © 2005-2013 Rich Felker + + +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. + +===== NOTICE 096 ===== +The MIT License (MIT) + +Copyright (c) 2017 The serde-humantime 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. + + +===== NOTICE 097 ===== + 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 2020 Andrew Straw + +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. + +===== NOTICE 098 ===== +Copyright (c) 2020 Andrew D. Straw + +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. + +===== NOTICE 099 ===== +Copyright (c) 2025 Oxide Computer Company + +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. + +===== NOTICE 100 ===== +Copyright (c) The rust-url 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. + +===== NOTICE 101 ===== +MIT License + +Copyright (c) 2017 Michael Bryan + +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. + +===== NOTICE 102 ===== +Copyright (c) 2016--2017 + +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. + +===== NOTICE 103 ===== +The ISC License + +Copyright (c) Kat Marchán and other contributors. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.Apache License + +===== NOTICE 104 ===== +MIT License + +Copyright (c) 2020 🐝🐝🐝 + +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. + +===== NOTICE 105 ===== +Copyright (c) 2010 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. + +===== NOTICE 106 ===== +Copyright (c) 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. + +===== NOTICE 107 ===== +Short version for non-lawyers: + +`linux-raw-sys` is triple-licensed under Apache 2.0 with the LLVM Exception, +Apache 2.0, and MIT terms. + + +Longer version: + +Copyrights in the `linux-raw-sys` project are retained by their contributors. +No copyright assignment is required to contribute to the `linux-raw-sys` +project. + +Some files include code derived from Rust's `libstd`; see the comments in +the code for details. + +Except as otherwise noted (below and/or in individual files), `linux-raw-sys` +is licensed under: + + - the Apache License, Version 2.0, with the LLVM Exception + or + + - the Apache License, Version 2.0 + or + , + - or the MIT license + or + , + +at your option. + +===== NOTICE 108 ===== + + 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 [yyyy] [name of copyright owner] + + 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. + + +--- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + + +===== NOTICE 109 ===== +Copyright (c) 2020 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. + +===== NOTICE 110 ===== +Copyright (c) The nextest Contributors + +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. + +===== NOTICE 111 ===== +MIT License + +Copyright (c) 2016 Jerome Froelich + +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. + +===== NOTICE 112 ===== + + 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 2018 Wesley Norris + + 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. + +===== NOTICE 113 ===== +Copyright © 2018 Wesley Norris + +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. + +===== NOTICE 114 ===== + 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 [2015] [Dan Burkert] + +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. + +===== NOTICE 115 ===== +Copyright (c) 2020 Yevhenii Reizner +Copyright (c) 2015 Dan Burkert + +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. + +===== NOTICE 116 ===== +Copyright (c) 2017 Gilad Naaman + +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. + +===== NOTICE 117 ===== +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 [yyyy] Kat +Marchán + +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. + +===== NOTICE 118 ===== +Minimal-lexical is dual licensed under the Apache 2.0 license as well as the MIT +license. See the LICENCE-MIT and the LICENCE-APACHE files for the licenses. + +--- + +`src/bellerophon.rs` is loosely based off the Golang implementation, +found [here](https://github.com/golang/go/blob/b10849fbb97a2244c086991b4623ae9f32c212d0/src/strconv/extfloat.go). +That code (used if the `compact` feature is enabled) is subject to a +[3-clause BSD license](https://github.com/golang/go/blob/b10849fbb97a2244c086991b4623ae9f32c212d0/LICENSE): + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +===== NOTICE 119 ===== +MIT License + +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC +Copyright (c) 2017 Frommi +Copyright (c) 2017-2024 oyvindln + + +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. + +===== NOTICE 120 ===== +MIT License + +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC +Copyright (c) 2017 Frommi +Copyright (c) 2017-2024 oyvindln + +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. + +===== NOTICE 121 ===== +Copyright 2013-2014 RAD Game Tools and Valve Software +Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC +Copyright (c) 2020 Frommi +Copyright (c) 2017-2024 oyvindln + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. + +===== NOTICE 122 ===== +Copyright (c) 2014 Carl Lerche and other MIO contributors + +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. + +===== NOTICE 123 ===== +The MIT License (MIT) + +Copyright (c) 2016 Johann Tuffe + +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. + +===== NOTICE 124 ===== +Copyright (c) 2024 Oxide Computer Company + +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. + +===== NOTICE 125 ===== +The MIT License (MIT) + +Copyright (c) 2015 Carl Lerche + nix-rust Authors + +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. + +===== NOTICE 126 ===== +Copyright (c) 2014-2019 Geoffroy Couprie + +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. + +===== NOTICE 127 ===== +Copyright (c) 2015 The Gimli 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. + +===== NOTICE 128 ===== +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + +===== NOTICE 129 ===== +Copyright (c) 2015 Jonathan Reem + +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. + +===== NOTICE 130 ===== +MIT License + +Copyright (c) 2020 - present The owo-colors 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. + +===== NOTICE 131 ===== + 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 2017 The Rust Project Developers + +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. + +===== NOTICE 132 ===== +Copyright (c) 2013-2025 The rust-url 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. + +===== NOTICE 133 ===== +Graphosaurus (c) by the petgraph project. + +Graphosaurus is licensed under a +Creative Commons Attribution-ShareAlike 4.0 International License. + +You should have received a copy of the license along with this +work. If not, see . + +===== NOTICE 134 ===== +The MIT License (MIT) + +Copyright (c) 2014-2022 Steven Fackler, Yuki Okushi + +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. + +===== NOTICE 135 ===== +Copyright (c) 2017 Plain contributors + +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. + +===== NOTICE 136 ===== + 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 2019 The CryptoCorrosion Contributors + +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. + +===== NOTICE 137 ===== +Copyright (c) 2019 The CryptoCorrosion Contributors + +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. + +===== NOTICE 138 ===== +MIT License + +Copyright (c) 2018 Andrei Volnin + +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. + +===== NOTICE 139 ===== +Copyright (c) 2016 FullContact, Inc + +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. + +===== NOTICE 140 ===== +The MIT License + +Copyright 2015 Google Inc. All rights reserved. + +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. + +===== NOTICE 141 ===== +Copyright (c) 2015 The quick-error 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. + +===== NOTICE 142 ===== +Copyrights in the Rand project are retained by their contributors. No +copyright assignment is required to contribute to the Rand project. + +For full authorship information, see the version control history. + +Except as otherwise noted (below and/or in individual files), Rand is +licensed under the Apache License, Version 2.0 or + or the MIT license + or , at your option. + +The Rand project includes code from the Rust project +published under these same licenses. + +===== NOTICE 143 ===== + Apache License + Version 2.0, January 2004 + https://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 + +===== NOTICE 144 ===== +Copyright 2018 Developers of the Rand project +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. + +===== NOTICE 145 ===== +Copyrights in the Rand project are retained by their contributors. No +copyright assignment is required to contribute to the Rand project. + +For full authorship information, see the version control history. + +Except as otherwise noted (below and/or in individual files), Rand is +licensed under the Apache License, Version 2.0 or + or the MIT license + or , at your option. + +===== NOTICE 146 ===== + Apache License + Version 2.0, January 2004 + https://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. + +===== NOTICE 147 ===== +Copyright (c) 2018-2026 The Rand 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. + +===== NOTICE 148 ===== +UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE + +Unicode Data Files include all data files under the directories +http://www.unicode.org/Public/, http://www.unicode.org/reports/, +http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and +http://www.unicode.org/utility/trac/browser/. + +Unicode Data Files do not include PDF online code charts under the +directory http://www.unicode.org/Public/. + +Software includes any source code published in the Unicode Standard +or under the directories +http://www.unicode.org/Public/, http://www.unicode.org/reports/, +http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and +http://www.unicode.org/utility/trac/browser/. + +NOTICE TO USER: Carefully read the following legal agreement. +BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S +DATA FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"), +YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +TERMS AND CONDITIONS OF THIS AGREEMENT. +IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE +THE DATA FILES OR SOFTWARE. + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 1991-2018 Unicode, Inc. All rights reserved. +Distributed under the Terms of Use in http://www.unicode.org/copyright.html. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Unicode data files and any associated documentation +(the "Data Files") or Unicode software and any associated documentation +(the "Software") to deal in the Data Files or Software +without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, and/or sell copies of +the Data Files or Software, and to permit persons to whom the Data Files +or Software are furnished to do so, provided that either +(a) this copyright and permission notice appear with all copies +of the Data Files or Software, or +(b) this copyright and permission notice appear in associated +Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF THIRD PARTY RIGHTS. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS +NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL +DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THE DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, +use or other dealings in these Data Files or Software without prior +written authorization of the copyright holder. + +===== NOTICE 149 ===== +Short version for non-lawyers: + +`rustix` is triple-licensed under Apache 2.0 with the LLVM Exception, +Apache 2.0, and MIT terms. + + +Longer version: + +Copyrights in the `rustix` project are retained by their contributors. +No copyright assignment is required to contribute to the `rustix` +project. + +Some files include code derived from Rust's `libstd`; see the comments in +the code for details. + +Except as otherwise noted (below and/or in individual files), `rustix` +is licensed under: + + - the Apache License, Version 2.0, with the LLVM Exception + or + + - the Apache License, Version 2.0 + or + , + - or the MIT license + or + , + +at your option. + +===== NOTICE 150 ===== +ISC License (ISC) +Copyright (c) 2016, Joseph Birr-Pixton + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted, provided that the +above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +===== NOTICE 151 ===== +Copyright (c) 2016 Joseph Birr-Pixton + +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. + +===== NOTICE 152 ===== + 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 2023 Dirkjan Ochtman + +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. + +===== NOTICE 153 ===== +Copyright (c) 2023 Dirkjan Ochtman + +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. + +===== NOTICE 154 ===== +Except as otherwise noted, this project is licensed under the following +(ISC-style) terms: + +Copyright 2015 Brian Smith. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +The files under third-party/chromium are licensed as described in +third-party/chromium/LICENSE. + +===== NOTICE 155 ===== +MIT License + +Copyright (c) 2019 Mark Juggurnauth-Thomas + +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. + +===== NOTICE 156 ===== +Copyright (c) 2016-2019 Ulrik Sverdrup "bluss" and scopeguard 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. + +===== NOTICE 157 ===== +The MIT License (MIT) + +Copyright (c) m4b 2016 + +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. + +===== NOTICE 158 ===== +MIT License + +Copyright (c) 2017 + +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. + +===== NOTICE 159 ===== +MIT License + +Copyright (c) 2017 James Kominick + +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. + +===== NOTICE 160 ===== +Copyright (c) 2006-2009 Graydon Hoare +Copyright (c) 2009-2013 Mozilla Foundation +Copyright (c) 2016 Artyom Pavlov + +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. + +===== NOTICE 161 ===== +Copyright (c) 2019 Eliza Weisman + +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. + +===== NOTICE 162 ===== + 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 [yyyy] [name of copyright owner] + +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. + +===== NOTICE 163 ===== +Copyright (c) 2016 Tomasz Miąsko + +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. + +===== NOTICE 164 ===== +Copyright 2015 Nicholas Allegra (comex). + +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. + +===== NOTICE 165 ===== +The MIT License (MIT) + +Copyright (c) 2015 Nicholas Allegra (comex). + +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. + +===== NOTICE 166 ===== +Copyright (c) 2017 tokio-jsonrpc 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. + +===== NOTICE 167 ===== +Copyright 2012-2016 The Rust Project Developers. +Copyright 2016-2024 Frank Denis. + +Licensed under the Apache License, Version 2.0 or the MIT license +, at your +option. + +===== NOTICE 168 ===== +Copyright (c) 2019 Carl Lerche + +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. + +===== NOTICE 169 ===== +Copyright (c) 2018 The Servo 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. + +===== NOTICE 170 ===== +Copyright (c) 2015 The rust-socks 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. + +===== NOTICE 171 ===== +MIT License + +Copyright (c) 2017 Nikolai Vazquez + +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. + +===== NOTICE 172 ===== +Copyright (c) 2018 Mozilla + +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. + +===== NOTICE 173 ===== +The MIT License (MIT) + +Copyright (c) 2015 Danny Guo +Copyright (c) 2016 Titus Wormer +Copyright (c) 2018 Akash Kurdekar + +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. + +===== NOTICE 174 ===== +Copyright (c) 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. +Copyright (c) 2016-2024 Isis Agora Lovecruft. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +===== NOTICE 175 ===== +MIT License + +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. + +===== NOTICE 176 ===== +Copyright (c) The tar-rs Project Contributors + +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. + +===== NOTICE 177 ===== +Copyright (c) 2015 Steven Allen + +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. + +===== NOTICE 178 ===== +Copyright (c) 2015 The terminal-size 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. + +===== NOTICE 179 ===== + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyleft (ↄ) meh. | http://meh.schizofreni.co + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + +===== NOTICE 180 ===== +Copyright (c) 2015 David Cuddeback + +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. + +===== NOTICE 181 ===== +MIT License + +Copyright (c) 2016 Martin Geisler + +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. + +===== NOTICE 182 ===== +Copyright 2017 Ruud van Asseldonk + +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. + +===== NOTICE 183 ===== +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. + +===== NOTICE 184 ===== + 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 2020 Tomasz "Soveu" Marx + + 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. + + +===== NOTICE 185 ===== +MIT License + +Copyright (c) 2020 Soveu + +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. + +===== NOTICE 186 ===== +zlib License + +(C) 2020 Tomasz "Soveu" Marx + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + + +===== NOTICE 187 ===== +MIT License + +Copyright (c) Tokio Contributors + +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. + +===== NOTICE 188 ===== +MIT License + +Copyright (c) 2019 Yoshua Wuyts +Copyright (c) Tokio Contributors + +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. + +===== NOTICE 189 ===== +Copyright (c) 2019 Tokio Contributors + +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. + +===== NOTICE 190 ===== +The MIT License (MIT) + +Copyright (c) 2014 Mathijs van de Nes + +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. + +===== NOTICE 191 ===== +MIT OR Apache-2.0 + +===== NOTICE 192 ===== + 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 2014 Paho Lurie-Gregg + +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. + +===== NOTICE 193 ===== +The MIT License (MIT) + +Copyright (c) 2014 Paho Lurie-Gregg + +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. + +===== NOTICE 194 ===== + 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 [yyyy] [name of copyright owner] + + 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. + +===== NOTICE 195 ===== +MIT License + +Copyright (c) [year] [fullname] + +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. + +===== NOTICE 196 ===== +Copyright (c) 2014-2026 Sean McArthur + +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. + + +===== NOTICE 197 ===== +Licensed under the Apache License, Version 2.0 + or the MIT +license , +at your option. All files in the project carrying such +notice may not be copied, modified, or distributed except +according to those terms. + +===== NOTICE 198 ===== +MIT License + +Copyright (c) 2024 Benjamin Sago, Fabio Valentini + +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. + +===== NOTICE 199 ===== +// Copyright 2015-2016 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +===== NOTICE 200 ===== +MIT License + +Copyright (c) 2019 Martin Algesten + +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. + +===== NOTICE 201 ===== +Copyright 2022 Martin Algesten + +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. + +===== NOTICE 202 ===== +© 2016 Bertram Truong +© 2021 Kornel Lesiński + +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. + +===== NOTICE 203 ===== +Copyright (c) 2016 Joe Wilm + +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. + +===== NOTICE 204 ===== +Copyright (c) 2014 The Rust Project Developers +Copyright (c) 2018 Ashley Mannix, Christopher Armstrong, Dylan DPC, Hunar Roop Kahlon + +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. + +===== NOTICE 205 ===== +The MIT License (MIT) +Copyright (c) 2017-2018 Sergio Benitez + +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. + +===== NOTICE 206 ===== +MIT License + +Copyright (c) 2018-Present Wez Furlong + +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. + +WezTerm bundles `JetBrains Mono`, `Noto Color Emoji` and `Roboto` fonts. +Those are distributed under the terms of the OFL 1.1, the text of which +can be found in the assets/fonts directory. + +WezTerm bundles `Symbols Nerd Font Mono`, built from only those icon sets +available from https://github.com/ryanoasis/nerd-fonts which are clearly +distributed under the terms of the OFL 1.1. +Note that WezTerm excludes the Pomicons icon set from this collection. + +===== NOTICE 207 ===== +# Community Data License Agreement - Permissive - Version 2.0 + +This is the Community Data License Agreement - Permissive, Version +2.0 (the "agreement"). Data Provider(s) and Data Recipient(s) agree +as follows: + +## 1. Provision of the Data + +1.1. A Data Recipient may use, modify, and share the Data made +available by Data Provider(s) under this agreement if that Data +Recipient follows the terms of this agreement. + +1.2. This agreement does not impose any restriction on a Data +Recipient's use, modification, or sharing of any portions of the +Data that are in the public domain or that may be used, modified, +or shared under any other legal exception or limitation. + +## 2. Conditions for Sharing Data + +2.1. A Data Recipient may share Data, with or without modifications, so +long as the Data Recipient makes available the text of this agreement +with the shared Data. + +## 3. No Restrictions on Results + +3.1. This agreement does not impose any restriction or obligations +with respect to the use, modification, or sharing of Results. + +## 4. No Warranty; Limitation of Liability + +4.1. All Data Recipients receive the Data subject to the following +terms: + +THE DATA IS PROVIDED ON AN "AS IS" BASIS, WITHOUT REPRESENTATIONS, +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. + +NO DATA PROVIDER SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING +WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE DATA OR RESULTS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +## 5. Definitions + +5.1. "Data" means the material received by a Data Recipient under +this agreement. + +5.2. "Data Provider" means any person who is the source of Data +provided under this agreement and in reliance on a Data Recipient's +agreement to its terms. + +5.3. "Data Recipient" means any person who receives Data directly +or indirectly from a Data Provider and agrees to the terms of this +agreement. + +5.4. "Results" means any outcome obtained by computational analysis +of Data, including for example machine learning models and models' +insights. + +===== NOTICE 208 ===== +Copyright (c) 2022-Present Wez Furlong + +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. + +Portions of this crate were derived from the bidiref reference implementation +of the UBA, and several data files are included that have the following +copyright and license: + +COPYRIGHT AND PERMISSION NOTICE + +Copyright © 1991-2022 Unicode, Inc. All rights reserved. +Distributed under the Terms of Use in https://www.unicode.org/license.txt +which is reproduced below: + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Unicode data files and any associated documentation +(the "Data Files") or Unicode software and any associated documentation +(the "Software") to deal in the Data Files or Software +without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, and/or sell copies of +the Data Files or Software, and to permit persons to whom the Data Files +or Software are furnished to do so, provided that either +(a) this copyright and permission notice appear with all copies +of the Data Files or Software, or +(b) this copyright and permission notice appear in associated +Documentation. + +THE DATA FILES AND SOFTWARE ARE 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 OF THIRD PARTY RIGHTS. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS +NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL +DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THE DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, +use or other dealings in these Data Files or Software without prior +written authorization of the copyright holder. + + +===== NOTICE 209 ===== +MIT License + +Copyright (c) 2023-Present Wez Furlong + +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. + +===== NOTICE 210 ===== +Copyright (c) 2018-Present Wez Furlong + +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. + +===== NOTICE 211 ===== +MIT License + +Copyright (c) 2018-Present Wez Furlong + +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. + +===== NOTICE 212 ===== + 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 {yyyy} {name of copyright owner} + + 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. + +===== NOTICE 213 ===== +Copyright (c) 2015-2018 The winapi-rs 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. + +===== NOTICE 214 ===== + 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 (c) Microsoft Corporation. + + 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. + +===== NOTICE 215 ===== + MIT License + + Copyright (c) Microsoft Corporation. + + 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 + +===== NOTICE 216 ===== +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. + +===== NOTICE 217 ===== +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +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, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +===== NOTICE 218 ===== + 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 2023 The Fuchsia Authors + + 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. + + +===== NOTICE 219 ===== +Copyright 2019 The Fuchsia Authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +===== NOTICE 220 ===== +Copyright 2023 The Fuchsia Authors + +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. + + +===== NOTICE 221 ===== +MIT License + +Copyright (c) 2018-2021 The RustCrypto 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. + +===== NOTICE 222 ===== +The MIT License (MIT) +Copyright (c) 2016 Alexandre Bury + +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. + +===== NOTICE 223 ===== +MIT or Apache-2.0 + +===== NOTICE 224 ===== +The auto-generated bindings are under the 3-clause BSD license: + +BSD License + +For Zstandard software + +Copyright (c) 2016-present, Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +===== NOTICE 225 ===== + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. + +===== NOTICE 226 ===== +BSD License + +For Zstandard software + +Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook, nor Meta, nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From 6debad3e2fae69aeb3760ad6b7965f02f7b4a81c Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:47:12 +0000 Subject: [PATCH 41/49] Record native notice verification and remaining release blockers --- docs/plans/code-polishy-rust-support-plan.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 9aa1fc04..7d6cbf9e 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -586,6 +586,15 @@ and every package's source reference. The complete file is 749,595 bytes, about 87 KB with gzip; the existing installers already carry and authenticate it. The SBOM reader accepts this bounded inventory up to 2 MiB. +Commit `3b229f6` passes the focused release unit and installer contract suites, +including oversized-notice rejection and the final carried notice file. +Formatting and whitespace checks pass. Its clean-checkout checkpoint against +`48b4668` remains rejected by 55 existing semantic-helper coverage errors: +48 check-provider, four source-comment and three Cargo supply-chain findings. +Go vet and staticcheck pass for the selected change; the blocked checkpoint +does not execute tests. This narrower selection does not supersede the broader +`8dede4b` checkpoint's architecture findings or establish release readiness. + Five packages still lack sufficient notice evidence: `async-scoped@0.9.0`, `backtrace-ext@0.2.1`, `guppy-workspace-hack@0.1.0`, `recursion@0.5.4` and `sigchld@0.2.4`. Their declarations are retained as metadata, never substituted From 3a973e2035216d0e22bc9eb6b062d6940ba4c15f Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:25:00 +0000 Subject: [PATCH 42/49] Target a bounded Rust beta branch before full release parity --- docs/plans/code-polishy-rust-support-plan.md | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 7d6cbf9e..1b19305f 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -39,6 +39,33 @@ feature and target combination. Reuse existing execution controls and expose their limits; a new Rust-only operating-system sandbox is not a prerequisite for the capabilities already delivered through equivalent Go execution. +## Immediate delivery: beta branch + +The immediate milestone is a usable, reviewable beta branch with a documented +supported subset. Complete Rust parity remains the longer-term objective. +Continue on `feature/rust-optional-install`, preserving the integrated mainline +changes. Branch preparation does not require a version bump or publication. + +The beta focuses on the existing end-to-end crate path: discovery, selected +formatting, compiler checks, supported quality and architecture analysis, Cargo +dependency policy, native tests and doctests. Keep the large Rust tools in the +authenticated optional companion and retain installation and repair evidence. +Describe supported configurations and known limitations beside their verification +results so adopters can assess whether their projects fit the current subset. + +Missing or altered tools, unsupported semantics and incomplete analysis continue +to produce blocking findings. Beta status introduces no skip switch, successful +empty result, policy exemption or weaker default. Full macro and semantic-helper +coverage can follow the branch milestone; the existing helper draft and its +admission failures remain visible until resolved. + +Native notice completion, all-host release evidence, publication and self-hosting +lock adoption remain later release work. They do not prevent continued beta +branch development. A beta branch may retain explicitly reported unfinished +work and failed checkpoints; any merge or published beta still needs its +applicable verification and release requirements. The current branch has not +passed those checkpoints. + ## Current baseline and architectural decisions The original implementation base included the accepted changes through `0.27.11`. From a1ca5a0d6c9cb246b1c9b0afb6e76533e01a789e Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:34:31 +0000 Subject: [PATCH 43/49] Scope the Rust beta to integrated capabilities and defer unused semantic draft --- README.md | 7 + docs/design/cargo-facts.md | 5 +- docs/installation.md | 26 + docs/plans/code-polishy-rust-support-plan.md | 64 +- internal/rustfacts/semantic/Cargo.lock | 1932 ------------------ internal/rustfacts/semantic/Cargo.toml | 23 - internal/rustfacts/semantic/src/graph.rs | 228 --- internal/rustfacts/semantic/src/main.rs | 52 - internal/rustfacts/semantic/src/tests.rs | 153 -- internal/rustfacts/semantic/src/workspace.rs | 77 - 10 files changed, 72 insertions(+), 2495 deletions(-) delete mode 100644 internal/rustfacts/semantic/Cargo.lock delete mode 100644 internal/rustfacts/semantic/Cargo.toml delete mode 100644 internal/rustfacts/semantic/src/graph.rs delete mode 100644 internal/rustfacts/semantic/src/main.rs delete mode 100644 internal/rustfacts/semantic/src/tests.rs delete mode 100644 internal/rustfacts/semantic/src/workspace.rs diff --git a/README.md b/README.md index 2d54dbdf..fb00ebc9 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,13 @@ Code Polishy keeps its built-in tools fixed until you upgrade, so local and CI checks stay consistent. Built-in support covers Go, JavaScript, TypeScript, Python, and shell scripts. +This branch also includes **Rust beta support** for contained Cargo projects. +Rust tools install separately with `./code-polishyw install-rust`; repositories +without Rust avoid the SDK and Nextest payload. The beta covers the supported +formatting, compiler, source-analysis, dependency and native-test paths. +Unsupported semantics produce blocking findings. Read the +[Rust beta scope](docs/installation.md#rust-beta-scope) before adopting it. + - **Go, JavaScript, and TypeScript:** Formats code and catches likely bugs, type errors, unused code, and overly complex functions. Locked dependencies are checked for known vulnerabilities. diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index 036a615e..9a33786d 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -198,7 +198,8 @@ therefore cannot establish complete macro dependency coverage. General expansion remains a required semantic-provider milestone; the written graph does not silently admit those missing edges. -The initial semantic helper source is in `internal/rustfacts/semantic/`. It uses +The deferred semantic helper draft is retained at commit `3a973e2` under +`internal/rustfacts/semantic/`, outside the beta source tree. It uses Rust Analyzer's HIR library to visit expanded syntax, resolve names with macro hygiene and map references back to physical source. An explicit Cargo manifest, native target and frozen build command establish its intended input boundary; @@ -209,7 +210,7 @@ coverage, complete bounded source ownership, execution integration and distribut remain unfinished. Its fixtures have not run: dependency admission currently prevents building this helper. It supplies no architecture coverage yet. -The candidate uses Rust Analyzer crates `0.0.346`, the first inspected published +The deferred draft uses Rust Analyzer crates `0.0.346`, the first inspected published line using Salsa `0.28`, with Salsa and its macro crates locked to `0.28.5`. Earlier candidates depend on the affected Salsa series in [RUSTSEC-2026-0308](https://rustsec.org/advisories/RUSTSEC-2026-0308.html). diff --git a/docs/installation.md b/docs/installation.md index ef7bbe8f..9ecd5ba6 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -100,6 +100,32 @@ source installer retains that developer installation; the portable release archive is the default distribution without Rust executables. Both archives retain their notice and dependency-inventory obligations. +### Rust beta scope + +The beta supports contained Cargo projects on the native host, using the pinned +toolchain and a reviewed `Cargo.lock`. Ordinary checks cover selected formatting, +compiler and Clippy diagnostics, written-source complexity and comment policy, +supported module dependencies, and Cargo dependency policy. Native suites cover +unit/integration tests through Nextest and separately declared doctests. Empty or +wholly skipped execution cannot satisfy a required test suite. + +Architecture analysis covers written modules, imports, aliases and re-exports, +plus supported standard macro arguments. Custom macros, non-builtin attribute +expansion, ambiguous or unresolved bindings and unsupported module layouts fail +coverage explicitly. This beta does not provide general macro-expanded semantic +analysis. Compiler checks use the native host and default feature configuration; +test suites can declare their own supported package and feature selections. +Project Nextest configuration and unsupported Cargo, rustfmt or Clippy settings +produce findings rather than silently changing policy or execution. + +Install the official companion before checking Rust code. Missing, damaged or +wrong-version tools remain blocking errors; choosing beta support does not +change required analysis. Host linkers and SDKs are still required as described +below. See [Cargo facts](design/cargo-facts.md), +[Rust architecture](design/rust-architecture.md) and +[Rust test declarations](design/rust-tests.md#suite-configuration) for the exact +supported boundaries and examples. + ## Native source installers Source acquisition stays outside the installers. They never select a version, diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 1b19305f..7501d98c 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -39,12 +39,14 @@ feature and target combination. Reuse existing execution controls and expose their limits; a new Rust-only operating-system sandbox is not a prerequisite for the capabilities already delivered through equivalent Go execution. -## Immediate delivery: beta branch +## Immediate delivery: releasable beta -The immediate milestone is a usable, reviewable beta branch with a documented -supported subset. Complete Rust parity remains the longer-term objective. +The immediate milestone is a releasable beta with a documented supported subset, +passing applicable checks and a concrete committed release candidate. +Complete Rust parity remains the longer-term objective. Continue on `feature/rust-optional-install`, preserving the integrated mainline -changes. Branch preparation does not require a version bump or publication. +changes. Prepare prerelease versioning and release notes once its supported +behavior is settled; publication remains a separate maintainer operation. The beta focuses on the existing end-to-end crate path: discovery, selected formatting, compiler checks, supported quality and architecture analysis, Cargo @@ -56,15 +58,17 @@ results so adopters can assess whether their projects fit the current subset. Missing or altered tools, unsupported semantics and incomplete analysis continue to produce blocking findings. Beta status introduces no skip switch, successful empty result, policy exemption or weaker default. Full macro and semantic-helper -coverage can follow the branch milestone; the existing helper draft and its -admission failures remain visible until resolved. - -Native notice completion, all-host release evidence, publication and self-hosting -lock adoption remain later release work. They do not prevent continued beta -branch development. A beta branch may retain explicitly reported unfinished -work and failed checkpoints; any merge or published beta still needs its -applicable verification and release requirements. The current branch has not -passed those checkpoints. +coverage can follow the beta. The unused semantic helper draft is preserved at +commit `3a973e2` on local branch `archive/rust-semantic-draft-3a973e2` and removed +from the beta source tree. It has no active caller or shipped executable. +Restoring it requires dependency admission, implementation and verification under +an engine that governs Rust source; no source exclusions or policy exceptions +stand in for that work. Existing runtime coverage failures remain enforced. + +Native notice completion and required platform verification belong to beta +release preparation. Publication and self-hosting lock adoption follow the +verified candidate. Development checkpoints may fail while work is in progress; +release readiness requires the applicable checks to pass. ## Current baseline and architectural decisions @@ -137,10 +141,12 @@ all concern the semantic helper's unsupported Rust source, imports or Cargo governance. It also retains 12 existing file-length warnings. The dependency cleanup does not establish checkpoint acceptance or authorize a release. -Full Rust delivery remains incomplete. The locked `0.28.3` engine cannot govern -the candidate semantic helper's Rust source and Cargo dependencies. Its admission, -compilation and semantic integration remain required; disabling those checks is -not an acceptable bootstrap. Complete native dependency notices/inventory and +Full Rust parity remains incomplete. The locked `0.28.3` engine cannot govern +the deferred semantic helper's Rust source and Cargo dependencies. Its admission, +compilation and semantic integration remain required for the expanded semantic +coverage milestone; disabling those checks is not an acceptable bootstrap. +The beta excludes the unused draft while retaining runtime coverage errors. +Complete native dependency notices/inventory and all-host execution also remain open. The 2026-10-15 JavaScript dependency updates remove the brace-expansion and fast-uri assessments by adopting the now-aged fixes. None of the remaining failures is waived by the @@ -332,13 +338,14 @@ Pinned Cargo packaging fixtures cover inherited workspace license metadata. ### Semantic helper admission checkpoint -`internal/rustfacts/semantic/` contains an initial, uncompiled Rust Analyzer -helper and fixtures for generated-only dependencies, nested expansion, `$crate` +Commit `3a973e2` retains an initial, uncompiled Rust Analyzer draft at +`internal/rustfacts/semantic/`, removed from the beta source tree. Its fixtures +cover generated-only dependencies, nested expansion, `$crate` hygiene and original call-site locations. It is not connected to public commands, shipped in releases or evidence of completed macro support. No helper build or fixture execution has been authorized by successful dependency admission yet. -The candidate pins Rust Analyzer `0.0.346` and locks Salsa, `salsa-macros` and +The deferred draft pins Rust Analyzer `0.0.346` and locks Salsa, `salsa-macros` and `salsa-macro-rules` to `0.28.5`. Earlier candidate Salsa releases fail [RUSTSEC-2026-0308](https://rustsec.org/advisories/RUSTSEC-2026-0308.html), reported 2026-09-24. The fixed release is younger than the 30-day baseline. Registry lock @@ -523,8 +530,10 @@ acceptance matrix are unchanged. ## Delivery and acceptance -Release only after the upstream repository's authorized release workflow proves -the entire capability matrix through the public CLI and installed artifact. +Release the beta only after its supported subset passes the applicable source +checks and installed-artifact verification. Unsupported cases retain blocking +coverage findings and documented limits. The entire capability matrix remains +the acceptance target for full Rust parity. Publish an exact version/digest through separately authorized publication. Masterbrainer then adopts that release through its dependency workflow. @@ -635,12 +644,11 @@ cannot authenticate the actual binary's dependency versions. Rust and Nextest SBOM entries retain explicit incomplete native compositions, host-specific authenticated download references and carried notice locations. -Bootstrap sequencing remains unresolved at the publication boundary: the native -Cargo engine must be published and adopted before the draft semantic helper can -be admitted under it. This does not authorize excluding required source from -governance, changing the lock by hand, or describing the draft as complete. -The helper remains in the branch with its coverage failures visible. No native -publication or self-hosting upgrade has occurred. +The beta delivers the native Cargo engine before the deferred semantic helper. +That engine must be published and adopted before the draft can be admitted under +it. The unused draft is retained at `3a973e2`; its removal from the beta changes +no active analyzer or target coverage rule. No native publication or self-hosting +upgrade has occurred. The next milestones extend ordinary macro/module coverage and complete installed-release integration. Existing coverage failures remain until diff --git a/internal/rustfacts/semantic/Cargo.lock b/internal/rustfacts/semantic/Cargo.lock deleted file mode 100644 index 197b502d..00000000 --- a/internal/rustfacts/semantic/Cargo.lock +++ /dev/null @@ -1,1932 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "arrayvec" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" - -[[package]] -name = "borsh" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" -dependencies = [ - "bytes", - "cfg_aliases", -] - -[[package]] -name = "boxcar" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" - -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" - -[[package]] -name = "camino" -version = "1.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "cargo_metadata" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" - -[[package]] -name = "cobs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" -dependencies = [ - "thiserror", -] - -[[package]] -name = "code-polishy-rust-semantic" -version = "0.1.0" -dependencies = [ - "ra_ap_hir", - "ra_ap_ide_db", - "ra_ap_load-cargo", - "ra_ap_project_model", - "ra_ap_syntax", - "ra_ap_vfs", - "serde", - "serde_json", -] - -[[package]] -name = "countme" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" - -[[package]] -name = "cov-mark" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90863d8442510cddf7f46618c4f92413774635771a3e80830c8b30d183420b14" - -[[package]] -name = "crossbeam-channel" -version = "0.5.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" - -[[package]] -name = "dashmap" -version = "6.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" - -[[package]] -name = "derive-where" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "dissimilar" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeda16ab4059c5fd2a83f2b9c9e9c981327b18aa8e3b313f7e6563799d4f093e" - -[[package]] -name = "drop_bomb" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1" - -[[package]] -name = "either" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" - -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - -[[package]] -name = "embedded-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" - -[[package]] -name = "ena" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" -dependencies = [ - "log", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - -[[package]] -name = "fst" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "hashlink" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" -dependencies = [ - "hashbrown 0.17.1", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "inotify" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "153be1941a183ec9ccd095ddbe17a8b8d435ef6c76e9e02451b933c3999af2c8" -dependencies = [ - "bitflags", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" -dependencies = [ - "libc", -] - -[[package]] -name = "intrusive-collections" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4275b20e6057cd7733fd8df8a5a31701e4fe44497dad0f3fa0e1c4fb971506be" - -[[package]] -name = "inventory" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" -dependencies = [ - "rustversion", -] - -[[package]] -name = "itertools" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jod-thread" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a037eddb7d28de1d0fc42411f501b53b75838d313908078d6698d064f3029b24" - -[[package]] -name = "kqueue" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" -dependencies = [ - "bitflags", - "libc", -] - -[[package]] -name = "la-arena" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3752f229dcc5a481d60f385fa479ff46818033d881d2d801aa27dffcfb5e8306" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "line-index" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e27e0ed5a392a7f5ba0b3808a2afccff16c64933312c84b57618b49d1209bd2" -dependencies = [ - "nohash-hasher", - "text-size", -] - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mio" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "miow" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - -[[package]] -name = "notify" -version = "8.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" -dependencies = [ - "bitflags", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "notify-types", - "walkdir", - "windows-sys 0.60.2", -] - -[[package]] -name = "notify-types" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" -dependencies = [ - "bitflags", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "perf-event" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4d6393d9238342159080d79b78cb59c67399a8e7ecfa5d410bd614169e4e823" -dependencies = [ - "libc", - "perf-event-open-sys", -] - -[[package]] -name = "perf-event-open-sys" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c44fb1c7651a45a3652c4afc6e754e40b3d6e6556f1487e2b230bfc4f33c2a8" -dependencies = [ - "libc", -] - -[[package]] -name = "petgraph" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" -dependencies = [ - "fixedbitset", - "hashbrown 0.15.5", - "indexmap", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "portable-atomic" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" - -[[package]] -name = "postcard" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "serde", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ra-ap-rustc_abi" -version = "0.166.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2cf1b1ffe31b6226c00b40cddfda65002b7729f9f4ed2d547b5856cdab0011c" -dependencies = [ - "bitflags", - "ra-ap-rustc_hashes", - "ra-ap-rustc_index", - "tracing", -] - -[[package]] -name = "ra-ap-rustc_ast_ir" -version = "0.166.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef42605e36e1305e815ccfc8830eb870f74d78534bca19a61629149536d8e98" - -[[package]] -name = "ra-ap-rustc_hashes" -version = "0.166.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f5542968215c17275920791b2fa13a43014287506ed0450777c79845102e86" -dependencies = [ - "rustc-stable-hash", -] - -[[package]] -name = "ra-ap-rustc_index" -version = "0.166.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9e47b9ca7d92cfb0d6653503adbabd41938b84474317397a664326b208d6c6" -dependencies = [ - "ra-ap-rustc_index_macros", -] - -[[package]] -name = "ra-ap-rustc_index_macros" -version = "0.166.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d744a7a2852a22f06210bcff9e4667ed0cacbfbe94894cc294044d25e876341" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "ra-ap-rustc_lexer" -version = "0.166.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527c12b3731b7d0692498012810b85b2b8dfdb8b514321ed6afc434bd1c70191" -dependencies = [ - "memchr", - "unicode-ident", - "unicode-properties", -] - -[[package]] -name = "ra-ap-rustc_next_trait_solver" -version = "0.166.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a9663a8d7c369e934aac2b74a638537ad7eb4be75b4530d765384dc071c936" -dependencies = [ - "derive-where", - "ra-ap-rustc_index", - "ra-ap-rustc_type_ir", - "ra-ap-rustc_type_ir_macros", - "tracing", -] - -[[package]] -name = "ra-ap-rustc_parse_format" -version = "0.166.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c038b7a8b0f784d4e441ad8ab991fbbdaa5e0be482e59c639a846a1c8126951" -dependencies = [ - "ra-ap-rustc_lexer", - "rustc-literal-escaper", -] - -[[package]] -name = "ra-ap-rustc_pattern_analysis" -version = "0.166.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ca286f90e99bb97cd9274c088f3c874a05d1ee90cabf40a3928afedabe99fd" -dependencies = [ - "ra-ap-rustc_index", - "rustc-hash 2.1.3", - "rustc_apfloat", - "smallvec", - "tracing", -] - -[[package]] -name = "ra-ap-rustc_type_ir" -version = "0.166.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d6efb6008f665a9485e0afecf9f4950a6c4bedd8ddd330a9df8986a6c0160b" -dependencies = [ - "arrayvec", - "bitflags", - "derive-where", - "ena", - "indexmap", - "ra-ap-rustc_abi", - "ra-ap-rustc_ast_ir", - "ra-ap-rustc_index", - "ra-ap-rustc_type_ir_macros", - "rustc-hash 2.1.3", - "smallvec", - "thin-vec", - "tracing", -] - -[[package]] -name = "ra-ap-rustc_type_ir_macros" -version = "0.166.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4fd2355e2bbf1f343c730f623596efc6e465b5e3685b606a437567ebb75bf8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - -[[package]] -name = "ra_ap_base_db" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ff14d8633ba5644be8e10c056ca82d97b87ee0f1b3833f022fed3c906578db" -dependencies = [ - "dashmap", - "indexmap", - "la-arena", - "ra_ap_cfg", - "ra_ap_intern", - "ra_ap_span", - "ra_ap_syntax", - "ra_ap_vfs", - "rustc-hash 2.1.3", - "salsa", - "salsa-macros", - "semver", - "tracing", - "triomphe", -] - -[[package]] -name = "ra_ap_cfg" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc4b72dc03a6a5b56b1aa10ddf7ef70fe59c75ae90dfd3ca97636760a32be47e" -dependencies = [ - "ra_ap_intern", - "ra_ap_syntax", - "ra_ap_tt", - "rustc-hash 2.1.3", - "tracing", -] - -[[package]] -name = "ra_ap_edition" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9783aa966d3a19c3a407babeb331ee4370f0aaa4a23646ab9bb45ba8a3a9dc2" - -[[package]] -name = "ra_ap_hir" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "539df9a88e106897dac9b9b82f92aec0bc64c09d0925abaea642b123a0971c48" -dependencies = [ - "arrayvec", - "either", - "itertools", - "la-arena", - "ra-ap-rustc_type_ir", - "ra_ap_base_db", - "ra_ap_cfg", - "ra_ap_hir_def", - "ra_ap_hir_expand", - "ra_ap_hir_ty", - "ra_ap_intern", - "ra_ap_span", - "ra_ap_stdx", - "ra_ap_syntax", - "ra_ap_tt", - "rustc-hash 2.1.3", - "salsa", - "serde_json", - "smallvec", - "tracing", - "triomphe", -] - -[[package]] -name = "ra_ap_hir_def" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e12599394e6fff890ee1aeb60f04bd41e19f65f4153534eb40ee1cffa9fc7a" -dependencies = [ - "arrayvec", - "bitflags", - "cov-mark", - "drop_bomb", - "either", - "fst", - "indexmap", - "itertools", - "la-arena", - "ra-ap-rustc_abi", - "ra-ap-rustc_parse_format", - "ra_ap_base_db", - "ra_ap_cfg", - "ra_ap_hir_expand", - "ra_ap_intern", - "ra_ap_span", - "ra_ap_stdx", - "ra_ap_syntax", - "ra_ap_syntax-bridge", - "ra_ap_tt", - "rustc-hash 2.1.3", - "rustc_apfloat", - "salsa", - "smallvec", - "thin-vec", - "tracing", - "triomphe", -] - -[[package]] -name = "ra_ap_hir_expand" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "485825528c2726e9f4206cc9528772ccb064a643b2d2927a9757e933905c0c99" -dependencies = [ - "cov-mark", - "either", - "itertools", - "ra_ap_base_db", - "ra_ap_cfg", - "ra_ap_intern", - "ra_ap_mbe", - "ra_ap_parser", - "ra_ap_span", - "ra_ap_stdx", - "ra_ap_syntax", - "ra_ap_syntax-bridge", - "ra_ap_tt", - "rustc-hash 2.1.3", - "salsa", - "smallvec", - "thin-vec", - "tracing", - "triomphe", -] - -[[package]] -name = "ra_ap_hir_ty" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd596760659be29203c609eee9347a39d51c9f846498c7f285d94b6fb0e7059" -dependencies = [ - "arrayvec", - "bitflags", - "cov-mark", - "either", - "ena", - "indexmap", - "itertools", - "la-arena", - "oorandom", - "petgraph", - "ra-ap-rustc_abi", - "ra-ap-rustc_ast_ir", - "ra-ap-rustc_index", - "ra-ap-rustc_next_trait_solver", - "ra-ap-rustc_pattern_analysis", - "ra-ap-rustc_type_ir", - "ra_ap_base_db", - "ra_ap_hir_def", - "ra_ap_hir_expand", - "ra_ap_intern", - "ra_ap_macros", - "ra_ap_span", - "ra_ap_stdx", - "ra_ap_syntax", - "rustc-hash 2.1.3", - "rustc_apfloat", - "salsa", - "serde", - "serde_derive", - "smallvec", - "thin-vec", - "tracing", - "tracing-subscriber", - "tracing-tree", - "triomphe", - "typed-arena", -] - -[[package]] -name = "ra_ap_ide_db" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8785799a963ab225c300c02a247fcd4a062956e5ad4883970ec6764adfa8d7ad" -dependencies = [ - "arrayvec", - "bitflags", - "cov-mark", - "crossbeam-channel", - "either", - "fst", - "itertools", - "line-index", - "memchr", - "nohash-hasher", - "ra_ap_base_db", - "ra_ap_hir", - "ra_ap_macros", - "ra_ap_parser", - "ra_ap_profile", - "ra_ap_span", - "ra_ap_stdx", - "ra_ap_syntax", - "ra_ap_test_fixture", - "ra_ap_test_utils", - "ra_ap_vfs", - "rayon", - "rustc-hash 2.1.3", - "salsa", - "smallvec", - "tracing", - "triomphe", -] - -[[package]] -name = "ra_ap_intern" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a36d2110812d6ac9206742cc8013c0957474c0a74b2952b56e957c0b0aa4e263" -dependencies = [ - "arrayvec", - "dashmap", - "hashbrown 0.14.5", - "rayon", - "rustc-hash 2.1.3", - "triomphe", -] - -[[package]] -name = "ra_ap_load-cargo" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84fe4286ce3645b3f54024a653313c30a039f847a52eb82e74a3418ca4a62903" -dependencies = [ - "anyhow", - "crossbeam-channel", - "itertools", - "ra_ap_hir_expand", - "ra_ap_ide_db", - "ra_ap_intern", - "ra_ap_proc_macro_api", - "ra_ap_project_model", - "ra_ap_span", - "ra_ap_tt", - "ra_ap_vfs", - "ra_ap_vfs-notify", - "tracing", -] - -[[package]] -name = "ra_ap_macros" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6d76a9a3a51852b3776106df0dd39b163e9c5b2d2cd517fe35462920c3a1706" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - -[[package]] -name = "ra_ap_mbe" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3d1851d6f2b0a56eec1540adb2203746012a32dbbe4e7afa37f9f0393d0afe" -dependencies = [ - "arrayvec", - "bitflags", - "cov-mark", - "ra-ap-rustc_lexer", - "ra_ap_intern", - "ra_ap_parser", - "ra_ap_span", - "ra_ap_stdx", - "ra_ap_syntax-bridge", - "ra_ap_tt", - "rustc-hash 2.1.3", - "salsa", - "smallvec", -] - -[[package]] -name = "ra_ap_parser" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbf11eb1b46bbba8aa1767436791234500562204c25654cc03900ffd043963a0" -dependencies = [ - "drop_bomb", - "ra-ap-rustc_lexer", - "ra_ap_edition", - "rustc-literal-escaper", - "tracing", - "winnow 0.7.15", -] - -[[package]] -name = "ra_ap_paths" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cbb13f567752c5478e2ddede6de942d508b5f0fa46bbc96b25651937c6790df" -dependencies = [ - "camino", -] - -[[package]] -name = "ra_ap_proc_macro_api" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91e2a4cc8104dd836becbafb722f1d8be42fb0a4d35a6d672b1a4e9a32984a8" -dependencies = [ - "indexmap", - "postcard", - "ra_ap_intern", - "ra_ap_paths", - "ra_ap_span", - "ra_ap_stdx", - "ra_ap_tt", - "rayon", - "rustc-hash 2.1.3", - "semver", - "serde", - "serde_derive", - "serde_json", - "tracing", -] - -[[package]] -name = "ra_ap_profile" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677e9d7e83cb38d48e7d94053035bbfad75b25b34089ae67406b1ea3300c0aed" -dependencies = [ - "libc", - "perf-event", - "windows-sys 0.61.2", -] - -[[package]] -name = "ra_ap_project_model" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23a97de2c4deb93a2605dc81a666fef29591770280cfefccf5d306a073f0cbbf" -dependencies = [ - "anyhow", - "cargo_metadata", - "itertools", - "la-arena", - "ra_ap_base_db", - "ra_ap_cfg", - "ra_ap_intern", - "ra_ap_paths", - "ra_ap_span", - "ra_ap_stdx", - "ra_ap_toolchain", - "rustc-hash 2.1.3", - "semver", - "serde", - "serde_derive", - "serde_json", - "temp-dir", - "toml", - "tracing", - "triomphe", -] - -[[package]] -name = "ra_ap_span" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0f06ed71c2e392acf635f8130856da36e6ced7365b53fbdc1da569fa04092b" -dependencies = [ - "hashbrown 0.17.1", - "la-arena", - "ra_ap_stdx", - "ra_ap_syntax", - "ra_ap_vfs", - "rustc-hash 2.1.3", - "salsa", - "text-size", -] - -[[package]] -name = "ra_ap_stdx" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1462ef1865500b292afd582af8de4661fce3986a7c4e94cdd5ce46ae5252f9" -dependencies = [ - "crossbeam-channel", - "crossbeam-utils", - "itertools", - "jod-thread", - "libc", - "miow", - "tracing", - "windows-sys 0.61.2", -] - -[[package]] -name = "ra_ap_syntax" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4b3e65ef77b752447610921a6d87d2a1e64ea3ab744b049467949302fa0f44" -dependencies = [ - "either", - "itertools", - "ra-ap-rustc_lexer", - "ra_ap_parser", - "ra_ap_stdx", - "rowan", - "rustc-hash 2.1.3", - "rustc-literal-escaper", - "smallvec", - "smol_str", - "tracing", - "triomphe", -] - -[[package]] -name = "ra_ap_syntax-bridge" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5fb8079020ea278c707e27974e7b47c502c65f0ff87feb27bddb9124b91b4f5" -dependencies = [ - "ra_ap_intern", - "ra_ap_parser", - "ra_ap_span", - "ra_ap_stdx", - "ra_ap_syntax", - "ra_ap_tt", - "rustc-hash 2.1.3", -] - -[[package]] -name = "ra_ap_test_fixture" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c244c8f3d21d89a14bd7a9cc1507b9f77b267c58da66e0081c533c949ee2b638" -dependencies = [ - "ra_ap_base_db", - "ra_ap_cfg", - "ra_ap_hir_expand", - "ra_ap_intern", - "ra_ap_paths", - "ra_ap_span", - "ra_ap_stdx", - "ra_ap_test_utils", - "ra_ap_tt", - "triomphe", -] - -[[package]] -name = "ra_ap_test_utils" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03eed74aed57120d73ad6db4fd7de371c791f6611b4cc7cd1998d77cf63da3fe" -dependencies = [ - "dissimilar", - "ra_ap_paths", - "ra_ap_profile", - "ra_ap_stdx", - "rustc-hash 2.1.3", - "text-size", -] - -[[package]] -name = "ra_ap_toolchain" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4ada3c3b061b51be63bfd736d967fb4651432ca8f74ed5c7dbfcd22e6f9a3c5" -dependencies = [ - "camino", - "semver", -] - -[[package]] -name = "ra_ap_tt" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88cf4ea21fbc05af1eaf2d2b0c4e94f41685a7700ef7a757f0c086e45a9bd350" -dependencies = [ - "arrayvec", - "indexmap", - "ra-ap-rustc_lexer", - "ra_ap_intern", - "ra_ap_span", - "ra_ap_stdx", - "rustc-hash 2.1.3", - "text-size", -] - -[[package]] -name = "ra_ap_vfs" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bdda57d4e2a11bc7a6aae94ab5e4593798903f665c8270e4d4472075edcb8c4" -dependencies = [ - "crossbeam-channel", - "fst", - "indexmap", - "nohash-hasher", - "ra_ap_paths", - "ra_ap_stdx", - "rustc-hash 2.1.3", - "tracing", -] - -[[package]] -name = "ra_ap_vfs-notify" -version = "0.0.346" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3efb5a0c22f810fbe2d50220df2be336e6cdc4852ca253fac108b0480a8d7d3" -dependencies = [ - "crossbeam-channel", - "notify", - "ra_ap_paths", - "ra_ap_stdx", - "ra_ap_vfs", - "rayon", - "rustc-hash 2.1.3", - "tracing", - "walkdir", -] - -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "rowan" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14b574c58582fa59fa43a2feb6608b8744184659f08a2e0117e4b8224d95ed61" -dependencies = [ - "countme", - "hashbrown 0.14.5", - "memoffset", - "rustc-hash 1.1.0", - "text-size", -] - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - -[[package]] -name = "rustc-literal-escaper" -version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be87abb9e40db7466e0681dc8ecd9dcfd40360cb10b4c8fe24a7c4c3669b198" - -[[package]] -name = "rustc-stable-hash" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "781442f29170c5c93b7185ad559492601acdc71d5bb0706f5868094f45cfcd08" - -[[package]] -name = "rustc_apfloat" -version = "0.2.3+llvm-462a31f5a5ab" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486c2179b4796f65bfe2ee33679acf0927ac83ecf583ad6c91c3b4570911b9ad" -dependencies = [ - "bitflags", - "smallvec", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "salsa" -version = "0.28.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27d2e6e19e39591cd99c01ac4141544b4d012cfd0831348629b8187fa9fe6d34" -dependencies = [ - "boxcar", - "crossbeam-queue", - "crossbeam-utils", - "hashbrown 0.17.1", - "hashlink", - "indexmap", - "intrusive-collections", - "inventory", - "parking_lot", - "portable-atomic", - "rayon", - "rustc-hash 2.1.3", - "salsa-macro-rules", - "salsa-macros", - "smallvec", - "thin-vec", - "tracing", - "triomphe", - "typeid", -] - -[[package]] -name = "salsa-macro-rules" -version = "0.28.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf90a551c38f25ed8489111d3d5827aa18e1b85963f5e6a298505b1d1e2cb7e" - -[[package]] -name = "salsa-macros" -version = "0.28.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69dd77f86ec10e72e314f4ddf7c44c8d349e73cdff1aad524ba9668603fb0549" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" -dependencies = [ - "serde", - "serde_core", -] - -[[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 2.0.119", -] - -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" - -[[package]] -name = "smol_str" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" -dependencies = [ - "borsh", - "serde_core", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "temp-dir" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016ef9739649996fcc983b9c588fe3d557cf216d4d98503ce1b057ab5a66d689" - -[[package]] -name = "text-size" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" - -[[package]] -name = "thin-vec" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79def32ffcd477db1ff26f76dab9e3a91f0bd42a85ca96577089b24623056f9d" - -[[package]] -name = "thiserror" -version = "2.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "thread_local" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" -dependencies = [ - "deranged", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" - -[[package]] -name = "time-macros" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "toml" -version = "1.1.4+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow 1.0.4", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.1.3+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" -dependencies = [ - "winnow 1.0.4", -] - -[[package]] -name = "toml_writer" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "sharded-slab", - "thread_local", - "time", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "tracing-tree" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac87aa03b6a4d5a7e4810d1a80c19601dbe0f8a837e9177f23af721c7ba7beec" -dependencies = [ - "nu-ansi-term", - "tracing-core", - "tracing-log", - "tracing-subscriber", -] - -[[package]] -name = "triomphe" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b40688ea6389c8171614b25491f71d4a27946e0c7ce2da1c6de27e25abf1a0ae" -dependencies = [ - "serde", - "stable_deref_trait", -] - -[[package]] -name = "typed-arena" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" - -[[package]] -name = "winnow" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/internal/rustfacts/semantic/Cargo.toml b/internal/rustfacts/semantic/Cargo.toml deleted file mode 100644 index 12ee15a4..00000000 --- a/internal/rustfacts/semantic/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "code-polishy-rust-semantic" -version = "0.1.0" -edition = "2024" -rust-version = "1.97.1" -license = "Apache-2.0" -publish = false - -[dependencies] -ra_ap_hir = "=0.0.346" -ra_ap_ide_db = "=0.0.346" -ra_ap_load-cargo = "=0.0.346" -ra_ap_project_model = "=0.0.346" -ra_ap_syntax = "=0.0.346" -ra_ap_vfs = "=0.0.346" -serde = { version = "=1.0.228", features = ["derive"] } -serde_json = "=1.0.150" - -[profile.dev] -debug = 0 - -[profile.release] -strip = "debuginfo" diff --git a/internal/rustfacts/semantic/src/graph.rs b/internal/rustfacts/semantic/src/graph.rs deleted file mode 100644 index cef7b658..00000000 --- a/internal/rustfacts/semantic/src/graph.rs +++ /dev/null @@ -1,228 +0,0 @@ -use ra_ap_hir::{ModuleDef, PathResolution, Semantics}; -use ra_ap_ide_db::RootDatabase; -use ra_ap_syntax::{AstNode, SyntaxNode, ast}; -use ra_ap_vfs::{FileId, Vfs}; -use serde::Serialize; - -#[derive(Serialize)] -pub(crate) struct Graph { - protocol: &'static str, - edges: Vec, - problems: Vec, -} - -#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord)] -struct Edge { - source: Location, - target: Location, - expanded: bool, -} - -#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord)] -struct Location { - path: String, - start: u32, - end: u32, -} - -#[derive(Serialize)] -struct Problem { - location: Location, - message: String, -} - -pub(crate) fn collect( - database: &RootDatabase, - vfs: &Vfs, - files: &[String], -) -> Result> { - let semantics = Semantics::new(database); - let mut graph = Graph { - protocol: "rust-semantic/v1", - edges: Vec::new(), - problems: Vec::new(), - }; - let mut remaining = 1_000_000; - for path in files { - let (file, _) = vfs - .iter() - .find(|(_, candidate)| candidate.to_string() == *path) - .ok_or_else(|| { - format!("selected file is absent from the semantic workspace: {path}") - })?; - let module = semantics - .file_to_module_def(file) - .ok_or_else(|| format!("selected file has no compiled module: {path}"))?; - let edition = module - .as_source_file_id(database) - .ok_or("selected file has no source-file identity")?; - let root = semantics.parse(edition); - graph.walk(&semantics, vfs, root.syntax(), 0, &mut remaining)?; - } - graph.edges.sort(); - graph.edges.dedup(); - Ok(graph) -} - -impl Graph { - fn walk( - &mut self, - semantics: &Semantics<'_, RootDatabase>, - vfs: &Vfs, - node: &SyntaxNode, - depth: usize, - remaining: &mut usize, - ) -> Result<(), Box> { - if depth > 128 || *remaining == 0 { - return Err("semantic expansion exceeds its traversal limit".into()); - } - *remaining -= 1; - if let Some(call) = ast::MacroCall::cast(node.clone()) { - return self.macro_call(semantics, vfs, &call, depth, remaining); - } - if let Some(path) = ast::Path::cast(node.clone()) { - self.path(semantics, vfs, &path, depth > 0); - } - if let Some(call) = ast::MethodCallExpr::cast(node.clone()) { - match semantics.resolve_method_call(&call) { - Some(function) => self.reference( - semantics, - vfs, - node, - ModuleDef::Function(function), - depth > 0, - ), - None => self.problem(semantics, vfs, node, "method resolution is incomplete"), - } - } - for child in node.children() { - self.walk(semantics, vfs, &child, depth, remaining)?; - } - Ok(()) - } - - fn macro_call( - &mut self, - semantics: &Semantics<'_, RootDatabase>, - vfs: &Vfs, - call: &ast::MacroCall, - depth: usize, - remaining: &mut usize, - ) -> Result<(), Box> { - let Some(id) = semantics.to_def(call) else { - self.problem( - semantics, - vfs, - call.syntax(), - "macro resolution is incomplete", - ); - return Ok(()); - }; - if let Some(definition) = semantics.resolve_macro_call(call) { - self.reference( - semantics, - vfs, - call.syntax(), - ModuleDef::Macro(definition), - depth > 0, - ); - } - let expansion = semantics.expand(id); - if let Some(error) = expansion.err { - self.problem( - semantics, - vfs, - call.syntax(), - &format!("macro expansion failed: {error:?}"), - ); - return Ok(()); - } - self.walk(semantics, vfs, &expansion.value, depth + 1, remaining) - } - - fn path( - &mut self, - semantics: &Semantics<'_, RootDatabase>, - vfs: &Vfs, - path: &ast::Path, - expanded: bool, - ) { - if path - .syntax() - .parent() - .is_some_and(|node| ast::Path::can_cast(node.kind())) - || path - .syntax() - .ancestors() - .any(|node| ast::Attr::can_cast(node.kind())) - { - return; - } - match semantics.resolve_path(path) { - Some(PathResolution::Def(definition)) => { - self.reference(semantics, vfs, path.syntax(), definition, expanded) - } - Some(_) => {} - None => self.problem( - semantics, - vfs, - path.syntax(), - "path resolution is incomplete", - ), - } - } - - fn reference( - &mut self, - semantics: &Semantics<'_, RootDatabase>, - vfs: &Vfs, - node: &SyntaxNode, - definition: ModuleDef, - expanded: bool, - ) { - let target = match definition { - ModuleDef::Module(module) => Some(module), - other => other.module(semantics.db), - }; - let Some(module) = target else { - return; - }; - let source = location(semantics, vfs, node); - let module_source = module.definition_source(semantics.db); - let target_node = semantics.parse_or_expand(module_source.file_id); - let target = location(semantics, vfs, &target_node); - if source.path != target.path { - self.edges.push(Edge { - source, - target, - expanded, - }); - } - } - - fn problem( - &mut self, - semantics: &Semantics<'_, RootDatabase>, - vfs: &Vfs, - node: &SyntaxNode, - message: &str, - ) { - self.problems.push(Problem { - location: location(semantics, vfs, node), - message: message.into(), - }); - } -} - -fn location(semantics: &Semantics<'_, RootDatabase>, vfs: &Vfs, node: &SyntaxNode) -> Location { - let range = semantics.original_range(node); - Location { - path: path(vfs, range.file_id), - start: range.range.start().into(), - end: range.range.end().into(), - } -} - -fn path(vfs: &Vfs, file: FileId) -> String { - vfs.file_path(file).to_string() -} diff --git a/internal/rustfacts/semantic/src/main.rs b/internal/rustfacts/semantic/src/main.rs deleted file mode 100644 index 8e19a7d0..00000000 --- a/internal/rustfacts/semantic/src/main.rs +++ /dev/null @@ -1,52 +0,0 @@ -mod graph; -mod workspace; - -#[cfg(test)] -mod tests; - -use serde::Deserialize; -use std::io::{self, Read}; - -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -struct Request { - protocol: String, - manifest: String, - target: String, - cargo: String, - proc_macro_server: String, - files: Vec, - #[serde(default)] - packages: Vec, - #[serde(default)] - features: Vec, - #[serde(default)] - no_default_features: bool, -} - -fn main() { - if let Err(error) = run() { - eprintln!("{error}"); - std::process::exit(1); - } -} - -fn run() -> Result<(), Box> { - let mut input = Vec::new(); - io::stdin() - .take(8 * 1024 * 1024 + 1) - .read_to_end(&mut input)?; - if input.len() > 8 * 1024 * 1024 { - return Err("semantic request exceeds 8 MiB".into()); - } - let request: Request = serde_json::from_slice(&input)?; - if request.protocol != "rust-semantic/v1" - || request.files.is_empty() - || request.files.len() > 4096 - { - return Err("unsupported semantic request protocol or file inventory".into()); - } - let result = workspace::analyze(&request)?; - serde_json::to_writer(io::stdout().lock(), &result)?; - Ok(()) -} diff --git a/internal/rustfacts/semantic/src/tests.rs b/internal/rustfacts/semantic/src/tests.rs deleted file mode 100644 index 6b8e4843..00000000 --- a/internal/rustfacts/semantic/src/tests.rs +++ /dev/null @@ -1,153 +0,0 @@ -use crate::{Request, workspace}; -use serde_json::Value; -use std::{ - fs, - path::PathBuf, - time::{SystemTime, UNIX_EPOCH}, -}; - -struct Fixture { - root: PathBuf, -} - -impl Fixture { - fn new() -> Self { - let nonce = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - let root = std::env::temp_dir().join(format!( - "code-polishy-semantic-{}-{nonce}", - std::process::id() - )); - fs::create_dir(&root).unwrap(); - let fixture = Self { root }; - fixture.write( - "Cargo.toml", - "[package]\nname='semantic-fixture'\nversion='1.0.0'\nedition='2024'\n", - ); - fixture.write( - "Cargo.lock", - "version=4\n[[package]]\nname='semantic-fixture'\nversion='1.0.0'\n", - ); - fixture - } - - fn write(&self, path: &str, source: &str) { - let path = self.root.join(path); - fs::create_dir_all(path.parent().unwrap()).unwrap(); - fs::write(path, source).unwrap(); - } - - fn request(&self, files: &[&str]) -> Request { - let compiler = PathBuf::from(std::env::var("RUSTC").expect("pinned RUSTC is required")); - let sdk = compiler.parent().unwrap().parent().unwrap(); - Request { - protocol: "rust-semantic/v1".into(), - manifest: self.root.join("Cargo.toml").to_str().unwrap().into(), - target: std::env::var("CODE_POLISHY_RUST_HOST") - .expect("governed native host is required"), - cargo: std::env::var("CARGO").expect("pinned CARGO is required"), - proc_macro_server: sdk - .join("libexec") - .join(format!( - "rust-analyzer-proc-macro-srv{}", - std::env::consts::EXE_SUFFIX - )) - .to_str() - .unwrap() - .into(), - files: files - .iter() - .map(|path| self.root.join(path).to_str().unwrap().into()) - .collect(), - packages: Vec::new(), - features: Vec::new(), - no_default_features: false, - } - } - - fn graph(&self, files: &[&str]) -> Value { - serde_json::to_value(workspace::analyze(&self.request(files)).unwrap()).unwrap() - } -} - -impl Drop for Fixture { - fn drop(&mut self) { - fs::remove_dir_all(&self.root).unwrap(); - } -} - -#[test] -fn generated_references_retain_dollar_crate_hygiene_and_call_site() { - let fixture = Fixture::new(); - fixture.write( - "src/lib.rs", - "#![no_std]\n#[macro_use] mod macros;\nmod consumer;\nmod lower;\npub use consumer::run;\n", - ); - fixture.write( - "src/macros.rs", - "macro_rules! hidden_call { () => { $crate::lower::value() }; }\n", - ); - fixture.write("src/lower.rs", "pub fn value() -> u8 { 7 }\n"); - let source = - "mod lower { pub fn value() -> u8 { 9 } }\npub fn run() -> u8 { hidden_call!() }\n"; - fixture.write("src/consumer.rs", source); - let graph = fixture.graph(&["src/consumer.rs"]); - assert_eq!(graph["problems"], serde_json::json!([])); - let target = fixture - .root - .join("src/lower.rs") - .to_str() - .unwrap() - .to_owned(); - let edge = graph["edges"] - .as_array() - .unwrap() - .iter() - .find(|edge| edge["target"]["path"] == target) - .expect("generated dependency was omitted"); - assert_eq!(edge["expanded"], true); - assert_eq!( - edge["source"]["start"], - source.find("hidden_call!").unwrap() - ); - assert_eq!( - edge["source"]["path"], - fixture.root.join("src/consumer.rs").to_str().unwrap() - ); -} - -#[test] -fn nested_expansion_keeps_dependencies_introduced_without_input_tokens() { - let fixture = Fixture::new(); - fixture.write( - "src/lib.rs", - "#![no_std]\n#[macro_use] mod macros;\nmod lower;\nmod consumer;\npub use consumer::run;\n", - ); - fixture.write("src/macros.rs", "macro_rules! inner { () => { $crate::lower::value() }; }\nmacro_rules! outer { () => { inner!() }; }\n"); - fixture.write("src/lower.rs", "pub fn value() -> u8 { 7 }\n"); - fixture.write("src/consumer.rs", "pub fn run() -> u8 { outer!() }\n"); - let graph = fixture.graph(&["src/consumer.rs"]); - assert_eq!(graph["problems"], serde_json::json!([])); - assert!( - graph["edges"] - .as_array() - .unwrap() - .iter() - .any(|edge| edge["expanded"] == true - && edge["target"]["path"] == fixture.root.join("src/lower.rs").to_str().unwrap()) - ); -} - -#[test] -fn invalid_macro_expansion_is_a_failed_compilation_not_an_empty_graph() { - let fixture = Fixture::new(); - fixture.write("src/lib.rs", "#![no_std]\nmacro_rules! broken { () => { missing::value() }; }\npub fn run() { broken!(); }\n"); - let result = workspace::analyze(&fixture.request(&["src/lib.rs"])); - let message = result - .err() - .expect("invalid expansion was accepted") - .to_string(); - assert!(message.contains("Cargo compilation failed"), "{message}"); -} diff --git a/internal/rustfacts/semantic/src/workspace.rs b/internal/rustfacts/semantic/src/workspace.rs deleted file mode 100644 index 19871275..00000000 --- a/internal/rustfacts/semantic/src/workspace.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::{Request, graph}; -use ra_ap_load_cargo::{LoadCargoConfig, ProcMacroServerChoice, load_workspace}; -use ra_ap_project_model::{ - CargoConfig, CargoFeatures, ProjectManifest, ProjectWorkspace, ProjectWorkspaceKind, -}; -use ra_ap_vfs::AbsPathBuf; - -pub(crate) fn analyze(request: &Request) -> Result> { - let manifest = ProjectManifest::from_manifest_file(absolute(&request.manifest)?)?; - let config = configuration(request); - let mut workspace = - ProjectWorkspace::load(manifest, &config, &|message| eprintln!("{message}"))?; - match &workspace.kind { - ProjectWorkspaceKind::Cargo { error: None, .. } => {} - _ => return Err("complete Cargo metadata is required".into()), - } - if workspace.target.is_err() || workspace.toolchain.is_none() { - return Err("compiler target or toolchain facts are unavailable".into()); - } - let scripts = workspace.run_build_scripts(&config, &|message| eprintln!("{message}"))?; - if let Some(error) = scripts.error() { - return Err(format!("Cargo compilation failed: {error}").into()); - } - workspace.set_build_scripts(scripts); - let load = LoadCargoConfig { - load_out_dirs_from_check: true, - with_proc_macro_server: ProcMacroServerChoice::Explicit(absolute( - &request.proc_macro_server, - )?), - prefill_caches: false, - num_worker_threads: 1, - proc_macro_processes: 1, - }; - let (database, vfs, server) = load_workspace(workspace, &config.extra_env, &load)?; - if server.is_none() { - return Err("the pinned procedural macro server is unavailable".into()); - } - graph::collect(&database, &vfs, &request.files) -} - -fn configuration(request: &Request) -> CargoConfig { - let mut command = vec![ - request.cargo.clone(), - "check".into(), - "--frozen".into(), - "--manifest-path".into(), - request.manifest.clone(), - "--message-format=json".into(), - "--target".into(), - request.target.clone(), - ]; - for package in &request.packages { - command.extend(["--package".into(), package.clone()]); - } - for feature in &request.features { - command.extend(["--features".into(), feature.clone()]); - } - if request.no_default_features { - command.push("--no-default-features".into()); - } - CargoConfig { - target: Some(request.target.clone()), - features: CargoFeatures::Selected { - features: request.features.clone(), - no_default_features: request.no_default_features, - }, - extra_args: vec!["--frozen".into()], - run_build_script_command: Some(command), - wrap_rustc_in_build_scripts: false, - set_test: false, - ..CargoConfig::default() - } -} - -fn absolute(path: &str) -> Result> { - Ok(AbsPathBuf::try_from(path.to_owned()).map_err(|_| "an absolute UTF-8 path is required")?) -} From e22eb5f054b353c191ccfa189a4a8d1f04a3cb04 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:41:19 +0000 Subject: [PATCH 44/49] Prepare 0.29.0-beta.1 with reviewed native tool license evidence --- CHANGELOG.md | 19 + VERSION | 2 +- docs/design/installed-release-verification.md | 25 +- docs/installation.md | 7 + docs/plans/code-polishy-rust-support-plan.md | 39 +- tools/cargo-nextest-license.txt | 599 +++++++++++------- 6 files changed, 427 insertions(+), 264 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 673ed924..b0d8e386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ ## Unreleased +## 0.29.0-beta.1 - 2026-10-15 + +- Add native Rust beta support for contained Cargo projects: discovery, + selected formatting, compiler and Clippy diagnostics, written-source quality + and architecture checks, Cargo dependency policy, native tests and doctests. + Unsupported macro expansion, configuration and source resolution remain + blocking coverage findings; this beta does not claim general Rust parity. +- Keep the Rust SDK and Nextest out of the default portable installation. + `install-rust` installs or repairs the official companion authenticated by + the core release. Missing or altered tools cannot disable required checks. +- Validate Cargo's actual workspace, lock and source containment before native + test execution or receipt reuse. Require nonempty structured test outcomes + and preserve compiler, test, timeout and operational failure distinctions. +- Carry upstream Rust notices and reviewed Nextest source license evidence. + Native transitive SBOM composition remains explicitly incomplete; target + dependency checks remain mandatory. +- Adopt aged brace-expansion fixes in both JavaScript dependency graphs and + fast-uri 3.1.8 in the JavaScript provider, removing obsolete assessments. + ## 0.28.3 - 2026-10-09 - Build and ship Go 1.26.9 to fix the reported standard-library vulnerabilities, diff --git a/VERSION b/VERSION index b79f04f4..f735712f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.28.3 +0.29.0-beta.1 diff --git a/docs/design/installed-release-verification.md b/docs/design/installed-release-verification.md index ac1e20cb..26fe3203 100644 --- a/docs/design/installed-release-verification.md +++ b/docs/design/installed-release-verification.md @@ -100,11 +100,26 @@ recorded in the crates' VCS metadata. Numbered shared texts preserve each packag source location without repeating identical license bodies. The file is bounded at 2 MiB and remains in the authenticated core alongside SDK notices. -The inventory explicitly identifies five packages with missing notice evidence. -It is not an exact linked-binary graph: the upstream build recipe does not request -locked resolution, and neither its release assets nor the inspected Linux binary -provide a dependency attestation. Complete native notice and linked-dependency -evidence remain publication requirements alongside native installed-Rust evidence. +Five packages publish standard license declarations without separate notice files. +Their original package declarations and selected standard license terms are +carried alongside the recovered notices. Apache-2.0 is selected where offered as +an alternative; sigchld declares MIT. Published author metadata is retained, +without inventing copyright holders or years or presenting license templates +as recovered upstream notices. Cargo's published license field identifies the +license under which a package is released; the separate text-file field is an +alternative for nonstandard licenses. The [Cargo manifest contract](https://doc.rust-lang.org/cargo/reference/manifest.html#the-license-and-license-file-fields) +defines those fields. This evidence does not certify all redistribution obligations. + +The beta distributes the checksum-pinned official binaries with this source +license evidence and explicitly incomplete native dependency compositions. +This is not an exact linked-binary graph: the upstream build recipe does not +request locked resolution, and its release assets provide no dependency SBOM. +Source paths in all four reviewed Nextest archives corroborate package versions +in the resolved source closure, but cannot establish completeness. A complete +native binary dependency attestation remains follow-up release engineering; +the beta makes no claim of exhaustive vulnerability coverage inside those tools. +This distribution-evidence limit does not exempt target Cargo dependencies or +Rust source from any required policy checks. The SBOM links each Rust tool to the selected host's reviewed upstream archive URLs and SHA-256 identities, reusing the installer's checksum inventory. Archive diff --git a/docs/installation.md b/docs/installation.md index 9ecd5ba6..249f8403 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -126,6 +126,13 @@ below. See [Cargo facts](design/cargo-facts.md), [Rust test declarations](design/rust-tests.md#suite-configuration) for the exact supported boundaries and examples. +The companion contains the official checksum-pinned Rust and Nextest binaries. +Their upstream notices and reviewed source license evidence are carried with the +core. The release SBOM explicitly marks native transitive dependencies incomplete: +upstream does not provide a complete binary dependency attestation, so the beta +cannot promise exhaustive vulnerability coverage inside those tools. Target +Cargo projects still receive the ordinary required dependency checks. + ## Native source installers Source acquisition stays outside the installers. They never select a version, diff --git a/docs/plans/code-polishy-rust-support-plan.md b/docs/plans/code-polishy-rust-support-plan.md index 7501d98c..c4558a4d 100644 --- a/docs/plans/code-polishy-rust-support-plan.md +++ b/docs/plans/code-polishy-rust-support-plan.md @@ -65,8 +65,12 @@ Restoring it requires dependency admission, implementation and verification unde an engine that governs Rust source; no source exclusions or policy exceptions stand in for that work. Existing runtime coverage failures remain enforced. -Native notice completion and required platform verification belong to beta -release preparation. Publication and self-hosting lock adoption follow the +Native license evidence and required platform verification belong to beta +release preparation. The beta uses reviewed official tool binaries, carries +upstream notices and declared license terms, and discloses incomplete native +transitive SBOM coverage. Full binary dependency attestation is follow-up work; +target-project policy coverage is unchanged. +Publication and self-hosting lock adoption follow the verified candidate. Development checkpoints may fail while work is in progress; release readiness requires the applicable checks to pass. @@ -146,8 +150,8 @@ the deferred semantic helper's Rust source and Cargo dependencies. Its admission compilation and semantic integration remain required for the expanded semantic coverage milestone; disabling those checks is not an acceptable bootstrap. The beta excludes the unused draft while retaining runtime coverage errors. -Complete native dependency notices/inventory and -all-host execution also remain open. The 2026-10-15 JavaScript dependency updates +Native binary inventory completeness remains a disclosed beta limitation; +all-host execution also remains open. The 2026-10-15 JavaScript dependency updates remove the brace-expansion and fast-uri assessments by adopting the now-aged fixes. None of the remaining failures is waived by the optional installation boundary. @@ -601,7 +605,8 @@ rejection, doctest example semantics and failures, and recovery through the verified launcher. The private installed artifact has release digest `5f4452b4fb01bdc396e948c761210c9e965f5246a377856213a89915c2fdc476`. All-host installed evidence -and complete bundled dependency notices/inventory remain publication requirements. +and the carried native license evidence remain publication requirements. Native +transitive compositions remain explicitly incomplete for this beta. No beta release has been published or adopted. The Nextest source audit at `fc97e97bbe0a3927482a694247da00c099f4269e` @@ -617,9 +622,11 @@ executed no dependency build scripts or procedural macros. `tools/cargo-nextest-license.txt` now carries 642 packaged notice files from that source closure and 18 supplemental upstream files covering 14 packages. Supplemental URLs use the immutable revisions in the authenticated crates' VCS -metadata. Deduplication and whitespace normalization retain 226 distinct texts -and every package's source reference. The complete file is 749,595 bytes, about -87 KB with gzip; the existing installers already carry and authenticate it. +metadata. Deduplication and whitespace normalization retain 226 distinct upstream +notice texts and every package's source reference. The beta additionally carries +five original package declarations and the selected standard license terms; +exact duplicates share a text. The resulting 232 texts occupy 753,317 bytes, +about 87 KB with gzip; the existing installers already carry and authenticate it. The SBOM reader accepts this bounded inventory up to 2 MiB. Commit `3b229f6` passes the focused release unit and installer contract suites, @@ -631,16 +638,22 @@ Go vet and staticcheck pass for the selected change; the blocked checkpoint does not execute tests. This narrower selection does not supersede the broader `8dede4b` checkpoint's architecture findings or establish release readiness. -Five packages still lack sufficient notice evidence: `async-scoped@0.9.0`, +Five packages have no separate upstream notice files: `async-scoped@0.9.0`, `backtrace-ext@0.2.1`, `guppy-workspace-hack@0.1.0`, `recursion@0.5.4` and -`sigchld@0.2.4`. Their declarations are retained as metadata, never substituted -for missing notices. The source inventory is not proof of the linked binary +`sigchld@0.2.4`. Their original manifest declarations and selected standard terms +are now carried explicitly. Apache-2.0 is selected for the four packages offering +it; sigchld declares MIT. Published author metadata is retained, and no copyright +holder or year is invented. Standard templates are distinguished from recovered +notices. The source inventory is not proof of the linked binary graph or license compliance. The [upstream release workflow](https://github.com/nextest-rs/nextest/blob/fc97e97bbe0a3927482a694247da00c099f4269e/.github/workflows/release.yml) does not request locked resolution from its pinned upload action; that [action defaults `locked` to false](https://github.com/taiki-e/upload-rust-binary-action/blob/f0d45ae91ee7b8ee928de7a9d04d893a08bcbec6/action.yml). The release has no dependency SBOM asset, and the inspected Linux x64 binary -has no cargo-auditable dependency section. Thus even this frozen source closure -cannot authenticate the actual binary's dependency versions. +has no cargo-auditable dependency section. Source-path inspection of all four +checksum-verified Nextest archives found 129 registry package identities for +each Linux architecture, 128 for universal macOS and 122 for Windows. Every +observed identity occurs in the reviewed source closure. Those observations +corroborate versions but do not establish the full set linked into each binary. Rust and Nextest SBOM entries retain explicit incomplete native compositions, host-specific authenticated download references and carried notice locations. diff --git a/tools/cargo-nextest-license.txt b/tools/cargo-nextest-license.txt index 4eb7349d..4ba631cc 100644 --- a/tools/cargo-nextest-license.txt +++ b/tools/cargo-nextest-license.txt @@ -27,7 +27,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -NEXTEST SOURCE NOTICE INVENTORY (INCOMPLETE) +NEXTEST SOURCE LICENSE EVIDENCE Source: https://github.com/nextest-rs/nextest/blob/fc97e97bbe0a3927482a694247da00c099f4269e/Cargo.lock @@ -37,19 +37,22 @@ aarch64-unknown-linux-gnu, x86_64-pc-windows-msvc, x86_64-apple-darwin and aarch64-apple-darwin. This inventory covers 362 registry packages. The five Nextest workspace packages use the upstream license above. -This is a source dependency inventory, not an attestation of the dependencies -linked into the downloaded binaries. It excludes development-only and inactive -locked packages. It does not establish complete native-tool notices or license -compliance. The native SBOM composition remains incomplete. Five packages lack -notice evidence: async-scoped 0.9.0, backtrace-ext 0.2.1, guppy-workspace-hack -0.1.0, recursion 0.5.4 and sigchld 0.2.4. +This is source license evidence, not an attestation of the dependencies linked +into the downloaded binaries. It excludes development-only and inactive locked +packages. The native SBOM composition remains incomplete. -Archive notices were read from the registry archives authenticated against the -source lock. Supplemental files come from immutable upstream revisions recorded -in each crate's .cargo_vcs_info.json. Exact duplicate texts share one numbered -notice; every package retains its source location. Line endings and trailing -whitespace are normalized. Declared license expressions -are package metadata, not replacement license texts or copyright notices. +Archive notices were read from registry archives authenticated against the source +lock. Supplemental files come from immutable upstream revisions recorded in each +crate's .cargo_vcs_info.json. Shared texts retain every package's source location; +line endings and trailing whitespace are normalized. + +Five packages publish standard license declarations without separate notice files: +async-scoped 0.9.0, backtrace-ext 0.2.1, guppy-workspace-hack 0.1.0, recursion +0.5.4 and sigchld 0.2.4. Their original package declarations and the selected +standard license terms appear below. Apache-2.0 is selected where offered as an +alternative; sigchld declares MIT. Author metadata is reproduced as published. +No missing copyright holder or year has been invented. Standard license templates +retain their original placeholders and are distinguished from upstream notices. PACKAGE INDEX @@ -136,7 +139,10 @@ Package: async-scoped 0.9.0 Declared license: Apache-2.0/MIT Archive: https://static.crates.io/crates/async-scoped/async-scoped-0.9.0.crate Archive SHA-256: 4042078ea593edffc452eef14e99fdb2b120caa4ad9618bcdeabc4a023b98740 - Notice evidence: unavailable; declared license is not a substitute. + Declaration 227: archive file Cargo.toml.orig + Selected license: Apache-2.0 + Standard terms 051: https://www.apache.org/licenses/LICENSE-2.0.txt + Separate upstream notice: not supplied; no copyright owner or year inferred. Package: atomic 0.6.1 Declared license: Apache-2.0/MIT @@ -183,7 +189,10 @@ Package: backtrace-ext 0.2.1 Declared license: MIT OR Apache-2.0 Archive: https://static.crates.io/crates/backtrace-ext/backtrace-ext-0.2.1.crate Archive SHA-256: 537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50 - Notice evidence: unavailable; declared license is not a substitute. + Declaration 228: archive file Cargo.toml.orig + Selected license: Apache-2.0 + Standard terms 051: https://www.apache.org/licenses/LICENSE-2.0.txt + Separate upstream notice: not supplied; no copyright owner or year inferred. Package: base64 0.22.1 Declared license: MIT OR Apache-2.0 @@ -852,7 +861,10 @@ Package: guppy-workspace-hack 0.1.0 Declared license: MIT OR Apache-2.0 Archive: https://static.crates.io/crates/guppy-workspace-hack/guppy-workspace-hack-0.1.0.crate Archive SHA-256: 92620684d99f750bae383ecb3be3748142d6095760afd5cbcf2261e9a279d780 - Notice evidence: unavailable; declared license is not a substitute. + Declaration 229: archive file Cargo.toml.orig + Selected license: Apache-2.0 + Standard terms 051: https://www.apache.org/licenses/LICENSE-2.0.txt + Separate upstream notice: not supplied; no copyright owner or year inferred. Package: hashbrown 0.15.5 Declared license: MIT OR Apache-2.0 @@ -1514,7 +1526,10 @@ Package: recursion 0.5.4 Declared license: MIT OR Apache-2.0 Archive: https://static.crates.io/crates/recursion/recursion-0.5.4.crate Archive SHA-256: 9dba2197bf7b1d87b4dd460c195f4edeb45a94e82e8054f8d5f317c1f0e93ca1 - Notice evidence: unavailable; declared license is not a substitute. + Declaration 230: archive file Cargo.toml.orig + Selected license: Apache-2.0 + Standard terms 051: https://www.apache.org/licenses/LICENSE-2.0.txt + Separate upstream notice: not supplied; no copyright owner or year inferred. Package: regex 1.12.4 Declared license: MIT OR Apache-2.0 @@ -1733,7 +1748,10 @@ Package: sigchld 0.2.4 Declared license: MIT Archive: https://static.crates.io/crates/sigchld/sigchld-0.2.4.crate Archive SHA-256: 47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1 - Notice evidence: unavailable; declared license is not a substitute. + Declaration 231: archive file Cargo.toml.orig + Selected license: MIT + Standard terms 232: https://raw.githubusercontent.com/spdx/license-list-data/v3.27.0/text/MIT.txt + Separate upstream notice: not supplied; no copyright owner or year inferred. Package: signal-hook 0.3.18 Declared license: Apache-2.0/MIT @@ -2528,9 +2546,9 @@ Archive SHA-256: eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a23 Notice 225: archive file zstd/COPYING Notice 226: archive file zstd/LICENSE -NOTICE TEXTS +NOTICE, DECLARATION AND LICENSE TEXTS -===== NOTICE 001 ===== +===== TEXT 001 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -2733,7 +2751,7 @@ 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. -===== NOTICE 002 ===== +===== TEXT 002 ===== Copyright (c) 2016-2018 The gimli Developers Permission is hereby granted, free of charge, to any @@ -2760,7 +2778,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 003 ===== +===== TEXT 003 ===== Copyright (C) Jonas Schievink Permission to use, copy, modify, and/or distribute this software for @@ -2774,7 +2792,7 @@ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -===== NOTICE 004 ===== +===== TEXT 004 ===== Apache License Version 2.0, January 2004 https://www.apache.org/licenses/LICENSE-2.0 @@ -2977,7 +2995,7 @@ 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. -===== NOTICE 005 ===== +===== TEXT 005 ===== 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 @@ -3002,7 +3020,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 006 ===== +===== TEXT 006 ===== Copyright (c) 2018 Tom Kaitchuck Permission is hereby granted, free of charge, to any @@ -3029,12 +3047,12 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 007 ===== +===== TEXT 007 ===== This project is dual-licensed under the Unlicense and MIT licenses. You may use this code under the terms of either license. -===== NOTICE 008 ===== +===== TEXT 008 ===== The MIT License (MIT) Copyright (c) 2015 Andrew Gallant @@ -3057,7 +3075,7 @@ 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. -===== NOTICE 009 ===== +===== TEXT 009 ===== This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or @@ -3083,7 +3101,7 @@ OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to -===== NOTICE 010 ===== +===== TEXT 010 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -3261,7 +3279,7 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION END OF TERMS AND CONDITIONS -===== NOTICE 011 ===== +===== TEXT 011 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -3465,7 +3483,7 @@ END OF TERMS AND CONDITIONS limitations under the License. -===== NOTICE 012 ===== +===== TEXT 012 ===== Copyright (c) Individual contributors Permission is hereby granted, free of charge, to any person obtaining a copy @@ -3486,7 +3504,7 @@ 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. -===== NOTICE 013 ===== +===== TEXT 013 ===== Copyright (c) 2015 Josh Triplett, 2022 The rust-cli Developers Permission is hereby granted, free of charge, to any person obtaining a copy @@ -3507,7 +3525,7 @@ 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. -===== NOTICE 014 ===== +===== TEXT 014 ===== Copyright (c) 2016 The Rust Project Developers Permission is hereby granted, free of charge, to any @@ -3534,7 +3552,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 015 ===== +===== TEXT 015 ===== Copyright (c) 2015 Markus Unterwaditzer Permission is hereby granted, free of charge, to any person obtaining a copy of @@ -3555,7 +3573,7 @@ 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. -===== NOTICE 016 ===== +===== TEXT 016 ===== Copyright (c) 2018 Josh Stone Permission is hereby granted, free of charge, to any @@ -3582,7 +3600,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 017 ===== +===== TEXT 017 ===== SPDX-License-Identifier: ISC AND (Apache-2.0 OR ISC) @@ -3786,7 +3804,7 @@ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -===== NOTICE 018 ===== +===== TEXT 018 ===== AWS Libcrypto (AWS-LC) AWS-LC is a fork of BoringSSL, which is itself a fork of OpenSSL. @@ -4104,7 +4122,7 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of 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. -===== NOTICE 019 ===== +===== TEXT 019 ===== The MIT License (MIT) Copyright (c) 2015-2020 the fiat-crypto authors (see @@ -4128,7 +4146,7 @@ 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. -===== NOTICE 020 ===== +===== TEXT 020 ===== Copyright (c) 2014 Alex Crichton Permission is hereby granted, free of charge, to any @@ -4155,7 +4173,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 021 ===== +===== TEXT 021 ===== The MIT License (MIT) Copyright (c) 2015 Alice Maz @@ -4178,7 +4196,7 @@ 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. -===== NOTICE 022 ===== +===== TEXT 022 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -4381,7 +4399,7 @@ 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. -===== NOTICE 023 ===== +===== TEXT 023 ===== Copyright (c) 2023 The Rust Project Developers Permission is hereby granted, free of charge, to any @@ -4408,7 +4426,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 024 ===== +===== TEXT 024 ===== Copyright (c) 2015 The Rust Project Developers Permission is hereby granted, free of charge, to any @@ -4435,7 +4453,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 025 ===== +===== TEXT 025 ===== Copyright (c) 2014 The Rust Project Developers Permission is hereby granted, free of charge, to any @@ -4462,7 +4480,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 026 ===== +===== TEXT 026 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -4665,7 +4683,7 @@ 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. -===== NOTICE 027 ===== +===== TEXT 027 ===== Copyright (c) 2018-2019 The RustCrypto Project Developers Permission is hereby granted, free of charge, to any @@ -4692,7 +4710,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 028 ===== +===== TEXT 028 ===== This project is licensed under either of * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or @@ -4702,7 +4720,7 @@ This project is licensed under either of at your option. -===== NOTICE 029 ===== +===== TEXT 029 ===== The MIT License (MIT) Copyright (c) 2018-2019 Andrew Gallant @@ -4725,7 +4743,7 @@ 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. -===== NOTICE 030 ===== +===== TEXT 030 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -4788,7 +4806,7 @@ 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. -===== NOTICE 031 ===== +===== TEXT 031 ===== MIT License Copyright (c) 2019 Daniel "Lokathor" Gee. @@ -4799,7 +4817,7 @@ The above copyright notice and this permission notice (including the next paragr 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. -===== NOTICE 032 ===== +===== TEXT 032 ===== Copyright (c) 2019 Daniel "Lokathor" Gee. This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. @@ -4812,7 +4830,7 @@ Permission is granted to anyone to use this software for any purpose, including 3. This notice may not be removed or altered from any source distribution. -===== NOTICE 033 ===== +===== TEXT 033 ===== Copyright (c) 2018 Carl Lerche Permission is hereby granted, free of charge, to any @@ -4839,7 +4857,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 034 ===== +===== TEXT 034 ===== Copyright (c) 2015 Steven Allen Copyright (c) The assert_fs Contributors Copyright (c) The camino-tempfile Contributors @@ -4868,7 +4886,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 035 ===== +===== TEXT 035 ===== Copyright (c) 2019 Embark Studios Permission is hereby granted, free of charge, to any @@ -4895,7 +4913,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 036 ===== +===== TEXT 036 ===== MIT License Copyright (c) 2020 Katharos Technology @@ -4906,7 +4924,7 @@ The above copyright notice and this permission notice shall be included in all c 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. -===== NOTICE 037 ===== +===== TEXT 037 ===== Copyright (c) 2019-2026 The RustCrypto Project Developers Permission is hereby granted, free of charge, to any @@ -4933,7 +4951,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 038 ===== +===== TEXT 038 ===== Rust-chrono is dual-licensed under The MIT License [1] and Apache 2.0 License [2]. Copyright (c) 2014--2026, Kang Seonghoon and contributors. @@ -5175,7 +5193,7 @@ limitations under the License. ~~~~ -===== NOTICE 039 ===== +===== TEXT 039 ===== The MIT License (MIT) Copyright (c) 2017 Armin Ronacher @@ -5199,7 +5217,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 040 ===== +===== TEXT 040 ===== MIT License Copyright (c) 2025 rutrum @@ -5222,7 +5240,7 @@ 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. -===== NOTICE 041 ===== +===== TEXT 041 ===== Copyright (c) 2012-2013 Mozilla Foundation Permission is hereby granted, free of charge, to any @@ -5249,7 +5267,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 042 ===== +===== TEXT 042 ===== MIT License Copyright (c) 2024 Spire @@ -5272,7 +5290,7 @@ 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. -===== NOTICE 043 ===== +===== TEXT 043 ===== Copyright (c) 2020-2025 The RustCrypto Project Developers Permission is hereby granted, free of charge, to any @@ -5299,7 +5317,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 044 ===== +===== TEXT 044 ===== MIT License Copyright (c) 2018 Sam Rijs, Alex Crichton and contributors @@ -5322,7 +5340,7 @@ 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. -===== NOTICE 045 ===== +===== TEXT 045 ===== MIT License Copyright (c) 2019 Timon @@ -5345,7 +5363,7 @@ 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. -===== NOTICE 046 ===== +===== TEXT 046 ===== Copyright (c) 2021 RustCrypto Developers Permission is hereby granted, free of charge, to any @@ -5372,7 +5390,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 047 ===== +===== TEXT 047 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -5575,7 +5593,7 @@ DEALINGS IN THE SOFTWARE. See the License for the specific language governing permissions and limitations under the License. -===== NOTICE 048 ===== +===== TEXT 048 ===== MIT License Copyright (c) 2020 Nor Khasyatillah @@ -5598,7 +5616,7 @@ 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. -===== NOTICE 049 ===== +===== TEXT 049 ===== MIT License Copyright 2019 Ryan O'Beirne @@ -5620,7 +5638,7 @@ 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. -===== NOTICE 050 ===== +===== TEXT 050 ===== These documentation pages include resources by third parties. This copyright file applies only to those resources. The following third party resources are included, and carry their own copyright notices and license terms: @@ -5672,7 +5690,7 @@ included, and carry their own copyright notices and license terms: This copyright file is intended to be distributed with rustdoc output. -===== NOTICE 051 ===== +===== TEXT 051 ===== Apache License Version 2.0, January 2004 @@ -5876,7 +5894,7 @@ This copyright file is intended to be distributed with rustdoc output. See the License for the specific language governing permissions and limitations under the License. -===== NOTICE 052 ===== +===== TEXT 052 ===== MIT License Copyright (c) 2021 Roland Fredenhagen @@ -5899,7 +5917,7 @@ 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. -===== NOTICE 053 ===== +===== TEXT 053 ===== The MIT License (MIT) Copyright (c) 2016 Jelte Fennema @@ -5922,7 +5940,7 @@ 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. -===== NOTICE 054 ===== +===== TEXT 054 ===== Copyright (c) 2017 Artyom Pavlov Permission is hereby granted, free of charge, to any @@ -5949,7 +5967,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 055 ===== +===== TEXT 055 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -6125,7 +6143,7 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. -===== NOTICE 056 ===== +===== TEXT 056 ===== Copyright (c) 2018-2019 dirs-rs contributors Permission is hereby granted, free of charge, to any person obtaining a copy @@ -6146,7 +6164,7 @@ 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. -===== NOTICE 057 ===== +===== TEXT 057 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -6221,7 +6239,7 @@ 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. -===== NOTICE 058 ===== +===== TEXT 058 ===== Copyright (c) 2020 Olivier Goffart Permission is hereby granted, free of charge, to any person obtaining a copy @@ -6242,7 +6260,7 @@ 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. -===== NOTICE 059 ===== +===== TEXT 059 ===== Apache License Version 2.0, January 2004 @@ -6421,7 +6439,7 @@ SOFTWARE. END OF TERMS AND CONDITIONS -===== NOTICE 060 ===== +===== TEXT 060 ===== The MIT License (MIT) Permission is hereby granted, free of charge, to any person obtaining a copy @@ -6442,7 +6460,7 @@ 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. -===== NOTICE 061 ===== +===== TEXT 061 ===== Creative Commons Legal Code CC0 1.0 Universal @@ -6565,7 +6583,7 @@ express Statement of Purpose. party to this document and has no duty or obligation with respect to this CC0 or use of the Work. -===== NOTICE 062 ===== +===== TEXT 062 ===== Copyright 2025 Benoît du Garreau Licensed under the Apache License, Version 2.0 (the "License"); @@ -6580,7 +6598,7 @@ 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. -===== NOTICE 063 ===== +===== TEXT 063 ===== Copyright 2025 Benoît du Garreau Permission is hereby granted, free of charge, to any @@ -6607,7 +6625,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 064 ===== +===== TEXT 064 ===== Copyright (c) 2015 Permission is hereby granted, free of charge, to any @@ -6634,7 +6652,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 065 ===== +===== TEXT 065 ===== 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 @@ -6653,7 +6671,7 @@ 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 -===== NOTICE 066 ===== +===== TEXT 066 ===== encoding_rs is copyright Mozilla Foundation. Licensed under the Apache License, Version 2.0 @@ -6672,7 +6690,7 @@ source code, are licensed under the BSD 3-Clause License Test code within encoding_rs is dedicated to the Public Domain when so designated (see the individual files for PD/CC0-dedicated sections). -===== NOTICE 067 ===== +===== TEXT 067 ===== Copyright Mozilla Foundation Permission is hereby granted, free of charge, to any @@ -6699,7 +6717,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 068 ===== +===== TEXT 068 ===== Copyright © WHATWG (Apple, Google, Mozilla, Microsoft). Redistribution and use in source and binary forms, with or without @@ -6727,7 +6745,7 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -===== NOTICE 069 ===== +===== TEXT 069 ===== MIT License Copyright (c) 2019 Anton Lazarev @@ -6750,7 +6768,7 @@ 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. -===== NOTICE 070 ===== +===== TEXT 070 ===== Copyright (c) 2016--2023 Permission is hereby granted, free of charge, to any @@ -6777,7 +6795,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 071 ===== +===== TEXT 071 ===== Copyright (c) 2014 Chris Wong Permission is hereby granted, free of charge, to any @@ -6804,14 +6822,14 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 072 ===== +===== TEXT 072 ===== Licensed under the Apache License, Version 2.0 or the MIT license , at your option. All files in the project carrying such notice may not be copied, modified, or distributed except according to those terms. -===== NOTICE 073 ===== +===== TEXT 073 ===== The MIT License Copyright 2015 The Fancy Regex Authors. @@ -6834,7 +6852,7 @@ 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. -===== NOTICE 074 ===== +===== TEXT 074 ===== MIT License Copyright (c) 2018 Wez Furlong @@ -6857,7 +6875,7 @@ 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. -===== NOTICE 075 ===== +===== TEXT 075 ===== UNICODE LICENSE V3 COPYRIGHT AND PERMISSION NOTICE @@ -6898,7 +6916,7 @@ not be used in advertising or otherwise to promote the sale, use or other dealings in these Data Files or Software without prior written authorization of the copyright holder. -===== NOTICE 076 ===== +===== TEXT 076 ===== Copyright (c) 2015-2017 Permission is hereby granted, free of charge, to any @@ -6925,7 +6943,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 077 ===== +===== TEXT 077 ===== Copyright (c) 2014-2025 Alex Crichton Permission is hereby granted, free of charge, to any @@ -6952,7 +6970,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 078 ===== +===== TEXT 078 ===== Copyright (c) 2017 Contributors Permission is hereby granted, free of charge, to any @@ -6979,7 +6997,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 079 ===== +===== TEXT 079 ===== Copyright (c) 2024 Orson Peters This software is provided 'as-is', without any express or implied warranty. In @@ -7000,7 +7018,7 @@ the following restrictions: 3. This notice may not be removed or altered from any source distribution. -===== NOTICE 080 ===== +===== TEXT 080 ===== MIT License Copyright (c) 2017 Denis Kurilenko @@ -7023,7 +7041,7 @@ 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. -===== NOTICE 081 ===== +===== TEXT 081 ===== Copyright (c) The future-queue Contributors Permission is hereby granted, free of charge, to any @@ -7082,7 +7100,7 @@ the Apache 2.0 and MIT licenses. > IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER > DEALINGS IN THE SOFTWARE. -===== NOTICE 082 ===== +===== TEXT 082 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -7286,7 +7304,7 @@ 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. -===== NOTICE 083 ===== +===== TEXT 083 ===== Copyright (c) 2016 Alex Crichton Copyright (c) 2017 The Tokio Authors @@ -7314,7 +7332,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 084 ===== +===== TEXT 084 ===== The MIT License (MIT) Copyright (c) 2015 Bartłomiej Kamiński @@ -7337,7 +7355,7 @@ 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. -===== NOTICE 085 ===== +===== TEXT 085 ===== Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ @@ -7540,7 +7558,7 @@ 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. -===== NOTICE 086 ===== +===== TEXT 086 ===== Copyright (c) 2018-2025 The rust-random Project Developers Copyright (c) 2014 The Rust Project Developers @@ -7568,7 +7586,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 087 ===== +===== TEXT 087 ===== Copyright (c) 2018-2026 The rust-random Project Developers Copyright (c) 2014 The Rust Project Developers @@ -7596,7 +7614,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 088 ===== +===== TEXT 088 ===== The MIT License (MIT) Copyright (c) m4b 2016-2024 @@ -7619,7 +7637,7 @@ 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. -===== NOTICE 089 ===== +===== TEXT 089 ===== Copyright (c) The cargo-guppy Contributors Permission is hereby granted, free of charge, to any @@ -7646,7 +7664,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 090 ===== +===== TEXT 090 ===== Copyright (c) 2016 Amanieu d'Antras Permission is hereby granted, free of charge, to any @@ -7673,7 +7691,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 091 ===== +===== TEXT 091 ===== Copyright (c) 2013-2014 The Rust Project Developers. Copyright (c) 2015-2020 The rust-hex Developers @@ -7695,7 +7713,7 @@ 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. -===== NOTICE 092 ===== +===== TEXT 092 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -7898,7 +7916,7 @@ 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. -===== NOTICE 093 ===== +===== TEXT 093 ===== Copyright (c) 2017 http-rs authors Permission is hereby granted, free of charge, to any @@ -7925,7 +7943,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 094 ===== +===== TEXT 094 ===== Copyright (c) 2015-2025 Sean McArthur Permission is hereby granted, free of charge, to any person obtaining a copy @@ -7947,7 +7965,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 095 ===== +===== TEXT 095 ===== Copyright (c) 2016 The humantime Developers Includes parts of http date with the following copyright: @@ -7975,7 +7993,7 @@ 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. -===== NOTICE 096 ===== +===== TEXT 096 ===== The MIT License (MIT) Copyright (c) 2017 The serde-humantime Developers @@ -7999,7 +8017,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 097 ===== +===== TEXT 097 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -8202,7 +8220,7 @@ 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. -===== NOTICE 098 ===== +===== TEXT 098 ===== Copyright (c) 2020 Andrew D. Straw Permission is hereby granted, free of charge, to any @@ -8229,7 +8247,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 099 ===== +===== TEXT 099 ===== Copyright (c) 2025 Oxide Computer Company Permission is hereby granted, free of charge, to any @@ -8256,7 +8274,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 100 ===== +===== TEXT 100 ===== Copyright (c) The rust-url developers Permission is hereby granted, free of charge, to any @@ -8283,7 +8301,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 101 ===== +===== TEXT 101 ===== MIT License Copyright (c) 2017 Michael Bryan @@ -8306,7 +8324,7 @@ 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. -===== NOTICE 102 ===== +===== TEXT 102 ===== Copyright (c) 2016--2017 Permission is hereby granted, free of charge, to any @@ -8333,7 +8351,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 103 ===== +===== TEXT 103 ===== The ISC License Copyright (c) Kat Marchán and other contributors. @@ -8350,7 +8368,7 @@ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.Apache License -===== NOTICE 104 ===== +===== TEXT 104 ===== MIT License Copyright (c) 2020 🐝🐝🐝 @@ -8373,7 +8391,7 @@ 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. -===== NOTICE 105 ===== +===== TEXT 105 ===== Copyright (c) 2010 The Rust Project Developers Permission is hereby granted, free of charge, to any @@ -8400,7 +8418,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 106 ===== +===== TEXT 106 ===== Copyright (c) The Rust Project Developers Permission is hereby granted, free of charge, to any @@ -8427,7 +8445,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 107 ===== +===== TEXT 107 ===== Short version for non-lawyers: `linux-raw-sys` is triple-licensed under Apache 2.0 with the LLVM Exception, @@ -8458,7 +8476,7 @@ is licensed under: at your option. -===== NOTICE 108 ===== +===== TEXT 108 ===== Apache License Version 2.0, January 2004 @@ -8680,7 +8698,7 @@ the License, but only in their entirety and only with respect to the Combined Software. -===== NOTICE 109 ===== +===== TEXT 109 ===== Copyright (c) 2020 Project Developers Permission is hereby granted, free of charge, to any @@ -8707,7 +8725,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 110 ===== +===== TEXT 110 ===== Copyright (c) The nextest Contributors Permission is hereby granted, free of charge, to any @@ -8734,7 +8752,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 111 ===== +===== TEXT 111 ===== MIT License Copyright (c) 2016 Jerome Froelich @@ -8757,7 +8775,7 @@ 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. -===== NOTICE 112 ===== +===== TEXT 112 ===== Apache License Version 2.0, January 2004 @@ -8961,7 +8979,7 @@ SOFTWARE. See the License for the specific language governing permissions and limitations under the License. -===== NOTICE 113 ===== +===== TEXT 113 ===== Copyright © 2018 Wesley Norris 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: @@ -8970,7 +8988,7 @@ The above copyright notice and this permission notice shall be included in all c 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. -===== NOTICE 114 ===== +===== TEXT 114 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -9173,7 +9191,7 @@ 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. -===== NOTICE 115 ===== +===== TEXT 115 ===== Copyright (c) 2020 Yevhenii Reizner Copyright (c) 2015 Dan Burkert @@ -9201,7 +9219,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 116 ===== +===== TEXT 116 ===== Copyright (c) 2017 Gilad Naaman Permission is hereby granted, free of charge, to any person obtaining a copy @@ -9222,7 +9240,7 @@ 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. -===== NOTICE 117 ===== +===== TEXT 117 ===== Apache License Version 2.0, January 2004 @@ -9453,7 +9471,7 @@ governing permissions and limitations under the License. -===== NOTICE 118 ===== +===== TEXT 118 ===== Minimal-lexical is dual licensed under the Apache 2.0 license as well as the MIT license. See the LICENCE-MIT and the LICENCE-APACHE files for the licenses. @@ -9492,7 +9510,7 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -===== NOTICE 119 ===== +===== TEXT 119 ===== MIT License Copyright 2013-2014 RAD Game Tools and Valve Software @@ -9519,7 +9537,7 @@ 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. -===== NOTICE 120 ===== +===== TEXT 120 ===== MIT License Copyright 2013-2014 RAD Game Tools and Valve Software @@ -9545,7 +9563,7 @@ 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. -===== NOTICE 121 ===== +===== TEXT 121 ===== Copyright 2013-2014 RAD Game Tools and Valve Software Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC Copyright (c) 2020 Frommi @@ -9561,7 +9579,7 @@ Permission is granted to anyone to use this software for any purpose, including 3. This notice may not be removed or altered from any source distribution. -===== NOTICE 122 ===== +===== TEXT 122 ===== Copyright (c) 2014 Carl Lerche and other MIO contributors Permission is hereby granted, free of charge, to any person obtaining a copy @@ -9582,7 +9600,7 @@ 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. -===== NOTICE 123 ===== +===== TEXT 123 ===== The MIT License (MIT) Copyright (c) 2016 Johann Tuffe @@ -9607,7 +9625,7 @@ 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. -===== NOTICE 124 ===== +===== TEXT 124 ===== Copyright (c) 2024 Oxide Computer Company Permission is hereby granted, free of charge, to any @@ -9634,7 +9652,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 125 ===== +===== TEXT 125 ===== The MIT License (MIT) Copyright (c) 2015 Carl Lerche + nix-rust Authors @@ -9657,7 +9675,7 @@ 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. -===== NOTICE 126 ===== +===== TEXT 126 ===== Copyright (c) 2014-2019 Geoffroy Couprie Permission is hereby granted, free of charge, to any person obtaining @@ -9679,7 +9697,7 @@ 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. -===== NOTICE 127 ===== +===== TEXT 127 ===== Copyright (c) 2015 The Gimli Developers Permission is hereby granted, free of charge, to any @@ -9706,7 +9724,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 128 ===== +===== TEXT 128 ===== Mozilla Public License Version 2.0 ================================== @@ -10081,7 +10099,7 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. -===== NOTICE 129 ===== +===== TEXT 129 ===== Copyright (c) 2015 Jonathan Reem Permission is hereby granted, free of charge, to any @@ -10108,7 +10126,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 130 ===== +===== TEXT 130 ===== MIT License Copyright (c) 2020 - present The owo-colors Developers @@ -10131,7 +10149,7 @@ 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. -===== NOTICE 131 ===== +===== TEXT 131 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -10334,7 +10352,7 @@ 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. -===== NOTICE 132 ===== +===== TEXT 132 ===== Copyright (c) 2013-2025 The rust-url developers Permission is hereby granted, free of charge, to any @@ -10361,7 +10379,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 133 ===== +===== TEXT 133 ===== Graphosaurus (c) by the petgraph project. Graphosaurus is licensed under a @@ -10370,7 +10388,7 @@ Creative Commons Attribution-ShareAlike 4.0 International License. You should have received a copy of the license along with this work. If not, see . -===== NOTICE 134 ===== +===== TEXT 134 ===== The MIT License (MIT) Copyright (c) 2014-2022 Steven Fackler, Yuki Okushi @@ -10392,7 +10410,7 @@ 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. -===== NOTICE 135 ===== +===== TEXT 135 ===== Copyright (c) 2017 Plain contributors Permission is hereby granted, free of charge, to any @@ -10419,7 +10437,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 136 ===== +===== TEXT 136 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -10622,7 +10640,7 @@ 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. -===== NOTICE 137 ===== +===== TEXT 137 ===== Copyright (c) 2019 The CryptoCorrosion Contributors Permission is hereby granted, free of charge, to any @@ -10649,7 +10667,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 138 ===== +===== TEXT 138 ===== MIT License Copyright (c) 2018 Andrei Volnin @@ -10672,7 +10690,7 @@ 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. -===== NOTICE 139 ===== +===== TEXT 139 ===== Copyright (c) 2016 FullContact, Inc Permission is hereby granted, free of charge, to any @@ -10699,7 +10717,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 140 ===== +===== TEXT 140 ===== The MIT License Copyright 2015 Google Inc. All rights reserved. @@ -10722,7 +10740,7 @@ 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. -===== NOTICE 141 ===== +===== TEXT 141 ===== Copyright (c) 2015 The quick-error Developers Permission is hereby granted, free of charge, to any person obtaining a copy @@ -10743,7 +10761,7 @@ 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. -===== NOTICE 142 ===== +===== TEXT 142 ===== Copyrights in the Rand project are retained by their contributors. No copyright assignment is required to contribute to the Rand project. @@ -10757,7 +10775,7 @@ licensed under the Apache License, Version 2.0 or The Rand project includes code from the Rust project published under these same licenses. -===== NOTICE 143 ===== +===== TEXT 143 ===== Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ @@ -10935,7 +10953,7 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION END OF TERMS AND CONDITIONS -===== NOTICE 144 ===== +===== TEXT 144 ===== Copyright 2018 Developers of the Rand project Copyright (c) 2014 The Rust Project Developers @@ -10963,7 +10981,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 145 ===== +===== TEXT 145 ===== Copyrights in the Rand project are retained by their contributors. No copyright assignment is required to contribute to the Rand project. @@ -10974,7 +10992,7 @@ licensed under the Apache License, Version 2.0 or or the MIT license or , at your option. -===== NOTICE 146 ===== +===== TEXT 146 ===== Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ @@ -11163,7 +11181,7 @@ APPENDIX: How to apply the Apache License to your work. same "printed page" as the copyright notice for easier identification within third-party archives. -===== NOTICE 147 ===== +===== TEXT 147 ===== Copyright (c) 2018-2026 The Rand Project Developers Permission is hereby granted, free of charge, to any @@ -11190,7 +11208,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 148 ===== +===== TEXT 148 ===== UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE Unicode Data Files include all data files under the directories @@ -11249,7 +11267,7 @@ shall not be used in advertising or otherwise to promote the sale, use or other dealings in these Data Files or Software without prior written authorization of the copyright holder. -===== NOTICE 149 ===== +===== TEXT 149 ===== Short version for non-lawyers: `rustix` is triple-licensed under Apache 2.0 with the LLVM Exception, @@ -11280,7 +11298,7 @@ is licensed under: at your option. -===== NOTICE 150 ===== +===== TEXT 150 ===== ISC License (ISC) Copyright (c) 2016, Joseph Birr-Pixton @@ -11297,7 +11315,7 @@ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -===== NOTICE 151 ===== +===== TEXT 151 ===== Copyright (c) 2016 Joseph Birr-Pixton Permission is hereby granted, free of charge, to any @@ -11324,7 +11342,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 152 ===== +===== TEXT 152 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -11527,7 +11545,7 @@ 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. -===== NOTICE 153 ===== +===== TEXT 153 ===== Copyright (c) 2023 Dirkjan Ochtman Permission is hereby granted, free of charge, to any @@ -11554,7 +11572,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 154 ===== +===== TEXT 154 ===== Except as otherwise noted, this project is licensed under the following (ISC-style) terms: @@ -11575,7 +11593,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. The files under third-party/chromium are licensed as described in third-party/chromium/LICENSE. -===== NOTICE 155 ===== +===== TEXT 155 ===== MIT License Copyright (c) 2019 Mark Juggurnauth-Thomas @@ -11598,7 +11616,7 @@ 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. -===== NOTICE 156 ===== +===== TEXT 156 ===== Copyright (c) 2016-2019 Ulrik Sverdrup "bluss" and scopeguard developers Permission is hereby granted, free of charge, to any @@ -11625,7 +11643,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 157 ===== +===== TEXT 157 ===== The MIT License (MIT) Copyright (c) m4b 2016 @@ -11648,7 +11666,7 @@ 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. -===== NOTICE 158 ===== +===== TEXT 158 ===== MIT License Copyright (c) 2017 @@ -11671,7 +11689,7 @@ 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. -===== NOTICE 159 ===== +===== TEXT 159 ===== MIT License Copyright (c) 2017 James Kominick @@ -11694,7 +11712,7 @@ 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. -===== NOTICE 160 ===== +===== TEXT 160 ===== Copyright (c) 2006-2009 Graydon Hoare Copyright (c) 2009-2013 Mozilla Foundation Copyright (c) 2016 Artyom Pavlov @@ -11723,7 +11741,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 161 ===== +===== TEXT 161 ===== Copyright (c) 2019 Eliza Weisman Permission is hereby granted, free of charge, to any person obtaining a copy @@ -11744,7 +11762,7 @@ 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. -===== NOTICE 162 ===== +===== TEXT 162 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -11947,7 +11965,7 @@ 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. -===== NOTICE 163 ===== +===== TEXT 163 ===== Copyright (c) 2016 Tomasz Miąsko Permission is hereby granted, free of charge, to any @@ -11974,7 +11992,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 164 ===== +===== TEXT 164 ===== Copyright 2015 Nicholas Allegra (comex). Licensed under the Apache License, Version 2.0 (the "License"); @@ -11989,7 +12007,7 @@ 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. -===== NOTICE 165 ===== +===== TEXT 165 ===== The MIT License (MIT) Copyright (c) 2015 Nicholas Allegra (comex). @@ -12012,7 +12030,7 @@ 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. -===== NOTICE 166 ===== +===== TEXT 166 ===== Copyright (c) 2017 tokio-jsonrpc developers Permission is hereby granted, free of charge, to any @@ -12039,7 +12057,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 167 ===== +===== TEXT 167 ===== Copyright 2012-2016 The Rust Project Developers. Copyright 2016-2024 Frank Denis. @@ -12048,7 +12066,7 @@ http://www.apache.org/licenses/LICENSE-2.0> or the MIT license , at your option. -===== NOTICE 168 ===== +===== TEXT 168 ===== Copyright (c) 2019 Carl Lerche Permission is hereby granted, free of charge, to any @@ -12075,7 +12093,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 169 ===== +===== TEXT 169 ===== Copyright (c) 2018 The Servo Project Developers Permission is hereby granted, free of charge, to any @@ -12102,7 +12120,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 170 ===== +===== TEXT 170 ===== Copyright (c) 2015 The rust-socks Developers Permission is hereby granted, free of charge, to any person obtaining a copy @@ -12123,7 +12141,7 @@ 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. -===== NOTICE 171 ===== +===== TEXT 171 ===== MIT License Copyright (c) 2017 Nikolai Vazquez @@ -12146,7 +12164,7 @@ 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. -===== NOTICE 172 ===== +===== TEXT 172 ===== Copyright (c) 2018 Mozilla Permission is hereby granted, free of charge, to any @@ -12173,7 +12191,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 173 ===== +===== TEXT 173 ===== The MIT License (MIT) Copyright (c) 2015 Danny Guo @@ -12198,7 +12216,7 @@ 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. -===== NOTICE 174 ===== +===== TEXT 174 ===== Copyright (c) 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. Copyright (c) 2016-2024 Isis Agora Lovecruft. All rights reserved. @@ -12229,7 +12247,7 @@ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -===== NOTICE 175 ===== +===== TEXT 175 ===== MIT License Permission is hereby granted, free of charge, to any person obtaining a copy @@ -12250,7 +12268,7 @@ 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. -===== NOTICE 176 ===== +===== TEXT 176 ===== Copyright (c) The tar-rs Project Contributors Permission is hereby granted, free of charge, to any @@ -12277,7 +12295,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 177 ===== +===== TEXT 177 ===== Copyright (c) 2015 Steven Allen Permission is hereby granted, free of charge, to any @@ -12304,7 +12322,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 178 ===== +===== TEXT 178 ===== Copyright (c) 2015 The terminal-size Developers Permission is hereby granted, free of charge, to any person obtaining a copy @@ -12325,7 +12343,7 @@ 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. -===== NOTICE 179 ===== +===== TEXT 179 ===== DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE Version 2, December 2004 @@ -12340,7 +12358,7 @@ SOFTWARE. 0. You just DO WHAT THE FUCK YOU WANT TO. -===== NOTICE 180 ===== +===== TEXT 180 ===== Copyright (c) 2015 David Cuddeback Permission is hereby granted, free of charge, to any person obtaining @@ -12362,7 +12380,7 @@ 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. -===== NOTICE 181 ===== +===== TEXT 181 ===== MIT License Copyright (c) 2016 Martin Geisler @@ -12385,7 +12403,7 @@ 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. -===== NOTICE 182 ===== +===== TEXT 182 ===== Copyright 2017 Ruud van Asseldonk Permission is hereby granted, free of charge, to any person obtaining a copy @@ -12406,14 +12424,14 @@ 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. -===== NOTICE 183 ===== +===== TEXT 183 ===== 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. -===== NOTICE 184 ===== +===== TEXT 184 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -12617,7 +12635,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI limitations under the License. -===== NOTICE 185 ===== +===== TEXT 185 ===== MIT License Copyright (c) 2020 Soveu @@ -12640,7 +12658,7 @@ 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. -===== NOTICE 186 ===== +===== TEXT 186 ===== zlib License (C) 2020 Tomasz "Soveu" Marx @@ -12662,7 +12680,7 @@ freely, subject to the following restrictions: 3. This notice may not be removed or altered from any source distribution. -===== NOTICE 187 ===== +===== TEXT 187 ===== MIT License Copyright (c) Tokio Contributors @@ -12685,7 +12703,7 @@ 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. -===== NOTICE 188 ===== +===== TEXT 188 ===== MIT License Copyright (c) 2019 Yoshua Wuyts @@ -12709,7 +12727,7 @@ 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. -===== NOTICE 189 ===== +===== TEXT 189 ===== Copyright (c) 2019 Tokio Contributors Permission is hereby granted, free of charge, to any @@ -12736,7 +12754,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 190 ===== +===== TEXT 190 ===== The MIT License (MIT) Copyright (c) 2014 Mathijs van de Nes @@ -12759,10 +12777,10 @@ 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. -===== NOTICE 191 ===== +===== TEXT 191 ===== MIT OR Apache-2.0 -===== NOTICE 192 ===== +===== TEXT 192 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -12965,7 +12983,7 @@ 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. -===== NOTICE 193 ===== +===== TEXT 193 ===== The MIT License (MIT) Copyright (c) 2014 Paho Lurie-Gregg @@ -12988,7 +13006,7 @@ 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. -===== NOTICE 194 ===== +===== TEXT 194 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -13191,7 +13209,7 @@ SOFTWARE. See the License for the specific language governing permissions and limitations under the License. -===== NOTICE 195 ===== +===== TEXT 195 ===== MIT License Copyright (c) [year] [fullname] @@ -13214,7 +13232,7 @@ 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. -===== NOTICE 196 ===== +===== TEXT 196 ===== Copyright (c) 2014-2026 Sean McArthur Permission is hereby granted, free of charge, to any person obtaining a copy @@ -13236,7 +13254,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 197 ===== +===== TEXT 197 ===== Licensed under the Apache License, Version 2.0 or the MIT @@ -13245,7 +13263,7 @@ at your option. All files in the project carrying such notice may not be copied, modified, or distributed except according to those terms. -===== NOTICE 198 ===== +===== TEXT 198 ===== MIT License Copyright (c) 2024 Benjamin Sago, Fabio Valentini @@ -13268,7 +13286,7 @@ 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. -===== NOTICE 199 ===== +===== TEXT 199 ===== // Copyright 2015-2016 Brian Smith. // // Permission to use, copy, modify, and/or distribute this software for any @@ -13283,7 +13301,7 @@ SOFTWARE. // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -===== NOTICE 200 ===== +===== TEXT 200 ===== MIT License Copyright (c) 2019 Martin Algesten @@ -13306,7 +13324,7 @@ 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. -===== NOTICE 201 ===== +===== TEXT 201 ===== Copyright 2022 Martin Algesten 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: @@ -13315,7 +13333,7 @@ The above copyright notice and this permission notice shall be included in all c 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. -===== NOTICE 202 ===== +===== TEXT 202 ===== © 2016 Bertram Truong © 2021 Kornel Lesiński @@ -13337,7 +13355,7 @@ 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. -===== NOTICE 203 ===== +===== TEXT 203 ===== Copyright (c) 2016 Joe Wilm Permission is hereby granted, free of charge, to any @@ -13364,7 +13382,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 204 ===== +===== TEXT 204 ===== Copyright (c) 2014 The Rust Project Developers Copyright (c) 2018 Ashley Mannix, Christopher Armstrong, Dylan DPC, Hunar Roop Kahlon @@ -13392,7 +13410,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 205 ===== +===== TEXT 205 ===== The MIT License (MIT) Copyright (c) 2017-2018 Sergio Benitez @@ -13413,7 +13431,7 @@ 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. -===== NOTICE 206 ===== +===== TEXT 206 ===== MIT License Copyright (c) 2018-Present Wez Furlong @@ -13445,7 +13463,7 @@ available from https://github.com/ryanoasis/nerd-fonts which are clearly distributed under the terms of the OFL 1.1. Note that WezTerm excludes the Pomicons icon set from this collection. -===== NOTICE 207 ===== +===== TEXT 207 ===== # Community Data License Agreement - Permissive - Version 2.0 This is the Community Data License Agreement - Permissive, Version @@ -13508,7 +13526,7 @@ agreement. of Data, including for example machine learning models and models' insights. -===== NOTICE 208 ===== +===== TEXT 208 ===== Copyright (c) 2022-Present Wez Furlong Permission is hereby granted, free of charge, to any person obtaining a copy @@ -13569,7 +13587,7 @@ use or other dealings in these Data Files or Software without prior written authorization of the copyright holder. -===== NOTICE 209 ===== +===== TEXT 209 ===== MIT License Copyright (c) 2023-Present Wez Furlong @@ -13592,7 +13610,7 @@ 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. -===== NOTICE 210 ===== +===== TEXT 210 ===== Copyright (c) 2018-Present Wez Furlong Permission is hereby granted, free of charge, to any person obtaining a copy @@ -13613,7 +13631,7 @@ 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. -===== NOTICE 211 ===== +===== TEXT 211 ===== MIT License Copyright (c) 2018-Present Wez Furlong @@ -13636,7 +13654,7 @@ 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. -===== NOTICE 212 ===== +===== TEXT 212 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -13839,7 +13857,7 @@ SOFTWARE. See the License for the specific language governing permissions and limitations under the License. -===== NOTICE 213 ===== +===== TEXT 213 ===== Copyright (c) 2015-2018 The winapi-rs Developers Permission is hereby granted, free of charge, to any person obtaining a copy @@ -13860,7 +13878,7 @@ 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. -===== NOTICE 214 ===== +===== TEXT 214 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -14063,7 +14081,7 @@ SOFTWARE. See the License for the specific language governing permissions and limitations under the License. -===== NOTICE 215 ===== +===== TEXT 215 ===== MIT License Copyright (c) Microsoft Corporation. @@ -14086,7 +14104,7 @@ SOFTWARE. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE -===== NOTICE 216 ===== +===== TEXT 216 ===== 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 @@ -14106,7 +14124,7 @@ 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. -===== NOTICE 217 ===== +===== TEXT 217 ===== Boost Software License - Version 1.0 - August 17th, 2003 Permission is hereby granted, free of charge, to any person or organization @@ -14131,7 +14149,7 @@ FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 218 ===== +===== TEXT 218 ===== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -14335,7 +14353,7 @@ DEALINGS IN THE SOFTWARE. limitations under the License. -===== NOTICE 219 ===== +===== TEXT 219 ===== Copyright 2019 The Fuchsia Authors. Redistribution and use in source and binary forms, with or without @@ -14361,7 +14379,7 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -===== NOTICE 220 ===== +===== TEXT 220 ===== Copyright 2023 The Fuchsia Authors Permission is hereby granted, free of charge, to any @@ -14389,7 +14407,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -===== NOTICE 221 ===== +===== TEXT 221 ===== MIT License Copyright (c) 2018-2021 The RustCrypto Project Developers @@ -14412,7 +14430,7 @@ 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. -===== NOTICE 222 ===== +===== TEXT 222 ===== The MIT License (MIT) Copyright (c) 2016 Alexandre Bury @@ -14422,10 +14440,10 @@ The above copyright notice and this permission notice shall be included in all c 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. -===== NOTICE 223 ===== +===== TEXT 223 ===== MIT or Apache-2.0 -===== NOTICE 224 ===== +===== TEXT 224 ===== The auto-generated bindings are under the 3-clause BSD license: BSD License @@ -14459,7 +14477,7 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -===== NOTICE 225 ===== +===== TEXT 225 ===== GNU GENERAL PUBLIC LICENSE Version 2, June 1991 @@ -14800,7 +14818,7 @@ consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. -===== NOTICE 226 ===== +===== TEXT 226 ===== BSD License For Zstandard software @@ -14831,3 +14849,94 @@ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +===== TEXT 227 ===== +[package] +name = "async-scoped" +version = "0.9.0" +authors = ["Rajsekar Manokaran "] +edition = "2018" + +documentation = "https://docs.rs/async-scoped" +description = "Spawn scoped (non 'static) asynchronous futures for async_std and tokio runtimes" +homepage = "https://github.com/rmanoka/async-scoped" +repository = "https://github.com/rmanoka/async-scoped" +readme = "README.md" + +categories = [ "asynchronous", "concurrency" ] +keywords = [ "async", "async-std", "tokio", "scoped", "spawn" ] +license = "Apache-2.0/MIT" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +===== TEXT 228 ===== +[package] +name = "backtrace-ext" +description = "minor conveniences on top of the backtrace crate" +version = "0.2.1" +edition = "2018" +license = "MIT OR Apache-2.0" +repository = "https://github.com/gankra/backtrace-ext" +rust-version = "1.56" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +===== TEXT 229 ===== +[package] +name = "guppy-workspace-hack" +version = "0.1.0" +description = "workspace-hack package, managed by hakari" +publish = true +license = "MIT OR Apache-2.0" +repository = "https://github.com/facebookincubator/cargo-guppy" + +# The parts of the file between the BEGIN HAKARI SECTION and END HAKARI SECTION comments +# are managed by hakari. + +### BEGIN HAKARI SECTION + +# Disabled by running `cargo hakari disable`. +# To re-enable, run: +# cargo hakari generate +### END HAKARI SECTION + +===== TEXT 230 ===== +[package] +name = "recursion" +version = "0.5.4" +edition = "2021" +description = "cache-aware stack safe recursion" +license = "MIT OR Apache-2.0" +repository = "https://github.com/inanna-malick/recursion" + +===== TEXT 231 ===== +[package] +name = "sigchld" +version = "0.2.4" +authors = ["Jack O'Connor"] +description = "waiting for SIGCHLD from many threads at once" +repository = "https://github.com/oconnor663/sigchld.rs" +readme = "README.md" +documentation = "https://docs.rs/sigchld" +license = "MIT" +edition = "2021" + +===== TEXT 232 ===== +MIT License + +Copyright (c) + +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. From 07d142a3931e7ab5ebd1289467cd2182a5b9ccaf Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Fri, 16 Oct 2026 03:24:43 +0000 Subject: [PATCH 45/49] Handle physical Cargo paths under aliased repository roots --- docs/design/cargo-facts.md | 7 ++ internal/rustfacts/compiler_output.go | 7 +- internal/rustfacts/metadata.go | 22 ++++-- internal/rustfacts/metadata_paths_test.go | 92 +++++++++++++++++++++++ 4 files changed, 118 insertions(+), 10 deletions(-) create mode 100644 internal/rustfacts/metadata_paths_test.go diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index 9a33786d..77b56b22 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -14,6 +14,13 @@ code nor proves that dependencies or any target configuration have been checked. The selected manifest and Cargo's actual workspace membership determine scope; directory naming is never a package identifier. +Cargo can report the physical checkout path while the caller uses an operating +system alias, such as macOS's `/var` or Windows short directory names. Input +containment recognizes both spellings of the repository root, then reads the +relative path through the existing contained regular-file boundary. Only the +root is resolved: links within source paths remain invalid, and absent inactive +remote targets retain their ordinary missing-file semantics. + Discovery retains each package's minimum Rust version as a fact. Cargo enforces compiler compatibility when the selected package/feature/target configuration is checked. An unselected member with a newer minimum version must not prevent diff --git a/internal/rustfacts/compiler_output.go b/internal/rustfacts/compiler_output.go index f259b301..3b991a8d 100644 --- a/internal/rustfacts/compiler_output.go +++ b/internal/rustfacts/compiler_output.go @@ -162,9 +162,12 @@ func validateCompilerTarget(repo repository.Repository, workspace Workspace, eve } func matchesDiscoveredTarget(repo repository.Repository, targets []Target, reported Target) bool { + path, err := containedPath(repo, reported.Path) + if err != nil { + return false + } return slices.ContainsFunc(targets, func(target Target) bool { - absolute := filepath.Join(repo.Root, filepath.FromSlash(target.Path)) - return reported.Name == target.Name && filepath.Clean(reported.Path) == absolute && slices.Equal(reported.Kinds, target.Kinds) && reported.Edition == target.Edition + return reported.Name == target.Name && path == target.Path && slices.Equal(reported.Kinds, target.Kinds) && reported.Edition == target.Edition }) } diff --git a/internal/rustfacts/metadata.go b/internal/rustfacts/metadata.go index 12de7c7a..325473da 100644 --- a/internal/rustfacts/metadata.go +++ b/internal/rustfacts/metadata.go @@ -9,7 +9,6 @@ import ( "io/fs" "path/filepath" "slices" - "strings" "github.com/riteofstring/code-polishy/internal/repository" "golang.org/x/mod/semver" @@ -233,13 +232,20 @@ func containedPath(repo repository.Repository, absolute string) (string, error) if !filepath.IsAbs(absolute) { return "", errors.New("cargo reported a non-absolute input path") } - relative, err := filepath.Rel(repo.Root, absolute) - if err != nil || relative == "." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) || relative == ".." { - return "", fmt.Errorf("cargo input %q escapes the repository", absolute) + physicalRoot, err := filepath.EvalSymlinks(repo.Root) + if err != nil { + return "", fmt.Errorf("cargo repository root: %w", err) } - path := filepath.ToSlash(relative) - if _, err := repo.ReadContainedFile(path, 8<<20); err != nil { - return path, fmt.Errorf("cargo input %q: %w", path, err) + for _, root := range []string{repo.Root, physicalRoot} { + relative, err := filepath.Rel(root, absolute) + if err != nil || relative == "." || !filepath.IsLocal(relative) { + continue + } + path := filepath.ToSlash(relative) + if _, err := repo.ReadContainedFile(path, 8<<20); err != nil { + return path, fmt.Errorf("cargo input %q: %w", path, err) + } + return path, nil } - return path, nil + return "", fmt.Errorf("cargo input %q escapes the repository", absolute) } diff --git a/internal/rustfacts/metadata_paths_test.go b/internal/rustfacts/metadata_paths_test.go new file mode 100644 index 00000000..115b9a03 --- /dev/null +++ b/internal/rustfacts/metadata_paths_test.go @@ -0,0 +1,92 @@ +package rustfacts + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestRustCompilationThroughRepositoryAlias(t *testing.T) { + t.Parallel() + repo := compilationRepository(t, "pub fn value() -> u32 { 42 }\n") + repo.Root = repositoryAlias(t, repo.Root) + plan, discovery := compilationDiscovery(t, repo, "src/lib.rs") + if findings := Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}); len(findings) != 0 { + t.Fatalf("compilation through root alias = %+v", findings) + } + write(t, repo.Root, "src/lib.rs", "pub fn value() -> u32 { \"wrong\" }\n") + findings := Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}) + if !slices.ContainsFunc(findings, func(finding policy.Finding) bool { + return finding.Check == "quality.rustCompiler" && finding.Subject == "E0308" && finding.Path == "src/lib.rs" && finding.Line == 1 + }) { + t.Fatalf("compiler diagnostic through root alias = %+v", findings) + } +} + +func TestCargoDiscoveryThroughRepositoryAlias(t *testing.T) { + t.Parallel() + repo := rustRepository(t) + write(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\nedition='2024'\n") + write(t, repo.Root, "src/lib.rs", "pub fn value() -> u32 { 42 }\n") + repo.Root = repositoryAlias(t, repo.Root) + plan := PlanDiscovery(repo, []string{"src/lib.rs"}, []string{"Cargo.toml", "src/lib.rs"}) + result := Discover(t.Context(), repo, plan, runner.OSRunner{}) + if len(result.Findings) != 0 || len(result.Workspaces) != 1 { + t.Fatalf("discovery through root alias = %+v", result) + } + pkg := result.Workspaces[0].Packages[0] + if pkg.Manifest != "Cargo.toml" || pkg.Targets[0].Path != "src/lib.rs" { + t.Fatalf("discovered source paths = %+v", pkg) + } +} + +func TestCargoInputContainmentThroughRepositoryAlias(t *testing.T) { + t.Parallel() + repo, _ := metadataFixture(t) + physical, err := filepath.EvalSymlinks(repo.Root) + if err != nil { + t.Fatal(err) + } + repo.Root = repositoryAlias(t, physical) + for _, root := range []string{repo.Root, physical} { + path, err := containedPath(repo, filepath.Join(root, "src", "lib.rs")) + if err != nil || path != "src/lib.rs" { + t.Fatalf("contained input under %s = %q, %v", root, path, err) + } + path, err = containedPath(repo, filepath.Join(root, "examples", "absent.rs")) + if path != "examples/absent.rs" || !errors.Is(err, fs.ErrNotExist) { + t.Fatalf("absent input under %s = %q, %v", root, path, err) + } + } + outside := t.TempDir() + write(t, outside, "lib.rs", "pub fn outside() {}\n") + if _, err := containedPath(repo, filepath.Join(outside, "lib.rs")); err == nil { + t.Fatal("outside input was accepted") + } + for name, target := range map[string]string{"internal": filepath.Join(physical, "src"), "external": outside} { + if err := os.Symlink(target, filepath.Join(physical, name)); err != nil { + t.Fatal(err) + } + for _, root := range []string{repo.Root, physical} { + if _, err := containedPath(repo, filepath.Join(root, name, "lib.rs")); err == nil || !strings.Contains(err.Error(), "symbolic link") { + t.Fatalf("linked input under %s: %v", root, err) + } + } + } +} + +func repositoryAlias(t *testing.T, root string) string { + t.Helper() + alias := filepath.Join(t.TempDir(), "checkout") + if err := os.Symlink(filepath.Dir(root), alias); err != nil { + t.Skipf("symlinks unavailable: %v", err) + } + return filepath.Join(alias, filepath.Base(root)) +} From da4ad81afaca4160346a98116c9ec48a4e120f43 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Fri, 16 Oct 2026 04:23:36 +0000 Subject: [PATCH 46/49] Fix native temporary directories and aliased OSV scopes --- docs/design/execution-observability.md | 12 +++++ internal/runner/runner.go | 2 + internal/runner/sealed_environment_test.go | 40 ++++++++++++++++ .../runner/sealed_environment_windows_test.go | 46 +++++++++++++++++++ internal/supplychain/osv.go | 22 +++++++-- .../supplychain/osv_cargo_runtime_test.go | 17 +++++++ internal/testing/rust_doctest_test.go | 16 +++---- internal/testing/rust_test.go | 33 ++++++++----- internal/testing/rust_workspace_test.go | 10 ++-- 9 files changed, 168 insertions(+), 30 deletions(-) create mode 100644 internal/runner/sealed_environment_test.go create mode 100644 internal/runner/sealed_environment_windows_test.go diff --git a/docs/design/execution-observability.md b/docs/design/execution-observability.md index 00f910fa..f1ca173e 100644 --- a/docs/design/execution-observability.md +++ b/docs/design/execution-observability.md @@ -33,6 +33,12 @@ a normal test-failure code. A partial stream cannot establish native outcome evidence or a regression-proof red result. Cancellation and timeout retain their more specific failure categories. +Sealed commands receive one private writable temporary directory through +`TMPDIR`, `TMP`, and `TEMP`. Windows native tools use `TMP` or `TEMP` rather than +`TMPDIR`; omitting them can send compiler scratch files to the Windows directory. +The runner removes the private directory after the command and its descendants +finish. Ambient temporary-directory settings are not required for native tools. + Repository-wide Python dead-code analysis runs only for an explicit complete selection or merge gate. Focused Python checks run selected Ruff and ty work; they do not construct global reachability evidence that cannot establish a @@ -79,6 +85,12 @@ Standalone scanner calls use the same exit conditions. A timeout or operational failure remains a scanner finding even when an advisory was emitted before the failure; assessing that advisory cannot erase the failed scan. +Scanner source paths can use either the caller's repository-root spelling or +its physical path. Scope normalization resolves only the repository root and +still rejects paths outside it. Projected scans require the exact planned input +path before attributing findings to the original lockfile; a root alias does not +authorize another projection or source. + Vulnerability reports identify the actual owner for every assessment, including agent-owned decisions, without implying human sign-off. Assessed findings stay visible with their disposition and expiry. See diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 717233b9..26afd192 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -316,6 +316,8 @@ func commandEnvironment(specification policy.Command, pathEntries []string) ([]s "PATH=", "HOME=" + home, "TMPDIR=" + temporary, + "TMP=" + temporary, + "TEMP=" + temporary, "XDG_CONFIG_HOME=" + filepath.Join(home, "config"), "XDG_CACHE_HOME=" + filepath.Join(home, "cache"), "XDG_DATA_HOME=" + filepath.Join(home, "data"), diff --git a/internal/runner/sealed_environment_test.go b/internal/runner/sealed_environment_test.go new file mode 100644 index 00000000..5809d2ff --- /dev/null +++ b/internal/runner/sealed_environment_test.go @@ -0,0 +1,40 @@ +package runner + +import ( + "os" + "strings" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" +) + +func TestSealedEnvironmentProvidesWritableTemporaryDirectories(t *testing.T) { + t.Parallel() + environment, cleanup, err := commandEnvironment(policy.Command{SealedEnvironment: true}, nil) + if err != nil { + t.Fatal(err) + } + defer cleanup() + values := map[string]string{} + for _, entry := range environment { + key, value, _ := strings.Cut(entry, "=") + values[key] = value + } + for _, key := range []string{"TMPDIR", "TMP", "TEMP"} { + path := values[key] + if path == "" || path == os.TempDir() { + t.Fatalf("%s does not select a private temporary directory: %q", key, path) + } + file, err := os.CreateTemp(path, "probe-") + if err != nil { + t.Fatal(err) + } + if err := file.Close(); err != nil { + t.Fatal(err) + } + } + cleanup() + if _, err := os.Stat(values["TEMP"]); !os.IsNotExist(err) { + t.Fatalf("temporary directory survived cleanup: %v", err) + } +} diff --git a/internal/runner/sealed_environment_windows_test.go b/internal/runner/sealed_environment_windows_test.go new file mode 100644 index 00000000..a7d3f8d7 --- /dev/null +++ b/internal/runner/sealed_environment_windows_test.go @@ -0,0 +1,46 @@ +package runner + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" +) + +func TestWindowsSealedTemporaryDirectory(t *testing.T) { + t.Parallel() + result, output, err := (OSRunner{}).RunWithOutput(t.Context(), t.TempDir(), policy.Command{ + Name: "private-temporary-directory", Argv: []string{os.Args[0], "-test.run=^TestWindowsSealedTemporaryDirectoryHelper$"}, + SealedEnvironment: true, TimeoutSeconds: 30, + EnvironmentOverrides: []string{"CODE_POLISHY_TEMP_PROBE=1"}, + }) + if err != nil || result.ExitStatus != 0 { + t.Fatalf("sealed temporary directory: %+v, %v; %s%s", result, err, output.Stdout, output.Stderr) + } + if !filepath.IsAbs(string(output.Stdout)) { + t.Fatalf("temporary-file probe returned no absolute path: %q", output.Stdout) + } + if _, err := os.Stat(string(output.Stdout)); !os.IsNotExist(err) { + t.Fatalf("private temporary file survived command cleanup: %q, %v", output.Stdout, err) + } +} + +func TestWindowsSealedTemporaryDirectoryHelper(t *testing.T) { + if os.Getenv("CODE_POLISHY_TEMP_PROBE") != "1" { + return + } + if filepath.Clean(os.TempDir()) != filepath.Clean(os.Getenv("TMPDIR")) { + t.Fatalf("Windows temporary directory %q differs from private directory %q", os.TempDir(), os.Getenv("TMPDIR")) + } + file, err := os.CreateTemp("", "native-probe-") + if err != nil { + t.Fatal(err) + } + if err := file.Close(); err != nil { + t.Fatal(err) + } + fmt.Fprint(os.Stdout, file.Name()) + os.Exit(0) +} diff --git a/internal/supplychain/osv.go b/internal/supplychain/osv.go index c64b062f..a53fdcb9 100644 --- a/internal/supplychain/osv.go +++ b/internal/supplychain/osv.go @@ -376,17 +376,29 @@ func osvScope(repo repository.Repository, root, source string) (string, error) { } path := filepath.Clean(source) if filepath.IsAbs(path) { - relative, err := filepath.Rel(repo.Root, path) - if err != nil { - return "", err - } - path = relative + return osvAbsoluteScope(repo, path) } else if root != "." { path = filepath.Join(filepath.FromSlash(root), path) } return repo.NormalizePath(path) } +func osvAbsoluteScope(repo repository.Repository, path string) (string, error) { + relative, err := filepath.Rel(repo.Root, path) + if err == nil && filepath.IsLocal(relative) { + return repo.NormalizePath(relative) + } + physicalRoot, err := filepath.EvalSymlinks(repo.Root) + if err != nil { + return "", err + } + relative, err = filepath.Rel(physicalRoot, path) + if err != nil { + return "", err + } + return repo.NormalizePath(relative) +} + func osvPackageFindings(scope string, observed osvPackageResult) []policy.Finding { if observed.Package.Name == "" || observed.Package.Version == "" { return []policy.Finding{{ diff --git a/internal/supplychain/osv_cargo_runtime_test.go b/internal/supplychain/osv_cargo_runtime_test.go index 88eac4fc..c0ef51a5 100644 --- a/internal/supplychain/osv_cargo_runtime_test.go +++ b/internal/supplychain/osv_cargo_runtime_test.go @@ -12,7 +12,24 @@ import ( func TestOSVCargoPinnedRuntimeUsesCompleteOfflineProjection(t *testing.T) { t.Parallel() + testOSVCargoPinnedRuntimeProjection(t, false) +} + +func TestOSVCargoPinnedRuntimeThroughRepositoryAlias(t *testing.T) { + t.Parallel() + testOSVCargoPinnedRuntimeProjection(t, true) +} + +func testOSVCargoPinnedRuntimeProjection(t *testing.T, aliased bool) { + t.Helper() repo := cargoOSVRepository(t, cargoOSVMixedLock()) + if aliased { + alias := filepath.Join(t.TempDir(), "parent") + if err := os.Symlink(filepath.Dir(repo.Root), alias); err != nil { + t.Skipf("symlinks unavailable: %v", err) + } + repo.Root = filepath.Join(alias, filepath.Base(repo.Root)) + } root, err := filepath.Abs("../..") if err != nil { t.Fatal(err) diff --git a/internal/testing/rust_doctest_test.go b/internal/testing/rust_doctest_test.go index 9845594b..033274dc 100644 --- a/internal/testing/rust_doctest_test.go +++ b/internal/testing/rust_doctest_test.go @@ -27,7 +27,7 @@ func TestNativeRustDoctestsRespectExampleSemantics(t *testing.T) { if err != nil { t.Fatal(err) } - execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + execution := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 1) if execution.Failed() || len(execution.Artifacts) != 1 || execution.Artifacts[0].Type != policy.RustdocArtifact { t.Fatalf("doctest execution: %+v", execution) } @@ -59,7 +59,7 @@ func TestNativeRustDoctestsKeepFailuresDistinct(t *testing.T) { if test.build != "" { writeRustSuiteFile(t, repo.Root, "build.rs", test.build) } - execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + execution := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 1) if !execution.Failed() || execution.FailureCategory != test.category || (len(execution.Artifacts) == 1) != test.artifact { t.Fatalf("doctest classification: %+v", execution) } @@ -74,16 +74,16 @@ func TestNativeRustDoctestsHonorFeaturesAndCancellation(t *testing.T) { suite.Rust.Doctests = true suite.Rust.Features = []string{"chosen"} suite.Rust.NoDefaultFeatures = true - if execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1); execution.Failed() { + if execution := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 1); execution.Failed() { t.Fatalf("doctest features: %+v", execution) } ctx, cancel := context.WithCancel(t.Context()) cancel() - if execution := ExecuteSuite(ctx, repo, runner.OSRunner{}, suite, 2); execution.FailureCategory != runner.FailureCanceled { + if execution := ExecuteSuite(ctx, repo, rustSuiteRunner(t), suite, 2); execution.FailureCategory != runner.FailureCanceled { t.Fatalf("doctest cancellation: %+v", execution) } writeRustSuiteFile(t, repo.Root, "Cargo.lock", "version=4\n") - if execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 3); execution.FailureCategory != runner.FailureOperational { + if execution := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 3); execution.FailureCategory != runner.FailureOperational { t.Fatalf("doctest stale lock: %+v", execution) } data, err := os.ReadFile(filepath.Join(repo.Root, "Cargo.lock")) @@ -102,15 +102,15 @@ func TestNativeRustDoctestsKeepWorkspaceBatches(t *testing.T) { writeRustSuiteFile(t, repo.Root, name+"/src/lib.rs", "/// ```\n/// assert_eq!(2 + 2, 4);\n/// ```\npub fn answer() {}\n") } suite.Rust.Doctests = true - if execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1); execution.Failed() { + if execution := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 1); execution.Failed() { t.Fatalf("workspace doctests: %+v", execution) } writeRustSuiteFile(t, repo.Root, "second/src/lib.rs", "/// ```\n/// assert_eq!(2 + 2, 5);\n/// ```\npub fn answer() {}\n") - if execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 2); execution.FailureCategory != runner.FailureCommandExit { + if execution := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 2); execution.FailureCategory != runner.FailureCommandExit { t.Fatalf("workspace doctest failure: %+v", execution) } suite.Rust.Packages = []string{"first"} - if execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 3); execution.Failed() { + if execution := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 3); execution.Failed() { t.Fatalf("selected package doctests: %+v", execution) } plan := ExecutionPlanForDirectTest(Plan{Suites: []policy.TestSuite{suite}}) diff --git a/internal/testing/rust_test.go b/internal/testing/rust_test.go index 88c14779..fa7656c1 100644 --- a/internal/testing/rust_test.go +++ b/internal/testing/rust_test.go @@ -21,7 +21,7 @@ func TestNativeRustSuiteProducesNonemptyEvidence(t *testing.T) { if err != nil { t.Fatal(err) } - execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + execution := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 1) if execution.Failed() || len(execution.Artifacts) != 1 || execution.Artifacts[0].Type != policy.NextestJUnitArtifact || execution.Artifacts[0].SHA256 == "" { t.Fatalf("native Rust execution: %+v", execution) } @@ -50,7 +50,7 @@ func TestNativeRustSuiteDistinguishesTestAndOperationalFailure(t *testing.T) { if test.build != "" { writeRustSuiteFile(t, repo.Root, "build.rs", test.build) } - execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + execution := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 1) if !execution.Failed() || execution.FailureCategory != test.category || (len(execution.Artifacts) == 1) != test.artifact { t.Fatalf("failure classification: %+v", execution) } @@ -65,12 +65,12 @@ func TestNativeRustSuiteHonorsCargoFeaturesAndFilter(t *testing.T) { suite.Rust.Features = []string{"chosen"} suite.Rust.NoDefaultFeatures = true suite.Rust.Filter = "test(=selected)" - execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + execution := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 1) if execution.Failed() || len(execution.Artifacts) != 1 { t.Fatalf("Cargo feature/filter selection: %+v", execution) } suite.Rust.Filter = "test(=absent)" - empty := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 2) + empty := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 2) if !empty.Failed() || empty.FailureCategory != runner.FailureOperational { t.Fatalf("empty filter was accepted: %+v", empty) } @@ -85,18 +85,18 @@ func TestNativeRustSuiteUsesWorkspaceDefaultsAndExplicitPackages(t *testing.T) { writeRustSuiteFile(t, repo.Root, "crates/service/Cargo.toml", "[package]\nname='service'\nversion='1.0.0'\nedition='2024'\n") writeRustSuiteFile(t, repo.Root, "crates/core/src/lib.rs", "#[test]\nfn behavior() { assert_eq!(2 + 2, 4); }\n") writeRustSuiteFile(t, repo.Root, "crates/service/src/lib.rs", "#[test]\nfn behavior() { assert_eq!(2 + 2, 5); }\n") - defaultRun := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + defaultRun := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 1) if defaultRun.Failed() { t.Fatalf("unselected workspace member was tested: %+v", defaultRun) } suite.Rust.Packages = []string{"service"} - explicit := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 2) + explicit := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 2) if !explicit.Failed() || explicit.FailureCategory != runner.FailureCommandExit { t.Fatalf("explicit package was not tested: %+v", explicit) } suite.Rust.Packages = []string{"core"} writeRustSuiteFile(t, repo.Root, "crates/core/tests/public.rs", "#[test]\nfn public_behavior() { assert_eq!(2 + 2, 5); }\n") - integration := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 3) + integration := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 3) if !integration.Failed() || integration.FailureCategory != runner.FailureCommandExit { t.Fatalf("integration target was not tested: %+v", integration) } @@ -106,7 +106,7 @@ func TestNativeRustSuiteNeverRepairsLocksOrIgnoresProjectSettings(t *testing.T) t.Parallel() repo, suite := rustSuiteRepository(t, "#[test]\nfn behavior() { assert_eq!(2 + 2, 4); }\n") writeRustSuiteFile(t, repo.Root, "Cargo.lock", "version = 4\n") - execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + execution := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 1) if !execution.Failed() || execution.FailureCategory != runner.FailureOperational { t.Fatalf("stale lock was accepted: %+v", execution) } @@ -115,7 +115,7 @@ func TestNativeRustSuiteNeverRepairsLocksOrIgnoresProjectSettings(t *testing.T) t.Fatalf("stale lock was changed: %q, %v", lock, err) } writeRustSuiteFile(t, repo.Root, ".config/nextest.toml", "[profile.default]\ndefault-filter='not test(live_probe)'\n") - preflight := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 2) + preflight := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 2) if !strings.Contains(preflight.FailureMessage, "cannot silently replace") { t.Fatalf("project settings were ignored: %+v", preflight) } @@ -128,7 +128,7 @@ func TestNativeRustSuiteFindsExplicitWorkspaceNextestSettings(t *testing.T) { writeRustSuiteFile(t, repo.Root, "workspace/Cargo.toml", "[workspace]\nmembers=['../crates/app']\nresolver='3'\n") writeRustSuiteFile(t, repo.Root, "workspace/.config/nextest.toml", "[profile.default]\ndefault-filter='not test(live_probe)'\n") suite.Rust.Manifest = "crates/app/Cargo.toml" - execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + execution := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 1) if !execution.Failed() || !strings.Contains(execution.FailureMessage, "workspace/.config/nextest.toml") { t.Fatalf("explicit workspace configuration was ignored: %+v", execution) } @@ -154,7 +154,7 @@ func TestNativeRustCommandAndCopiesRetainGovernedSelection(t *testing.T) { } ctx, cancel := context.WithCancel(t.Context()) cancel() - execution := ExecuteSuite(ctx, repo, runner.OSRunner{}, suite, 1) + execution := ExecuteSuite(ctx, repo, rustSuiteRunner(t), suite, 1) if !execution.Failed() || execution.FailureCategory != runner.FailureCanceled { t.Fatalf("canceled test became another outcome: %+v", execution) } @@ -184,3 +184,14 @@ func writeRustSuiteFile(t *testing.T, root, name, content string) { t.Fatal(err) } } + +func rustSuiteRunner(t *testing.T) runner.OSRunner { + t.Helper() + stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{} + t.Cleanup(func() { + if t.Failed() { + t.Logf("Rust command stdout:\n%s\nstderr:\n%s", stdout, stderr) + } + }) + return runner.OSRunner{Stdout: stdout, Stderr: stderr} +} diff --git a/internal/testing/rust_workspace_test.go b/internal/testing/rust_workspace_test.go index bc849db9..5c17883b 100644 --- a/internal/testing/rust_workspace_test.go +++ b/internal/testing/rust_workspace_test.go @@ -5,8 +5,6 @@ import ( "path/filepath" "strings" "testing" - - "github.com/riteofstring/code-polishy/internal/runner" ) func TestNativeRustRejectsExternalWorkspaceBeforeCompilation(t *testing.T) { @@ -25,7 +23,7 @@ func TestNativeRustRejectsExternalWorkspaceBeforeCompilation(t *testing.T) { writeRustSuiteFile(t, parent, "Cargo.lock", "version=4\n[[package]]\nname='app'\nversion='1.0.0'\n") writeRustSuiteFile(t, root, "build.rs", "fn main() { std::fs::write(\"build-started\", b\"ran\").unwrap(); }\n") for attempt := 1; attempt <= 2; attempt++ { - execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, attempt) + execution := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, attempt) if !execution.Failed() || !strings.Contains(execution.FailureMessage, "workspace root") { t.Fatalf("external workspace accepted for doctests=%v attempt=%d: %+v", doctests, attempt, execution) } @@ -50,7 +48,7 @@ func TestNativeRustRejectsLinkedWorkspaceLockBeforeCompilation(t *testing.T) { t.Skipf("symlink creation unavailable: %v", err) } writeRustSuiteFile(t, repo.Root, "build.rs", "fn main() { std::fs::write(\"build-started\", b\"ran\").unwrap(); }\n") - execution := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 2) + execution := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 2) if !execution.Failed() || !strings.Contains(execution.FailureMessage, "contained Cargo.lock") { t.Fatalf("linked lock accepted for doctests=%v: %+v", doctests, execution) } @@ -69,12 +67,12 @@ func TestNativeRustUsesActualExplicitWorkspaceLock(t *testing.T) { writeRustSuiteFile(t, repo.Root, "crates/app/src/lib.rs", "#[test]\nfn behavior() { assert_eq!(2 + 2, 4); }\n") lock := "version=4\n[[package]]\nname='app'\nversion='1.0.0'\n" writeRustSuiteFile(t, repo.Root, "crates/app/Cargo.lock", lock) - missing := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 1) + missing := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 1) if !missing.Failed() || !strings.Contains(missing.FailureMessage, "contained Cargo.lock") { t.Fatalf("unrelated root or member lock replaced the workspace lock: %+v", missing) } writeRustSuiteFile(t, repo.Root, "workspace/Cargo.lock", lock) - if recovered := ExecuteSuite(t.Context(), repo, runner.OSRunner{}, suite, 2); recovered.Failed() { + if recovered := ExecuteSuite(t.Context(), repo, rustSuiteRunner(t), suite, 2); recovered.Failed() { t.Fatalf("valid explicit workspace failed: %+v", recovered) } } From 7acd1f5fcdcf079e4d934389b54f9b0cafbefbd3 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:07:33 +0000 Subject: [PATCH 47/49] Preserve Windows SDK discovery in sealed Rust commands Use native environment-name lookup and retain Visual Studio installation roots. Focused runner, rustfacts, and testing suites and source checks pass; Windows runner tests cross-compile. Native Windows CI remains required before release readiness. --- docs/design/cargo-facts.md | 5 +++++ docs/design/execution-observability.md | 4 ++++ internal/runner/runner.go | 7 +++---- internal/runner/sealed_environment_test.go | 15 +++++++++++++++ .../runner/sealed_environment_windows_test.go | 16 ++++++++++++++++ internal/rustfacts/execution.go | 2 +- 6 files changed, 44 insertions(+), 5 deletions(-) diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index 77b56b22..aa359b1a 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -282,6 +282,11 @@ scripts and procedural macros may execute during checking. Their output directories, emitted configuration and native linkage are compiler evidence, not a claim of filesystem or network sandboxing. Native SDK failures remain ordinary failed checks; the Rust distribution does not supply every native SDK. +Windows commands retain the standard `ProgramFiles` and `ProgramFiles(x86)` +installation roots so rustc can discover Visual Studio through its native +discovery mechanism, including `vswhere.exe`. Without those roots, discovery can +fail and leave an unrelated `link.exe` on PATH selected as the linker. This does +not admit ambient Rust compiler selection or Cargo configuration. Structured success requires a final Cargo outcome and a compiled target for every selected package. An empty stream, unrecognized output, partial compilation diff --git a/docs/design/execution-observability.md b/docs/design/execution-observability.md index f1ca173e..c25f6ac5 100644 --- a/docs/design/execution-observability.md +++ b/docs/design/execution-observability.md @@ -39,6 +39,10 @@ Sealed commands receive one private writable temporary directory through The runner removes the private directory after the command and its descendants finish. Ambient temporary-directory settings are not required for native tools. +Explicit environment selection uses operating-system name lookup: Windows names +are case-insensitive, while Unix names remain case-sensitive. Only requested +names are inherited, including values explicitly set to an empty string. + Repository-wide Python dead-code analysis runs only for an explicit complete selection or merge gate. Focused Python checks run selected Ruff and ty work; they do not construct global reachability evidence that cannot establish a diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 26afd192..3ee79289 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -380,10 +380,9 @@ func asciiDigit(character rune) bool { func explicitlySelectedEnvironment(requested []string) []string { environment := []string{} - for _, entry := range os.Environ() { - name, _, found := strings.Cut(entry, "=") - if found && slices.Contains(requested, name) { - environment = append(environment, entry) + for _, name := range requested { + if value, found := os.LookupEnv(name); found { + environment = append(environment, name+"="+value) } } return environment diff --git a/internal/runner/sealed_environment_test.go b/internal/runner/sealed_environment_test.go index 5809d2ff..e83ee227 100644 --- a/internal/runner/sealed_environment_test.go +++ b/internal/runner/sealed_environment_test.go @@ -2,12 +2,27 @@ package runner import ( "os" + "runtime" + "slices" "strings" "testing" "github.com/riteofstring/code-polishy/internal/policy" ) +func TestSelectedEnvironmentRetainsNativeNameSemantics(t *testing.T) { + t.Setenv("Code_Polishy_Selected", "sdk-root") + t.Setenv("CODE_POLISHY_EMPTY", "") + t.Setenv("CODE_POLISHY_UNSELECTED", "not-inherited") + environment := explicitlySelectedEnvironment([]string{"CODE_POLISHY_SELECTED", "CODE_POLISHY_EMPTY"}) + if slices.Contains(environment, "CODE_POLISHY_SELECTED=sdk-root") != (runtime.GOOS == "windows") { + t.Fatalf("incorrect native environment-name lookup: %v", environment) + } + if !slices.Contains(environment, "CODE_POLISHY_EMPTY=") || slices.Contains(environment, "CODE_POLISHY_UNSELECTED=not-inherited") { + t.Fatalf("selected environment lost an empty value or inherited an unselected value: %v", environment) + } +} + func TestSealedEnvironmentProvidesWritableTemporaryDirectories(t *testing.T) { t.Parallel() environment, cleanup, err := commandEnvironment(policy.Command{SealedEnvironment: true}, nil) diff --git a/internal/runner/sealed_environment_windows_test.go b/internal/runner/sealed_environment_windows_test.go index a7d3f8d7..f3837678 100644 --- a/internal/runner/sealed_environment_windows_test.go +++ b/internal/runner/sealed_environment_windows_test.go @@ -4,11 +4,27 @@ import ( "fmt" "os" "path/filepath" + "slices" "testing" "github.com/riteofstring/code-polishy/internal/policy" ) +func TestWindowsSealedEnvironmentUsesNativeNameLookup(t *testing.T) { + t.Setenv("Code_Polishy_Selected", "sdk-root") + t.Setenv("CODE_POLISHY_UNSELECTED", "not-inherited") + environment, cleanup, err := commandEnvironment(policy.Command{ + SealedEnvironment: true, Environment: []string{"CODE_POLISHY_SELECTED"}, + }, nil) + if err != nil { + t.Fatal(err) + } + defer cleanup() + if !slices.Contains(environment, "CODE_POLISHY_SELECTED=sdk-root") || slices.Contains(environment, "CODE_POLISHY_UNSELECTED=not-inherited") { + t.Fatalf("selected Windows environment: %v", environment) + } +} + func TestWindowsSealedTemporaryDirectory(t *testing.T) { t.Parallel() result, output, err := (OSRunner{}).RunWithOutput(t.Context(), t.TempDir(), policy.Command{ diff --git a/internal/rustfacts/execution.go b/internal/rustfacts/execution.go index 1d1fa398..7c591d3a 100644 --- a/internal/rustfacts/execution.go +++ b/internal/rustfacts/execution.go @@ -25,7 +25,7 @@ func Command(repo repository.Repository, name, tool string, arguments ...string) return policy.Command{ Name: name, Argv: append([]string{repo.RustTool(tool)}, arguments...), Cwd: ".", Managed: true, SealedEnvironment: true, TimeoutSeconds: 300, - Environment: []string{"SYSTEMROOT", "WINDIR"}, + Environment: []string{"SYSTEMROOT", "WINDIR", "ProgramFiles", "ProgramFiles(x86)"}, EnvironmentOverrides: []string{ "RUSTC=" + repo.RustTool("rustc"), "RUSTDOC=" + repo.RustTool("rustdoc"), "CARGO_HOME=" + CargoHome(repo), "CARGO_TARGET_DIR=" + TargetDirectory(repo), From cab809f1ad3d4214f5ab347cf8810e6001c5496c Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:29:11 +0000 Subject: [PATCH 48/49] Initialize the native Windows SDK before Rust CI execution Enter Microsoft's x64 developer shell and verify the selected linker before native contracts and release smoke tests. Document the Windows developer-shell prerequisite. Formatting, source checks, and workflow-unit pass; native Windows execution remains pending. --- .github/workflows/ci.yml | 1 + .github/workflows/release.yml | 1 + docs/design/cargo-facts.md | 4 ++++ docs/installation.md | 5 ++++- tools/initialize-windows-sdk.ps1 | 20 ++++++++++++++++++++ 5 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 tools/initialize-windows-sdk.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1ce9a7c..5a1ab62a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -206,6 +206,7 @@ jobs: shell: pwsh run: | $ErrorActionPreference = 'Stop' + .\tools\initialize-windows-sdk.ps1 $go = '.tools\go\windows-amd64\go\bin\go.exe' & $go test -run '^$' ./... if ($LASTEXITCODE -ne 0) { throw 'Go package compilation failed.' } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 17dc40df..b097a295 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -205,6 +205,7 @@ jobs: throw "The runner is not the requested $env:RELEASE_HOST native host." } .\tools\install-policy-tools.ps1 + .\tools\initialize-windows-sdk.ps1 $publicationDir = Join-Path $env:RUNNER_TEMP "publication\$env:RELEASE_HOST" $rawArchive = Join-Path $env:RUNNER_TEMP 'code-polishy-release.zip' .\scripts\build-release.ps1 -Output $rawArchive -PublicationDirectory $publicationDir diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index aa359b1a..ea567c83 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -287,6 +287,10 @@ installation roots so rustc can discover Visual Studio through its native discovery mechanism, including `vswhere.exe`. Without those roots, discovery can fail and leave an unrelated `link.exe` on PATH selected as the linker. This does not admit ambient Rust compiler selection or Cargo configuration. +Native Windows CI and release smoke tests explicitly initialize Microsoft's x64 +developer shell and verify that the active linker belongs to the selected Visual +Studio installation. The SDK's existing PATH and library inputs reach sealed +Rust commands; automatic discovery alone is not a supported CI prerequisite. Structured success requires a final Cargo outcome and a compiled target for every selected package. An empty stream, unrecognized output, partial compilation diff --git a/docs/installation.md b/docs/installation.md index 249f8403..c78cfc99 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -455,7 +455,10 @@ not use a target `.venv` or target Python installation for either package. Rust target builds also need the host's native linker and SDK: a C toolchain on Linux, Xcode command-line tools on macOS, or the Visual Studio C++ build tools -and Windows SDK for MSVC. These host prerequisites are separate from the carried +and Windows SDK for MSVC. Run Windows Rust checks from an x64 Visual Studio +Developer PowerShell session so Microsoft's linker and SDK libraries are active. +A regular shell can select an unrelated `link.exe` supplied by Unix tools. +These host prerequisites are separate from the carried Rust SDK; Code Polishy does not install system development packages. See the [Rust platform prerequisites](https://doc.rust-lang.org/book/ch01-01-installation.html). diff --git a/tools/initialize-windows-sdk.ps1 b/tools/initialize-windows-sdk.ps1 new file mode 100644 index 00000000..119c19d6 --- /dev/null +++ b/tools/initialize-windows-sdk.ps1 @@ -0,0 +1,20 @@ +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' +$installations = @(& $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath) +if ($LASTEXITCODE -ne 0 -or $installations.Count -ne 1 -or -not $installations[0]) { + throw 'A Visual Studio installation with the C++ x64 build tools is required.' +} +$installation = [System.IO.Path]::GetFullPath($installations[0]) +$developerShell = Join-Path $installation 'Common7\Tools\Launch-VsDevShell.ps1' +& $developerShell -Arch amd64 -HostArch amd64 -SkipAutomaticLocation +if (-not $env:LIB -or -not $env:INCLUDE) { + throw 'The Visual Studio developer shell did not initialize the native SDK libraries and headers.' +} +$linker = (Get-Command link.exe -CommandType Application -ErrorAction Stop).Source +$installationPrefix = $installation.TrimEnd('\') + '\' +if (-not $linker.StartsWith($installationPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "The active linker is outside the selected Visual Studio installation: $linker" +} +Write-Output "Initialized Windows x64 SDK with linker $linker" From a182a3a50814070151756b3a1406378cfd323d55 Mon Sep 17 00:00:00 2001 From: Jeff Musgrave <1578618+JeffMusgrave@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:09:00 +0000 Subject: [PATCH 49/49] Keep core task sessions independent of optional Rust tools Associate Nextest with the installed Rust SDK inventory and retain rejection of incomplete companions. Remove superseded ProgramFiles discovery inputs that violate gate environment validation. Engine, repository, and rustfacts suites pass; source checks and formatting pass. Native CI and the final candidate gate remain pending. --- docs/design/cargo-facts.md | 5 --- docs/design/optional-rust-tools.md | 6 +++ internal/engine/governed_environment_test.go | 39 ++++++++++++++++++++ internal/repository/repository.go | 2 +- internal/rustfacts/execution.go | 2 +- 5 files changed, 47 insertions(+), 7 deletions(-) diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index ea567c83..df8d8665 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -282,11 +282,6 @@ scripts and procedural macros may execute during checking. Their output directories, emitted configuration and native linkage are compiler evidence, not a claim of filesystem or network sandboxing. Native SDK failures remain ordinary failed checks; the Rust distribution does not supply every native SDK. -Windows commands retain the standard `ProgramFiles` and `ProgramFiles(x86)` -installation roots so rustc can discover Visual Studio through its native -discovery mechanism, including `vswhere.exe`. Without those roots, discovery can -fail and leave an unrelated `link.exe` on PATH selected as the linker. This does -not admit ambient Rust compiler selection or Cargo configuration. Native Windows CI and release smoke tests explicitly initialize Microsoft's x64 developer shell and verify that the active linker belongs to the selected Visual Studio installation. The SDK's existing PATH and library inputs reach sealed diff --git a/docs/design/optional-rust-tools.md b/docs/design/optional-rust-tools.md index 4570fa15..c2a2106d 100644 --- a/docs/design/optional-rust-tools.md +++ b/docs/design/optional-rust-tools.md @@ -14,6 +14,12 @@ the setup command. Configuration cannot disable Rust recognition, select an ambient compiler, change the approved tool versions, or substitute an arbitrary successful command for native evidence. Unsupported semantics remain failures. +Task-session environment receipts include Nextest with the installed Rust SDK, +not with the core tools that share its executable directory. A core-only +installation can start a task session without Rust. Once the SDK is present, +the receipt requires its complete compiler and Nextest inventory. Rust checks +retain their separate tool-integrity validation before execution. + The authenticated core release owns the exact Rust tool inventory, versions, host, and file identities. Its default archive omits the corresponding tool payload; a companion archive carries it. Installing that archive validates its diff --git a/internal/engine/governed_environment_test.go b/internal/engine/governed_environment_test.go index 8287b99a..b41b36a9 100644 --- a/internal/engine/governed_environment_test.go +++ b/internal/engine/governed_environment_test.go @@ -5,8 +5,47 @@ import ( "path/filepath" "strings" "testing" + + "github.com/riteofstring/code-polishy/internal/repository" ) +func TestGovernedEnvironmentKeepsRustCompanionOptionalAndComplete(t *testing.T) { + root := contentRepository(t, nil) + repo := repository.Repository{Root: root, PolicyRoot: root} + writeEngineFile(t, root, "tools/cargo-nextest-version.txt", "0.9.138\n", 0o600) + writeEngineFile(t, root, "tools/rust-version.txt", "1.97.1\n", 0o600) + if err := os.MkdirAll(filepath.Dir(repo.PolicyTool("cargo-nextest")), 0o700); err != nil { + t.Fatal(err) + } + if _, err := FreezeGovernedEnvironment(repo); err != nil { + t.Fatalf("core-only task environment requires Rust: %v", err) + } + for _, name := range []string{"cargo", "rustc", "rustdoc", "rustfmt", "cargo-fmt", "cargo-clippy", "clippy-driver"} { + path, err := filepath.Rel(root, repo.RustTool(name)) + if err != nil { + t.Fatal(err) + } + writeEngineFile(t, root, path, "tool", 0o700) + } + if _, err := FreezeGovernedEnvironment(repo); err == nil || !strings.Contains(err.Error(), `required locked tool "cargo-nextest"`) { + t.Fatalf("partial Rust companion was accepted: %v", err) + } + if err := os.WriteFile(repo.PolicyTool("cargo-nextest"), []byte("tool"), 0o700); err != nil { + t.Fatal(err) + } + receipt, err := FreezeGovernedEnvironment(repo) + if err != nil { + t.Fatal(err) + } + data, err := MarshalGovernedEnvironment(receipt) + if err != nil { + t.Fatal(err) + } + if _, err := ParseGovernedEnvironment(data); err != nil { + t.Fatalf("complete Rust companion receipt: %v", err) + } +} + func TestGovernedEnvironmentFailsClosedWhenTheLockedToolIsMissing(t *testing.T) { root := contentRepository(t, nil) policyEngine, err := Open(root, root, "") diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 055e669a..1bc5a8d6 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -495,12 +495,12 @@ func (repo Repository) CommandEnvironment() GovernedEnvironment { {Name: "osv-scanner", Path: repo.PolicyTool("osv-scanner"), Version: repo.ToolPin("osv-scanner")}, {Name: "ruff", Path: repo.PolicyTool("ruff"), Version: repo.ToolPin("ruff")}, {Name: "ty", Path: repo.PolicyTool("ty"), Version: repo.ToolPin("ty")}, - {Name: "cargo-nextest", Path: repo.PolicyTool("cargo-nextest"), Version: repo.ToolPin("cargo-nextest")}, }}, {directory: shellcheckDirectory, tools: []GovernedTool{{Name: "shellcheck", Path: filepath.Join(shellcheckDirectory, executableName("shellcheck")), Version: repo.ToolPin("shellcheck")}}}, {directory: nodeDirectory, tools: []GovernedTool{{Name: "node", Path: filepath.Join(nodeDirectory, executableName("node")), Version: repo.ToolPin("node")}}}, {directory: bundleBin}, {directory: filepath.Dir(repo.RustTool("cargo")), tools: []GovernedTool{ + {Name: "cargo-nextest", Path: repo.PolicyTool("cargo-nextest"), Version: repo.ToolPin("cargo-nextest")}, {Name: "cargo", Path: repo.RustTool("cargo"), Version: repo.ToolPin("rust")}, {Name: "rustc", Path: repo.RustTool("rustc"), Version: repo.ToolPin("rust")}, {Name: "rustdoc", Path: repo.RustTool("rustdoc"), Version: repo.ToolPin("rust")}, diff --git a/internal/rustfacts/execution.go b/internal/rustfacts/execution.go index 7c591d3a..1d1fa398 100644 --- a/internal/rustfacts/execution.go +++ b/internal/rustfacts/execution.go @@ -25,7 +25,7 @@ func Command(repo repository.Repository, name, tool string, arguments ...string) return policy.Command{ Name: name, Argv: append([]string{repo.RustTool(tool)}, arguments...), Cwd: ".", Managed: true, SealedEnvironment: true, TimeoutSeconds: 300, - Environment: []string{"SYSTEMROOT", "WINDIR", "ProgramFiles", "ProgramFiles(x86)"}, + Environment: []string{"SYSTEMROOT", "WINDIR"}, EnvironmentOverrides: []string{ "RUSTC=" + repo.RustTool("rustc"), "RUSTDOC=" + repo.RustTool("rustdoc"), "CARGO_HOME=" + CargoHome(repo), "CARGO_TARGET_DIR=" + TargetDirectory(repo),