Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented Delta Fossil Compression #104

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions Example/SwiftCentrifuge.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
3A58470528C9CF4800D85C0E /* SwiftCentrifuge in Frameworks */ = {isa = PBXBuildFile; productRef = 3A58470428C9CF4800D85C0E /* SwiftCentrifuge */; };
3A451D4028D2EE33004F85A5 /* PrintLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A451D3F28D2EE33004F85A5 /* PrintLogger.swift */; };
3A58470528C9CF4800D85C0E /* SwiftCentrifuge in Frameworks */ = {isa = PBXBuildFile; productRef = 3A58470428C9CF4800D85C0E /* SwiftCentrifuge */; };
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; };
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; };
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; };
91D46D152CA2148500223592 /* DeltaFossilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D46D142CA2148500223592 /* DeltaFossilTests.swift */; };
91D46D312CA2216800223592 /* delta in Resources */ = {isa = PBXBuildFile; fileRef = 91D46D2E2CA2216800223592 /* delta */; };
91D46D322CA2216800223592 /* origin in Resources */ = {isa = PBXBuildFile; fileRef = 91D46D2F2CA2216800223592 /* origin */; };
91D46D332CA2216800223592 /* target in Resources */ = {isa = PBXBuildFile; fileRef = 91D46D302CA2216800223592 /* target */; };
DE2E2CA9220FEAC8004541DE /* BasicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7C1AA8220F0E1A0037AE1E /* BasicTests.swift */; };
DE7C1AA4220F0D950037AE1E /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE7C1AA3220F0D950037AE1E /* Launch Screen.storyboard */; };
/* End PBXBuildFile section */
Expand All @@ -35,6 +39,10 @@
607FACD71AFB9204008FA782 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
91D46D142CA2148500223592 /* DeltaFossilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeltaFossilTests.swift; sourceTree = "<group>"; };
91D46D2E2CA2216800223592 /* delta */ = {isa = PBXFileReference; lastKnownFileType = file; path = delta; sourceTree = "<group>"; };
91D46D2F2CA2216800223592 /* origin */ = {isa = PBXFileReference; lastKnownFileType = file; path = origin; sourceTree = "<group>"; };
91D46D302CA2216800223592 /* target */ = {isa = PBXFileReference; lastKnownFileType = file; path = target; sourceTree = "<group>"; };
DE2E2C9F220FEAC4004541DE /* SwiftCentrifuge_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftCentrifuge_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
DE2E2CA3220FEAC4004541DE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
DE7C1AA3220F0D950037AE1E /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -122,6 +130,10 @@
isa = PBXGroup;
children = (
DE7C1AA8220F0E1A0037AE1E /* BasicTests.swift */,
91D46D142CA2148500223592 /* DeltaFossilTests.swift */,
91D46D2E2CA2216800223592 /* delta */,
91D46D2F2CA2216800223592 /* origin */,
91D46D302CA2216800223592 /* target */,
);
name = SwiftCentrifugeTests;
path = ../../Tests/SwiftCentrifugeTests;
Expand Down Expand Up @@ -224,6 +236,9 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
91D46D312CA2216800223592 /* delta in Resources */,
91D46D322CA2216800223592 /* origin in Resources */,
91D46D332CA2216800223592 /* target in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -244,6 +259,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
91D46D152CA2148500223592 /* DeltaFossilTests.swift in Sources */,
DE2E2CA9220FEAC8004541DE /* BasicTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
28 changes: 24 additions & 4 deletions Sources/SwiftCentrifuge/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public class CentrifugeClient {
fileprivate var client: String?
fileprivate var token: String?
fileprivate var data: Data?
fileprivate var originalSourceData: Data? // to check and handle delta fossil implementation
fileprivate var commandId: UInt32 = 0
fileprivate var commandIdLock: NSLock = NSLock()
fileprivate var opCallbacks: [UInt32: ((CentrifugeResolveData) -> ())] = [:]
Expand Down Expand Up @@ -518,8 +519,8 @@ internal extension CentrifugeClient {
subscriptionsLock.unlock()
}

func subscribe(channel: String, token: String, data: Data?, recover: Bool, streamPosition: StreamPosition, positioned: Bool, recoverable: Bool, joinLeave: Bool, completion: @escaping (Centrifugal_Centrifuge_Protocol_SubscribeResult?, Error?)->()) {
self.sendSubscribe(channel: channel, token: token, data: data, recover: recover, streamPosition: streamPosition, positioned: positioned, recoverable: recoverable, joinLeave: joinLeave, completion: completion)
func subscribe(channel: String, token: String, delta: String?, data: Data?, recover: Bool, streamPosition: StreamPosition, positioned: Bool, recoverable: Bool, joinLeave: Bool, completion: @escaping (Centrifugal_Centrifuge_Protocol_SubscribeResult?, Error?)->()) {
self.sendSubscribe(channel: channel, token: token, delta: delta, data: data, recover: recover, streamPosition: streamPosition, positioned: positioned, recoverable: recoverable, joinLeave: joinLeave, completion: completion)
}

func reconnect(code: UInt32, reason: String) {
Expand Down Expand Up @@ -778,7 +779,22 @@ fileprivate extension CentrifugeClient {
if pub.hasInfo {
info = CentrifugeClientInfo(client: pub.info.client, user: pub.info.user, connInfo: pub.info.connInfo, chanInfo: pub.info.chanInfo)
}
let event = CentrifugePublicationEvent(data: pub.data, offset: pub.offset, tags: pub.tags, info: info)

var eventData: Data!

if let originalSourceData {
// Assuming the pub.data contains delta-encoded data
do {
let data = try DeltaFossil.applyDelta(source: originalSourceData, delta: pub.data)
eventData = data
} catch {
eventData = pub.data
}
} else {
eventData = pub.data
}
self.originalSourceData = eventData
let event = CentrifugePublicationEvent(data: eventData, offset: pub.offset, tags: pub.tags, info: info)
if pub.offset > 0 {
sub.setOffset(offset: pub.offset)
}
Expand Down Expand Up @@ -842,6 +858,7 @@ fileprivate extension CentrifugeClient {

private func handleSubscribe(channel: String, sub: Centrifugal_Centrifuge_Protocol_Subscribe) {
self.serverSubs[channel] = ServerSubscription(recoverable: sub.recoverable, offset: sub.offset, epoch: sub.epoch)
self.originalSourceData = nil // make sure data will be nil on a new subscription
let event = CentrifugeServerSubscribedEvent(channel: channel, wasRecovering: false, recovered: false, positioned: sub.positioned, recoverable: sub.recoverable, streamPosition: sub.positioned || sub.recoverable ? StreamPosition(offset: sub.offset, epoch: sub.epoch): nil, data: sub.data)
self.delegate?.onSubscribed(self, event)
}
Expand Down Expand Up @@ -1123,7 +1140,7 @@ fileprivate extension CentrifugeClient {
})
}

private func sendSubscribe(channel: String, token: String, data: Data?, recover: Bool, streamPosition: StreamPosition, positioned: Bool, recoverable: Bool, joinLeave: Bool, completion: @escaping (Centrifugal_Centrifuge_Protocol_SubscribeResult?, Error?)->()) {
private func sendSubscribe(channel: String, token: String, delta: String?, data: Data?, recover: Bool, streamPosition: StreamPosition, positioned: Bool, recoverable: Bool, joinLeave: Bool, completion: @escaping (Centrifugal_Centrifuge_Protocol_SubscribeResult?, Error?)->()) {
var req = Centrifugal_Centrifuge_Protocol_SubscribeRequest()
req.channel = channel
if recover {
Expand All @@ -1134,6 +1151,9 @@ fileprivate extension CentrifugeClient {
req.positioned = positioned
req.recoverable = recoverable
req.joinLeave = joinLeave
if let delta {
req.delta = delta
}
if data != nil {
req.data = data!
}
Expand Down
200 changes: 200 additions & 0 deletions Sources/SwiftCentrifuge/DeltaFossil.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
//
// DeltaFossil.swift
// iOSApp
//
// Created by Mehdi on 9/21/24.
//

import Foundation

public enum DeltaFossil {
private struct ZValue {
static let values: [Int8] = [
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1,
-1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, 36, -1, 37,
38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
57, 58, 59, 60, 61, 62, -1, -1, -1, 63, -1,
]
}

enum DeltaError: Error {
case outOfBounds
case sizeIntegerNotTerminated
case copyCommandNotTerminated
case copyExceedsOutputSize
case copyExtendsPastEnd
case insertExceedsOutputSize
case insertCountExceedsDelta
case unknownDeltaOperator
case unterminatedDelta
case badChecksum
case sizeMismatch
}

// Apply delta fossil algorithm
public static func applyDelta(source: Data, delta: Data) throws -> Data {
var total: UInt32 = 0
var deltaReader = Reader(array: [UInt8](delta)) // Convert Data to ByteArray for Reader
var outputWriter = Writer()

let sourceLength = source.count
let deltaLength = delta.count

let limit = try deltaReader.getInt()

guard try deltaReader.getChar() == "\n" else {
throw DeltaError.sizeIntegerNotTerminated
}

while deltaReader.haveBytes() {
let count = try deltaReader.getInt()
let command = try deltaReader.getChar()

switch command {
case "@":
let offset = try deltaReader.getInt()
guard try deltaReader.getChar() == "," else {
throw DeltaError.copyCommandNotTerminated
}
total += count
guard total <= limit else { throw DeltaError.copyExceedsOutputSize }
guard offset + count <= sourceLength else { throw DeltaError.copyExtendsPastEnd }
outputWriter.putArray([UInt8](source), start: Int(offset), end: Int(offset + count))

case ":":
total += count
guard total <= limit else { throw DeltaError.insertExceedsOutputSize }
guard count <= deltaLength else { throw DeltaError.insertCountExceedsDelta }
outputWriter.putArray(deltaReader.array, start: deltaReader.position, end: deltaReader.position + Int(count))
deltaReader.position += Int(count)

case ";":
let output = outputWriter.toByteArray()
guard count == checksum(array: output) else { throw DeltaError.badChecksum }
guard total == limit else { throw DeltaError.sizeMismatch }
return Data(output) // Convert ByteArray back to Data

default:
throw DeltaError.unknownDeltaOperator
}
}

throw DeltaError.unterminatedDelta
}

// Reader
private struct Reader {
let array: [UInt8]
var position: Int = 0

init(array: [UInt8]) {
self.array = array
}

func haveBytes() -> Bool {
return position < array.count
}

mutating func getByte() throws -> UInt8 {
guard position < array.count else { throw DeltaError.outOfBounds }
let byte = array[position]
position += 1
return byte
}

mutating func getChar() throws -> Character {
return Character(UnicodeScalar(try getByte()))
}

mutating func getInt() throws -> UInt32 {
var value: UInt32 = 0
while haveBytes() {
let byte = try getByte()
let c = ZValue.values[Int(0x7f & byte)]
if c < 0 {
position -= 1 // Adjust for invalid character
return value
}
value = (value << 6) + UInt32(c)
}
return value
}
}

// Writer
private struct Writer {
private var array: [UInt8] = []

func toByteArray() -> [UInt8] {
return array
}

mutating func putArray(_ array: [UInt8], start: Int, end: Int) {
self.array.append(contentsOf: array[start..<end])
}
}

// Checksum
private static func checksum(array: [UInt8]) -> UInt32 {
var sum0: UInt32 = 0
var sum1: UInt32 = 0
var sum2: UInt32 = 0
var sum3: UInt32 = 0
var z: Int = 0
var N = array.count

while N >= 16 {
sum0 = sum0 &+ UInt32(array[z + 0])
sum1 = sum1 &+ UInt32(array[z + 1])
sum2 = sum2 &+ UInt32(array[z + 2])
sum3 = sum3 &+ UInt32(array[z + 3])

sum0 = sum0 &+ UInt32(array[z + 4])
sum1 = sum1 &+ UInt32(array[z + 5])
sum2 = sum2 &+ UInt32(array[z + 6])
sum3 = sum3 &+ UInt32(array[z + 7])

sum0 = sum0 &+ UInt32(array[z + 8])
sum1 = sum1 &+ UInt32(array[z + 9])
sum2 = sum2 &+ UInt32(array[z + 10])
sum3 = sum3 &+ UInt32(array[z + 11])

sum0 = sum0 &+ UInt32(array[z + 12])
sum1 = sum1 &+ UInt32(array[z + 13])
sum2 = sum2 &+ UInt32(array[z + 14])
sum3 = sum3 &+ UInt32(array[z + 15])

z += 16
N -= 16
}

while N >= 4 {
sum0 = sum0 &+ UInt32(array[z + 0])
sum1 = sum1 &+ UInt32(array[z + 1])
sum2 = sum2 &+ UInt32(array[z + 2])
sum3 = sum3 &+ UInt32(array[z + 3])
z += 4
N -= 4
}

sum3 = (((sum3 &+ (sum2 << 8)) &+ (sum1 << 16)) &+ (sum0 << 24))

switch N {
case 3:
sum3 = sum3 &+ (UInt32(array[z + 2]) << 8)
fallthrough // Allow fallthrough for the next case
case 2:
sum3 = sum3 &+ (UInt32(array[z + 1]) << 16)
fallthrough // Allow fallthrough for the next case
case 1:
sum3 = sum3 &+ (UInt32(array[z + 0]) << 24)
default:
break
}

return sum3
}
}
Loading
Loading