From 71af1beaa0f744fa94df3aca1effebfeaa772eef Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sat, 7 Nov 2026 11:30:18 +0000 Subject: [PATCH] Finish portability review controls without requiring Git for Cargo tests --- .github/workflows/ci-integrity.yml | 11 ++ docs/echo-362-portability.adoc | 30 +++++ scripts/ci/check-repository-paths.rs | 160 +++++++++++++++++++++++++++ tests/repository_paths.rs | 72 ------------ 4 files changed, 201 insertions(+), 72 deletions(-) create mode 100644 docs/echo-362-portability.adoc create mode 100644 scripts/ci/check-repository-paths.rs delete mode 100644 tests/repository_paths.rs diff --git a/.github/workflows/ci-integrity.yml b/.github/workflows/ci-integrity.yml index 20e1e8a..c8c0959 100644 --- a/.github/workflows/ci-integrity.yml +++ b/.github/workflows/ci-integrity.yml @@ -117,6 +117,16 @@ jobs: done exit "$fail" + - name: Repository path portability + run: | + set -euo pipefail + rustc --version + CHECKER="$RUNNER_TEMP/check-repository-paths" + rustc --edition 2021 --test scripts/ci/check-repository-paths.rs -o "$CHECKER-tests" + "$CHECKER-tests" + rustc --edition 2021 scripts/ci/check-repository-paths.rs -o "$CHECKER" + "$CHECKER" "$GITHUB_WORKSPACE" + - name: Retain raw native result and immutable source identity if: always() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 @@ -140,6 +150,7 @@ jobs: echo '| Native lock | Official v0.2.0 result, unchanged published reconciliation and bidirectional lock sync |' echo '| Tree allowlist | Complete action/workflow path and exact SHA, without a live Settings claim |' echo '| YAML parse | Readable workflow documents with a jobs map |' + echo '| Repository paths | Windows device/component restrictions and case/file-directory collisions in the complete tracked inventory |' echo '| Startup triage | Source/API observations; no inferred platform root cause from zero jobs alone |' echo '' echo 'The raw artifact records not_run if consumer verification never executed, or running if no native exit was recorded.' diff --git a/docs/echo-362-portability.adoc b/docs/echo-362-portability.adoc new file mode 100644 index 0000000..cfb9f84 --- /dev/null +++ b/docs/echo-362-portability.adoc @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 += Windows checkout portability — Echo Types issue 362 + +The eight historical reports previously containing colons in their filenames +are renamed with hyphens. Their original Git blob hashes are unchanged; +report contents and ISO timestamps remain untouched. A fresh complete Windows +clone of the repair branch succeeded without omitting any tracked artifact. + +The Chapel writer removes colons from filenames only. The existing Chapel +end-to-end gate checks the names of both generated report types. No local +Chapel execution is claimed: the compiler is not installed locally. + +The dedicated CI path checker tests invalid Windows components, reserved +device names (including superscript digits and console devices), and +case/file-directory collisions. It checks the explicit Git worktree root; +it is not a Cargo integration test and does not break packaged-source tests +or accidentally inspect a vendored crate's enclosing repository. + +The two original portability controls failed on the old tracked inventory +and passed after the eight artifact renames. The expanded checker has its +own unit controls and a real source-checkout run in CI. + +Creusot with Pearlite contracts and Verus are `OPEN`, not run for this CI +utility; the executable controls are tests, not a formal Windows filesystem +or Chapel proof. Exact hosted results remain in this change's pull request. + +The repository Actions policy was brought into the already approved canonical +namespace-transfer policy by adding +`metadatastician/standards/.github/workflows/*@*`. Existing allowlist entries, +GitHub/verified-creator settings and mandatory SHA pinning were preserved. diff --git a/scripts/ci/check-repository-paths.rs b/scripts/ci/check-repository-paths.rs new file mode 100644 index 0000000..5ee6c33 --- /dev/null +++ b/scripts/ci/check-repository-paths.rs @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MPL-2.0 +//! Checkout-only CI check; deliberately outside Cargo's integration tests. +use std::{collections::BTreeMap, path::Path, process::Command}; + +fn portable_component(component: &str) -> bool { + if component.is_empty() + || component.ends_with(['.', ' ']) + || component + .chars() + .any(|c| c.is_control() || "<>:\"\\|?*".contains(c)) + { + return false; + } + let stem = component + .split('.') + .next() + .unwrap_or("") + .to_ascii_uppercase(); + let numbered_device = stem + .strip_prefix("COM") + .or_else(|| stem.strip_prefix("LPT")); + !matches!( + stem.as_str(), + "CON" | "PRN" | "AUX" | "NUL" | "CONIN$" | "CONOUT$" + ) && !numbered_device.is_some_and(|n| { + matches!( + n, + "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "¹" | "²" | "³" + ) + }) +} + +fn inventory_errors(paths: &[&str]) -> Vec { + let mut errors = Vec::new(); + let mut seen = BTreeMap::::new(); + for path in paths { + let components: Vec<_> = path.split('/').collect(); + let mut prefix = String::new(); + for (index, component) in components.iter().enumerate() { + if !portable_component(component) { + errors.push(format!("invalid component in {path:?}")); + } + if !prefix.is_empty() { + prefix.push('/'); + } + prefix.push_str(component); + let file = index + 1 == components.len(); + let key = prefix.to_uppercase(); + if let Some((old, old_file)) = seen.get(&key) { + if old != &prefix || *old_file != file { + errors.push(format!( + "case or file/directory collision: {old:?} and {prefix:?}" + )); + } + } else { + seen.insert(key, (prefix.clone(), file)); + } + } + } + errors +} + +fn check_checkout(root: &Path) -> Result<(), String> { + let root = root.canonicalize().map_err(|e| e.to_string())?; + let top = Command::new("git") + .current_dir(&root) + .args(["rev-parse", "--show-toplevel"]) + .output() + .map_err(|e| format!("git is required: {e}"))?; + if !top.status.success() { + return Err("the requested root is not a Git checkout".into()); + } + let top = std::str::from_utf8(&top.stdout) + .map_err(|e| e.to_string())? + .trim(); + if Path::new(top).canonicalize().map_err(|e| e.to_string())? != root { + return Err("refusing to inspect an enclosing repository's index".into()); + } + let output = Command::new("git") + .current_dir(&root) + .args(["ls-files", "-z"]) + .output() + .map_err(|e| e.to_string())?; + if !output.status.success() { + return Err("git ls-files failed".into()); + } + let text = std::str::from_utf8(&output.stdout).map_err(|e| e.to_string())?; + let paths: Vec<_> = text.split('\0').filter(|p| !p.is_empty()).collect(); + if paths.is_empty() { + return Err("empty inventory cannot establish portability".into()); + } + let errors = inventory_errors(&paths); + if !errors.is_empty() { + return Err(errors.join("\n")); + } + println!( + "{} tracked paths passed the portability checks", + paths.len() + ); + Ok(()) +} + +fn main() -> Result<(), String> { + let root = std::env::args_os() + .nth(1) + .ok_or("usage: check-repository-paths REPOSITORY_ROOT")?; + check_checkout(Path::new(&root)) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn windows_device_and_component_controls() { + for bad in [ + "report:12.json", + "CON", + "nul.txt", + "COM1.log", + "LPT9", + "COM¹.txt", + "COM²", + "COM³", + "LPT¹", + "LPT²", + "LPT³", + "CONIN$", + "CONOUT$", + "tail.", + "tail ", + "a?b", + "a\\b", + "line\nbreak", + ] { + assert!(!portable_component(bad), "accepted {bad:?}"); + } + for good in [ + "assemblyline-2026-04-12T11-23-37Z.json", + ".github", + "Cargo.toml", + "com10.txt", + ] { + assert!(portable_component(good), "rejected {good:?}"); + } + } + #[test] + fn inventory_detects_case_and_file_directory_collisions() { + for paths in [ + vec!["reports/Run.json", "reports/run.json"], + vec!["src/Foo/a.rs", "src/foo/b.rs"], + vec!["foo", "FOO/bar.rs"], + ] { + assert!( + !inventory_errors(&paths).is_empty(), + "accepted collision: {paths:?}" + ); + } + assert!(inventory_errors(&["src/a.rs", "src/b.rs"]).is_empty()); + } +} diff --git a/tests/repository_paths.rs b/tests/repository_paths.rs deleted file mode 100644 index 703c1b6..0000000 --- a/tests/repository_paths.rs +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -//! Repository checkout contract; run from the Git source checkout. -use std::process::Command; - -fn portable_component(component: &str) -> bool { - if component.is_empty() - || component.ends_with(['.', ' ']) - || component - .chars() - .any(|c| c.is_control() || "<>:\"\\|?*".contains(c)) - { - return false; - } - let stem = component - .split('.') - .next() - .unwrap_or("") - .to_ascii_uppercase(); - !matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL") - && !(stem.len() == 4 - && (stem.starts_with("COM") || stem.starts_with("LPT")) - && matches!(stem.as_bytes()[3], b'1'..=b'9')) -} - -#[test] -fn path_controls_cover_windows_restrictions() { - for invalid in [ - "report:12.json", - "CON", - "nul.txt", - "com1.log", - "LPT9", - "tail.", - "tail ", - "a?b", - "a\\b", - "line\nbreak", - ] { - assert!(!portable_component(invalid), "accepted {invalid:?}"); - } - for valid in [ - "assemblyline-2026-04-12T11-23-37Z.json", - ".github", - "Cargo.toml", - "com10.txt", - ] { - assert!(portable_component(valid), "rejected {valid:?}"); - } -} - -#[test] -fn every_tracked_path_can_be_checked_out_on_windows() { - let output = Command::new("git") - .args(["ls-files", "-z"]) - .output() - .expect("git is required for the source-checkout contract"); - assert!(output.status.success(), "git ls-files failed"); - let paths = std::str::from_utf8(&output.stdout).expect("tracked paths must be UTF-8"); - assert!( - !paths.is_empty(), - "an empty inventory cannot establish portability" - ); - let bad: Vec<_> = paths - .split('\0') - .filter(|path| !path.is_empty()) - .filter(|path| !path.split('/').all(portable_component)) - .collect(); - assert!( - bad.is_empty(), - "Windows-incompatible tracked paths: {bad:?}" - ); -}