176 lines
3.7 KiB
Go
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
|
|
}
|