src/Private/Core/Write-FotonPackageFiles.ps1

function Write-FotonManifestFiles {
    $manifestCsv = Join-Path $script:FotonSession.Dirs['Collection'] 'collection_manifest.csv'
    $script:FotonSession.Manifest | Export-Csv -Path $manifestCsv -NoTypeInformation -Encoding UTF8

    $findingsCsv = Join-Path $script:FotonSession.Dirs['Collection'] 'findings.csv'
    if ($script:FotonSession.Findings.Count -gt 0) {
        $script:FotonSession.Findings | Export-Csv -Path $findingsCsv -NoTypeInformation -Encoding UTF8
    }

    if ($script:FotonSession.Errors.Count -gt 0) {
        $script:FotonSession.Errors | Export-Csv -Path (Join-Path $script:FotonSession.Dirs['Collection'] 'collection_errors.csv') -NoTypeInformation -Encoding UTF8
    }
}

function Write-FotonArtifactHashes {
    Write-FotonStatus 'Hashing collected artifacts (SHA-256)' 'Step'
    $rows = @()
    $root = $script:FotonSession.CaseRoot
    $nested = Get-FotonNestedDuplicateRoot -CaseRoot $root
    foreach ($f in (Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue)) {
        if ($f.Name -eq 'hashes.csv' -or $f.Name -eq 'hashes.txt') { continue }
        if ($nested -and $f.FullName.StartsWith($nested, [StringComparison]::OrdinalIgnoreCase)) { continue }
        $hash = ''
        try { $hash = Get-FotonSha256Hex -LiteralPath $f.FullName } catch { $hash = 'HASH_ERROR' }
        $rel = $f.FullName.Substring($root.Length).TrimStart('\')
        $rows += [PSCustomObject]@{
            RelativePath = $rel
            SizeBytes    = $f.Length
            SHA256       = $hash
            ModifiedUtc  = $f.LastWriteTimeUtc.ToString('yyyy-MM-dd HH:mm:ss')
        }
    }
    $rows | Export-Csv -Path (Join-Path $script:FotonSession.Dirs['Collection'] 'hashes.csv') -NoTypeInformation -Encoding UTF8
    $txt = @()
    $txt += "Foton artifact hashes (SHA-256) - generated $((Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss')) UTC"
    $txt += ''
    foreach ($r in $rows) { $txt += ("{0} {1}" -f $r.SHA256, $r.RelativePath) }
    $txt | Out-File -FilePath (Join-Path $script:FotonSession.Dirs['Collection'] 'hashes.txt') -Encoding UTF8
    Write-FotonStatus ("Hashed {0} artifacts" -f $rows.Count) 'Good'
    return $rows.Count
}

function Compress-FotonCasePackage {
    Write-FotonStatus 'Compressing case package' 'Step'
    $zipPath = "$($script:FotonSession.CaseRoot).zip"

    # Refuse to ship a package that contains a copy of itself. Hashing already ran,
    # so hashes.csv describes one tree; zipping a nested copy would double the
    # transfer and make every recursive count wrong downstream.
    $nested = Get-FotonNestedDuplicateRoot -CaseRoot $script:FotonSession.CaseRoot
    if ($nested) {
        Write-FotonStatus ("Case folder contains a nested copy of itself at {0}. It is excluded from the ZIP; the collected tree is unaffected." -f (Split-Path -Leaf $nested)) 'Warn'
    }

    try {
        if (Test-Path -LiteralPath $zipPath) { Remove-Item -LiteralPath $zipPath -Force }
        if ($nested) {
            Compress-Archive -Path (@(Get-ChildItem -LiteralPath $script:FotonSession.CaseRoot -Force |
                        Where-Object { $_.FullName -ne $nested } | ForEach-Object { $_.FullName })) `
                -DestinationPath $zipPath -Force -ErrorAction Stop
            return $zipPath
        }
        Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue
        [System.IO.Compression.ZipFile]::CreateFromDirectory($script:FotonSession.CaseRoot, $zipPath,
            [System.IO.Compression.CompressionLevel]::Optimal, $true)
        return $zipPath
    }
    catch {
        try {
            Compress-Archive -Path (Join-Path $script:FotonSession.CaseRoot '*') -DestinationPath $zipPath -Force -ErrorAction Stop
            return $zipPath
        }
        catch {
            Write-FotonStatus "Compression failed: $($_.Exception.Message). The uncompressed case folder is still available." 'Warn'
            return $null
        }
    }
}