Add save file option for profile sharing
Add "Save File" and "Save Content JSON" options to profile share menu, allowing users to save profiles directly to a chosen location using fileExporter instead of the system share sheet.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import Foundation
|
||||
|
||||
enum CRC32 {
|
||||
private static let table: [UInt32] = {
|
||||
var table = [UInt32](repeating: 0, count: 256)
|
||||
for i in 0 ..< 256 {
|
||||
var crc = UInt32(i)
|
||||
for _ in 0 ..< 8 {
|
||||
if crc & 1 != 0 {
|
||||
crc = (crc >> 1) ^ 0xEDB8_8320
|
||||
} else {
|
||||
crc = crc >> 1
|
||||
}
|
||||
}
|
||||
table[i] = crc
|
||||
}
|
||||
return table
|
||||
}()
|
||||
|
||||
static func checksum(_ data: Data, k: Int) -> UInt32 {
|
||||
var crc: UInt32 = 0xFFFF_FFFF
|
||||
for byte in data {
|
||||
let index = Int((crc ^ UInt32(byte)) & 0xFF)
|
||||
crc = (crc >> 8) ^ table[index]
|
||||
}
|
||||
return crc ^ UInt32(k) ^ 0xFFFF_FFFF
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import Foundation
|
||||
|
||||
struct EncodedBlock {
|
||||
var indices: [Int]
|
||||
var data: Data
|
||||
let k: Int
|
||||
let bytes: Int
|
||||
let checksum: UInt32
|
||||
|
||||
// Binary format: degree(4) + indices(4*n) + k(4) + bytes(4) + checksum(4) + data
|
||||
func toBinary() -> Data {
|
||||
var result = Data()
|
||||
|
||||
// Write degree (number of indices)
|
||||
var degree = UInt32(indices.count).littleEndian
|
||||
result.append(Data(bytes: °ree, count: 4))
|
||||
|
||||
// Write indices
|
||||
for index in indices {
|
||||
var idx = UInt32(index).littleEndian
|
||||
result.append(Data(bytes: &idx, count: 4))
|
||||
}
|
||||
|
||||
// Write k, bytes, checksum
|
||||
var kVal = UInt32(k).littleEndian
|
||||
var bytesVal = UInt32(bytes).littleEndian
|
||||
var checksumVal = checksum.littleEndian
|
||||
result.append(Data(bytes: &kVal, count: 4))
|
||||
result.append(Data(bytes: &bytesVal, count: 4))
|
||||
result.append(Data(bytes: &checksumVal, count: 4))
|
||||
|
||||
// Write data
|
||||
result.append(data)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static func fromBinary(_ binary: Data) -> EncodedBlock? {
|
||||
guard binary.count >= 16 else { return nil }
|
||||
|
||||
var offset = 0
|
||||
|
||||
let degree = binary.withUnsafeBytes {
|
||||
$0.load(fromByteOffset: offset, as: UInt32.self).littleEndian
|
||||
}
|
||||
offset += 4
|
||||
|
||||
guard binary.count >= 4 + Int(degree) * 4 + 12 else { return nil }
|
||||
|
||||
var indices: [Int] = []
|
||||
for _ in 0 ..< degree {
|
||||
let idx = binary.withUnsafeBytes {
|
||||
$0.load(fromByteOffset: offset, as: UInt32.self).littleEndian
|
||||
}
|
||||
indices.append(Int(idx))
|
||||
offset += 4
|
||||
}
|
||||
|
||||
let k = Int(binary.withUnsafeBytes {
|
||||
$0.load(fromByteOffset: offset, as: UInt32.self).littleEndian
|
||||
})
|
||||
offset += 4
|
||||
|
||||
let bytes = Int(binary.withUnsafeBytes {
|
||||
$0.load(fromByteOffset: offset, as: UInt32.self).littleEndian
|
||||
})
|
||||
offset += 4
|
||||
|
||||
let checksum = binary.withUnsafeBytes {
|
||||
$0.load(fromByteOffset: offset, as: UInt32.self).littleEndian
|
||||
}
|
||||
offset += 4
|
||||
|
||||
let data = binary.subdata(in: offset ..< binary.count)
|
||||
|
||||
return EncodedBlock(indices: indices, data: data, k: k, bytes: bytes, checksum: checksum)
|
||||
}
|
||||
|
||||
func toBase64() -> String {
|
||||
toBinary().base64EncodedString()
|
||||
}
|
||||
|
||||
static func fromBase64(_ string: String) -> EncodedBlock? {
|
||||
guard let data = Data(base64Encoded: string) else { return nil }
|
||||
return fromBinary(data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
import Compression
|
||||
import Foundation
|
||||
|
||||
final class LubyTransformDecoder {
|
||||
private(set) var decodedData: [Data?] = []
|
||||
private(set) var decodedCount = 0
|
||||
private(set) var encodedCount = 0
|
||||
private var encodedBlocks: Set<BlockWrapper> = []
|
||||
private var encodedBlockKeyMap: [String: BlockWrapper] = [:]
|
||||
private var encodedBlockSubkeyMap: [String: Set<BlockWrapper>] = [:]
|
||||
private var encodedBlockIndexMap: [Int: Set<BlockWrapper>] = [:]
|
||||
private var disposedEncodedBlocks: [Int: [() -> Void]] = [:]
|
||||
private(set) var meta: EncodedBlock?
|
||||
|
||||
var k: Int { meta?.k ?? 0 }
|
||||
var progress: Double {
|
||||
guard k > 0 else { return 0 }
|
||||
return Double(decodedCount) / Double(k)
|
||||
}
|
||||
|
||||
var isComplete: Bool { meta != nil && decodedCount == k }
|
||||
|
||||
private class BlockWrapper: Hashable {
|
||||
var block: EncodedBlock
|
||||
let id = UUID()
|
||||
|
||||
init(_ block: EncodedBlock) { self.block = block }
|
||||
|
||||
static func == (lhs: BlockWrapper, rhs: BlockWrapper) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
enum DecoderError: Error {
|
||||
case checksumMismatch
|
||||
case incomplete
|
||||
case noMeta
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func addBlock(_ block: EncodedBlock) throws -> Bool {
|
||||
if meta == nil {
|
||||
meta = block
|
||||
decodedData = Array(repeating: nil, count: block.k)
|
||||
}
|
||||
|
||||
guard block.checksum == meta?.checksum else {
|
||||
throw DecoderError.checksumMismatch
|
||||
}
|
||||
|
||||
encodedCount += 1
|
||||
|
||||
var mutableBlock = block
|
||||
mutableBlock.indices.sort()
|
||||
let wrapper = BlockWrapper(mutableBlock)
|
||||
propagateDecoded(key: indicesToKey(mutableBlock.indices), wrapper: wrapper)
|
||||
|
||||
return decodedCount == k
|
||||
}
|
||||
|
||||
private func indicesToKey(_ indices: [Int]) -> String {
|
||||
indices.map(String.init).joined(separator: ",")
|
||||
}
|
||||
|
||||
private func xorData(_ a: Data, _ b: Data) -> Data {
|
||||
var result = a
|
||||
let count = min(a.count, b.count)
|
||||
for i in 0 ..< count {
|
||||
result[i] ^= b[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func propagateDecoded(key: String, wrapper: BlockWrapper) {
|
||||
var block = wrapper.block
|
||||
var indices = block.indices
|
||||
var indicesSet = Set(indices)
|
||||
|
||||
if encodedBlockKeyMap[key] != nil || indices.allSatisfy({ decodedData[$0] != nil }) {
|
||||
return
|
||||
}
|
||||
|
||||
// XOR with already decoded blocks to reduce degree
|
||||
if indices.count > 1 {
|
||||
for index in indices {
|
||||
if let decoded = decodedData[index] {
|
||||
block.data = xorData(block.data, decoded)
|
||||
indicesSet.remove(index)
|
||||
}
|
||||
}
|
||||
if indicesSet.count != indices.count {
|
||||
indices = Array(indicesSet).sorted()
|
||||
block.indices = indices
|
||||
}
|
||||
}
|
||||
|
||||
// Try subset matching for blocks with degree > 2
|
||||
if indices.count > 2 {
|
||||
var subkeys: [(index: Int, subkey: String)] = []
|
||||
for index in indices {
|
||||
let subIndices = indices.filter { $0 != index }
|
||||
let subkey = indicesToKey(subIndices)
|
||||
if let subWrapper = encodedBlockKeyMap[subkey] {
|
||||
block.data = xorData(block.data, subWrapper.block.data)
|
||||
for i in subWrapper.block.indices {
|
||||
indicesSet.remove(i)
|
||||
}
|
||||
indices = Array(indicesSet).sorted()
|
||||
block.indices = indices
|
||||
subkeys.removeAll()
|
||||
break
|
||||
} else {
|
||||
subkeys.append((index, subkey))
|
||||
}
|
||||
}
|
||||
|
||||
// Store subkeys for future matching if still high degree
|
||||
if indicesSet.count > 1 {
|
||||
for (index, subkey) in subkeys {
|
||||
let dispose = { [weak self] in
|
||||
self?.encodedBlockSubkeyMap[subkey]?.remove(wrapper)
|
||||
}
|
||||
if encodedBlockSubkeyMap[subkey] == nil {
|
||||
encodedBlockSubkeyMap[subkey] = []
|
||||
}
|
||||
encodedBlockSubkeyMap[subkey]?.insert(wrapper)
|
||||
if disposedEncodedBlocks[index] == nil {
|
||||
disposedEncodedBlocks[index] = []
|
||||
}
|
||||
disposedEncodedBlocks[index]?.append(dispose)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wrapper.block = block
|
||||
|
||||
// If still degree > 1, store as pending
|
||||
if indices.count > 1 {
|
||||
encodedBlocks.insert(wrapper)
|
||||
for i in indices {
|
||||
if encodedBlockIndexMap[i] == nil {
|
||||
encodedBlockIndexMap[i] = []
|
||||
}
|
||||
encodedBlockIndexMap[i]?.insert(wrapper)
|
||||
}
|
||||
|
||||
let newKey = indicesToKey(indices)
|
||||
encodedBlockKeyMap[newKey] = wrapper
|
||||
|
||||
// Check if this can decode pending supersets
|
||||
if let superset = encodedBlockSubkeyMap[newKey] {
|
||||
encodedBlockSubkeyMap.removeValue(forKey: newKey)
|
||||
for superWrapper in superset {
|
||||
var superBlock = superWrapper.block
|
||||
superBlock.data = xorData(superBlock.data, block.data)
|
||||
var superIndicesSet = Set(superBlock.indices)
|
||||
for i in indices {
|
||||
superIndicesSet.remove(i)
|
||||
}
|
||||
superBlock.indices = Array(superIndicesSet).sorted()
|
||||
superWrapper.block = superBlock
|
||||
propagateDecoded(key: indicesToKey(superBlock.indices), wrapper: superWrapper)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Degree 1: directly decode
|
||||
else if let index = indices.first, decodedData[index] == nil {
|
||||
encodedBlocks.remove(wrapper)
|
||||
disposedEncodedBlocks[index]?.forEach { $0() }
|
||||
decodedData[index] = block.data
|
||||
decodedCount += 1
|
||||
|
||||
// Propagate to waiting blocks
|
||||
if let waitingBlocks = encodedBlockIndexMap[index] {
|
||||
encodedBlockIndexMap.removeValue(forKey: index)
|
||||
for waiting in waitingBlocks {
|
||||
let waitingKey = indicesToKey(waiting.block.indices)
|
||||
encodedBlockKeyMap.removeValue(forKey: waitingKey)
|
||||
propagateDecoded(key: waitingKey, wrapper: waiting)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getDecoded() throws -> Data {
|
||||
guard decodedCount == k else {
|
||||
throw DecoderError.incomplete
|
||||
}
|
||||
guard decodedData.allSatisfy({ $0 != nil }) else {
|
||||
throw DecoderError.incomplete
|
||||
}
|
||||
guard let meta else {
|
||||
throw DecoderError.noMeta
|
||||
}
|
||||
|
||||
let sliceSize = meta.data.count
|
||||
var result = Data(capacity: meta.bytes)
|
||||
|
||||
for (i, block) in decodedData.enumerated() {
|
||||
guard let block else { continue }
|
||||
let start = i * sliceSize
|
||||
let copyLength = min(sliceSize, meta.bytes - start)
|
||||
if copyLength > 0 {
|
||||
result.append(block.prefix(copyLength))
|
||||
}
|
||||
}
|
||||
|
||||
// Try decompression
|
||||
if let decompressed = Self.inflate(result) {
|
||||
let checksum = CRC32.checksum(decompressed, k: meta.k)
|
||||
if checksum == meta.checksum {
|
||||
return decompressed
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to uncompressed
|
||||
let checksum = CRC32.checksum(result, k: meta.k)
|
||||
if checksum == meta.checksum {
|
||||
return result
|
||||
}
|
||||
|
||||
throw DecoderError.checksumMismatch
|
||||
}
|
||||
|
||||
private static func inflate(_ data: Data) -> Data? {
|
||||
let sourceSize = data.count
|
||||
let destinationSize = sourceSize * 10
|
||||
|
||||
let destinationBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: destinationSize)
|
||||
defer { destinationBuffer.deallocate() }
|
||||
|
||||
let decompressedSize = data.withUnsafeBytes { sourcePtr -> Int in
|
||||
guard let baseAddress = sourcePtr.baseAddress else { return 0 }
|
||||
return compression_decode_buffer(
|
||||
destinationBuffer,
|
||||
destinationSize,
|
||||
baseAddress.assumingMemoryBound(to: UInt8.self),
|
||||
sourceSize,
|
||||
nil,
|
||||
COMPRESSION_ZLIB
|
||||
)
|
||||
}
|
||||
|
||||
guard decompressedSize > 0 else { return nil }
|
||||
return Data(bytes: destinationBuffer, count: decompressedSize)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import Compression
|
||||
import Foundation
|
||||
|
||||
final class LubyTransformEncoder {
|
||||
let k: Int
|
||||
let sliceSize: Int
|
||||
let checksum: UInt32
|
||||
let bytes: Int
|
||||
private let sourceBlocks: [Data]
|
||||
|
||||
init(data: Data, sliceSize: Int = 500, 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? {
|
||||
let sourceSize = data.count
|
||||
let destinationSize = sourceSize + 1024
|
||||
|
||||
let destinationBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: destinationSize)
|
||||
defer { destinationBuffer.deallocate() }
|
||||
|
||||
let compressedSize = data.withUnsafeBytes { sourcePtr -> Int in
|
||||
guard let baseAddress = sourcePtr.baseAddress else { return 0 }
|
||||
return compression_encode_buffer(
|
||||
destinationBuffer,
|
||||
destinationSize,
|
||||
baseAddress.assumingMemoryBound(to: UInt8.self),
|
||||
sourceSize,
|
||||
nil,
|
||||
COMPRESSION_ZLIB
|
||||
)
|
||||
}
|
||||
|
||||
guard compressedSize > 0 else { return nil }
|
||||
return Data(bytes: destinationBuffer, count: compressedSize)
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
for i in 0 ..< k {
|
||||
if random < cumulative[i] {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user