package store import ( "encoding/binary" "fmt" "os" "path/filepath" "sync" "github.com/niko/qcc/internal/crypto" "github.com/niko/qcc/pkg/types" ) type IndexEntry struct { FrameID uint64 Offset uint32 } type Table struct { mu sync.RWMutex engine *Engine name string path string lastHash [32]byte nextID uint64 frameCount int index map[uint64]IndexEntry } func (t *Table) Insert(keyHash uint64, recType types.RecType, tableID uint16, payload []byte, key [32]byte) error { t.mu.Lock() defer t.mu.Unlock() nonce, ciphertext, authTag, err := crypto.EncryptRecord(payload, key) if err != nil { return err } var authTagArr [16]byte copy(authTagArr[:], authTag) header := RecordHeader{ RecType: recType, Flags: 0x01, TableID: tableID, KeyHash: keyHash, PayloadSize: uint32(len(ciphertext)), Nonce: nonce, AuthTag: authTagArr, } frame := &Frame{ Header: NewFrameHeader(t.nextID-1, t.lastHash), Records: []Record{ {Header: header, Payload: ciphertext}, }, } data, err := MarshalFrame(frame) if err != nil { return err } frameName := fmt.Sprintf("%016x.frm", t.nextID) framePath := filepath.Join(t.path, frameName) if err := os.WriteFile(framePath, data, 0600); err != nil { return err } checksum := ComputeFrameChecksum(data[:len(data)-32]) t.engine.UpdateFrameState(t.name, t.nextID, checksum) t.lastHash = checksum t.index[keyHash] = IndexEntry{FrameID: t.nextID, Offset: 0} t.nextID++ t.frameCount++ if t.frameCount%10 == 0 { return t.saveIndex() } return nil } func (t *Table) Get(keyHash uint64, key [32]byte) ([]byte, error) { t.mu.RLock() entry, ok := t.index[keyHash] t.mu.RUnlock() if !ok { return nil, fmt.Errorf("key not found: %x", keyHash) } return t.readRecord(entry, key) } func (t *Table) readRecord(entry IndexEntry, key [32]byte) ([]byte, error) { frameName := fmt.Sprintf("%016x.frm", entry.FrameID) framePath := filepath.Join(t.path, frameName) data, err := os.ReadFile(framePath) if err != nil { return nil, err } f, err := UnmarshalFrame(data) if err != nil { return nil, err } if len(f.Records) == 0 { return nil, fmt.Errorf("empty frame") } rec := f.Records[0] return crypto.DecryptRecord(rec.Payload, rec.Header.Nonce, rec.Header.AuthTag[:], key) } func (t *Table) Has(keyHash uint64) bool { t.mu.RLock() defer t.mu.RUnlock() _, ok := t.index[keyHash] return ok } func (t *Table) Delete(keyHash uint64) { t.mu.Lock() defer t.mu.Unlock() delete(t.index, keyHash) } func (t *Table) loadLastIndex() error { indexPath := filepath.Join(t.engine.DataDir, "indexes", t.name+".idx") data, err := os.ReadFile(indexPath) if err != nil { return nil } t.index = make(map[uint64]IndexEntry) off := 0 for off+16 <= len(data) { keyHash := binary.BigEndian.Uint64(data[off:]) frameID := binary.BigEndian.Uint64(data[off+8:]) t.index[keyHash] = IndexEntry{FrameID: frameID, Offset: 0} off += 16 } return nil } func (t *Table) saveIndex() error { indexPath := filepath.Join(t.engine.DataDir, "indexes", t.name+".idx") data := make([]byte, 0, len(t.index)*16) for keyHash, entry := range t.index { var buf [16]byte binary.BigEndian.PutUint64(buf[0:8], keyHash) binary.BigEndian.PutUint64(buf[8:16], entry.FrameID) data = append(data, buf[:]...) } return os.WriteFile(indexPath, data, 0600) } func (t *Table) flushIndex() error { t.mu.Lock() defer t.mu.Unlock() return t.saveIndex() } func (t *Table) Iterate(fn func(keyHash uint64, payload []byte) bool, key [32]byte) error { t.mu.RLock() defer t.mu.RUnlock() for keyHash, entry := range t.index { payload, err := t.readRecord(entry, key) if err != nil { continue } if !fn(keyHash, payload) { break } } return nil }