package main import ( "encoding/binary" "errors" "fmt" "io" "os" ) // positionBytes is the size of an encoded position. const positionBytes = 16 // records is a file of fixed-size records, numbered from 0. Record n starts at // byte n*size. A record that was never written reads as zeros. An index is data // that the day files can give again, so records cuts a partial record at the // end of the file. type records struct { file *os.File size int count int } // openRecords opens or creates the record file at path with records of size // bytes. func openRecords(path string, size int) (*records, error) { file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600) if err != nil { return nil, fmt.Errorf("open index: %w", err) } info, err := file.Stat() if err != nil { return nil, errors.Join(fmt.Errorf("read index size: %w", err), file.Close()) } r := &records{ file: file, size: size, count: int(info.Size()) / size, } if info.Size()%int64(size) != 0 { err = file.Truncate(int64(r.count * size)) if err != nil { return nil, errors.Join(fmt.Errorf("cut partial index record: %w", err), file.Close()) } } return r, nil } // Read returns record n, or false when the file does not reach it. func (r *records) Read(n int) ([]byte, bool, error) { if n >= r.count { return nil, false, nil } record := make([]byte, r.size) _, err := r.file.ReadAt(record, int64(n*r.size)) if err != nil && !errors.Is(err, io.EOF) { return nil, false, fmt.Errorf("read index record %d: %w", n, err) } return record, true, nil } // Write writes record n. func (r *records) Write(n int, record []byte) error { _, err := r.file.WriteAt(record, int64(n*r.size)) if err != nil { return fmt.Errorf("write index record %d: %w", n, err) } r.count = max(r.count, n+1) return nil } // Append writes the record after the last one. func (r *records) Append(record []byte) error { return r.Write(r.count, record) } // Close closes the file. func (r *records) Close() error { return r.file.Close() } // encodePosition writes place into the first positionBytes bytes of record. func encodePosition(record []byte, place position) { binary.LittleEndian.PutUint32(record[0:], place.day) binary.LittleEndian.PutUint32(record[4:], place.length) binary.LittleEndian.PutUint64(record[8:], place.offset) } // decodePosition reads a position from the first positionBytes bytes of record. func decodePosition(record []byte) position { return position{ day: binary.LittleEndian.Uint32(record[0:]), length: binary.LittleEndian.Uint32(record[4:]), offset: binary.LittleEndian.Uint64(record[8:]), } }