Files
2026-02-18 12:21:57 +08:00

264 lines
12 KiB
Swift

// Copyright (c) 2026 Timofey Solomko
// Licensed under MIT License
//
// See LICENSE for license information
import Foundation
/// Provides functions for work with TAR containers.
public class TarContainer: Container {
/**
Represents the "format" of a TAR container: a minimal set of extensions to basic TAR format required to
successfully read a particular container.
*/
public enum Format {
/// Pre POSIX format (aka "basic TAR format").
case prePosix
/// "UStar" format introduced by POSIX IEEE P1003.1 standard.
case ustar
/// "UStar"-like format with GNU extensions (e.g. special container entries for long file and link names).
case gnu
/// "PAX" format introduced by POSIX.1-2001 standard, a set of extensions to "UStar" format.
case pax
}
/**
Processes TAR container and returns its "format": a minimal set of extensions to basic TAR format required to
successfully read this container.
- Parameter container: TAR container's data.
- Throws: `TarError`, which may indicate that either container is damaged or it might not be TAR container at all.
- SeeAlso: `TarContainer.Format`
*/
public static func formatOf(container data: Data) throws -> Format {
var parser = TarParser(data)
var ustarEncountered = false
parsingLoop: while true {
let result = try parser.next()
switch result {
case .specialEntry(let specialEntryType):
if specialEntryType == .globalExtendedHeader || specialEntryType == .localExtendedHeader {
return .pax
} else if specialEntryType == .longName || specialEntryType == .longLinkName {
return .gnu
}
case .entryInfo(_, _, let headerFormat):
switch headerFormat {
case .pax:
fatalError("Unexpected format of basic header: pax")
case .gnu:
return .gnu
case .ustar:
ustarEncountered = true
case .prePosix:
break
}
case .truncated:
// We don't have an error with a more suitable name.
throw TarError.tooSmallFileIsPassed
case .finished:
fallthrough
case .eofMarker:
break parsingLoop
}
}
return ustarEncountered ? .ustar : .prePosix
}
/**
Creates a new TAR container with `entries` as its content and generates its `Data`.
- Parameter entries: TAR entries to store in the container.
- SeeAlso: `TarEntryInfo` properties documenation to see how their values are connected with the specific TAR
format used during container creation.
*/
public static func create(from entries: [TarEntry]) -> Data {
return create(from: entries, force: .pax)
}
/**
Creates a new TAR container with `entries` as its content and generates its `Data` using the specified `format`.
This function forces the usage of the `format`, meaning that certain properties of the `entries` may be missing
from the resulting container data if the chosen format does not support corresponding features. For example,
relatively long names (and linknames) will be truncated if the `.ustar` or `.prePosix` format is specified.
It is highly recommended to use the `TarContainer.create(from:)` function (or use the `.pax` format) to ensure the
best representation of the `entries` in the output. Other (non-PAX) formats should only be used if you have a
specific need for them and you understand limitations of those formats.
- Parameter entries: TAR entries to store in the container.
- Parameter force: Force the usage of the specified format.
- SeeAlso: `TarEntryInfo` properties documenation to see how their values are connected with the specific TAR
format used during container creation.
*/
public static func create(from entries: [TarEntry], force format: TarContainer.Format) -> Data {
// The general strategy is as follows. For each entry we:
// 1. Create special entries if required by the entry's info and if supported by the format.
// 2. For each special entry we create a TarHeader.
// 3. For each TarHeader we generate binary data, and then append it with the content of the special entry to
// the output.
// 4. Perform the previous two steps for the entry itself.
// Every time we append something to the output we also make sure that the data is padded to 512 byte-long blocks.
// In theory if the counters are big enough, the names of the special entries can become long enough to cause
// problems (truncation, etc.). In practice, the largest possible counter (UInt.max) is 20 symbols long, which
// when combined with the longest used special entry name can never cause any problems, since it is still
// shorter then 99 symbols available in the "name" field of the TAR header.
// However, if in the distant future Int.max becomes large enough to cause any issues (e.g. 128-bit and higher
// integers), the following check will catch it.
assert(String(UInt.max).count < 100 - 19) // "SWC_LocalPaxHeader_".count == 19
// We also use &+ when incrementing counters to prevent integer overflow crashes: we rather deal with the special
// entries having repeating names, then crash the program.
var longNameCounter = 0 as UInt
var longLinkNameCounter = 0 as UInt
var localPaxHeaderCounter = 0 as UInt
var out = Data()
for entry in entries {
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)
if let data = entry.data {
out.appendAsTarBlock(data)
}
}
// Two 512-byte blocks consisting of zeros as an EOF marker.
out.append(Data(count: 1024))
return out
}
/**
Processes TAR container and returns an array of `TarEntry` with information and data for all entries.
- Important: The order of entries is defined by TAR container and, particularly, by the creator of a given TAR
container. It is likely that directories will be encountered earlier than files stored in those directories, but no
particular order is guaranteed.
- Parameter container: TAR container's data.
- Throws: `TarError`, which may indicate that either container is damaged or it might not be TAR container at all.
- Returns: Array of `TarEntry`.
*/
public static func open(container data: Data) throws -> [TarEntry] {
var parser = TarParser(data)
var entries = [TarEntry]()
parsingLoop: while true {
let result = try parser.next()
switch result {
case .specialEntry:
continue parsingLoop
case .entryInfo(let info, let blockStartIndex, _):
if info.type == .directory {
var entry = TarEntry(info: info, data: nil)
entry.info.size = 0
entries.append(entry)
} else {
let dataStartIndex = blockStartIndex + 512
let dataEndIndex = dataStartIndex + info.size!
// Verify that data is not truncated.
// The data.startIndex inequality is strict since by this point at least one header (i.e. 512 bytes)
// has been processed. The data.endIndex inequality is strict since there can be a 1024 bytes-long EOF
// marker block which isn't included into any entry.
guard dataStartIndex > data.startIndex && dataEndIndex < data.endIndex
else { throw TarError.tooSmallFileIsPassed }
let entryData = data.subdata(in: dataStartIndex..<dataEndIndex)
entries.append(TarEntry(info: info, data: entryData))
}
case .truncated:
// We don't have an error with a more suitable name.
throw TarError.tooSmallFileIsPassed
case .finished:
fallthrough
case .eofMarker:
break parsingLoop
}
}
return entries
}
/**
Processes TAR container and returns an array of `TarEntryInfo` with information about entries in this container.
- Important: The order of entries is defined by TAR container and, particularly, by the creator of a given TAR
container. It is likely that directories will be encountered earlier than files stored in those directories, but no
particular order is guaranteed.
- Parameter container: TAR container's data.
- Throws: `TarError`, which may indicate that either container is damaged or it might not be TAR container at all.
- Returns: Array of `TarEntryInfo`.
*/
public static func info(container data: Data) throws -> [TarEntryInfo] {
var parser = TarParser(data)
var entries = [TarEntryInfo]()
parsingLoop: while true {
let result = try parser.next()
switch result {
case .specialEntry:
continue parsingLoop
case .entryInfo(let info, _, _):
entries.append(info)
case .truncated:
// We don't have an error with a more suitable name.
throw TarError.tooSmallFileIsPassed
case .finished:
fallthrough
case .eofMarker:
break parsingLoop
}
}
return entries
}
}