256 lines
6.1 KiB
Go
256 lines
6.1 KiB
Go
package ourdb
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"hash/crc32"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// calculateCRC computes CRC32 for the data
|
|
func calculateCRC(data []byte) uint32 {
|
|
return crc32.ChecksumIEEE(data)
|
|
}
|
|
|
|
// dbFileSelect opens the specified database file
|
|
func (db *OurDB) dbFileSelect(fileNr uint16) error {
|
|
// Check file number limit
|
|
if fileNr > 65535 {
|
|
return errors.New("file_nr needs to be < 65536")
|
|
}
|
|
|
|
path := filepath.Join(db.path, fmt.Sprintf("%d.db", fileNr))
|
|
|
|
// Always close the current file if it's open
|
|
if db.file != nil {
|
|
db.file.Close()
|
|
db.file = nil
|
|
}
|
|
|
|
// Create file if it doesn't exist
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
if err := db.createNewDbFile(fileNr); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Open the file fresh
|
|
file, err := os.OpenFile(path, os.O_RDWR, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
db.file = file
|
|
db.fileNr = fileNr
|
|
return nil
|
|
}
|
|
|
|
// createNewDbFile creates a new database file
|
|
func (db *OurDB) createNewDbFile(fileNr uint16) error {
|
|
newFilePath := filepath.Join(db.path, fmt.Sprintf("%d.db", fileNr))
|
|
f, err := os.Create(newFilePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
// Write a single byte to make all positions start from 1
|
|
_, err = f.Write([]byte{0})
|
|
return err
|
|
}
|
|
|
|
// getFileNr returns the file number to use for the next write
|
|
func (db *OurDB) getFileNr() (uint16, error) {
|
|
path := filepath.Join(db.path, fmt.Sprintf("%d.db", db.lastUsedFileNr))
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
if err := db.createNewDbFile(db.lastUsedFileNr); err != nil {
|
|
return 0, err
|
|
}
|
|
return db.lastUsedFileNr, nil
|
|
}
|
|
|
|
stat, err := os.Stat(path)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if uint32(stat.Size()) >= db.fileSize {
|
|
db.lastUsedFileNr++
|
|
if err := db.createNewDbFile(db.lastUsedFileNr); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
|
|
return db.lastUsedFileNr, nil
|
|
}
|
|
|
|
// set_ stores data at position x
|
|
func (db *OurDB) set_(x uint32, oldLocation Location, data []byte) error {
|
|
// Get file number to use
|
|
fileNr, err := db.getFileNr()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Select the file
|
|
if err := db.dbFileSelect(fileNr); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get current file position for lookup
|
|
pos, err := db.file.Seek(0, os.SEEK_END)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newLocation := Location{
|
|
FileNr: fileNr,
|
|
Position: uint32(pos),
|
|
}
|
|
|
|
// Calculate CRC of data
|
|
crc := calculateCRC(data)
|
|
|
|
// Create header (12 bytes total)
|
|
header := make([]byte, headerSize)
|
|
|
|
// Write size (2 bytes)
|
|
size := uint16(len(data))
|
|
header[0] = byte(size & 0xFF)
|
|
header[1] = byte((size >> 8) & 0xFF)
|
|
|
|
// Write CRC (4 bytes)
|
|
header[2] = byte(crc & 0xFF)
|
|
header[3] = byte((crc >> 8) & 0xFF)
|
|
header[4] = byte((crc >> 16) & 0xFF)
|
|
header[5] = byte((crc >> 24) & 0xFF)
|
|
|
|
// Convert previous location to bytes and store in header
|
|
prevBytes, err := oldLocation.ToBytes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := 0; i < 6; i++ {
|
|
header[6+i] = prevBytes[i]
|
|
}
|
|
|
|
// Write header
|
|
if _, err := db.file.Write(header); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write actual data
|
|
if _, err := db.file.Write(data); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := db.file.Sync(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update lookup table with new position
|
|
return db.lookup.Set(x, newLocation)
|
|
}
|
|
|
|
// get_ retrieves data at specified location
|
|
func (db *OurDB) get_(location Location) ([]byte, error) {
|
|
if err := db.dbFileSelect(location.FileNr); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if location.Position == 0 {
|
|
return nil, fmt.Errorf("record not found, location: %+v", location)
|
|
}
|
|
|
|
// Read header
|
|
header := make([]byte, headerSize)
|
|
if _, err := db.file.ReadAt(header, int64(location.Position)); err != nil {
|
|
return nil, fmt.Errorf("failed to read header: %w", err)
|
|
}
|
|
|
|
// Parse size (2 bytes)
|
|
size := uint16(header[0]) | (uint16(header[1]) << 8)
|
|
|
|
// Parse CRC (4 bytes)
|
|
storedCRC := uint32(header[2]) | (uint32(header[3]) << 8) | (uint32(header[4]) << 16) | (uint32(header[5]) << 24)
|
|
|
|
// Read data
|
|
data := make([]byte, size)
|
|
if _, err := db.file.ReadAt(data, int64(location.Position+headerSize)); err != nil {
|
|
return nil, fmt.Errorf("failed to read data: %w", err)
|
|
}
|
|
|
|
// Verify CRC
|
|
calculatedCRC := calculateCRC(data)
|
|
if calculatedCRC != storedCRC {
|
|
return nil, errors.New("CRC mismatch: data corruption detected")
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// getPrevPos_ retrieves the previous position for a record
|
|
func (db *OurDB) getPrevPos_(location Location) (Location, error) {
|
|
if location.Position == 0 {
|
|
return Location{}, errors.New("record not found")
|
|
}
|
|
|
|
if err := db.dbFileSelect(location.FileNr); err != nil {
|
|
return Location{}, err
|
|
}
|
|
|
|
// Skip size and CRC (6 bytes)
|
|
prevBytes := make([]byte, 6)
|
|
if _, err := db.file.ReadAt(prevBytes, int64(location.Position+6)); err != nil {
|
|
return Location{}, fmt.Errorf("failed to read previous location bytes: %w", err)
|
|
}
|
|
|
|
return db.lookup.LocationNew(prevBytes)
|
|
}
|
|
|
|
// delete_ zeros out the record at specified location
|
|
func (db *OurDB) delete_(x uint32, location Location) error {
|
|
if location.Position == 0 {
|
|
return errors.New("record not found")
|
|
}
|
|
|
|
if err := db.dbFileSelect(location.FileNr); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Read size first
|
|
sizeBytes := make([]byte, 2)
|
|
if _, err := db.file.ReadAt(sizeBytes, int64(location.Position)); err != nil {
|
|
return err
|
|
}
|
|
size := uint16(sizeBytes[0]) | (uint16(sizeBytes[1]) << 8)
|
|
|
|
// Write zeros for the entire record (header + data)
|
|
zeros := make([]byte, int(size)+headerSize)
|
|
if _, err := db.file.WriteAt(zeros, int64(location.Position)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// close_ closes the database file
|
|
func (db *OurDB) close_() error {
|
|
if db.file != nil {
|
|
return db.file.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Condense removes empty records and updates positions
|
|
// This is a complex operation that creates a new file without the deleted records
|
|
func (db *OurDB) Condense() error {
|
|
// This would be a complex implementation that would:
|
|
// 1. Create a temporary file
|
|
// 2. Copy all non-deleted records to the temp file
|
|
// 3. Update all lookup entries to point to new locations
|
|
// 4. Replace the original file with the temp file
|
|
|
|
// For now, this is a placeholder for future implementation
|
|
return errors.New("condense operation not implemented yet")
|
|
}
|