3 files changed,
+315,
-0
+0,
-0
+283,
-0
1@@ -0,0 +1,283 @@
2+package ygo
3+
4+import (
5+ "encoding/binary"
6+ "errors"
7+ "io"
8+ "os"
9+ "sort"
10+)
11+
12+const (
13+ ysweetV1Prefix = 0x00
14+ ysweetKeyspaceOID = 0x00
15+ ysweetKeyspaceDoc = 0x01
16+ ysweetSubDoc = 0x00
17+ ysweetSubStateVec = 0x01
18+ ysweetSubUpdate = 0x02
19+ ysweetSubMeta = 0x03
20+ ysweetTerminator = 0x00
21+)
22+
23+var (
24+ ErrNoDocument = errors.New("document not found")
25+ ErrNoOID = errors.New("document OID not found")
26+ ErrInvalidData = errors.New("invalid ysweet data format")
27+)
28+
29+type ysweetEntry struct {
30+ key []byte
31+ value []byte
32+}
33+
34+func (e *ysweetEntry) Key() []byte {
35+ return e.key
36+}
37+
38+func (e *ysweetEntry) Value() []byte {
39+ return e.value
40+}
41+
42+type ysweetMap map[string][]byte
43+
44+func (m ysweetMap) RangeIterate(from, to []byte) []ysweetEntry {
45+ var entries []ysweetEntry
46+ fromStr := string(from)
47+ toStr := string(to)
48+ for k, v := range m {
49+ if len(k) >= len(fromStr) && k[:len(fromStr)] >= fromStr {
50+ if toStr == "" || (len(k) >= len(toStr) && k[:len(toStr)] < toStr) {
51+ entries = append(entries, ysweetEntry{key: []byte(k), value: v})
52+ }
53+ }
54+ }
55+ sort.Slice(entries, func(i, j int) bool {
56+ return string(entries[i].key) < string(entries[j].key)
57+ })
58+ return entries
59+}
60+
61+func ysweetKeyOID(docName string) []byte {
62+ key := []byte{ysweetV1Prefix, ysweetKeyspaceOID}
63+ key = append(key, docName...)
64+ key = append(key, ysweetTerminator)
65+ return key
66+}
67+
68+func ysweetKeyDoc(oid uint32) []byte {
69+ key := []byte{ysweetV1Prefix, ysweetKeyspaceDoc}
70+ key = append(key, uint32ToBytesBE(oid)...)
71+ key = append(key, ysweetSubDoc)
72+ return key
73+}
74+
75+func ysweetKeyStateVector(oid uint32) []byte {
76+ key := []byte{ysweetV1Prefix, ysweetKeyspaceDoc}
77+ key = append(key, uint32ToBytesBE(oid)...)
78+ key = append(key, ysweetSubStateVec)
79+ return key
80+}
81+
82+func ysweetKeyUpdate(oid uint32, clock uint32) []byte {
83+ key := []byte{ysweetV1Prefix, ysweetKeyspaceDoc}
84+ key = append(key, uint32ToBytesBE(oid)...)
85+ key = append(key, ysweetSubUpdate)
86+ key = append(key, uint32ToBytesBE(clock)...)
87+ key = append(key, ysweetTerminator)
88+ return key
89+}
90+
91+func ysweetKeyUpdateRangeStart(oid uint32) []byte {
92+ return ysweetKeyUpdate(oid, 0)
93+}
94+
95+func ysweetKeyUpdateRangeEnd(oid uint32) []byte {
96+ key := []byte{ysweetV1Prefix, ysweetKeyspaceDoc}
97+ key = append(key, uint32ToBytesBE(oid)...)
98+ key = append(key, ysweetSubUpdate)
99+ key = append(key, 0xFF, 0xFF, 0xFF, 0xFF)
100+ return key
101+}
102+
103+func uint32ToBytesBE(v uint32) []byte {
104+ b := make([]byte, 4)
105+ binary.BigEndian.PutUint32(b, v)
106+ return b
107+}
108+
109+func bytesBEToUint32(b []byte) uint32 {
110+ return binary.BigEndian.Uint32(b)
111+}
112+
113+func extractDocNameFromOIDKey(key []byte) string {
114+ if len(key) >= 3 && key[0] == ysweetV1Prefix && key[1] == ysweetKeyspaceOID {
115+ return string(key[2 : len(key)-1])
116+ }
117+ return ""
118+}
119+
120+func parseYsweetData(r io.Reader) (map[string][]byte, error) {
121+ data, err := io.ReadAll(r)
122+ if err != nil {
123+ return nil, err
124+ }
125+
126+ return bincodeDeserialize(data), nil
127+}
128+
129+func min(a, b int) int {
130+ if a < b {
131+ return a
132+ }
133+ return b
134+}
135+
136+// readBincodeFixint reads a bincode fixint u64 from data at the given offset.
137+// Bincode fixint encoding: always 8 bytes, little-endian
138+func readBincodeFixint(data []byte, offset int) (uint64, int, error) {
139+ if offset+8 > len(data) {
140+ return 0, offset, io.EOF
141+ }
142+
143+ val := binary.LittleEndian.Uint64(data[offset : offset+8])
144+ return val, offset + 8, nil
145+}
146+
147+// readVarint is kept for backward compatibility but now uses fixint encoding
148+var readVarint = readBincodeFixint
149+
150+// bincodeDeserialize deserializes a bincode-encoded map[string][]byte.
151+// Uses bincode fixint encoding (8-byte little-endian u64 for all length fields).
152+func bincodeDeserialize(data []byte) map[string][]byte {
153+ m := make(map[string][]byte)
154+ if len(data) == 0 {
155+ return m
156+ }
157+
158+ offset := 0
159+
160+ // Read map length (u64 LE)
161+ mapLen, nextOffset, err := readBincodeFixint(data, offset)
162+ if err != nil {
163+ return m
164+ }
165+ offset = nextOffset
166+
167+ // Now read exactly mapLen entries
168+ for i := uint64(0); i < mapLen && offset < len(data); i++ {
169+ keyLen, nextOffset, err := readBincodeFixint(data, offset)
170+ if err != nil {
171+ break
172+ }
173+ offset = nextOffset
174+
175+ // Sanity check - key should be reasonable
176+ if keyLen > 1000 {
177+ break
178+ }
179+
180+ if offset+int(keyLen) > len(data) {
181+ break
182+ }
183+ key := string(data[offset : offset+int(keyLen)])
184+ offset += int(keyLen)
185+
186+ valLen, nextOffset, err := readBincodeFixint(data, offset)
187+ if err != nil {
188+ break
189+ }
190+ offset = nextOffset
191+
192+ // Sanity check - val should be reasonable
193+ if valLen > 10000 {
194+ break
195+ }
196+
197+ if offset+int(valLen) > len(data) {
198+ break
199+ }
200+ value := data[offset : offset+int(valLen)]
201+ offset += int(valLen)
202+
203+ m[key] = value
204+ }
205+
206+ return m
207+}
208+
209+// NewDocFromYSweet reconstructs a Y document from y-sweet persisted data.
210+// The provided io.Reader should contain the binary data stored in a y-sweet
211+// data.ysweet file (bincode-serialized BTreeMap).
212+//
213+// The returned Doc is fully reconstructed and ready for use.
214+// Callers must call Destroy on the returned Doc when done to free resources.
215+func NewDocFromYSweet(r io.Reader) (*Doc, error) {
216+ m, err := parseYsweetData(r)
217+ if err != nil {
218+ return nil, err
219+ }
220+
221+ var oid uint32
222+
223+ for _, v := range m {
224+ // OID entries have 4-byte big-endian uint32 values
225+ if len(v) == 4 {
226+ oid = bytesBEToUint32(v)
227+ break
228+ }
229+ }
230+
231+ if oid == 0 {
232+ return nil, ErrNoOID
233+ }
234+
235+ docKey := string(ysweetKeyDoc(oid))
236+ docState, ok := m[docKey]
237+ if !ok {
238+ return nil, ErrNoDocument
239+ }
240+
241+ updateEntries := ysweetMap(m).RangeIterate(ysweetKeyUpdateRangeStart(oid), ysweetKeyUpdateRangeEnd(oid))
242+
243+ doc, err := NewDoc()
244+ if err != nil {
245+ return nil, err
246+ }
247+
248+ err = doc.WithWriteTransaction(func(txn *Transaction) error {
249+ update := UpdateFromBytes(docState)
250+ if err := txn.ApplyUpdate(update); err != nil {
251+ return err
252+ }
253+
254+ for _, entry := range updateEntries {
255+ update = UpdateFromBytes(entry.value)
256+ if err := txn.ApplyUpdate(update); err != nil {
257+ return err
258+ }
259+ }
260+
261+ return nil
262+ })
263+ if err != nil {
264+ doc.Destroy()
265+ return nil, err
266+ }
267+
268+ return doc, nil
269+}
270+
271+// NewDocFromYSweetFile opens a y-sweet data file and reconstructs a Y document.
272+// The path should point to a data.ysweet file persisted by a y-sweet server.
273+//
274+// The returned Doc is fully reconstructed and ready for use.
275+// Callers must call Destroy on the returned Doc when done to free resources.
276+func NewDocFromYSweetFile(path string) (*Doc, error) {
277+ f, err := os.Open(path)
278+ if err != nil {
279+ return nil, err
280+ }
281+ defer f.Close()
282+
283+ return NewDocFromYSweet(f)
284+}
+32,
-0
1@@ -0,0 +1,32 @@
2+package ygo
3+
4+import (
5+ "testing"
6+)
7+
8+func TestNewDocFromYSweetFile(t *testing.T) {
9+ doc, err := NewDocFromYSweetFile("testdata/data.ysweet")
10+ if err != nil {
11+ t.Fatalf("NewDocFromYSweetFile failed: %v", err)
12+ }
13+ defer doc.Destroy()
14+
15+ txt, err := doc.GetText("content")
16+ if err != nil {
17+ t.Fatalf("GetText failed: %v", err)
18+ }
19+
20+ var content string
21+ err = doc.WithReadTransaction(func(txn *Transaction) error {
22+ content, err = txt.String(txn)
23+ return err
24+ })
25+ if err != nil {
26+ t.Fatalf("WithReadTransaction failed: %v", err)
27+ }
28+
29+ expected := "Hello from client 1 Right back at you from client 2"
30+ if content != expected {
31+ t.Errorf("got %q, want %q", content, expected)
32+ }
33+}