Files
2026-01-31 15:30:24 +08:00

221 lines
8.3 KiB
Swift

// Copyright (c) 2026 Timofey Solomko
// Licensed under MIT License
//
// See LICENSE for license information
import Foundation
import BitByteData
/// Provides unarchive function for XZ archives.
public class XZArchive: Archive {
/**
Unarchives XZ archive. Archives with multiple streams are supported,
but uncompressed data from each stream will be combined into single `Data` object.
If an error happens during LZMA2 decompression, then `LZMAError` or `LZMA2Error` will be thrown.
- Parameter archive: Data archived using XZ format.
- Throws: `LZMAError`, `LZMA2Error` or `XZError` depending on the type of the problem.
Particularly, if filters other than LZMA2 are used in archive, then `XZError.wrongFilterID` will be thrown,
but it may also indicate that either the archive is damaged or
it might not be compressed with XZ or LZMA(2) at all.
- Returns: Unarchived data.
*/
public static func unarchive(archive data: Data) throws -> Data {
/// Object with input data which supports convenient work with bytes.
let byteReader = LittleEndianByteReader(data: data)
// Note: We don't check footer's magic bytes at the beginning,
// because it is impossible to determine the end of each stream in multi-stream archives
// without fully processing them, and checking last stream's footer doesn't
// guarantee correctness of other streams.
var result = Data()
while !byteReader.isFinished {
// Valid XZ archive must contain at least 32 bytes of data.
guard byteReader.bytesLeft >= 32
else { throw XZError.wrongMagic }
let streamResult = try processStream(byteReader)
result.append(streamResult.data)
guard !streamResult.checkError
else { throw XZError.wrongCheck([result]) }
try processPadding(byteReader)
}
return result
}
/**
Unarchives XZ archive. Archives with multiple streams are supported,
and uncompressed data from each stream will be stored in a separate element in the array
If data passed is not actually XZ archive, `XZError` will be thrown.
Particularly, if filters other than LZMA2 are used in archive, then `XZError.wrongFilterID` will be thrown.
If an error happens during LZMA2 decompression, then `LZMAError` or `LZMA2Error` will be thrown.
- Parameter archive: Data archived using XZ format.
- Throws: `LZMAError`, `LZMA2Error` or `XZError` depending on the type of the problem.
It may indicate that either the archive is damaged or it might not be compressed with XZ or LZMA(2) at all.
- Returns: Array of unarchived data from every stream in archive.
*/
public static func splitUnarchive(archive data: Data) throws -> [Data] {
// Same code as in `unarchive(archive:)` but with different type of `result`.
let byteReader = LittleEndianByteReader(data: data)
var result = [Data]()
while !byteReader.isFinished {
// Valid XZ archive must contain at least 32 bytes of data.
guard byteReader.bytesLeft >= 32
else { throw XZError.wrongMagic }
let streamResult = try processStream(byteReader)
result.append(streamResult.data)
guard !streamResult.checkError
else { throw XZError.wrongCheck(result) }
try processPadding(byteReader)
}
return result
}
private static func processStream(_ byteReader: LittleEndianByteReader) throws -> (data: Data, checkError: Bool) {
var out = Data()
let streamHeader = try XZStreamHeader(byteReader)
// BLOCKS AND INDEX
var blockInfos: [(unpaddedSize: Int, uncompSize: Int)] = []
var indexSize = -1
while true {
let blockHeaderSize = byteReader.byte()
if blockHeaderSize == 0 { // Zero value of blockHeaderSize means that we've encountered the index.
indexSize = try processIndex(blockInfos, byteReader)
break
} else {
let block = try XZBlock(blockHeaderSize, byteReader, streamHeader.checkType.size)
out.append(block.data)
switch streamHeader.checkType {
case .none:
break
case .crc32:
let check = byteReader.uint32()
guard CheckSums.crc32(block.data) == check
else { return (out, true) }
case .crc64:
let check = byteReader.uint64()
guard CheckSums.crc64(block.data) == check
else { return (out, true) }
case .sha256:
let check = byteReader.bytes(count: 32)
guard Sha256.hash(data: block.data) == check
else { return (out, true) }
}
blockInfos.append((block.unpaddedSize, block.uncompressedSize))
}
}
// STREAM FOOTER
try processFooter(streamHeader, indexSize, byteReader)
return (out, false)
}
private static func processIndex(_ blockInfos: [(unpaddedSize: Int, uncompSize: Int)],
_ byteReader: LittleEndianByteReader) throws -> Int {
let indexStartIndex = byteReader.offset - 1
let recordsCount = try byteReader.multiByteDecode()
guard recordsCount == blockInfos.count
else { throw XZError.wrongField }
for blockInfo in blockInfos {
let unpaddedSize = try byteReader.multiByteDecode()
guard unpaddedSize == blockInfo.unpaddedSize
else { throw XZError.wrongField }
let uncompSize = try byteReader.multiByteDecode()
guard uncompSize == blockInfo.uncompSize
else { throw XZError.wrongDataSize }
}
var indexSize = byteReader.offset - indexStartIndex
if indexSize % 4 != 0 {
let paddingSize = 4 - indexSize % 4
for _ in 0..<paddingSize {
let byte = byteReader.byte()
guard byte == 0x00
else { throw XZError.wrongPadding }
indexSize += 1
}
}
let indexCRC = byteReader.uint32()
byteReader.offset = indexStartIndex
guard CheckSums.crc32(byteReader.bytes(count: indexSize)) == indexCRC
else { throw XZError.wrongInfoCRC }
byteReader.offset += 4
return indexSize + 4
}
private static func processFooter(_ streamHeader: XZStreamHeader, _ indexSize: Int,
_ byteReader: LittleEndianByteReader) throws {
let footerCRC = byteReader.uint32()
/// Indicates the size of Index field. Should match its real size.
let backwardSize = (byteReader.int(fromBytes: 4) + 1) * 4
let streamFooterFlags = byteReader.int(fromBytes: 2)
byteReader.offset -= 6
guard CheckSums.crc32(byteReader.bytes(count: 6)) == footerCRC
else { throw XZError.wrongInfoCRC }
guard backwardSize == indexSize
else { throw XZError.wrongField }
// Flags in the footer should be the same as in the header.
guard streamFooterFlags & 0xFF == 0 &&
(streamFooterFlags & 0xF00) >> 8 == streamHeader.checkType.rawValue &&
streamFooterFlags & 0xF000 == 0
else { throw XZError.wrongField }
// Check footer's magic number
guard byteReader.bytes(count: 2) == [0x59, 0x5A]
else { throw XZError.wrongMagic }
}
private static func processPadding(_ byteReader: LittleEndianByteReader) throws {
guard !byteReader.isFinished
else { return }
var paddingBytes = 0
while true {
let byte = byteReader.byte()
if byte != 0 {
if paddingBytes % 4 != 0 {
throw XZError.wrongPadding
} else {
break
}
}
if byteReader.isFinished {
if byte != 0 || paddingBytes % 4 != 3 {
throw XZError.wrongPadding
} else {
return
}
}
paddingBytes += 1
}
byteReader.offset -= 1
}
}