adds ability to reconstruct a document from a ysweet-formatted data file
3 files changed,  +315, -0
A testdata/data.ysweet
+0, -0
A ysweet_format.go
+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+}
A ysweet_format_test.go
+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+}