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