import Foundation import zlib final class LubyTransformEncoder { let k: Int let sliceSize: Int let checksum: UInt32 let bytes: Int private let sourceBlocks: [Data] init(data: Data, sliceSize: Int = 512, compress: Bool = true) { self.sliceSize = sliceSize let compressed: Data if compress { compressed = Self.deflateCompress(data) ?? data } else { compressed = data } bytes = compressed.count sourceBlocks = Self.sliceData(compressed, sliceSize: sliceSize) k = sourceBlocks.count checksum = CRC32.checksum(data, k: k) } private static func deflateCompress(_ data: Data) -> Data? { var stream = z_stream() // Use 15 for zlib format (with header/trailer) to match pako's default guard deflateInit2_( &stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15, 8, Z_DEFAULT_STRATEGY, ZLIB_VERSION, Int32(MemoryLayout.size) ) == Z_OK else { return nil } defer { deflateEnd(&stream) } let destSize = Int(deflateBound(&stream, UInt(data.count))) var dest = Data(count: destSize) let result = data.withUnsafeBytes { srcPtr -> Int32 in dest.withUnsafeMutableBytes { destPtr -> Int32 in stream.next_in = UnsafeMutablePointer(mutating: srcPtr.bindMemory(to: Bytef.self).baseAddress) stream.avail_in = uInt(data.count) stream.next_out = destPtr.bindMemory(to: Bytef.self).baseAddress stream.avail_out = uInt(destSize) return deflate(&stream, Z_FINISH) } } guard result == Z_STREAM_END else { return nil } dest.count = Int(stream.total_out) return dest } private static func sliceData(_ data: Data, sliceSize: Int) -> [Data] { var blocks: [Data] = [] var offset = 0 while offset < data.count { let end = min(offset + sliceSize, data.count) var block = data.subdata(in: offset ..< end) if block.count < sliceSize { block.append(Data(count: sliceSize - block.count)) } blocks.append(block) offset += sliceSize } return blocks } func createBlock(indices: [Int]) -> EncodedBlock { var result = Data(count: sliceSize) for index in indices { let source = sourceBlocks[index] for i in 0 ..< sliceSize { result[i] ^= source[i] } } return EncodedBlock( indices: indices, data: result, k: k, bytes: bytes, checksum: checksum ) } /// Ideal Soliton Distribution for degree selection private func getRandomDegree() -> Int { guard k > 1 else { return k } var probabilities = [Double](repeating: 0, count: k) probabilities[0] = 1.0 / Double(k) for d in 2 ... k { probabilities[d - 1] = 1.0 / Double(d * (d - 1)) } var cumulative = [Double](repeating: 0, count: k) cumulative[0] = probabilities[0] for i in 1 ..< k { cumulative[i] = cumulative[i - 1] + probabilities[i] } let random = Double.random(in: 0 ... 1) if let i = cumulative.firstIndex(where: { random < $0 }) { return i + 1 } return k } private func getRandomIndices(degree: Int) -> [Int] { var indices = Set() while indices.count < degree { indices.insert(Int.random(in: 0 ..< k)) } return Array(indices) } func fountain() -> AnyIterator { AnyIterator { let degree = self.getRandomDegree() let indices = self.getRandomIndices(degree: degree) return self.createBlock(indices: indices) } } #if DEBUG static func runSelfTest() -> Bool { // Test 1: Round-trip tests with various sizes let testCases: [(size: Int, sliceSize: Int)] = [ (1, 100), (100, 100), (1000, 100), (1031, 100), ] for (size, sliceSize) in testCases { let data = Data((0 ..< size).map { UInt8($0 % 256) }) let encoder = LubyTransformEncoder(data: data, sliceSize: sliceSize, compress: true) let decoder = LubyTransformDecoder() var blockCount = 0 for block in encoder.fountain() { blockCount += 1 if blockCount > encoder.k * 3 { print("LubyTransform self-test FAILED: too many blocks for size=\(size)") return false } do { if try decoder.addBlock(block) { break } } catch { print("LubyTransform self-test FAILED: \(error)") return false } } do { let decoded = try decoder.getDecoded() if decoded != data { print("LubyTransform self-test FAILED: data mismatch for size=\(size)") return false } } catch { print("LubyTransform self-test FAILED: \(error)") return false } } // Test 2: TypeScript compatibility (decode blocks generated by TypeScript) // Test vector: 5 bytes [0xAB, 0xCD, 0xEF, 0x12, 0x34], uncompressed, sliceSize=10 let tsBlockBase64 = "AQAAAAAAAAABAAAABQAAALt8DL6rze8SNAAAAAAA" let expectedData = Data([0xAB, 0xCD, 0xEF, 0x12, 0x34]) guard let blockData = Data(base64Encoded: tsBlockBase64), let block = EncodedBlock.fromBinary(blockData) else { print("LubyTransform self-test FAILED: cannot parse TypeScript block") return false } let tsDecoder = LubyTransformDecoder() do { _ = try tsDecoder.addBlock(block) let decoded = try tsDecoder.getDecoded() if decoded != expectedData { print("LubyTransform self-test FAILED: TypeScript compatibility mismatch") print("Expected: \(expectedData.map { String(format: "%02X", $0) }.joined())") print("Got: \(decoded.map { String(format: "%02X", $0) }.joined())") return false } } catch { print("LubyTransform self-test FAILED: TypeScript decode error: \(error)") return false } print("LubyTransform self-test PASSED (including TypeScript compatibility)") return true } #endif }