qcc/internal/store/table.go
2026-06-30 12:52:09 +03:00

176 lines
3.7 KiB
Go

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
}