Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This library has no dependencies other than the Nim standard library.
* Aim of `flatty` is to be the fastest and simplest serializer/deserializer for Nim.
* Also includes `hexprint` to print out binary data.
* Also includes `binny` a simpler replacement for StringStream (no IO effects, operates on a string)
* Also includes `cursor` for bounds-checked sequential binary reads with catchable errors.
* Also includes `hashy` a hash for any objects based on the flatty serializer.
* Also includes `encode` a way to convert to/from utf16 BE/LE and with BOM and utf32.
* Only Nim-owned values are serialized. Raw pointers, cstrings, procs, and distinct OS handles are rejected.
Expand Down
151 changes: 151 additions & 0 deletions src/flatty/cursor.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
## Bounds-checked sequential reads over an in-memory binary blob.
##
## `binny` is ideal for fixed-offset primitive reads. `BinaryCursor` adds a
## small amount of state for parsers that consume fields in order and need
## catchable errors at an untrusted-input boundary.

import flatty/binny

type
BinaryCursorError* = object of CatchableError

BinaryCursor* = object
data: string
position*: int

proc fail(message: string) {.noreturn.} =
raise newException(BinaryCursorError, message)

proc initBinaryCursor*(data: sink string; position = 0): BinaryCursor =
## Creates a cursor over `data`, initially positioned at `position`.
if position < 0 or position > data.len:
fail("binary cursor position is outside the input")
BinaryCursor(data: move(data), position: position)

proc len*(cursor: BinaryCursor): int {.inline.} =
## Returns the total byte length of the cursor input.
cursor.data.len

proc remaining*(cursor: BinaryCursor): int {.inline.} =
## Returns the number of unread bytes.
cursor.data.len - cursor.position

proc atEnd*(cursor: BinaryCursor): bool {.inline.} =
## Returns true when no unread bytes remain.
cursor.position == cursor.data.len

proc require*(cursor: BinaryCursor; count: int) =
## Ensures that `count` bytes can be consumed from the current position.
if count < 0 or count > cursor.remaining:
fail(
"binary cursor needs " & $count & " bytes at " & $cursor.position &
", but only " & $cursor.remaining & " remain"
)

proc seek*(cursor: var BinaryCursor; position: int) =
## Moves the cursor to an absolute byte position.
if position < 0 or position > cursor.data.len:
fail("binary cursor seek is outside the input")
cursor.position = position

proc skip*(cursor: var BinaryCursor; count: int) =
## Advances the cursor by `count` bytes.
cursor.require(count)
cursor.position += count

proc align*(cursor: var BinaryCursor; alignment: int) =
## Advances to the next `alignment` byte boundary.
if alignment <= 0:
fail("binary cursor alignment must be positive")
let remainder = cursor.position mod alignment
if remainder != 0:
cursor.skip(alignment - remainder)

proc readBytes*(cursor: var BinaryCursor; count: int): string =
## Reads exactly `count` bytes.
cursor.require(count)
result = cursor.data[cursor.position ..< cursor.position + count]
cursor.position += count

proc readCString*(cursor: var BinaryCursor; maxBytes = -1): string =
## Reads a NUL-terminated string, optionally bounded by `maxBytes`.
if maxBytes < -1:
fail("binary cursor string bound cannot be negative")
let limit =
if maxBytes == -1:
cursor.data.len
else:
cursor.position + min(maxBytes, cursor.remaining)
var stop = cursor.position
while stop < limit and cursor.data[stop] != '\0':
inc stop
if stop == limit:
fail("binary cursor string is not NUL-terminated within its bound")
result = cursor.data[cursor.position ..< stop]
cursor.position = stop + 1

proc readSubcursor*(cursor: var BinaryCursor; count: int): BinaryCursor =
## Reads a bounded region and returns a cursor over that region.
initBinaryCursor(cursor.readBytes(count))

proc readUint8*(cursor: var BinaryCursor): uint8 =
cursor.require(1)
result = cursor.data.readUint8(cursor.position)
inc cursor.position

