Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 6 additions & 22 deletions .github/workflows/governance.yml
Original file line number Diff line number Diff line change
@@ -1,32 +1,16 @@
# // Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
# SPDX-License-Identifier: MPL-2.0
# governance.yml β€” single wrapper calling the shared estate governance bundle
# in hyperpolymath/standards instead of carrying per-repo copies.
#
# Replaces the per-repo governance scaffolding removed in the same commit:
# quality.yml, guix-nix-policy.yml, npm-bun-blocker.yml, ts-blocker.yml,
# security-policy.yml, rsr-antipattern.yml, wellknown-enforcement.yml,
# workflow-linter.yml
#
# Load-bearing build/security workflows stay standalone in the repo
# (rust-ci, codeql, dependabot, release, scan/mirror/pages plumbing).

# SPDX-License-Identifier: PMPL-1.0-or-later
name: Governance

on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
workflow_dispatch:
# Estate guardrail: cancel superseded runs so re-pushes / rebased PR
# updates do not pile up queued runs against the shared account-wide
# Actions concurrency pool. Applied only to read-only check workflows
# (no publish/mutation), so cancelling a superseded run is always safe.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
governance:
uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@861b5e911d9e5dcfb3c0ab3dd2a9a3c8fd0a1613
timeout-minutes: 10
uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@5a93d9d57cc04de4002d6d0ecd336fc7a8698910
310 changes: 6 additions & 304 deletions .github/workflows/hypatia-scan.yml

Large diffs are not rendered by default.

78 changes: 0 additions & 78 deletions .github/workflows/scorecard-enforcer.yml

This file was deleted.

38 changes: 8 additions & 30 deletions .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
@@ -1,38 +1,16 @@
# // Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
# SPDX-License-Identifier: MPL-2.0
name: Scorecards supply-chain security
# SPDX-License-Identifier: PMPL-1.0-or-later
name: OSSF Scorecard

