Signal-iOS/SignalServiceKit/Util/StreamTransform/Output/TransformingOutputStream.swift
2025-12-30 11:34:05 -08:00

75 lines
2.4 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
/// Simple wrapper around an OutputStream that allows passing data through an
/// array of `StreamTransform` objects to alter the data before being written
/// out to the final destination.
public final class TransformingOutputStream: OutputStreamable {
private let transforms: [any StreamTransform]
private let outputStream: OutputStreamable
private let runLoop: RunLoop?
public init(
transforms: [any StreamTransform],
outputStream: OutputStreamable,
runLoop: RunLoop? = nil,
) {
self.transforms = transforms
self.outputStream = outputStream
self.runLoop = runLoop
}
public func write(data: Data) throws {
let data = try transforms.reduce(data) { try $1.transform(data: $0) }
if data.count > 0 {
try outputStream.write(data: data)
}
}
/// Iterates through each transform, allowing it to finalize the output and write any one-time
/// footers required by the internal transform implementation.
public func finalizeAndWriteFooter() throws {
while hasPendingBytes {
let footerData = try transforms.readNextRemainingBytes()
if footerData.count > 0 {
try outputStream.write(data: footerData)
}
}
}
/// Checks the list of transforms and returns if any data may be available from any in the list,
/// including buffered data or pending footer date.
/// Once hasPendingBytes returns `false`, the stream should be considered closed
/// and should no longer be used.
public var hasPendingBytes: Bool {
return
transforms.contains { $0.hasPendingBytes }
|| transforms.compactMap { $0 as? FinalizableStreamTransform }.contains { !$0.hasFinalized }
}
public func close() throws {
try finalizeAndWriteFooter()
if let runLoop {
outputStream.remove(from: runLoop, forMode: .default)
}
try outputStream.close()
}
// MARK: - OutputStreamable passthrough
public func remove(from runLoop: RunLoop, forMode mode: RunLoop.Mode) {
self.outputStream.remove(from: runLoop, forMode: mode)
}
public func schedule(in runLoop: RunLoop, forMode mode: RunLoop.Mode) {
self.outputStream.schedule(in: runLoop, forMode: mode)
}
}