207 lines
7.0 KiB
Swift
207 lines
7.0 KiB
Swift
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<z_stream>.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<Int>()
|
|
while indices.count < degree {
|
|
indices.insert(Int.random(in: 0 ..< k))
|
|
}
|
|
return Array(indices)
|
|
}
|
|
|
|
func fountain() -> AnyIterator<EncodedBlock> {
|
|
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
|
|
}
|