on:
branch_protection_rule:
push:
branches: [main, master]
schedule:
- cron: '0 4 * * *'
workflow_dispatch:
# Estate guardrail: cancel superseded runs so re-pushes / rebased PR
# updates do not pile up queued runs against the shared account-wide
# Actions concurrency pool. Applied only to read-only check workflows
# (no publish/mutation), so cancelling a superseded run is always safe.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
analysis:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
security-events: write
id-token: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run Scorecard
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.3.1
with:
results_file: results.sarif
results_format: sarif
- name: Upload results
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v3.31.8
with:
sarif_file: results.sarif
scorecard:
uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@5a93d9d57cc04de4002d6d0ecd336fc7a8698910
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"exports": "./EmptyLinter.deno.js",
"tasks": {
"build": "affinescript compile --deno-esm EmptyLinter.affine -o EmptyLinter.deno.js && affinescript compile --deno-esm src/cli/Main.affine -o src/cli/Main.deno.js",
"build-all": "for f in stdlib/SafeHex.affine stdlib/SafeWhitespace.affine stdlib/SafePath.affine stdlib/SafeString.affine src/core/ByteDetector.affine src/core/TextTransform.affine src/core/PathHandler.affine EmptyLinter.affine src/cli/Main.affine; do affinescript compile --deno-esm $f -o ${f%.affine}.deno.js; done",
"build-all": "bash scripts/build-all.sh",
"clean": "find . -name '*.deno.js' ! -path './stdlib/*' -delete",
"dev": "while true; do affinescript compile --deno-esm EmptyLinter.affine -o EmptyLinter.deno.js 2>&1; sleep 2; done",
"test": "deno test --allow-read --allow-write tests/",
Expand Down
23 changes: 23 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions scripts/build-all.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MPL-2.0
# SPDX-FileCopyrightText: 2026 hyperpolymath
set -euo pipefail

SOURCES=(
stdlib/SafeHex.affine
stdlib/SafeWhitespace.affine
stdlib/SafePath.affine
stdlib/SafeString.affine
src/core/ByteDetector.affine
src/core/TextTransform.affine
src/core/PathHandler.affine
EmptyLinter.affine
src/cli/Main.affine
)

for f in "${SOURCES[@]}"; do
affinescript compile --deno-esm "$f" -o "${f%.affine}.deno.js"
done

# Workaround: AffineScript alpha compiler (issue #122) does not fully inline
# cross-module dependencies into TextTransform.deno.js. Inject missing symbols
# after compilation: LF/CRLF/CR (zero-arg enum constructors), is_invisible
# (private helper from SafeWhitespace), concat (string stdlib fn).
TARGET="src/core/TextTransform.deno.js"
MARKER="// ---- end runtime ----"
PATCH='const LF={tag:"LF"};const CRLF={tag:"CRLF"};const CR={tag:"CR"};\nfunction is_invisible(c){return(c===0||c===160||c===8203||c===65279||c===173||c===8206||c===8207||c===8204||c===8205||c===8288);}\nfunction concat(a,b){return __as_concat(a,b);}'
if [ -f "$TARGET" ] && ! grep -qF 'const LF={tag:"LF"}' "$TARGET"; then
awk -v marker="$MARKER" -v patch="$PATCH" '
{ print }
$0 == marker { printf "%s\n", patch }
' "$TARGET" > "$TARGET.tmp" && mv "$TARGET.tmp" "$TARGET"
fi

echo "build-all complete"
8 changes: 7 additions & 1 deletion src/core/ByteDetector.affine
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ pub fn get_artifact_def(byte_val: Int) -> Option<ArtifactDef> {
}

pub fn byte_to_hex(v: Int) -> String {
encode_byte(v & 255)
if v <= 255 {
encode_byte(v)
} else if v <= 65535 {
encode_byte((v >> 8) & 255) ++ encode_byte(v & 255)
} else {
encode_byte((v >> 16) & 255) ++ encode_byte((v >> 8) & 255) ++ encode_byte(v & 255)
}
}

pub fn scan(content: String) -> [Artifact] {
Expand Down
6 changes: 3 additions & 3 deletions src/core/TextTransform.affine
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ pub fn transform(content: String, options: TransformOptions) -> String {
if options.remove_invisibles_opt {
s = remove_invisibles(s);
}
if options.normalize_line_endings_opt {
s = normalize_line_endings(s, options.target_line_ending);
}
if options.collapse_spaces_opt {
s = collapse_spaces(s);
}
Expand All @@ -65,6 +62,9 @@ pub fn transform(content: String, options: TransformOptions) -> String {
if options.trim_document {
s = trim(s);
}
if options.normalize_line_endings_opt {
s = normalize_line_endings(s, options.target_line_ending);
}
if options.ensure_final_newline_opt {
s = ensure_final_newline(s);
}
Expand Down
Binary file added tests/ByteDetector_test.js
Binary file not shown.
100 changes: 100 additions & 0 deletions tests/PathHandler_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
import { assertEquals } from "jsr:@std/assert";
import {
validate, unwrap_path, path_join, sanitize,
is_within, get_parent, filename, has_extension,
is_excluded, from_trusted,
TraversalDetected,
} from "../src/core/PathHandler.deno.js";

Deno.test("PathHandler: validate accepts relative paths", () => {
const p = validate("src/main.affine");
assertEquals(p.tag, "Some");
assertEquals(unwrap_path(p.value), "src/main.affine");
});

Deno.test("PathHandler: validate rejects absolute paths", () => {
assertEquals(validate("/etc/passwd").tag, "None");
});

Deno.test("PathHandler: validate rejects path traversal", () => {
assertEquals(validate("../../etc/passwd").tag, "None");
});

Deno.test("PathHandler: validate rejects embedded traversal", () => {
assertEquals(validate("src/../../../etc").tag, "None");
});

Deno.test("PathHandler: sanitize removes dangerous characters", () => {
const clean = sanitize("file<name>.txt");
assertEquals(clean.includes("<"), false);
assertEquals(clean.includes(">"), false);
});

Deno.test("PathHandler: sanitize replaces slashes", () => {
const clean = sanitize("path/to/file");
assertEquals(clean.includes("/"), false);
});

Deno.test("PathHandler: path_join creates valid joined path", () => {
const base = from_trusted("docs");
const result = path_join(base, ["notes", "file.txt"]);
assertEquals(result.tag, "Ok");
assertEquals(unwrap_path(result.value), "docs/notes/file.txt");
});

Deno.test("PathHandler: path_join rejects traversal in components", () => {
const base = from_trusted("home");
const result = path_join(base, ["..", "..", "etc"]);
assertEquals(result.tag, "Err");
assertEquals(result.error.tag, "TraversalDetected");
});

Deno.test("PathHandler: filename extracts basename", () => {
const p = from_trusted("docs/reports/file.pdf");
assertEquals(filename(p), "file.pdf");
});

Deno.test("PathHandler: filename handles no directory", () => {
assertEquals(filename(from_trusted("file.txt")), "file.txt");
});

Deno.test("PathHandler: has_extension checks extension", () => {
const p = from_trusted("src/main.affine");
assertEquals(has_extension(p, ".affine"), true);
assertEquals(has_extension(p, ".js"), false);
});

Deno.test("PathHandler: get_parent extracts directory", () => {
const p = from_trusted("home/user/docs/file.txt");
const parent = get_parent(p);
assertEquals(parent.tag, "Some");
assertEquals(unwrap_path(parent.value), "home/user/docs");
});

Deno.test("PathHandler: get_parent returns None for no directory", () => {
assertEquals(get_parent(from_trusted("file.txt")).tag, "None");
});

Deno.test("PathHandler: is_within checks path containment", () => {
const p = from_trusted("home/user/docs");
const base = from_trusted("home/user");
assertEquals(is_within(p, base), true);
});

Deno.test("PathHandler: is_within rejects unrelated paths", () => {
const p = from_trusted("etc/passwd");
const base = from_trusted("home/user");
assertEquals(is_within(p, base), false);
});

Deno.test("PathHandler: is_excluded matches excluded dirs", () => {
const p = from_trusted("project/node_modules/pkg/index.js");
assertEquals(is_excluded(p, ["node_modules", ".git"]), true);
});

Deno.test("PathHandler: is_excluded allows non-excluded paths", () => {
const p = from_trusted("project/src/main.affine");
assertEquals(is_excluded(p, ["node_modules", ".git"]), false);
});
Loading