module Daybreak
# Records define how data is serialized and read from disk.
class Record
# Thrown when either key or data is missing
class UnnacceptableDataError < Exception; end
# Thrown when there is a CRC mismatch between the data from the disk
# and what was written to disk previously.
class CorruptDataError < Exception; end
include Locking
# The mask a record uses to check for deletion.
DELETION_MASK = (1 << 31)
attr_accessor :key, :data
def initialize(key = nil, data = nil, deleted = false)
@key = key
@data = data
if deleted
@deleted = DELETION_MASK
else
@deleted = 0
end
end
# Read a record from an open io source, check the CRC, and set @key
# and @data.
# @param [#read] io an IO instance to read from
def read(io)
lock io do
@key = read_key(io)
@data = read_data(io)
crc = io.read(4)
raise CorruptDataError, "CRC mismatch #{crc} should be #{crc_string}" unless crc == crc_string
end
self
end
# The serialized representation of the key value pair plus the CRC.
# @return [String]
def representation
raise UnnacceptableDataError, "key and data must be defined" if @key.nil? || @data.nil?
byte_string + crc_string
end
# Create a new record to read from IO.
# @param [#read] io an IO instance to read from
def self.read(io)
new.read(io)
end
def deleted?
@deleted > 0
end
private
def byte_string
@byte_string ||= part(@key, @key.bytesize + @deleted) + part(@data, @data.bytesize)
end
def crc_string
[Zlib.crc32(byte_string, 0)].pack('N')
end
def read_data(io)
io.read read32(io)
end
def read_key(io)
masked = read32 io
@deleted = masked & DELETION_MASK
length = masked & (DELETION_MASK - 1)
io.read length
end
def read32(io)
raw = io.read(4)
raw.unpack('N')[0]
end
def part(data, length)
[length].pack('N') + data
end
end
end