#r "System.Formats.Tar" #r "/opt/fsi/packages/ZstdSharp.dll" open System open System.Collections.Generic open System.Diagnostics open System.Formats.Tar open System.IO open System.IO.Compression open System.Net.Security open System.Runtime.InteropServices open System.Security.Cryptography open System.Security.Cryptography.X509Certificates open System.Text.Json open System.Text.Json.Serialization open System.Threading open System.Threading.Tasks open ZstdSharp open ZstdSharp.Unsafe [] type Target = { [] Kind: string [] Repository: string } [] type Published = { [] Kind: string [] Repository: string [] Reference: string } [] type Delivery = { [] SourceRevision: string [] Targets: Published array } type ProcessResult = { Code: int Error: string Output: string } let json = JsonSerializerOptions( PropertyNamingPolicy = null, WriteIndented = false, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull ) let private stopping = new CancellationTokenSource() let mutable private signalExitCode = 143 let private registerSignal signal exitCode = PosixSignalRegistration.Create( signal, fun context -> context.Cancel <- true signalExitCode <- exitCode stopping.Cancel() ) let private interrupt = registerSignal PosixSignal.SIGINT 130 let private terminate = registerSignal PosixSignal.SIGTERM 143 let cancellationToken = stopping.Token let private exitForSignal<'T> () = Environment.Exit signalExitCode Unchecked.defaultof<'T> let fail message = invalidOp message let require valid message = if not valid then fail message let directory (path: string) = Directory.CreateDirectory path |> ignore path let sha256 (path: string) = use input = File.OpenRead path Convert.ToHexString(SHA256.HashData input).ToLowerInvariant() let hashText (value: string) = value |> Text.Encoding.UTF8.GetBytes |> SHA256.HashData |> Convert.ToHexString |> _.ToLowerInvariant() let validRevision (value: string) = not <| isNull value && (value.Length = 40 || value.Length = 64) && value |> Seq.forall (fun character -> Char.IsAsciiHexDigit character && not <| Char.IsUpper character) let validSha256 (value: string) = not <| isNull value && value.Length = 64 && value |> Seq.forall (fun character -> Char.IsAsciiHexDigit character && not <| Char.IsUpper character) let validDigest (value: string) = not <| isNull value && value.StartsWith("sha256:", StringComparison.Ordinal) && validSha256 value[7..] let decode<'T> (value: string) = let result = JsonSerializer.Deserialize<'T>(value, json) require (not <| isNull (box result)) "JSON document is missing" result let sleep (milliseconds: int) = try Task.Delay(milliseconds, cancellationToken).GetAwaiter().GetResult() with :? OperationCanceledException when cancellationToken.IsCancellationRequested -> exitForSignal () let run (executable: string) (arguments: string seq) (workingDirectory: string option) (environment: (string * string) seq) = let start = ProcessStartInfo executable start.UseShellExecute <- false start.RedirectStandardOutput <- true start.RedirectStandardError <- true workingDirectory |> Option.iter (fun path -> start.WorkingDirectory <- path) arguments |> Seq.iter start.ArgumentList.Add environment |> Seq.iter (fun (name, value) -> start.Environment[name] <- value) use child = Process.Start start let output = child.StandardOutput.ReadToEndAsync() let error = child.StandardError.ReadToEndAsync() try child.WaitForExitAsync(cancellationToken).GetAwaiter().GetResult() with :? OperationCanceledException when cancellationToken.IsCancellationRequested -> if not child.HasExited then child.Kill true child.WaitForExit() exitForSignal () { Code = child.ExitCode Error = error.GetAwaiter().GetResult() Output = output.GetAwaiter().GetResult() } let requireSuccess executable arguments workingDirectory environment = let result = run executable arguments workingDirectory environment require (result.Code = 0) $"{Path.GetFileName executable} failed: {result.Error.Trim()}" result.Output let private descendants (root: string) = seq { let pending = Stack(Directory.EnumerateFileSystemEntries root) while pending.Count > 0 do let path = pending.Pop() yield path let attributes = File.GetAttributes path if attributes.HasFlag FileAttributes.Directory && not (attributes.HasFlag FileAttributes.ReparsePoint) then Directory.EnumerateFileSystemEntries path |> Seq.iter pending.Push } let private tree (root: string) = seq { yield root if Directory.Exists root then yield! descendants root } let private safePath (root: string) (name: string) = require (not <| Path.IsPathFullyQualified name) "archive path is absolute" let path = Path.GetFullPath(Path.Combine(root, name)) let prefix = root + string Path.DirectorySeparatorChar require (path = root || path.StartsWith(prefix, StringComparison.Ordinal)) "archive path escapes destination" path let private extract (input: Stream) (destination: string) = let root = Path.GetFullPath destination directory root |> ignore let directoryModes = ResizeArray() use reader = new TarReader(input, leaveOpen = false) for entry in Seq.initInfinite (fun _ -> reader.GetNextEntry()) |> Seq.takeWhile (isNull >> not) do let path = safePath root entry.Name match entry.EntryType with | TarEntryType.Directory -> directory path |> ignore directoryModes.Add(path, entry.Mode) | TarEntryType.RegularFile | TarEntryType.V7RegularFile | TarEntryType.ContiguousFile -> directory (Path.GetDirectoryName path) |> ignore use output = File.Create path if not <| isNull entry.DataStream then entry.DataStream.CopyTo output output.Dispose() File.SetUnixFileMode(path, entry.Mode) | TarEntryType.SymbolicLink -> require (not <| Path.IsPathFullyQualified entry.LinkName) "archive link is absolute" let target = Path.GetFullPath(Path.Combine(Path.GetDirectoryName path, entry.LinkName)) let prefix = root + string Path.DirectorySeparatorChar require (target = root || target.StartsWith(prefix, StringComparison.Ordinal)) "archive link escapes destination" directory (Path.GetDirectoryName path) |> ignore File.CreateSymbolicLink(path, entry.LinkName) |> ignore | TarEntryType.GlobalExtendedAttributes -> () | _ -> fail $"unsupported archive entry: {entry.Name}" directoryModes |> Seq.sortByDescending (fun (path, _) -> path.Length) |> Seq.iter File.SetUnixFileMode let extractGzip (source: string) (destination: string) = use input = File.OpenRead source use gzip = new GZipStream(input, CompressionMode.Decompress) extract gzip destination let extractZstd (source: string) (destination: string) = use input = File.OpenRead source use zstd = new DecompressionStream(input, 131072, false, false) extract zstd destination let private relativeName (root: string) (path: string) = Path.GetRelativePath(root, path).Replace(Path.DirectorySeparatorChar, '/') let private writeEntry (writer: TarWriter) (root: string) (path: string) = let name = relativeName root path let attributes = File.GetAttributes path if attributes.HasFlag FileAttributes.ReparsePoint then let item: FileSystemInfo = if attributes.HasFlag FileAttributes.Directory then DirectoryInfo path else FileInfo path require (not <| isNull item.LinkTarget) "invalid symbolic link" require (not <| Path.IsPathFullyQualified item.LinkTarget) "archive link is absolute" let entry = PaxTarEntry(TarEntryType.SymbolicLink, name) entry.LinkName <- item.LinkTarget entry.Mode <- File.GetUnixFileMode path entry.ModificationTime <- DateTimeOffset.UnixEpoch writer.WriteEntry entry elif attributes.HasFlag FileAttributes.Directory then let entry = PaxTarEntry(TarEntryType.Directory, name) entry.Mode <- File.GetUnixFileMode path entry.ModificationTime <- DateTimeOffset.UnixEpoch writer.WriteEntry entry else let entry = PaxTarEntry(TarEntryType.RegularFile, name) entry.Mode <- File.GetUnixFileMode path entry.ModificationTime <- DateTimeOffset.UnixEpoch entry.DataStream <- File.OpenRead path writer.WriteEntry entry entry.DataStream.Dispose() let createZstd (root: string) (paths: string seq) (destination: string) = use algorithm = SHA256.Create() use output = File.Create destination use hashing = new CryptoStream(output, algorithm, CryptoStreamMode.Write, true) use zstd = new CompressionStream(hashing, 1, 131072, true) zstd.SetParameter(ZSTD_cParameter.ZSTD_c_nbWorkers, 2) use writer = new TarWriter(zstd, TarEntryFormat.Pax, leaveOpen = false) paths |> Seq.collect (fun path -> tree (Path.Combine(root, path))) |> Seq.distinct |> Seq.sortBy (relativeName root) |> Seq.iter (writeEntry writer root) writer.Dispose() zstd.Dispose() hashing.FlushFinalBlock() Convert.ToHexString(algorithm.Hash).ToLowerInvariant() let validateCertificate (root: X509Certificate2) (certificate: X509Certificate) (providedChain: X509Chain) (errors: SslPolicyErrors) = not (isNull certificate) && not (isNull providedChain) && not (errors.HasFlag SslPolicyErrors.RemoteCertificateNameMismatch) && (use chain = new X509Chain() chain.ChainPolicy.TrustMode <- X509ChainTrustMode.CustomRootTrust chain.ChainPolicy.CustomTrustStore.Add root |> ignore chain.ChainPolicy.RevocationMode <- X509RevocationMode.NoCheck providedChain.ChainElements |> Seq.iter (fun item -> chain.ChainPolicy.ExtraStore.Add item.Certificate |> ignore) chain.Build(new X509Certificate2(certificate)))