diff --git a/.github/workflows/chapel-ci.yml b/.github/workflows/chapel-ci.yml index 3ae7720..0fd479f 100644 --- a/.github/workflows/chapel-ci.yml +++ b/.github/workflows/chapel-ci.yml @@ -212,6 +212,12 @@ jobs: done ./chapel/mass-panic --repoDirectory="$WORK/corpus" --numLocales=1 --quiet --outputDir="$WORK/out" ls "$WORK/out"/system-image-*.json >/dev/null + for report in "$WORK/out"/assemblyline-*.json "$WORK/out"/system-image-*.json; do + test -f "$report" + case "${report##*/}" in + *:*) echo "Non-portable report filename: ${report##*/}" >&2; exit 1 ;; + esac + done echo "chapel-e2e: PASS" chapel-cli-contract: diff --git a/chapel/src/MassPanic.chpl b/chapel/src/MassPanic.chpl index c6ccfe6..320a14b 100644 --- a/chapel/src/MassPanic.chpl +++ b/chapel/src/MassPanic.chpl @@ -1072,7 +1072,9 @@ module MassPanic { mkdir(outputDir, parents=true); } catch { } - const timestamp = dateString(); + // Keep ISO timestamps inside reports, but use portable filenames. + // Colons are forbidden in Windows checkout paths. + const timestamp = dateString().replace(":", "-"); // Write assemblyline report const reportPath = joinPath(outputDir, "assemblyline-" + timestamp + ".json"); diff --git a/mass-panic-results/assemblyline-2026-04-12T11:23:37Z.json b/mass-panic-results/assemblyline-2026-04-12T11-23-37Z.json similarity index 100% rename from mass-panic-results/assemblyline-2026-04-12T11:23:37Z.json rename to mass-panic-results/assemblyline-2026-04-12T11-23-37Z.json diff --git a/mass-panic-results/assemblyline-2026-04-12T11:26:50Z.json b/mass-panic-results/assemblyline-2026-04-12T11-26-50Z.json similarity index 100% rename from mass-panic-results/assemblyline-2026-04-12T11:26:50Z.json rename to mass-panic-results/assemblyline-2026-04-12T11-26-50Z.json diff --git a/mass-panic-results/assemblyline-2026-04-12T11:43:42Z.json b/mass-panic-results/assemblyline-2026-04-12T11-43-42Z.json similarity index 100% rename from mass-panic-results/assemblyline-2026-04-12T11:43:42Z.json rename to mass-panic-results/assemblyline-2026-04-12T11-43-42Z.json diff --git a/mass-panic-results/assemblyline-2026-04-12T11:54:16Z.json b/mass-panic-results/assemblyline-2026-04-12T11-54-16Z.json similarity index 100% rename from mass-panic-results/assemblyline-2026-04-12T11:54:16Z.json rename to mass-panic-results/assemblyline-2026-04-12T11-54-16Z.json diff --git a/mass-panic-results/system-image-2026-04-12T11:23:37Z.json b/mass-panic-results/system-image-2026-04-12T11-23-37Z.json similarity index 100% rename from mass-panic-results/system-image-2026-04-12T11:23:37Z.json rename to mass-panic-results/system-image-2026-04-12T11-23-37Z.json diff --git a/mass-panic-results/system-image-2026-04-12T11:26:50Z.json b/mass-panic-results/system-image-2026-04-12T11-26-50Z.json similarity index 100% rename from mass-panic-results/system-image-2026-04-12T11:26:50Z.json rename to mass-panic-results/system-image-2026-04-12T11-26-50Z.json diff --git a/mass-panic-results/system-image-2026-04-12T11:43:42Z.json b/mass-panic-results/system-image-2026-04-12T11-43-42Z.json similarity index 100% rename from mass-panic-results/system-image-2026-04-12T11:43:42Z.json rename to mass-panic-results/system-image-2026-04-12T11-43-42Z.json diff --git a/mass-panic-results/system-image-2026-04-12T11:54:16Z.json b/mass-panic-results/system-image-2026-04-12T11-54-16Z.json similarity index 100% rename from mass-panic-results/system-image-2026-04-12T11:54:16Z.json rename to mass-panic-results/system-image-2026-04-12T11-54-16Z.json diff --git a/tests/repository_paths.rs b/tests/repository_paths.rs new file mode 100644 index 0000000..703c1b6 --- /dev/null +++ b/tests/repository_paths.rs @@ -0,0 +1,72 @@ +// 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:?}" + ); +}