proc readUint16Le*(cursor: var BinaryCursor): uint16 =
cursor.require(2)
result = cursor.data.readUint16(cursor.position)
cursor.position += 2

proc readUint16Be*(cursor: var BinaryCursor): uint16 =
cursor.readUint16Le().swap()

proc readUint32Le*(cursor: var BinaryCursor): uint32 =
cursor.require(4)
result = cursor.data.readUint32(cursor.position)
cursor.position += 4

proc readUint32Be*(cursor: var BinaryCursor): uint32 =
cursor.readUint32Le().swap()

proc readUint64Le*(cursor: var BinaryCursor): uint64 =
cursor.require(8)
result = cursor.data.readUint64(cursor.position)
cursor.position += 8

proc readUint64Be*(cursor: var BinaryCursor): uint64 =
cursor.readUint64Le().swap()

proc readInt8*(cursor: var BinaryCursor): int8 =
cast[int8](cursor.readUint8())

proc readInt16Le*(cursor: var BinaryCursor): int16 =
cast[int16](cursor.readUint16Le())

proc readInt16Be*(cursor: var BinaryCursor): int16 =
cast[int16](cursor.readUint16Be())

proc readInt32Le*(cursor: var BinaryCursor): int32 =
cast[int32](cursor.readUint32Le())

proc readInt32Be*(cursor: var BinaryCursor): int32 =
cast[int32](cursor.readUint32Be())

proc readInt64Le*(cursor: var BinaryCursor): int64 =
cast[int64](cursor.readUint64Le())

proc readInt64Be*(cursor: var BinaryCursor): int64 =
cast[int64](cursor.readUint64Be())

proc readFloat32Le*(cursor: var BinaryCursor): float32 =
cast[float32](cursor.readUint32Le())

proc readFloat32Be*(cursor: var BinaryCursor): float32 =
cast[float32](cursor.readUint32Be())

proc readFloat64Le*(cursor: var BinaryCursor): float64 =
cast[float64](cursor.readUint64Le())

proc readFloat64Be*(cursor: var BinaryCursor): float64 =
cast[float64](cursor.readUint64Be())
50 changes: 50 additions & 0 deletions tests/test_cursor.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import std/strutils

import flatty/cursor

block:
var cursor = initBinaryCursor(
"\x12\x34\x56\x78\x00\x00\x50\x40hello\x00tail"
)
doAssert cursor.readUint8() == 0x12'u8
doAssert cursor.readUint16Be() == 0x3456'u16
doAssert cursor.readUint8() == 0x78'u8
doAssert cursor.readFloat32Le() == 3.25'f32
doAssert cursor.readCString() == "hello"
doAssert cursor.readBytes(4) == "tail"
doAssert cursor.atEnd

block:
var cursor = initBinaryCursor("\x01\x02\x03\x04payload")
let header = cursor.readSubcursor(4)
var readableHeader = header
doAssert readableHeader.readUint32Le() == 0x04030201'u32
doAssert readableHeader.atEnd
doAssert cursor.remaining == 7
cursor.seek(4)
cursor.align(4)
doAssert cursor.readBytes(7) == "payload"

block:
var cursor = initBinaryCursor("abc")
try:
discard cursor.readUint32Le()
doAssert false
except BinaryCursorError as error:
doAssert "only 3 remain" in error.msg

block:
var cursor = initBinaryCursor("unterminated")
try:
discard cursor.readCString(5)
doAssert false
except BinaryCursorError as error:
doAssert "not NUL-terminated" in error.msg

block:
var cursor = initBinaryCursor("1234")
try:
cursor.align(0)
doAssert false
except BinaryCursorError:
discard
2 changes: 1 addition & 1 deletion tests/tests.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import test_binny, test_datetime, test_flatty, test_hashy, test_hexprint,
import test_binny, test_cursor, test_datetime, test_flatty, test_hashy, test_hexprint,
test_issue_objects, test_memoryused, test_modes, test_objvar

echo "all tests pass"