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 .github/workflows/qodana.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion docs/ci/001_PIPELINE_CI.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
2 changes: 1 addition & 1 deletion docs/ci/101_PIPELINE_CI.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
142 changes: 53 additions & 89 deletions src/FileTypeDetection/Infrastructure/ArchiveInternals.vb
Original file line number Diff line number Diff line change
Expand Up @@ -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

''' <summary>
''' Interne Hilfsklasse <c>ArchiveTypeResolver</c> zur kapselnden Umsetzung von Guard-, I/O- und Policy-Logik.
''' </summary>
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

''' <summary>
Expand Down Expand Up @@ -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) _
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions tests/PackageBacked.Tests/PackageBacked.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Keep net10 only: package-backed xUnit v3 execution in CI requires apphost/runtimeconfig generation path that is stable on net10. -->
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<UseAppHost>true</UseAppHost>
Expand Down
2 changes: 2 additions & 0 deletions tools/run-coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
Loading