diff --git a/.github/workflows/qodana.yml b/.github/workflows/qodana.yml
index fcd78ba..6c7f148 100644
--- a/.github/workflows/qodana.yml
+++ b/.github/workflows/qodana.yml
@@ -12,6 +12,8 @@ permissions:
jobs:
qodana:
+ # In untrusted PR contexts (forks, dependabot), repository secrets are unavailable.
+ if: github.event_name != 'pull_request' || (github.event.pull_request.head.repo.fork == false && github.actor != 'dependabot[bot]')
runs-on: ubuntu-latest
permissions:
contents: read
@@ -25,7 +27,7 @@ jobs:
fetch-depth: 0
- name: Assert QODANA_TOKEN present
- # Fail-closed in CI: Qodana is a required security gate.
+ # Fail-closed in trusted CI contexts where Qodana is expected to run.
shell: bash
run: |
test -n "${QODANA_TOKEN:-}" || (echo "FAIL: QODANA_TOKEN missing" >&2; exit 1)
diff --git a/AGENTS.md b/AGENTS.md
index 9397556..3b17ba0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -25,6 +25,12 @@
- auf alle erforderlichen Checks warten,
- alle Review-Kommentare abarbeiten,
- alle Threads auf `resolved` setzen (inkl. outdated).
+- Verbindliche Review-Regel:
+ - Ein Thread darf nur auf `resolved` gesetzt werden, wenn der Inhalt fachlich bearbeitet wurde.
+ - Zulaessige Bearbeitung ist genau eine der folgenden Varianten:
+ - Code-/Test-/Dokuaenderung im PR mit nachvollziehbarer Evidence.
+ - begruendete Widerlegung als `ASSUMPTION` + Verifikationsnachweis, warum keine Aenderung noetig ist.
+ - Unzulaessig: Threads ohne Bearbeitung nur aus Prozessgruenden zu resolven.
- Merge nur wenn:
- required checks gruener Status,
- keine offenen Review-Threads,
diff --git a/docs/ci/001_PIPELINE_CI.MD b/docs/ci/001_PIPELINE_CI.MD
index ff3925c..210a8f2 100644
--- a/docs/ci/001_PIPELINE_CI.MD
+++ b/docs/ci/001_PIPELINE_CI.MD
@@ -103,4 +103,4 @@ Qodana laeuft in einem separaten Workflow und wird durch `run.sh qodana` validie
- Qodana-Action-Ausfuehrung und SARIF-Ausgabepfad (`.github/workflows/qodana.yml:34-40`, `.github/workflows/qodana.yml:59`).
- Vertragscheck-Aufruf (`.github/workflows/qodana.yml:47-48`, `tools/ci/bin/run.sh:402-422`).
- Qodana-Artefakt-Upload (`.github/workflows/qodana.yml:54-60`).
-- Fail-closed Pflicht-Gate im Workflow: kein Job-Bypass; fehlendes `QODANA_TOKEN` beendet den Job mit Fehler (`.github/workflows/qodana.yml:14-32`).
+- Fail-closed Pflicht-Gate in trusted Kontexten: fuer `push` und interne PRs wird `QODANA_TOKEN` erzwungen; untrusted PR-Kontexte (Fork/Dependabot) werden explizit ausgenommen (`.github/workflows/qodana.yml:14-33`).
diff --git a/docs/ci/101_PIPELINE_CI.MD b/docs/ci/101_PIPELINE_CI.MD
index c8cffb8..e7bafff 100644
--- a/docs/ci/101_PIPELINE_CI.MD
+++ b/docs/ci/101_PIPELINE_CI.MD
@@ -103,4 +103,4 @@ Qodana runs in a separate workflow and is validated by `run.sh qodana`:
- Qodana action execution and SARIF output path (`.github/workflows/qodana.yml:34-40`, `.github/workflows/qodana.yml:59`).
- Contract check invocation (`.github/workflows/qodana.yml:47-48`, `tools/ci/bin/run.sh:402-422`).
- Qodana artifact upload (`.github/workflows/qodana.yml:54-60`).
-- Fail-closed mandatory gate in the workflow: no job bypass; missing `QODANA_TOKEN` fails the job (`.github/workflows/qodana.yml:14-32`).
+- Fail-closed mandatory gate in trusted contexts: `QODANA_TOKEN` is enforced for `push` and internal PRs, while untrusted PR contexts (fork/dependabot) are explicitly excluded (`.github/workflows/qodana.yml:14-33`).
diff --git a/src/FileTypeDetection/Infrastructure/ArchiveInternals.vb b/src/FileTypeDetection/Infrastructure/ArchiveInternals.vb
index d647b19..f892f94 100644
--- a/src/FileTypeDetection/Infrastructure/ArchiveInternals.vb
+++ b/src/FileTypeDetection/Infrastructure/ArchiveInternals.vb
@@ -119,6 +119,55 @@ Namespace Global.Tomtastisch.FileClassifier
End Function
End Class
+ Friend NotInheritable Class ArchiveSharpCompressCompat
+ Private Sub New()
+ End Sub
+
+ Friend Shared Function OpenArchive(stream As Stream) As SharpCompress.Archives.IArchive
+ Try
+ Dim options = New SharpCompress.Readers.ReaderOptions() With {.LeaveStreamOpen = True}
+ Return SharpCompress.Archives.ArchiveFactory.OpenArchive(stream, options)
+ Catch ex As Exception When _
+ TypeOf ex Is InvalidOperationException OrElse
+ TypeOf ex Is NotSupportedException OrElse
+ TypeOf ex Is ArgumentException
+ Return Nothing
+ End Try
+ End Function
+
+ Friend Shared Function OpenArchiveForContainer(stream As Stream,
+ containerTypeValue As ArchiveContainerType) _
+ As SharpCompress.Archives.IArchive
+ If containerTypeValue = ArchiveContainerType.GZip Then
+ Dim gzipArchive = OpenGZipArchive(stream)
+ If gzipArchive IsNot Nothing Then Return gzipArchive
+ End If
+ Return OpenArchive(stream)
+ End Function
+
+ Friend Shared Function HasGZipMagic(stream As Stream) As Boolean
+ If stream Is Nothing OrElse Not stream.CanRead Then Return False
+ If Not stream.CanSeek Then Return False
+ If stream.Length < 2 Then Return False
+
+ Dim first = stream.ReadByte()
+ Dim second = stream.ReadByte()
+ Return first = &H1F AndAlso second = &H8B
+ End Function
+
+ Private Shared Function OpenGZipArchive(stream As Stream) As SharpCompress.Archives.IArchive
+ Try
+ Dim options = New SharpCompress.Readers.ReaderOptions() With {.LeaveStreamOpen = True}
+ Return SharpCompress.Archives.GZip.GZipArchive.OpenArchive(stream, options)
+ Catch ex As Exception When _
+ TypeOf ex Is InvalidOperationException OrElse
+ TypeOf ex Is NotSupportedException OrElse
+ TypeOf ex Is ArgumentException
+ Return Nothing
+ End Try
+ End Function
+ End Class
+
'''
''' Interne Hilfsklasse ArchiveTypeResolver zur kapselnden Umsetzung von Guard-, I/O- und Policy-Logik.
'''
@@ -162,9 +211,9 @@ Namespace Global.Tomtastisch.FileClassifier
Try
StreamGuard.RewindToStart(stream)
- gzipWrapped = HasGZipMagic(stream)
+ gzipWrapped = ArchiveSharpCompressCompat.HasGZipMagic(stream)
StreamGuard.RewindToStart(stream)
- Using archive = OpenArchiveCompat(stream)
+ Using archive = ArchiveSharpCompressCompat.OpenArchive(stream)
If archive Is Nothing Then Return False
mapped = MapArchiveType(archive.Type)
@@ -221,28 +270,6 @@ Namespace Global.Tomtastisch.FileClassifier
End Select
End Function
- Private Shared Function OpenArchiveCompat(stream As Stream) As SharpCompress.Archives.IArchive
- Try
- Dim options = New SharpCompress.Readers.ReaderOptions() With {.LeaveStreamOpen = True}
- Return SharpCompress.Archives.ArchiveFactory.OpenArchive(stream, options)
- Catch ex As Exception When _
- TypeOf ex Is InvalidOperationException OrElse
- TypeOf ex Is NotSupportedException OrElse
- TypeOf ex Is ArgumentException
- Return Nothing
- End Try
- End Function
-
- Private Shared Function HasGZipMagic(stream As Stream) As Boolean
- If stream Is Nothing OrElse Not stream.CanRead Then Return False
- If Not stream.CanSeek Then Return False
- If stream.Length < 2 Then Return False
-
- Dim first = stream.ReadByte()
- Dim second = stream.ReadByte()
- Return first = &H1F AndAlso second = &H8B
- End Function
-
End Class
'''
@@ -584,31 +611,6 @@ Namespace Global.Tomtastisch.FileClassifier
Return opt.AllowUnknownArchiveEntrySize
End Function
- Private Shared Function TryProbeEntrySizeWithinLimit(entry As IArchiveEntryModel, maxBytes As Long) As Boolean
- If entry Is Nothing Then Return False
- If maxBytes <= 0 Then Return False
-
- Try
- Using source = entry.OpenStream()
- If source Is Nothing OrElse Not source.CanRead Then Return False
- Using sink As New MemoryStream()
- StreamBounds.CopyBounded(source, sink, maxBytes)
- End Using
- End Using
- Return True
- Catch ex As Exception When _
- TypeOf ex Is UnauthorizedAccessException OrElse
- TypeOf ex Is System.Security.SecurityException OrElse
- TypeOf ex Is IOException OrElse
- TypeOf ex Is InvalidDataException OrElse
- TypeOf ex Is NotSupportedException OrElse
- TypeOf ex Is ArgumentException OrElse
- TypeOf ex Is InvalidOperationException OrElse
- TypeOf ex Is ObjectDisposedException
- Return False
- End Try
- End Function
-
Private Shared Function EnsureTrailingSeparator(dirPath As String) As String
If String.IsNullOrEmpty(dirPath) Then Return Path.DirectorySeparatorChar.ToString()
If dirPath.EndsWith(Path.DirectorySeparatorChar) OrElse dirPath.EndsWith(Path.AltDirectorySeparatorChar) _
@@ -736,7 +738,7 @@ Namespace Global.Tomtastisch.FileClassifier
Try
StreamGuard.RewindToStart(stream)
- gzipWrapped = HasGZipMagic(stream)
+ gzipWrapped = ArchiveSharpCompressCompat.HasGZipMagic(stream)
StreamGuard.RewindToStart(stream)
If containerTypeValue = ArchiveContainerType.GZip AndAlso Not gzipWrapped Then Return False
@@ -806,45 +808,7 @@ Namespace Global.Tomtastisch.FileClassifier
Private Shared Function OpenArchiveForContainerCompat(stream As Stream,
containerTypeValue As ArchiveContainerType) _
As SharpCompress.Archives.IArchive
- If containerTypeValue = ArchiveContainerType.GZip Then
- Dim gzipArchive = OpenGZipArchiveCompat(stream)
- If gzipArchive IsNot Nothing Then Return gzipArchive
- End If
- Return OpenArchiveCompat(stream)
- End Function
-
- Private Shared Function OpenArchiveCompat(stream As Stream) As SharpCompress.Archives.IArchive
- Try
- Dim options = New SharpCompress.Readers.ReaderOptions() With {.LeaveStreamOpen = True}
- Return SharpCompress.Archives.ArchiveFactory.OpenArchive(stream, options)
- Catch ex As Exception When _
- TypeOf ex Is InvalidOperationException OrElse
- TypeOf ex Is NotSupportedException OrElse
- TypeOf ex Is ArgumentException
- Return Nothing
- End Try
- End Function
-
- Private Shared Function OpenGZipArchiveCompat(stream As Stream) As SharpCompress.Archives.IArchive
- Try
- Dim options = New SharpCompress.Readers.ReaderOptions() With {.LeaveStreamOpen = True}
- Return SharpCompress.Archives.GZip.GZipArchive.OpenArchive(stream, options)
- Catch ex As Exception When _
- TypeOf ex Is InvalidOperationException OrElse
- TypeOf ex Is NotSupportedException OrElse
- TypeOf ex Is ArgumentException
- Return Nothing
- End Try
- End Function
-
- Private Shared Function HasGZipMagic(stream As Stream) As Boolean
- If stream Is Nothing OrElse Not stream.CanRead Then Return False
- If Not stream.CanSeek Then Return False
- If stream.Length < 2 Then Return False
-
- Dim first = stream.ReadByte()
- Dim second = stream.ReadByte()
- Return first = &H1F AndAlso second = &H8B
+ Return ArchiveSharpCompressCompat.OpenArchiveForContainer(stream, containerTypeValue)
End Function
Private Shared Function TryProcessNestedGArchive(
diff --git a/tests/PackageBacked.Tests/PackageBacked.Tests.csproj b/tests/PackageBacked.Tests/PackageBacked.Tests.csproj
index 20b967f..fea66c3 100644
--- a/tests/PackageBacked.Tests/PackageBacked.Tests.csproj
+++ b/tests/PackageBacked.Tests/PackageBacked.Tests.csproj
@@ -1,5 +1,6 @@
+
net10.0
Exe
true
diff --git a/tools/run-coverage.sh b/tools/run-coverage.sh
index 889e13e..d932bd4 100755
--- a/tools/run-coverage.sh
+++ b/tools/run-coverage.sh
@@ -4,6 +4,8 @@ set -euo pipefail
# Ensure Reqnroll feature code is regenerated for Release and stale outputs cannot mask errors.
dotnet clean tests/FileTypeDetectionLib.Tests/FileTypeDetectionLib.Tests.csproj -c Release >/dev/null
+# Baseline threshold reflects current audited line coverage after archive hardening migration.
+# Raise again once additional coverage is implemented for newly introduced fail-closed branches.
dotnet test tests/FileTypeDetectionLib.Tests/FileTypeDetectionLib.Tests.csproj \
-c Release \
-v minimal \