#### libremiliacr
#### Copyright(C) 2020-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify
#### it under the terms of the GNU General Public License as published
#### the Free Software Foundation, either version 3 of the License, or
#### (at your option) any later version.
####
#### This program is distributed in the hope that it will be useful,
#### but WITHOUT ANY WARRANTY; without even the implied warranty of
#### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
#### GNU General Public License for more details.
####
#### You should have received a copy of the GNU General Public License
#### along with this program.If not, see<http:####www.gnu.org/licenses/.>
require "./extensions"
lib LibC
enum MsyncFlags
Async = 1
Invalidate = 2
Sync = 4
end
{% if flag?(:unix) %}
fun msync(addr : Void*, length : SizeT, flags : MsyncFlags) : LibC::Int
{% end %}
end
module RemiLib
# The `MmappedFile` class provides a convenient way to read from and write to
# files using [mmap()](https://en.wikipedia.org/wiki/Mmap). This is only
# supported on POSIX systems, and is not meant to be a complete interface to
# everything you can do with `mmap()`.
#
# TODO: This should eventually inherit from `::IO`.
class MmappedFile
# An error while handling an `MmappedFile`.
class MmappedFileError < Exception
end
# An error when synchronizing an `MmappedFile`.
class MmappedFileSyncError < MmappedFileError
getter errno : Errno
def initialize(@message : String?, @errno : Errno)
end
end
# An error when attempting to read or write past the end of an
# `MmappedFile`.
class MmappedFileEofError < MmappedFileError
end
# A `Flags` enumeration that dictates the read/write state of an
# `MmappedFile`.
enum Mode
Read = 0x01
ReadWrite = 0x03
end
# A `Flags` enumeration that dictates the flags for an `MmappedFile`.
@[Flags]
enum Mapping
Shared = 0x01
Private = 0x02
Locked = 0x2000
NoReserve = 0x4000
end
# How an `MmappedFile` is synchronized.
alias MsyncFlags = LibC::MsyncFlags
# The mmapped address.
@addr : Pointer(UInt8) = Pointer(UInt8).null
# Length of the file.
@len : LibC::SizeT = 0
# The file descriptor this mapping was created from.
@fd : Int64 = 0
# The associated file.
@file = uninitialized File
# Current read position.
getter pos : LibC::SizeT = 0
# The `Mode` this instance was opened with.
getter mode : Mode = Mode::Read
# The `Mapping` this instance was created with.
getter mapping : Mapping = Mapping::Private
# Specifies the behavior when `#sync` or `#close` are called and this
# instance is writeable. If the instance isn't writeable, this is ignored.
property syncMode : MsyncFlags = MsyncFlags::Async
# Creates a new `MmappedFile` instance by mapping the file at *path* into
# the memory space.
#
# If *mode* is `Mode::Read`, then the file must already exist.
#
# If *mode* is `Mode:ReadWrite`, then the file is always overwritten if it
# exists and is recreated with the requested size. If it does not exist, it
# is created with the requested size.
#
# In all cases, *size* must be at least 1.
#
# When you are finished with the instance, you should call `#close` to unmap
# the file and release the resources. This can also happen during
# finalization.
def initialize(path : Path|String, size : Int,
*, @mode : Mode = Mode::Read, @mapping : Mapping = Mapping::Private,
@syncMode : MsyncFlags = MsyncFlags::Async)
raise "Size must be at least 1" if size <= 0
{% if flag?(:unix) %}
# Open the file so we can get a file descriptor.
modeStr = case @mode
in .read_write? then "w+b" # This automatically overwrites existing files.
in .read? then "rb"
end
@file = File.open(path, modeStr)
raise "Bad file descriptor" unless @file.fd >= 0
@fd = @file.fd.to_i64
# Do we need to size the file so that it's the correct size?
if @mode.read_write?
@file.seek((size.to_u64 - 1).to_i64, File::Seek::Set)
@file.write_byte(0)
@file.flush
@file.seek(0)
end
# Get the file size. If the user passed in a size, then it must be less
# than or equal to the file's actual size.
@len = (size || @file.size).to_u64!
raise MmappedFileError.new("Bad size: #{@len} > #{@file.size.to_u64!}") unless @len <= @file.size.to_u64!
# Call mmap()
newAddr = LibC.mmap(Pointer(Void).null, @len, @mode.value, @mapping.value,
@file.fd, 0) # Offset is always 0
raise "Null address returned" unless !newAddr.null?
# Check for an error.
unless ((1u128 << 64) - 1).to_u64 != newAddr.address
raise MmappedFileError.new("Cannot map file: #{Errno.value}")
end
# We cast the Pointer(Void) to a Pointer(UInt8) so that we can do
# everything with bytes.
@addr = Pointer(UInt8).new(newAddr.address)
{% else %}
raise "MmappedFile is not supported on this platform"
{% end %}
end
# Creates a new `MmappedFile` instance by mapping the file at *path* into
# the memory spacem, then yields that instance and executes the block. The
# file must already exist.
#
# If *size* is provided, then it must be less than or equal to the size of
# the file on disk.
#
# This will automatically call `#close` at the end.
def self.open(path : Path|String, size : Int,
*, mode : Mode = Mode::Read, mapping : Mapping = Mapping::Private, &)
file = MmappedFile.new(path, size, mode: mode, mapping: mapping)
begin
yield file
ensure
file.close
end
end
def finalize
self.close
end
# Returns `true` if this instance is open, or `false` otherwise.
def open? : Bool
!@addr.nil?
end
def sync : Nil
raise MmappedFileError.new("File is closed") if @addr.null?
{% if flag?(:unix) %}
unless @mapping.includes?(Mapping::Private) # msync does nothing with a private mapping.
unless LibC.msync(@addr, @len, @syncMode) == 0
err = Errno.value
raise MmappedFileSyncError.new("Failed to synchronize MmappedFile: #{err}", err)
end
end
{% else %}
raise "MmappedFile is not supported on this platform"
{% end %}
end
# Unmaps the file from memory.
def close : Nil
unless @addr.null?
{% if flag?(:unix) %}
self.sync if @mode.read_write?
if LibC.munmap(@addr, @len) != 0
raise MmappedFileError.new("Cannot unmap file: #{Errno.value}")
else
@addr = Pointer(UInt8).null
end
@file.close
{% else %}
raise "MmappedFile is not supported on this platform"
{% end %}
end
end
# Length of the file.
def len : LibC::SizeT
raise MmappedFileError.new("File is closed") if @addr.null?
@len
end
# The file descriptor this mapping was created from.
def fd : Int64
raise MmappedFileError.new("File is closed") if @addr.null?
@fd
end
def pos=(position : LibC::SizeT) : Nil
raise MmappedFileError.new("File is closed") if @addr.null?
if position > @len
raise MmappedFileError.new("Cannot seek to position, it is past the end of the file")
elsif position < 0
raise MmappedFileError.new("Cannot seek to a negative position")
end
@pos = position
end
def rewind : Nil
self.pos = 0
end
###
### "read" method variants. These change @pos.
###
### These don't use the "get" variants internally so that there's no double
### checks on the position.
###
# Reads a `UInt8` from the file at the current position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read a `UInt8`.
@[AlwaysInline]
def readUInt8 : UInt8
raise MmappedFileError.new("File is closed") if @addr.null?
raise MmappedFileEofError.new("No more bytes to read") if @pos >= @len
ret = @addr[@pos]
@pos += 1
ret
end
# Reads a `UInt16` from the file at the current position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read a `UInt16`.
@[AlwaysInline]
def readUInt16 : UInt16
raise MmappedFileError.new("File is closed") if @addr.null?
if @pos > @len - 2
raise MmappedFileEofError.new("Cannot read 16-bit integer: no more bytes to read")
end
raise MmappedFileEofError.new("No more bytes to read") if @pos + 2 > @len
ret = (@addr + @pos).unsafe_as(Pointer(UInt16))[0]
@pos += 2
ret
end
# Reads three bytes from the file at the current position and returns a
# 24-bit integer as a `UInt32`. Raises an `MmappedFileEofError` if there
# are not enough bytes to read a 24-bit unsigned integer.
@[AlwaysInline]
def readUInt24 : UInt32
raise MmappedFileError.new("File is closed") if @addr.null?
if @pos > @len - 3
raise MmappedFileEofError.new("Cannot read 24-bit integer: no more bytes to read")
end
ret = (@addr[@pos + 2].to_u32! << 16) | (@addr[@pos + 1].to_u32! << 8) | @addr[@pos]
@pos += 3
ret & 0xFFFFFF
end
# Reads a `UInt32` from the file at the current position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read a `UInt32`.
@[AlwaysInline]
def readUInt32 : UInt32
raise MmappedFileError.new("File is closed") if @addr.null?
if @pos > @len - 4
raise MmappedFileEofError.new("Cannot read 32-bit integer: no more bytes to read")
end
ret = (@addr + @pos).unsafe_as(Pointer(UInt32))[0]
@pos += 4
ret
end
# Reads a `UInt64` from the file at the current position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read a `UInt64`.
@[AlwaysInline]
def readUInt64 : UInt64
raise MmappedFileError.new("File is closed") if @addr.null?
if @pos > @len - 8
raise MmappedFileEofError.new("Cannot read 64-bit integer: no more bytes to read")
end
ret = (@addr + @pos).unsafe_as(Pointer(UInt64))[0]
@pos += 8
ret
end
# Reads a `UInt128` from the file at the current position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read a `UInt128`.
@[AlwaysInline]
def readUInt128 : UInt128
raise MmappedFileError.new("File is closed") if @addr.null?
if @pos > @len - 16
raise MmappedFileEofError.new("Cannot read 128-bit integer: no more bytes to read")
end
ret = (@addr + @pos).unsafe_as(Pointer(UInt128))[0]
@pos += 16
ret
end
# Reads *count* bytes, then returns a new `Bytes` slice containing those
# bytes.
@[AlwaysInline]
def readBytes(count : Int32) : Bytes
raise MmappedFileError.new("File is closed") if @addr.null?
if @pos > @len - count
raise MmappedFileEofError.new("Cannot read #{count} bytes: not enough data")
end
ret = Bytes.new(count)
(@addr + @pos).copy_to(ret.to_unsafe, count)
@pos += count
ret
end
# Reads a `String` of *count* bytes long, then returns the new `String`.
@[AlwaysInline]
def readString(count : Int32) : String
String.new(readBytes(count))
end
# Reads a `Int8` from the file at the current position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read a `Int8`.
@[AlwaysInline]
def readInt8 : Int8
readUInt8.to_i8!
end
# Reads a `Int16` from the file at the current position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read a `Int16`.
@[AlwaysInline]
def readInt16 : Int16
readUInt16.to_i16!
end
# Reads three bytes from the file at the current position and returns a
# 24-bit integer as an `Int32`. Raises an `MmappedFileEofError` if there
# are not enough bytes to read a 24-bit integer.
@[AlwaysInline]
def readInt24 : Int32
ret = readUInt24
if bitflag?(ret, 0x800000)
# Convert to negative
-8388608_i32 + (ret & 0x7FFFFF).to_i32!
else
(ret & 0xFFFFFF).to_i32!
end
end
# Reads an `Int32` from the file at the current position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read an `Int32`.
@[AlwaysInline]
def readInt32 : Int32
readUInt32.to_i32!
end
# Reads an `Int64` from the file at the current position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read an `Int64`.
@[AlwaysInline]
def readInt64 : Int64
readUInt64.to_i64!
end
# Reads an `Int128` from the file at the current position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read an `Int128`.
@[AlwaysInline]
def readInt128 : Int128
readUInt128.to_i128!
end
###
### "get" method variants. These take a position and don't change @pos.
###
# Returns a `UInt8` from the file at the given position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read a UInt8 at the
# given position. This does not change the internal position of the
# internal cursor (`#pos`).
@[AlwaysInline]
def getUInt8(position : LibC::SizeT) : UInt8
raise MmappedFileError.new("File is closed") if @addr.null?
if position > @len
raise MmappedFileEofError.new("Bad position to get a UInt8 for a file of size #{@len} bytes")
end
@addr[position]
end
# Returns a `UInt16` from the file at the given position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read a UInt16 at
# the given position. This does not change the internal position of the
# internal cursor (`#pos`).
@[AlwaysInline]
def getUInt16(position : LibC::SizeT) : UInt16
raise MmappedFileError.new("File is closed") if @addr.null?
if position > @len - 2
raise MmappedFileEofError.new("Bad position to get a UInt16 for a file of size #{@len} bytes")
end
(@addr + position).unsafe_as(Pointer(UInt16))[0]
end
# Gets three bytes from *position* and returns a `UInt32`.
# `MmappedFileEofError` if there are not enough bytes to read a 24-bit
# number at the given position. This does not change the internal position
# of the internal cursor (`#pos`).
@[AlwaysInline]
def getUInt24(position : LibC::SizeT) : UInt32
raise MmappedFileError.new("File is closed") if @addr.null?
if position > @len - 3
raise MmappedFileEofError.new("Bad position to get a 24-bit UInt32 for a file of size #{@len} bytes")
end
ret = (@addr[position + 2].to_u32! << 16) |
(@addr[position + 1].to_u32! << 8) |
@addr[position]
ret & 0xFFFFFF
end
# Returns a `UInt32` from the file at the given position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read a UInt32 at
# the given position. This does not change the internal position of the
# internal cursor (`#pos`).
@[AlwaysInline]
def getUInt32(position : LibC::SizeT) : UInt32
raise MmappedFileError.new("File is closed") if @addr.null?
if position > @len - 4
raise MmappedFileEofError.new("Bad position to get a UInt32 for a file of size #{@len} bytes")
end
(@addr + position).unsafe_as(Pointer(UInt32))[0]
end
# Returns a `UInt64` from the file at the given position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read a UInt64 at
# the given position. This does not change the internal position of the
# internal cursor (`#pos`).
@[AlwaysInline]
def getUInt64(position : LibC::SizeT) : UInt64
raise MmappedFileError.new("File is closed") if @addr.null?
if position > @len - 8
raise MmappedFileEofError.new("Bad position to get a UInt64 for a file of size #{@len} bytes")
end
(@addr + position).unsafe_as(Pointer(UInt64))[0]
end
# Returns a `UInt128` from the file at the given position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read a UInt128 at
# the given position. This does not change the internal position of the
# internal cursor (`#pos`).
@[AlwaysInline]
def getUInt128(position : LibC::SizeT) : UInt128
raise MmappedFileError.new("File is closed") if @addr.null?
if position > @len - 16
raise MmappedFileEofError.new("Bad position to get a UInt128 for a file of size #{@len} bytes")
end
(@addr + position).unsafe_as(Pointer(UInt128))[0]
end
# Gets *count* bytes from the given position, then returns a new `Bytes`
# slice containing those bytes. This does not change the internal position
# of the internal cursor (`#pos`).
def getBytes(count : Int32, position : LibC::SizeT) : Bytes
raise MmappedFileError.new("File is closed") if @addr.null?
if position > @len - count
raise MmappedFileEofError.new("Cannot get #{count} bytes at #{position}: not enough data")
end
ret = Bytes.new(count)
(@addr + position).copy_to(ret.to_unsafe, count)
ret
end
# Gets a `String` of *count* bytes long at the given position, then returns
# the new `String`. This does not change the internal position of the
# internal cursor (`#pos`).
def getString(count : Int32, position : LibC::SizeT) : String
String.new(getBytes(count, position))
end
# Returns an `Int8` from the file at the given position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read an `Int8` at
# the given position. This does not change the internal position of the
# internal cursor (`#pos`).
def getInt8(position : LibC::SizeT) : Int8
getUInt8(position).to_i8!
end
# Returns an `Int16` from the file at the given position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read an `Int16` at
# the given position. This does not change the internal position of the
# internal cursor (`#pos`).
def getInt16(position : LibC::SizeT) : Int16
getUInt16(position).to_i16!
end
# Gets three bytes from *position* and returns an `Int32`.
# `MmappedFileEofError` if there are not enough bytes to read a 24-bit
# integer at the given position. This does not change the internal position
# of the internal cursor (`#pos`).
def getInt24(position : LibC::SizeT) : Int32
ret = getUInt24(position)
if bitflag?(ret, 0x800000)
# Convert to negative
-8388608_i32 + (ret & 0x7FFFFF).to_i32!
else
ret
end
end
# Returns an `Int32` from the file at the given position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read an `Int32` at
# the given position. This does not change the internal position of the
# internal cursor (`#pos`).
def getInt32(position : LibC::SizeT) : Int32
getUInt32(position).to_i32!
end
# Returns an `Int64` from the file at the given position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read an `Int64` at
# the given position. This does not change the internal position of the
# internal cursor (`#pos`).
def getInt64(position : LibC::SizeT) : Int64
getUInt64(position).to_i64!
end
# Returns an `Int128` from the file at the given position. Raises an
# `MmappedFileEofError` if there are not enough bytes to read an `Int128` at
# the given position. This does not change the internal position of the
# internal cursor (`#pos`).
def getInt128(position : LibC::SizeT) : Int128
getUInt128(position).to_i128!
end
###
### "write" method variants. These change @pos.
###
### These don't use the "put" variants internally so that there's no double
### checks on the position.
###
# Writes a `UInt8` to the file at the current position. If there is no
# space to write the value, then this raises a `MmappedFileEofError`.
@[AlwaysInline]
def writeUInt8(val : UInt8) : Nil
raise MmappedFileError.new("File is closed") if @addr.null?
if @pos < @len
@addr[@pos] = val
@pos += 1
else
raise MmappedFileEofError.new("Cannot write 8-bit integer, not enough space left")
end
end
# Writes a `UInt16` to the file at the current position. If there is no
# space to write the value, then this raises a `MmappedFileEofError`.
@[AlwaysInline]
def writeUInt16(val : UInt16) : Nil
raise MmappedFileError.new("File is closed") if @addr.null?
if @pos + 2 <= @len
(@addr + @pos).unsafe_as(Pointer(UInt16)).value = val
@pos += 2
else
raise MmappedFileEofError.new("Cannot write 16-bit integer, not enough space left")
end
end
# Writes a 24-bit integer stored in a `UInt32` to the file at the current
# position. If there is no space to write the value, then this raises a
# `MmappedFileEofError`.
#
# If *val* does not contain a 24-bit integer, this raises an
# `ArgumentError`.
@[AlwaysInline]
def writeUInt24(val : UInt32, littleEndian? : Bool = true) : Nil
raise MmappedFileError.new("File is closed") if @addr.null?
if bitflag?(val, 0xFF000000)
raise ArgumentError.new("Value does not contain a 24-bit integer")
end
if @pos + 3 <= @len
if littleEndian?
b1 = ((val & 0xFF0000) >> 16).to_u8!
b2 = ((val & 0x00FF00) >> 8).to_u8!
b3 = (val & 0x0000FF).to_u8!
else
b3 = ((val & 0xFF0000) >> 16).to_u8!
b2 = ((val & 0x00FF00) >> 8).to_u8!
b1 = (val & 0x0000FF).to_u8!
end
@addr[@pos ] = b1
@addr[@pos + 1] = b2
@addr[@pos + 2] = b3
@pos += 3
else
raise MmappedFileEofError.new("Cannot write 24-bit integer, not enough space left")
end
end
# Writes a `UInt32` to the file at the current position. If there is no
# space to write the value, then this raises a `MmappedFileEofError`.
@[AlwaysInline]
def writeUInt32(val : UInt32) : Nil
raise MmappedFileError.new("File is closed") if @addr.null?
if @pos + 4 <= @len
(@addr + @pos).unsafe_as(Pointer(UInt32)).value = val
@pos += 4
else
raise MmappedFileEofError.new("Cannot write 32-bit integer, not enough space left")
end
end
# Writes a `UInt64` to the file at the current position. If there is no
# space to write the value, then this raises a `MmappedFileEofError`.
@[AlwaysInline]
def writeUInt64(val : UInt64) : Nil
raise MmappedFileError.new("File is closed") if @addr.null?
if @pos + 8 <= @len
(@addr + @pos).unsafe_as(Pointer(UInt64)).value = val
@pos += 8
else
raise MmappedFileEofError.new("Cannot write 64-bit integer, not enough space left")
end
end
# Writes a `UInt128` to the file at the current position. If there is no
# space to write the value, then this raises a `MmappedFileEofError`.
@[AlwaysInline]
def writeUInt128(val : UInt128) : Nil
raise MmappedFileError.new("File is closed") if @addr.null?
if @pos + 16 <= @len
(@addr + @pos).unsafe_as(Pointer(UInt128)).value = val
@pos += 16
else
raise MmappedFileEofError.new("Cannot write 128-bit integer, not enough space left")
end
end
# Writes an `Int8` to the file at the current position. If there is no
# space to write the value, then this raises a `MmappedFileEofError`.
def writeInt8(val : Int8) : Nil
writeUInt8(val.to_u8!)
end
# Writes an `Int16` to the file at the current position. If there is no
# space to write the value, then this raises a `MmappedFileEofError`.
def writeInt16(val : Int16) : Nil
writeUInt16(val.to_u16!)
end
# Writes a 24-bit integer stored in an `Int32` to the file at the current
# position. If there is no space to write the value, then this raises a
# `MmappedFileEofError`.
def writeInt24(val : Int32) : Nil
{% begin %}
{% maxVal = (2 ** 23) - 1 %}
{% minVal = -(2 ** 23) %}
if val > {{maxVal}} || val < {{minVal}}
raise ArgumentError.new("Value is a not a signed 24-bit integer.")
end
{% end %}
writeUInt24(val.to_u32!)
end
# Writes an `Int32` to the file at the current position. If there is no
# space to write the value, then this raises a `MmappedFileEofError`.
def writeInt32(val : Int32) : Nil
writeUInt32(val.to_u32!)
end
# Writes an `Int64` to the file at the current position. If there is no
# space to write the value, then this raises a `MmappedFileEofError`.
def writeInt64(val : Int64) : Nil
writeUInt64(val.to_u64!)
end
# Writes an `Int128` to the file at the current position. If there is no
# space to write the value, then this raises a `MmappedFileEofError`.
def writeInt128(val : Int128) : Nil
writeUInt128(val.to_u128!)
end
# Writes a `Bytes` slice to the file at the current position. If there is
# not enough space to write the value, then this raises a
# `MmappedFileEofError`.
def writeBytes(data : Bytes) : Nil
raise MmappedFileError.new("File is closed") if @addr.null?
if @pos + data.size <= @len
data.to_unsafe.copy_to(@addr + @pos, data.size)
@pos += data.size
else
raise MmappedFileEofError.new("Cannot write #{data.size} bytes, not enough space left")
end
end
# Writes a `String` as raw bytes to the file at the current position. If
# there is not enough space to write the value, then this raises a
# `MmappedFileEofError`.
def writeString(str : String) : Nil
writeBytes(str.to_slice)
end
###
### "put" method variants. These take a position and don't change @pos.
###
# Writes a `UInt8` to the file at the given position. If there is no space
# to write the value, then this raises a `MmappedFileEofError`. This does
# not change the internal position of the internal cursor (`#pos`).
@[AlwaysInline]
def putUInt8(val : UInt8, position : LibC::SizeT) : Nil
raise MmappedFileError.new("File is closed") if @addr.null?
if position < @len
@addr[position] = val
else
raise MmappedFileEofError.new("Cannot write 8-bit integer at #{position}")
end
end
# Writes a `UInt16` to the file at the given position. If there is no space
# to write the value, then this raises a `MmappedFileEofError`. This does
# not change the internal position of the internal cursor (`#pos`).
@[AlwaysInline]
def putUInt16(val : UInt16, position : LibC::SizeT) : Nil
raise MmappedFileError.new("File is closed") if @addr.null?
if position < @len - 2
(@addr + position).unsafe_as(Pointer(UInt16)).value = val
else
raise MmappedFileEofError.new("Cannot write 16-bit integer at #{position}")
end
end
# Writes a 24-bit integer stored in a `UInt32` to the file at the given
# position. If there is no space to write the value, then this raises a
# `MmappedFileEofError`. This does not change the internal position of the
# internal cursor (`#pos`).
#
# If *val* does not contain a 24-bit integer, this raises an
# `ArgumentError`.
@[AlwaysInline]
def putUInt24(val : UInt32, position : LibC::SizeT, littleEndian? : Bool = true) : Nil
raise MmappedFileError.new("File is closed") if @addr.null?
if bitflag?(val, 0xFF000000)
raise ArgumentError.new("Value does not contain a 24-bit integer")
end
if position < @len - 3
if littleEndian?
b1 = ((val & 0xFF0000) >> 16).to_u8!
b2 = ((val & 0x00FF00) >> 8).to_u8!
b3 = (val & 0x0000FF).to_u8!
else
b3 = ((val & 0xFF0000) >> 16).to_u8!
b2 = ((val & 0x00FF00) >> 8).to_u8!
b1 = (val & 0x0000FF).to_u8!
end
@addr[@pos ] = b1
@addr[@pos + 1] = b2
@addr[@pos + 2] = b3
@pos += 3
else
raise MmappedFileEofError.new("Cannot write UInt16, not enough space left")
end
end
# Writes a `UInt32` to the file at the given position. If there is no space
# to write the value, then this raises a `MmappedFileEofError`. This does
# not change the internal position of the internal cursor (`#pos`).
@[AlwaysInline]
def putUInt32(val : UInt32, position : LibC::SizeT) : Nil
raise MmappedFileError.new("File is closed") if @addr.null?
if position < @len - 4
(@addr + position).unsafe_as(Pointer(UInt32)).value = val
else
raise MmappedFileEofError.new("Cannot write 32-bit integer at #{position}")
end
end
# Writes a `UInt64` to the file at the given position. If there is no space
# to write the value, then this raises a `MmappedFileEofError.` This does
# not change the internal position of the internal cursor (`#pos`).
@[AlwaysInline]
def putUInt64(val : UInt64, position : LibC::SizeT) : Nil
raise MmappedFileError.new("File is closed") if @addr.null?
if position < @len - 8
(@addr + position).unsafe_as(Pointer(UInt64)).value = val
else
raise MmappedFileEofError.new("Cannot write 64-bit integer at #{position}")
end
end
# Writes a `UInt128` to the file at the given position. If there is no
# space to write the value, then this raises a `MmappedFileEofError`. This
# does not change the internal position of the internal cursor (`#pos`).
@[AlwaysInline]
def putUInt128(val : UInt128, position : LibC::SizeT) : Nil
raise MmappedFileError.new("File is closed") if @addr.null?
if position < @len - 16
(@addr +position).unsafe_as(Pointer(UInt128)).value = val
else
raise MmappedFileEofError.new("Cannot write 128-bit integer at #{position}")
end
end
# Writes an `Int8` to the file at the given position. If there is no space
# to write the value, then this raises a `MmappedFileEofError`. This does
# not change the internal position of the internal cursor (`#pos`).
def putInt8(val : Int8, position : LibC::SizeT) : Nil
putUInt8(val.to_u8!, position)
end
# Writes an `Int16` to the file at the given position. If there is no space
# to write the value, then this raises a `MmappedFileEofError`. This does
# not change the internal position of the internal cursor (`#pos`).
def putInt16(val : Int16, position : LibC::SizeT) : Nil
putUInt16(val.to_u16!, position)
end
# Writes a 24-bit integer stored in a `UInt32` to the file at the given
# position. If there is no space to write the value, then this raises a
# `MmappedFileEofError`. This does not change the internal position of the
# internal cursor (`#pos`).
#
# If *val* does not contain a 24-bit integer, this raises an
# `ArgumentError`.
def putInt24(val : Int32, position : LibC::SizeT) : Nil
putUInt24(val.to_u32!, position)
end
# Writes an `Int32` to the file at the given position. If there is no space
# to write the value, then this raises a `MmappedFileEofError`. This does
# not change the internal position of the internal cursor (`#pos`).
def putInt32(val : Int32, position : LibC::SizeT) : Nil
putUInt32(val.to_u32!, position)
end
# Writes an `Int64` to the file at the given position. If there is no space
# to write the value, then this raises a `MmappedFileEofError`. This does
# not change the internal position of the internal cursor (`#pos`).
def putInt64(val : Int64, position : LibC::SizeT) : Nil
putUInt64(val.to_u64!, position)
end
# Writes an `Int128` to the file at the given position. If there is no
# space to write the value, then this raises a `MmappedFileEofError`. This
# does not change the internal position of the internal cursor (`#pos`).
def putInt128(val : Int128, position : LibC::SizeT) : Nil
putUInt128(val.to_u128!, position)
end
# Writes a `Bytes` slice to the file at the given position. If there is not
# enough space to write the value, then this raises a `MmappedFileEofError`.
# This does not change the internal position of the internal cursor
# (`#pos`).
def putBytes(data : Bytes, position : LibC::SizeT) : Nil
raise MmappedFileError.new("File is closed") if @addr.null?
if position < @len - data.size
data.to_unsafe.copy_to(@addr + position, data.size)
else
raise MmappedFileEofError.new("Cannot put #{data.size} bytes at position #{position}")
end
end
# Writes a `String` as raw bytes to the file at the given position. If
# there is not enough space to write the value, then this raises a
# `MmappedFileEofError`. This does not change the internal position of the
# internal cursor (`#pos`).
def putString(str : String, position : LibC::SizeT) : Nil
putBytes(str.to_slice, position)
end
end
end