1package cache
2
3import (
4 "encoding/binary"
5 "fmt"
6 "os"
7 "syscall"
8 "unsafe"
9)
10
11// Binary cache format layout:
12// Header: [magic:4][version:4][entryCount:4][indexOffset:8][lastModified:8]
13// Entries: [url_len:2][url:N][title_len:2][title:N][visit:2][lastVisit:4][score:2]
14// Index: [trigramCount:4][trigram:3][count:4][ids:4*count]...
15
16const (
17 CacheMagic = 0x44554d42 // "DUMB" in little-endian
18 CacheVersion = 1
19 HeaderSize = 28 // Size of binary header
20)
21
22// BinaryHeader represents the cache file header.
23type BinaryHeader struct {
24 Magic uint32 // Magic number for format identification
25 Version uint32 // Cache format version
26 EntryCount uint32 // Number of entries in cache
27 IndexOffset uint64 // Offset to trigram index section
28 LastModified int64 // Unix timestamp of last modification
29}
30
31// SaveToBinary writes the cache to a binary file using memory mapping for performance.
32func (c *DmenuFuzzyCache) SaveToBinary(filename string) error {
33 c.mu.RLock()
34 defer c.mu.RUnlock()
35
36 // Calculate total file size
37 totalSize := c.calculateFileSize()
38
39 // Create the file with the exact size needed
40 file, err := os.Create(filename)
41 if err != nil {
42 return fmt.Errorf("failed to create cache file: %w", err)
43 }
44 defer file.Close()
45
46 // Pre-allocate file space
47 if err := file.Truncate(int64(totalSize)); err != nil {
48 return fmt.Errorf("failed to allocate file space: %w", err)
49 }
50
51 // Memory map the file for fast writing
52 data, err := syscall.Mmap(int(file.Fd()), 0, int(totalSize), syscall.PROT_WRITE, syscall.MAP_SHARED)
53 if err != nil {
54 return fmt.Errorf("failed to mmap file: %w", err)
55 }
56 defer syscall.Munmap(data)
57
58 // Write data using unsafe pointer operations for maximum speed
59 offset := 0
60
61 // Write header
62 offset = c.writeHeader(data, offset, totalSize)
63
64 // Write entries
65 entriesEndOffset := c.writeEntries(data, offset)
66
67 // Write trigram index
68 trigramEndOffset := c.writeTrigramIndex(data, entriesEndOffset)
69
70 // Write sorted index
71 c.writeSortedIndex(data, trigramEndOffset)
72
73 // Force sync to disk (Linux-specific, skip on error)
74 // syscall.Msync may not be available on all platforms
75 // _ = syscall.Msync(data, syscall.MS_SYNC)
76
77 return nil
78}
79
80// LoadFromBinary loads cache from a binary file using memory mapping.
81func (c *DmenuFuzzyCache) LoadFromBinary(filename string) error {
82 file, err := os.Open(filename)
83 if err != nil {
84 return fmt.Errorf("failed to open cache file: %w", err)
85 }
86 defer file.Close()
87
88 // Get file size
89 stat, err := file.Stat()
90 if err != nil {
91 return fmt.Errorf("failed to stat cache file: %w", err)
92 }
93 fileSize := int(stat.Size())
94
95 if fileSize < HeaderSize {
96 return fmt.Errorf("cache file too small: %d bytes", fileSize)
97 }
98
99 // Memory map the file for fast reading
100 data, err := syscall.Mmap(int(file.Fd()), 0, fileSize, syscall.PROT_READ, syscall.MAP_SHARED)
101 if err != nil {
102 return fmt.Errorf("failed to mmap cache file: %w", err)
103 }
104 // Note: We don't defer Munmap here as we want to keep the mapping alive
105 // The mapping will be cleaned up when the process exits
106
107 // Parse header
108 header, err := c.parseHeader(data)
109 if err != nil {
110 syscall.Munmap(data)
111 return fmt.Errorf("failed to parse header: %w", err)
112 }
113
114 c.mu.Lock()
115 defer c.mu.Unlock()
116
117 // Parse entries
118 entries, err := c.parseEntries(data, HeaderSize, header.EntryCount)
119 if err != nil {
120 syscall.Munmap(data)
121 return fmt.Errorf("failed to parse entries: %w", err)
122 }
123
124 // Parse trigram index
125 trigramIndex, trigramEndOffset, err := c.parseTrigramIndex(data, int(header.IndexOffset))
126 if err != nil {
127 syscall.Munmap(data)
128 return fmt.Errorf("failed to parse trigram index: %w", err)
129 }
130
131 // Parse sorted index
132 sortedIndex, err := c.parseSortedIndex(data, trigramEndOffset)
133 if err != nil {
134 syscall.Munmap(data)
135 return fmt.Errorf("failed to parse sorted index: %w", err)
136 }
137
138 // Update cache state
139 c.entries = entries
140 c.trigramIndex = trigramIndex
141 c.sortedIndex = sortedIndex
142 c.entryCount = header.EntryCount
143 c.lastModified = header.LastModified
144 c.version = header.Version
145
146 return nil
147}
148
149// calculateFileSize estimates the total file size needed.
150func (c *DmenuFuzzyCache) calculateFileSize() int {
151 size := HeaderSize
152
153 // Calculate entries section size
154 for _, entry := range c.entries {
155 size += 2 + len(entry.URL) // url_len + url
156 size += 2 + len(entry.Title) // title_len + title
157 size += 2 + 4 + 2 // visit + lastVisit + score
158 }
159
160 // Calculate trigram index size
161 size += 4 // trigram count
162 for trigram, ids := range c.trigramIndex {
163 size += len(trigram) + 4 + len(ids)*4 // trigram + count + ids
164 }
165
166 // Calculate sorted index size
167 size += 4 + len(c.sortedIndex)*4 // count + indices
168
169 return size
170}
171
172// writeHeader writes the binary header.
173func (c *DmenuFuzzyCache) writeHeader(data []byte, offset int, totalSize int) int {
174 // Calculate index offset (header + all entries)
175 indexOffset := HeaderSize
176 for _, entry := range c.entries {
177 indexOffset += 2 + len(entry.URL) + 2 + len(entry.Title) + 8
178 }
179
180 header := BinaryHeader{
181 Magic: CacheMagic,
182 Version: CacheVersion,
183 EntryCount: uint32(len(c.entries)),
184 IndexOffset: uint64(indexOffset),
185 LastModified: c.lastModified,
186 }
187
188 // Write header using unsafe for maximum performance
189 *(*BinaryHeader)(unsafe.Pointer(&data[offset])) = header
190 return offset + HeaderSize
191}
192
193// writeEntries writes all cache entries in binary format.
194func (c *DmenuFuzzyCache) writeEntries(data []byte, offset int) int {
195 for _, entry := range c.entries {
196 // Write URL
197 urlLen := uint16(len(entry.URL))
198 *(*uint16)(unsafe.Pointer(&data[offset])) = urlLen
199 offset += 2
200 copy(data[offset:], entry.URL)
201 offset += len(entry.URL)
202
203 // Write Title
204 titleLen := uint16(len(entry.Title))
205 *(*uint16)(unsafe.Pointer(&data[offset])) = titleLen
206 offset += 2
207 copy(data[offset:], entry.Title)
208 offset += len(entry.Title)
209
210 // Write metadata
211 *(*uint16)(unsafe.Pointer(&data[offset])) = entry.VisitCount
212 offset += 2
213 *(*uint32)(unsafe.Pointer(&data[offset])) = entry.LastVisit
214 offset += 4
215 *(*uint16)(unsafe.Pointer(&data[offset])) = entry.Score
216 offset += 2
217 }
218 return offset
219}
220
221// writeTrigramIndex writes the trigram index in binary format.
222func (c *DmenuFuzzyCache) writeTrigramIndex(data []byte, offset int) int {
223 // Write trigram count
224 trigramCount := uint32(len(c.trigramIndex))
225 *(*uint32)(unsafe.Pointer(&data[offset])) = trigramCount
226 offset += 4
227
228 // Write trigrams and their entry lists
229 for trigram, ids := range c.trigramIndex {
230 // Write trigram (exactly 3 bytes)
231 copy(data[offset:offset+3], trigram)
232 offset += 3
233
234 // Write entry count
235 idCount := uint32(len(ids))
236 *(*uint32)(unsafe.Pointer(&data[offset])) = idCount
237 offset += 4
238
239 // Write entry IDs
240 for _, id := range ids {
241 *(*uint32)(unsafe.Pointer(&data[offset])) = id
242 offset += 4
243 }
244 }
245 return offset
246}
247
248// writeSortedIndex writes the sorted index in binary format.
249func (c *DmenuFuzzyCache) writeSortedIndex(data []byte, offset int) int {
250 // Write sorted index count
251 indexCount := uint32(len(c.sortedIndex))
252 *(*uint32)(unsafe.Pointer(&data[offset])) = indexCount
253 offset += 4
254
255 // Write sorted indices
256 for _, id := range c.sortedIndex {
257 *(*uint32)(unsafe.Pointer(&data[offset])) = id
258 offset += 4
259 }
260 return offset
261}
262
263// parseHeader parses the binary header from mapped data.
264func (c *DmenuFuzzyCache) parseHeader(data []byte) (*BinaryHeader, error) {
265 if len(data) < HeaderSize {
266 return nil, fmt.Errorf("insufficient data for header")
267 }
268
269 header := (*BinaryHeader)(unsafe.Pointer(&data[0]))
270
271 if header.Magic != CacheMagic {
272 return nil, fmt.Errorf("invalid magic number: 0x%x", header.Magic)
273 }
274
275 if header.Version != CacheVersion {
276 return nil, fmt.Errorf("unsupported cache version: %d", header.Version)
277 }
278
279 return header, nil
280}
281
282// parseEntries parses cache entries from binary data.
283func (c *DmenuFuzzyCache) parseEntries(data []byte, offset int, count uint32) ([]CompactEntry, error) {
284 entries := make([]CompactEntry, 0, count)
285
286 for i := uint32(0); i < count; i++ {
287 if offset+2 > len(data) {
288 return nil, fmt.Errorf("insufficient data for URL length at entry %d", i)
289 }
290
291 // Read URL
292 urlLen := *(*uint16)(unsafe.Pointer(&data[offset]))
293 offset += 2
294 if offset+int(urlLen) > len(data) {
295 return nil, fmt.Errorf("insufficient data for URL at entry %d", i)
296 }
297 url := string(data[offset : offset+int(urlLen)])
298 offset += int(urlLen)
299
300 // Read Title
301 if offset+2 > len(data) {
302 return nil, fmt.Errorf("insufficient data for title length at entry %d", i)
303 }
304 titleLen := *(*uint16)(unsafe.Pointer(&data[offset]))
305 offset += 2
306 if offset+int(titleLen) > len(data) {
307 return nil, fmt.Errorf("insufficient data for title at entry %d", i)
308 }
309 title := string(data[offset : offset+int(titleLen)])
310 offset += int(titleLen)
311
312 // Read metadata
313 if offset+8 > len(data) {
314 return nil, fmt.Errorf("insufficient data for metadata at entry %d", i)
315 }
316 visitCount := *(*uint16)(unsafe.Pointer(&data[offset]))
317 offset += 2
318 lastVisit := *(*uint32)(unsafe.Pointer(&data[offset]))
319 offset += 4
320 score := *(*uint16)(unsafe.Pointer(&data[offset]))
321 offset += 2
322
323 entries = append(entries, CompactEntry{
324 URL: url,
325 Title: title,
326 VisitCount: visitCount,
327 LastVisit: lastVisit,
328 Score: score,
329 })
330 }
331
332 return entries, nil
333}
334
335// parseTrigramIndex parses the trigram index from binary data.
336func (c *DmenuFuzzyCache) parseTrigramIndex(data []byte, offset int) (map[string][]uint32, int, error) {
337 if offset+4 > len(data) {
338 return nil, 0, fmt.Errorf("insufficient data for trigram count")
339 }
340
341 trigramCount := *(*uint32)(unsafe.Pointer(&data[offset]))
342 offset += 4
343
344 trigramIndex := make(map[string][]uint32, trigramCount)
345
346 for i := uint32(0); i < trigramCount; i++ {
347 // Read trigram
348 if offset+3 > len(data) {
349 return nil, 0, fmt.Errorf("insufficient data for trigram %d", i)
350 }
351 trigram := string(data[offset : offset+3])
352 offset += 3
353
354 // Read entry count
355 if offset+4 > len(data) {
356 return nil, 0, fmt.Errorf("insufficient data for entry count in trigram %d", i)
357 }
358 idCount := *(*uint32)(unsafe.Pointer(&data[offset]))
359 offset += 4
360
361 // Read entry IDs
362 if offset+int(idCount)*4 > len(data) {
363 return nil, 0, fmt.Errorf("insufficient data for entry IDs in trigram %d", i)
364 }
365
366 ids := make([]uint32, idCount)
367 for j := uint32(0); j < idCount; j++ {
368 ids[j] = *(*uint32)(unsafe.Pointer(&data[offset]))
369 offset += 4
370 }
371
372 trigramIndex[trigram] = ids
373 }
374
375 return trigramIndex, offset, nil
376}
377
378// parseSortedIndex parses the sorted index from binary data.
379func (c *DmenuFuzzyCache) parseSortedIndex(data []byte, offset int) ([]uint32, error) {
380 if offset+4 > len(data) {
381 return nil, fmt.Errorf("insufficient data for sorted index count")
382 }
383
384 indexCount := *(*uint32)(unsafe.Pointer(&data[offset]))
385 offset += 4
386
387 if offset+int(indexCount)*4 > len(data) {
388 return nil, fmt.Errorf("insufficient data for sorted index entries")
389 }
390
391 sortedIndex := make([]uint32, indexCount)
392 for i := uint32(0); i < indexCount; i++ {
393 sortedIndex[i] = *(*uint32)(unsafe.Pointer(&data[offset]))
394 offset += 4
395 }
396
397 return sortedIndex, nil
398}
399
400// IsValidCacheFile checks if a file is a valid cache file without fully loading it.
401func IsValidCacheFile(filename string) bool {
402 file, err := os.Open(filename)
403 if err != nil {
404 return false
405 }
406 defer file.Close()
407
408 var header BinaryHeader
409 err = binary.Read(file, binary.LittleEndian, &header)
410 if err != nil {
411 return false
412 }
413
414 return header.Magic == CacheMagic && header.Version == CacheVersion
415}