// Copyright (c) 2026 Timofey Solomko // Licensed under MIT License // // See LICENSE for license information import Foundation /** A type that allows to iteratively create TAR containers with the output being written into a `FileHandle`. The `TarWriter` may be helpful in reducing the peak memory usage on certain platforms. However, to achieve this both the creation of TAR entries and the calls to `TarWriter` should be wrapped inside the `autoreleasepool`. Since the `autoreleasepool` is available only on Darwin platforms, the memory reducing effect may be not as significant on non-Darwin platforms (such as Linux or Windows). The following code demonstrates an example usage of the `TarWriter`: ```swift let handle: FileHandle = ... let writer = TarWriter(fileHandle: handle) try autoreleasepool { let entry: TarEntry = ... try writer.append(entry) } try writer.finalize() try handle.close() ``` Note that `TarWriter.finalize()` must be called after finishing appending entries to the container. In addition, closing the `FileHandle` remains the responsibility of the caller. */ public struct TarWriter { private let format: TarContainer.Format private let handle: FileHandle private var longNameCounter: UInt private var longLinkNameCounter: UInt private var localPaxHeaderCounter: UInt /** Creates a new instance for writing TAR entries using the specified `format` into the provided `fileHandle`. The `TarWriter` will be forced to use the provided `format`, meaning that certain properties of the `entries` may be missing from the output data if the chosen format does not support corresponding features. The default `.pax` format supports the largest set of features. Other (non-PAX) formats should only be used if you have a specific need for them and you understand limitations of those formats. - Parameter fileHandle: A handle into which the output will be written. Note that the `TarWriter` does not close the `fileHandle` and this remains the responsibility of the caller. - Parameter force: Force the usage of the specified format. - Important: `TarWriter.finalize()` must be called after all entries have been appended. */ public init(fileHandle: FileHandle, force format: TarContainer.Format = .pax) { self.handle = fileHandle self.format = format self.longNameCounter = 0 self.longLinkNameCounter = 0 self.localPaxHeaderCounter = 0 } /** Adds a new TAR entry at the end of the TAR container. On Darwin platforms it is recommended to wrap both the initialization of a `TarEntry` and the call to this function inside the `autoreleasepool` in order to reduce the peak memory usage. - Throws: Errors from the `FileHandle` operations. */ public mutating func append(_ entry: TarEntry) throws { var out = Data() if format == .gnu { if entry.info.name.utf8.count > 100 { let nameData = Data(entry.info.name.utf8) let longNameHeader = TarHeader(specialName: "SWC_LongName_\(longNameCounter)", specialType: .longName, size: nameData.count, uid: entry.info.ownerID, gid: entry.info.groupID) out.append(longNameHeader.generateContainerData(.gnu)) assert(out.count % 512 == 0) out.appendAsTarBlock(nameData) longNameCounter &+= 1 } if entry.info.linkName.utf8.count > 100 { let linkNameData = Data(entry.info.linkName.utf8) let longLinkNameHeader = TarHeader(specialName: "SWC_LongLinkName_\(longLinkNameCounter)", specialType: .longLinkName, size: linkNameData.count, uid: entry.info.ownerID, gid: entry.info.groupID) out.append(longLinkNameHeader.generateContainerData(.gnu)) assert(out.count % 512 == 0) out.appendAsTarBlock(linkNameData) longLinkNameCounter &+= 1 } } else if format == .pax { let extHeader = TarExtendedHeader(entry.info) let extHeaderData = extHeader.generateContainerData() if !extHeaderData.isEmpty { let extHeaderHeader = TarHeader(specialName: "SWC_LocalPaxHeader_\(localPaxHeaderCounter)", specialType: .localExtendedHeader, size: extHeaderData.count, uid: entry.info.ownerID, gid: entry.info.groupID) out.append(extHeaderHeader.generateContainerData(.pax)) assert(out.count % 512 == 0) out.appendAsTarBlock(extHeaderData) localPaxHeaderCounter &+= 1 } } let header = TarHeader(entry.info) out.append(header.generateContainerData(format)) assert(out.count % 512 == 0) try write(out) if let data = entry.data { try write(data) let paddingSize = data.count.roundTo512() - data.count try write(Data(count: paddingSize)) } } /** Finalizes the TAR container by adding an EOF marker. - Throws: Errors from the `FileHandle` operations. */ public func finalize() throws { // First, we append two 512-byte blocks consisting of zeros as an EOF marker. try write(Data(count: 1024)) // The synchronization is performed by the write(_:) function automatically. } private func write(_ data: Data) throws { try handle.write(contentsOf: data) try handle.synchronize() } }