Skip to content
Merged
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
4 changes: 3 additions & 1 deletion scripts/test-source-clean-guard.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ $cases = @(
@{ Path = "evidence/fixture.json"; Text = "{`"reference`": `"$identifier`"}"; Expected = "text" },
@{ Path = ".github/workflows/smart-discovery-post-merge-production.yml"; Text = "name: $identifier"; Expected = "text" },
@{ Path = "tests/ARSAS.Tests/SyntheticFixture.cs"; Text = "// $identifier"; Expected = "text" },
@{ Path = "docs/${identifier}-fixture.md"; Text = "# independently generated fixture"; Expected = "path" }
@{ Path = "docs/${identifier}-fixture.md"; Text = "# independently generated fixture"; Expected = "path" },
@{ Path = "docs/split-name.md"; Text = $identifier.Substring(0, 3) + " " + $identifier.Substring(3); Expected = "text" },
@{ Path = "docs/mixed-case.md"; Text = $identifier.ToUpperInvariant(); Expected = "text" }
)

function Invoke-Case {
Expand Down
112 changes: 66 additions & 46 deletions scripts/verify-source-clean.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,6 @@ $ForbiddenIdentifierHashes = [System.Collections.Generic.HashSet[string]]::new([
"0e443fe512c39ce723fc1be519b8e2a13a4ba75916989123078b59308480b2f8"
) | ForEach-Object { [void]$ForbiddenIdentifierHashes.Add($_) }

$CandidateLengths = [System.Collections.Generic.HashSet[int]]::new()
@(7, 8, 12, 22) | ForEach-Object { [void]$CandidateLengths.Add($_) }

$ForbiddenTextPatterns = @(
"C:\Users\",
"C:\Program Files\dotnet\sdk",
Expand All @@ -72,57 +69,80 @@ function Normalize-RelativePath {
return $Path.Replace('\', '/').TrimStart('/')
}

$Sha256 = [System.Security.Cryptography.SHA256]::Create()
$IdentifierCandidateCache = [System.Collections.Generic.Dictionary[string,bool]]::new([System.StringComparer]::Ordinal)

function Test-ForbiddenIdentifierCandidate {
param([Parameter(Mandatory=$true)][string]$Value)

if (-not $CandidateLengths.Contains($Value.Length)) { return $false }
if ($IdentifierCandidateCache.ContainsKey($Value)) {
return $IdentifierCandidateCache[$Value]
}
# Keep the one-way fingerprint policy; execute the exhaustive substring scan in
# compiled .NET code rather than millions of PowerShell pipeline/function calls.
# The matcher preserves the original four-token, exact-length and embedded-token rules.
$MatcherSource = @'
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;

namespace ArsasSourceClean
{
public sealed class IdentifierMatcher : IDisposable
{
private static readonly Regex Words = new Regex("[a-z0-9]+", RegexOptions.Compiled | RegexOptions.CultureInvariant);
private readonly HashSet<string> hashes;
private readonly HashSet<int> lengths;
private readonly Dictionary<string, bool> cache = new Dictionary<string, bool>(StringComparer.Ordinal);
private readonly SHA256 sha = SHA256.Create();

public IdentifierMatcher(string[] forbiddenHashes, int[] candidateLengths)
{
hashes = new HashSet<string>(forbiddenHashes, StringComparer.OrdinalIgnoreCase);
lengths = new HashSet<int>(candidateLengths);
}

$bytes = [System.Text.Encoding]::UTF8.GetBytes($Value)
$hash = -join ($Sha256.ComputeHash($bytes) | ForEach-Object { $_.ToString("x2") })
$isForbidden = $ForbiddenIdentifierHashes.Contains($hash)
$IdentifierCandidateCache[$Value] = $isForbidden
return $isForbidden
}
private bool IsForbidden(string candidate)
{
if (!lengths.Contains(candidate.Length)) return false;
bool found;
if (cache.TryGetValue(candidate, out found)) return found;
string digest = BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(candidate))).Replace("-", "");
found = hashes.Contains(digest);
cache[candidate] = found;
return found;
}

function Test-ContainsForbiddenIdentifier {
param([AllowEmptyString()][string]$Text)
public bool ContainsForbiddenIdentifier(string text)
{
if (String.IsNullOrWhiteSpace(text)) return false;
MatchCollection words = Words.Matches(text.ToLowerInvariant());
for (int index = 0; index < words.Count; index++)
{
string word = words[index].Value;
foreach (int length in lengths)
{
for (int offset = 0; offset <= word.Length - length; offset++)
{
if (IsForbidden(word.Substring(offset, length))) return true;
}
}

if ([string]::IsNullOrWhiteSpace($Text)) { return $false }
$words = @([regex]::Matches($Text.ToLowerInvariant(), '[a-z0-9]+') | ForEach-Object { $_.Value })

for ($index = 0; $index -lt $words.Count; $index++) {
$word = $words[$index]

# Detect identifiers embedded in source/path tokens such as TypeNameSuffix.
# This closes the common case where a prohibited product name is attached
# to a class, fixture, job, or filename rather than separated by punctuation.
foreach ($length in $CandidateLengths) {
if ($word.Length -lt $length) { continue }
for ($offset = 0; $offset -le ($word.Length - $length); $offset++) {
$fragment = $word.Substring($offset, $length)
if (Test-ForbiddenIdentifierCandidate $fragment) {
return $true
string candidate = "";
for (int count = 1; count <= 4 && index + count - 1 < words.Count; count++)
{
candidate += words[index + count - 1].Value;
if (candidate.Length > 22) break;
if (IsForbidden(candidate)) return true;
}
}
return false;
}

$candidate = ""
for ($count = 1; $count -le 4 -and ($index + $count - 1) -lt $words.Count; $count++) {
$candidate += $words[$index + $count - 1]
if ($candidate.Length -gt 22) { break }
if (Test-ForbiddenIdentifierCandidate $candidate) {
return $true
}
}
public void Dispose() { sha.Dispose(); }
}
}
'@
Add-Type -TypeDefinition $MatcherSource -Language CSharp -ErrorAction Stop
$IdentifierMatcher = [ArsasSourceClean.IdentifierMatcher]::new(
[string[]]@($ForbiddenIdentifierHashes), [int[]]@(7, 8, 12, 22))

return $false
function Test-ContainsForbiddenIdentifier {
param([AllowEmptyString()][string]$Text)
return $IdentifierMatcher.ContainsForbiddenIdentifier($Text)
}

function Get-TrackedRelativePaths {
Expand Down Expand Up @@ -185,5 +205,5 @@ if (-not $ScanOnly) {
& (Join-Path $PSScriptRoot "verify-auto-update.ps1")
}

$Sha256.Dispose()
$IdentifierMatcher.Dispose()
Write-Host "All Git-tracked ARSAS content passed source and external-identifier checks." -ForegroundColor Green
Loading