diff --git a/Sources/LZ4/LZ4+Compress.swift b/Sources/LZ4/LZ4+Compress.swift index ff77d8d1..a55fb986 100644 --- a/Sources/LZ4/LZ4+Compress.swift +++ b/Sources/LZ4/LZ4+Compress.swift @@ -93,7 +93,7 @@ extension LZ4: CompressionAlgorithm { fatalError("Patalogical size of compressed block.") } let blockSize = UInt32(truncatingIfNeeded: compressedBlock.count) - for i:UInt32 in 0..<4 { + for i: UInt32 in 0..<4 { out.append(UInt8(truncatingIfNeeded: (blockSize & (0xFF << (i * 8))) >> (i * 8))) } out.append(contentsOf: compressedBlock) @@ -122,8 +122,129 @@ extension LZ4: CompressionAlgorithm { } private static func compress(block: Data, _ dict: Data?) -> [UInt8] { - // TODO: - return block.withUnsafeBytes { $0.map { $0 } } + var out = dict?.withUnsafeBytes { $0.map { $0 } } ?? [UInt8]() + let outStartIndex = out.endIndex + + let blockBytes = block.withUnsafeBytes { $0.map { $0 } } + var matchStorage = [UInt32: Int]() + + /// Literals of the currently constructed sequence. + /// If the array isn't empty this indicates that there is an in-progress sequence. + var currentLiterals = [UInt8]() + var i = blockBytes.startIndex + + // Match searching algorithm is mostly the same as the one that we use for Deflate. + + while i < blockBytes.endIndex - 5 { + let matchCrc = CheckSums.crc32(Data(blockBytes[i.. i { + repeatIndex = matchStartIndex + 1 + } + } + + if blockBytes.endIndex - distance < 12 { + // The last match must start at least 12 bytes before the end of block. + break + } + + // Writing a sequence. + // We start by constructing a token. + var token = UInt8(truncatingIfNeeded: min(15, currentLiterals.count)) << 4 + token |= UInt8(truncatingIfNeeded: min(15, matchLength - 4)) + out.append(token) + // Then we output additional bytes of the literals count. + var literalsCount = currentLiterals.count - 15 + while literalsCount >= 0 { + // TODO: Maybe this can be simplified with some fancy math expression. + if literalsCount > 255 { + out.append(255) + } else { + out.append(UInt8(truncatingIfNeeded: literalsCount)) + } + literalsCount -= 255 + } + for literal in currentLiterals { + out.append(literal) + } + // Next we write the distance ("offset" in LZ4 terms) as little-endian UInt16 number. + out.append(UInt8(truncatingIfNeeded: distance & 0xFF)) + out.append(UInt8(truncatingIfNeeded: (distance >> 8) & 0xFF)) + // Finally, we output match length in the same as we did for literals count. + // But before that we need to skip the entire match in the input. + i += matchLength + matchLength -= 19 // 4 (min match length) + 15 (token value) + while matchLength >= 0 { + // TODO: Maybe this can be simplified with some fancy math expression. + if matchLength > 255 { + out.append(255) + } else { + out.append(UInt8(truncatingIfNeeded: matchLength)) + } + matchLength -= 255 + } + currentLiterals = [UInt8]() + } + + // The last 5 bytes should be processed as literals. They will be either included in the unfinished sequence, + // or they will form a new sequence. This also covers the case when the size of input is less than 5 bytes. + if blockBytes.endIndex - i >= 5 { + while i < blockBytes.endIndex { + currentLiterals.append(blockBytes[i]) + i += 1 + } + } + + // It may happen that we haven't found any matches, so no sequences have been written to the output yet. + // In this case we need to write a sequence into the output that contains the entire input as literals. + if currentLiterals.count > 0 { + out.append(UInt8(truncatingIfNeeded: min(15, currentLiterals.count)) << 4) + var literalsCount = currentLiterals.count - 15 + while literalsCount >= 0 { + // TODO: Maybe this can be simplified with some fancy math expression. + if literalsCount > 255 { + out.append(255) + } else { + out.append(UInt8(truncatingIfNeeded: literalsCount)) + } + literalsCount -= 255 + } + for literal in currentLiterals { + out.append(literal) + } + } + + return Array(out[outStartIndex...]) } }