From a55177ab19e1dc361611f8ab9d1dd7955856a02d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel=20D=C3=ADaz?= Date: Fri, 12 Jul 2024 00:46:59 +0200 Subject: [PATCH 1/4] expose heads raw that represent an arbitrary document's state minor --- Sources/Automerge/ChangeHash.swift | 12 +++++++++ Sources/Automerge/Document.swift | 11 +++++++++ Tests/AutomergeTests/TestChanges.swift | 34 ++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/Sources/Automerge/ChangeHash.swift b/Sources/Automerge/ChangeHash.swift index 43dd836f..cc2055fe 100644 --- a/Sources/Automerge/ChangeHash.swift +++ b/Sources/Automerge/ChangeHash.swift @@ -9,3 +9,15 @@ public struct ChangeHash: Equatable, Hashable, CustomDebugStringConvertible, Sen bytes.map { String(format: "%02hhx", $0) }.joined() } } + +public extension Set { + + /// Transforms each `ChangeHash` in the set into its byte array (`[UInt8]`). This raw byte representation + /// captures the state of the document at a specific point in its history, allowing for efficient storage + /// and retrieval of document states. + func raw() -> [[UInt8]] { + map(\.bytes).sorted { lhs, rhs in + lhs[0] > rhs[0] + } + } +} diff --git a/Sources/Automerge/Document.swift b/Sources/Automerge/Document.swift index 58fa1b8b..71504975 100644 --- a/Sources/Automerge/Document.swift +++ b/Sources/Automerge/Document.swift @@ -1035,6 +1035,17 @@ public final class Document: @unchecked Sendable { } } + /// Returns the related set of changes of a state representation within an Automerge document. + public func heads(raw: [[UInt8]]) -> Set? { + var output: Set = [] + for bytes in raw { + guard let changeHash = change(hash: ChangeHash(bytes: bytes))?.hash + else { return nil } + output.insert(changeHash) + } + return output + } + /// Generates patches between two points in the document history. /// /// Use: diff --git a/Tests/AutomergeTests/TestChanges.swift b/Tests/AutomergeTests/TestChanges.swift index c5acccea..07ee4c34 100644 --- a/Tests/AutomergeTests/TestChanges.swift +++ b/Tests/AutomergeTests/TestChanges.swift @@ -84,4 +84,38 @@ class ChangeSetTests: XCTestCase { XCTAssertEqual(patches1.count, 1) XCTAssertEqual(patches1, patches2) } + + func testRelationBetweenChangeHashAndRaw() throws { + let doc = Document() + let textId = try! doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text) + let doc1 = doc.fork() + try doc.spliceText(obj: textId, start: 0, delete: 0, value: "Hello") + try doc1.spliceText(obj: textId, start: 0, delete: 0, value: " World!") + try doc.merge(other: doc1) + + let heads = doc.heads() + let restored = doc.heads(raw: heads.raw()) + + XCTAssertEqual(heads, restored) + } + + func testChangeHash_WhenRawIsManipulated_DocumentDoesNotAccept() throws { + let doc = Document() + let textId = try! doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text) + let doc1 = doc.fork() + try doc.spliceText(obj: textId, start: 0, delete: 0, value: "Hello") + try doc1.spliceText(obj: textId, start: 0, delete: 0, value: " World!") + try doc.merge(other: doc1) + + let headsRaw = doc.heads().raw().map { raw in + var raw = raw + raw[0] = 0 + raw[7] = 0 + raw[14] = 0 + return raw + } + let restored = doc.heads(raw: headsRaw) + + XCTAssertNil(restored) + } } From 5fa398804b74199dc99be71e1388b454940a0fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel=20D=C3=ADaz?= Date: Fri, 12 Jul 2024 08:43:15 +0200 Subject: [PATCH 2/4] making heads raw encode into opaque Data type --- Sources/Automerge/ChangeHash.swift | 25 ++++++++++++++++++++++--- Sources/Automerge/Document.swift | 11 ----------- Tests/AutomergeTests/TestChanges.swift | 25 ++++++++++++------------- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/Sources/Automerge/ChangeHash.swift b/Sources/Automerge/ChangeHash.swift index cc2055fe..f774c27c 100644 --- a/Sources/Automerge/ChangeHash.swift +++ b/Sources/Automerge/ChangeHash.swift @@ -1,4 +1,5 @@ import AutomergeUniffi +import Foundation /// An opaque hash that represents a change within an Automerge document. public struct ChangeHash: Equatable, Hashable, CustomDebugStringConvertible, Sendable { @@ -15,9 +16,27 @@ public extension Set { /// Transforms each `ChangeHash` in the set into its byte array (`[UInt8]`). This raw byte representation /// captures the state of the document at a specific point in its history, allowing for efficient storage /// and retrieval of document states. - func raw() -> [[UInt8]] { - map(\.bytes).sorted { lhs, rhs in - lhs[0] > rhs[0] + func raw() -> Data { + let rawBytes = map(\.bytes).sorted { lhs, rhs in + lhs.hashValue > rhs.hashValue } + return Data(rawBytes.joined()) + } +} + +public extension Data { + + /// Returns the related set of changes of a state representation within an Automerge document. + func heads() -> Set? { + let rawBytes = Array(self) + guard rawBytes.count % 32 == 0 else { return nil } + let totalHashes = rawBytes.count / 32 + let heads = (0.. Set? { - var output: Set = [] - for bytes in raw { - guard let changeHash = change(hash: ChangeHash(bytes: bytes))?.hash - else { return nil } - output.insert(changeHash) - } - return output - } - /// Generates patches between two points in the document history. /// /// Use: diff --git a/Tests/AutomergeTests/TestChanges.swift b/Tests/AutomergeTests/TestChanges.swift index 07ee4c34..c247822f 100644 --- a/Tests/AutomergeTests/TestChanges.swift +++ b/Tests/AutomergeTests/TestChanges.swift @@ -94,28 +94,27 @@ class ChangeSetTests: XCTestCase { try doc.merge(other: doc1) let heads = doc.heads() - let restored = doc.heads(raw: heads.raw()) + let restored = doc.heads().raw().heads() XCTAssertEqual(heads, restored) } - func testChangeHash_WhenRawIsManipulated_DocumentDoesNotAccept() throws { + func testChangeHash_SameHeads_ResultSameRawData() throws { let doc = Document() let textId = try! doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text) let doc1 = doc.fork() - try doc.spliceText(obj: textId, start: 0, delete: 0, value: "Hello") - try doc1.spliceText(obj: textId, start: 0, delete: 0, value: " World!") + let doc2 = doc.fork() + let doc3 = doc.fork() + try doc.spliceText(obj: textId, start: 0, delete: 0, value: "[0]") + try doc1.spliceText(obj: textId, start: 0, delete: 0, value: "[1]") + try doc2.spliceText(obj: textId, start: 0, delete: 0, value: "[2]") + try doc3.spliceText(obj: textId, start: 0, delete: 0, value: "[3]") try doc.merge(other: doc1) + try doc.merge(other: doc2) + try doc.merge(other: doc3) - let headsRaw = doc.heads().raw().map { raw in - var raw = raw - raw[0] = 0 - raw[7] = 0 - raw[14] = 0 - return raw - } - let restored = doc.heads(raw: headsRaw) + let rawHashes = (0..<100).map { _ in doc.heads().raw().hashValue } - XCTAssertNil(restored) + XCTAssertEqual(Set(rawHashes).count, 1) } } From 84896a24018160b0297de9c7b788403a826946e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel=20D=C3=ADaz?= Date: Fri, 12 Jul 2024 08:53:36 +0200 Subject: [PATCH 3/4] preserve sort integrity between executions to calculate heads raw minor --- Sources/Automerge/ChangeHash.swift | 4 ++-- Tests/AutomergeTests/TestChanges.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Automerge/ChangeHash.swift b/Sources/Automerge/ChangeHash.swift index f774c27c..ab8ecad4 100644 --- a/Sources/Automerge/ChangeHash.swift +++ b/Sources/Automerge/ChangeHash.swift @@ -18,7 +18,7 @@ public extension Set { /// and retrieval of document states. func raw() -> Data { let rawBytes = map(\.bytes).sorted { lhs, rhs in - lhs.hashValue > rhs.hashValue + lhs.debugDescription > rhs.debugDescription } return Data(rawBytes.joined()) } @@ -28,7 +28,7 @@ public extension Data { /// Returns the related set of changes of a state representation within an Automerge document. func heads() -> Set? { - let rawBytes = Array(self) + let rawBytes: [UInt8] = Array(self) guard rawBytes.count % 32 == 0 else { return nil } let totalHashes = rawBytes.count / 32 let heads = (0.. Date: Fri, 12 Jul 2024 19:23:00 +0200 Subject: [PATCH 4/4] Update Sources/Automerge/ChangeHash.swift Co-authored-by: Joseph Heck --- Sources/Automerge/ChangeHash.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Automerge/ChangeHash.swift b/Sources/Automerge/ChangeHash.swift index ab8ecad4..f839f4ff 100644 --- a/Sources/Automerge/ChangeHash.swift +++ b/Sources/Automerge/ChangeHash.swift @@ -26,7 +26,7 @@ public extension Set { public extension Data { - /// Returns the related set of changes of a state representation within an Automerge document. + /// Interprets the data to return the data as a set of change hashes that represent a state within an Automerge document. If the data is not a multiple of 32 bytes, returns nil. func heads() -> Set? { let rawBytes: [UInt8] = Array(self) guard rawBytes.count % 32 == 0 else { return nil }