Files

82 lines
3.2 KiB
Swift

// Copyright (c) 2026 Timofey Solomko
// Licensed under MIT License
//
// See LICENSE for license information
import Foundation
import BitByteData
// While it is tempting to make the parser conform to `IteratorProtocol` and `Sequence` protocols, it is in fact
// impossible to do so, since `TarHeader.init(_)` is throwing and `IteratorProtocol.next()` cannot be throwing.
struct TarParser {
enum ParsingResult {
case truncated
case eofMarker
case finished
case specialEntry(TarHeader.SpecialEntryType)
case entryInfo(TarEntryInfo, Int, TarContainer.Format)
}
private let reader: LittleEndianByteReader
private var lastGlobalExtendedHeader: TarExtendedHeader?
private var lastLocalExtendedHeader: TarExtendedHeader?
private var longLinkName: String?
private var longName: String?
init(_ data: Data) {
self.reader = LittleEndianByteReader(data: data)
self.lastGlobalExtendedHeader = nil
self.lastLocalExtendedHeader = nil
self.longLinkName = nil
self.longName = nil
}
mutating func next() throws -> ParsingResult {
if reader.isFinished {
return .finished
} else if reader.bytesLeft >= 1024 && reader.data[reader.offset..<reader.offset + 1024] == Data(count: 1024) {
return .eofMarker
} else if reader.bytesLeft < 512 {
return .truncated
}
let header = try TarHeader(reader)
// For header we read at most 512 bytes.
assert(reader.offset - header.blockStartIndex <= 512)
// Check, just in case, since we use blockStartIndex = -1 when creating TAR containers.
assert(header.blockStartIndex >= 0)
let dataStartIndex = header.blockStartIndex + 512
if case .special(let specialEntryType) = header.type {
switch specialEntryType {
case .globalExtendedHeader:
let dataEndIndex = dataStartIndex + header.size
lastGlobalExtendedHeader = try TarExtendedHeader(reader.data[dataStartIndex..<dataEndIndex])
case .sunExtendedHeader:
fallthrough
case .localExtendedHeader:
let dataEndIndex = dataStartIndex + header.size
lastLocalExtendedHeader = try TarExtendedHeader(reader.data[dataStartIndex..<dataEndIndex])
case .longLinkName:
reader.offset = dataStartIndex
longLinkName = reader.tarCString(maxLength: header.size)
case .longName:
reader.offset = dataStartIndex
longName = reader.tarCString(maxLength: header.size)
}
reader.offset = dataStartIndex + header.size.roundTo512()
return .specialEntry(specialEntryType)
} else {
let info = TarEntryInfo(header, lastGlobalExtendedHeader, lastLocalExtendedHeader, longName, longLinkName)
// Skip file data.
reader.offset = dataStartIndex + header.size.roundTo512()
lastLocalExtendedHeader = nil
longName = nil
longLinkName = nil
return .entryInfo(info, header.blockStartIndex, header.format)
}
}
}