nupp.io

nupp.io is bytes: storage for them, and the readers and writers that move them. Reach for it when a program needs a growable buffer, an immutable snapshot of one, or a parser that works the same over a buffer, a file and an HTTP response body.

local bytes = nupp.io.newBuffer("hello")
local reader = bytes:newReader()
assert(reader:read(5) == "hello")

A buffer holds its bytes in the target's physical storage: a native array on LuaJIT, or a rooted allocation in a Wasm application. Reader and Writer are interfaces rather than the concrete things that satisfy them, which is what lets code written against the contract work over any of them without knowing which it has.

The rest of what io means is a module of its own, because each has a luacase name and so can have one. See nupp.io.path for filesystem names, nupp.io.uri for resource identifiers, nupp.io.files for the filesystem itself, nupp.io.process for running a child process, nupp.io.net for network connections, nupp.io.tls for encrypting one, and nupp.io.http for the asynchronous HTTP client. Keeping them apart is what lets a program that wants a byte buffer carry a byte buffer rather than every provider behind the name.

The last four move their bytes through the Reader and Writer contracts on this page, so a parser written against byte I/O reads a file, a child's output, a connection, or a response body without knowing which it has.

Every value here is an owner. close is safe to call repeatedly and answers whether the thing it was writing into was still open, so a caller that cares can tell a clean finish from a destination that went away first. isReleased reports that state without changing it.

Buffers#

newBuffer() creates an empty growable buffer. A string supplies initial bytes; an integer reserves capacity without changing the length.

local bytes = nupp.io.newBuffer("hello")
bytes:setString("!", bytes:length())
assert(bytes:getString() == "hello!")

Buffer offsets are zero-based, as they are wherever a count is an offset into storage rather than a position in a Lua string. See Byte positions for the rule and where the other convention applies.

getString(offset, count) copies a range. setString overwrites from an offset and grows the buffer as needed, filling any gap with zero bytes. clear sets the length to zero without discarding capacity, and resize truncates or zero-fills:

local bytes = nupp.io.newBuffer("hello")
bytes:ensureCapacity(4096)
assert(bytes:capacity() >= 4096)
bytes:resize(3)
assert(bytes:getString() == "hel")

Capacity is the allocation, not a recorded number. Growing at least doubles it, so appending through a writer costs amortized constant time per byte and capacity() reports bytes that are actually held. ensureCapacity reserves at least the minimum asked for.

close() releases the buffer's storage, and an operation on a released buffer raises rather than answering a reason.

readSpan() borrows the buffer's initialized bytes without separating its pointer from its length. Native producers write through reserveWrite(offset, count): the returned lease supplies a checked writable span, and commit(written) changes the logical length only after the producer succeeds. Dropping the lease aborts it and leaves the length unchanged.

Byte views#

view(offset, count) returns an immutable snapshot, not a mutable alias into the buffer. It remains valid if the source buffer changes or closes:

local buffer = nupp.io.newBuffer("header-body")
local header = buffer:view(0, 6)
buffer:clear()
assert(header:getString() == "header")
header:close()

A view can make a smaller view, open its own snapshot reader, report its byte length, copy to a string, borrow a checked span with readSpan(), and be closed. Its offsets are zero-based like a buffer's. The data and UTF-8 modules accept views, which is what keeps those APIs from being coupled to a mutable buffer.

Readers#

A nupp.io.Reader is a forward-only byte source. newStringReader(text) reads a string; buffer:newReader() reads a snapshot of the buffer's current contents. A reader owns its close obligation and closes automatically at the end of its scope unless close() consumes it earlier.

local reader = nupp.io.newStringReader("abcdef")
assert(reader:read(2) == "ab")

local destination = nupp.io.newBuffer()
assert(reader:readInto(destination, 0, 3) == 3)
assert(destination:getString() == "cde")
assert(reader:read(8) == "f")
assert(reader:read(8) == "") -- EOF

read(count) requires a positive count and returns at most that many bytes. It returns an empty string at EOF and nil with a reason after close. readSpan(writable) fills a positive-sized checked writable span. readInto(buffer, offset, count) returns zero at EOF and reads 64 KiB when given no count; an explicit count must be positive. A failed or EOF read does not extend the destination. transferTo(writer) copies the entire remaining source and returns the byte count.

Writers#

buffer:newWriter() clears the buffer and returns a forward-only writer targeting it. A writer likewise closes automatically; flush() publishes pending work without consuming that ownership.

local destination = nupp.io.newBuffer()
local writer = destination:newWriter()
assert(writer:write("prefix:"))

local payload = nupp.io.newBuffer("body")
assert(writer:writeSpan(payload:readSpan()) == 4)
assert(writer:flush())
assert(destination:getString() == "prefix:body")

write answers a boolean and writeSpan answers the byte count. Both answer a reason when the writer is closed or the destination was released. The span is the common zero-copy input contract for buffers, views, files, processes, and HTTP transfers; there is no unchecked raw-pointer writer alongside it. flush does nothing for memory and is part of the contract so that a later file or socket writer implements the same interface.

Typed scalars#

A reader and a writer move bytes. newScalarReader and newScalarWriter add the missing piece, a sized integer or float landing on those bytes without an ffi.cast at every call site.

local writer = nupp.io.newScalarWriter()
writer:writeUint32(1447383632 as uint32):writeFloat64(1.5)

local stored = writer:buffer()
assert(stored ~= nil)
local reader = nupp.io.newScalarReader(stored as nupp.io.Buffer)
assert(reader:readUint32() == (1447383632 as uint32))
assert(reader:readFloat64() == 1.5)
assert(reader:atEnd())

Every write answers the writer, so calls chain. A short read raises rather than answering a reason, which is the one place this pair departs from the reader and writer contracts above: a scalar either landed whole or the source was not what the format said, and there is no partial value to hand back.

Host byte order

Both directions read and write host-endian, the only order LuaJIT ships, so that is the default. A format fixed to one byte order casts and swaps explicitly, and there are no LE and BE variants here until something needs them. Adding a second set without a caller would double the surface and leave half of it untested.

Scalar sources#

newScalarReader takes whatever holds the bytes. A string, a ByteView or a Buffer is read from a copy taken there and then, so remaining() knows the count:

local reader = nupp.io.newScalarReader("\1\2\3\4")
assert(reader:remaining() == 4)

A Reader is consumed as it goes, which is what lets a file or an HTTP body be read a field at a time. It cannot say how much is left, so remaining() answers nil and atEnd() is the question to ask instead. The scalar adapter takes that reader: closing or dropping the adapter closes or drops the reader exactly once.

Scalar destinations#

newScalarWriter appends to a Buffer, keeping what that buffer already holds, or writes through a Writer. Given nothing it starts a buffer of its own, which buffer() hands back:

local destination = nupp.io.newBuffer("header:")
nupp.io.newScalarWriter(destination):writeUint8(33 as uint32)
assert(destination:getString() == "header:!")

A writer pointed at somebody else's Writer answers nil from buffer(), since the bytes are already gone. That writer is taken, so closing or dropping the scalar adapter closes or drops it exactly once. A buffer supplied by the caller is only borrowed. The buffer created by the no-argument form belongs to the scalar writer; buffer() borrows it for as long as the scalar writer remains live.

Byte queues#

A nupp.text.buffer is a byte queue: put appends to the back and get consumes from the front. newScalarReader accepts one directly, so bytes assembled there are read without copying everything in it first, and the queue's own get stays usable over the same bytes.

local buffer = nupp.text.buffer

local queue = buffer.new()
queue:put("header:")
queue:put("!")

local reader = nupp.io.newScalarReader(queue)
assert(reader:readBytes(7) == "header:")
assert(queue:tostring() == "!")
assert(reader:readUint8() == (33 as uint8))

Reading drains the queue, and remaining() reports what it still holds. newQueueReader is the same bridge one layer down: it answers an ordinary Reader over the queue, for transferTo, readInto, and everything else that contract already covers.

local buffer = nupp.text.buffer

local destination = nupp.io.newBuffer()
local queue = buffer.new()
queue:put("payload")
assert(nupp.io.newQueueReader(queue):transferTo(destination:newWriter()) == 7)
assert(destination:getString() == "payload")

Neither takes the queue over. Nothing here frees or closes it, and one read to empty leaves an empty queue rather than a released one.

Submodules

ModuleDescription
nupp.io.filesnupp.io.files reads what the filesystem knows about a name: whether it resolves, what it refers to, what a directory...
nupp.io.httpnupp.io.http sends HTTP requests through the selected host implementation without blocking the caller's frame.
nupp.io.netnupp.io.net is the network as bytes: a listener, the connections it accepts, and connections this process opens itself.
nupp.io.pathnupp.io.path answers an immutable path value whose joining, normalizing and splitting follow the platform's own rules.
nupp.io.processStarts a child process and drains its streams without deadlocking.
nupp.io.storagePersistent key/value storage owned by the current host.
nupp.io.tlsnupp.io.tls takes ownership of a connection nupp.io.net already opened and turns it into an encrypted connection.
nupp.io.urinupp.io.uri parses one absolute URI into an immutable value and answers its components from the parse rather than...

Module contents

Constructors

ConstructorDescription
newBufferCreates a buffer, optionally over starting bytes or a starting capacity.
newLinesTakes a reader apart into lines.
newQueueReaderOpens a reader over a byte queue, which it does not own.
newScalarReaderOpens a cursor for sized integer and float reads.
newScalarWriterOpens a cursor for sized integer and float writes.
newStringReaderOpens a reader over bytes held as a string.

Types

TypeKindDescription
BufferinterfaceGrowable byte storage whose pointer and logical length never escape separately.
BufferWriteLeaseinterfaceAn exclusive prepared region of a buffer.
ByteQueueinterfaceA queue of bytes something else is filling, which a reader may be opened over without owning it.
ByteViewinterfaceA snapshot of bytes that cannot be written through.
LinesinterfaceA reader taken apart into lines.
ReaderinterfaceA forward-only byte source.
ScalarReaderinterfaceReads fixed-width numbers from a source of bytes.
ScalarWriterinterfaceWrites fixed-width numbers into a destination.
WriterinterfaceAn appending byte sink.

Constructors#

newBufferconstructor#

function newBuffer(initial: integer | string?): affine(Buffer, destroyOwner)

Creates a buffer, optionally over starting bytes or a starting capacity.

The buffer is an owner, so it closes at its lexical boundary unless it is transferred out of one. A number is a capacity to allocate rather than content, and the buffer is still empty afterwards.

const io = nupp.io

do
    local buffer = io.newBuffer("hello")
    assert(buffer:length() == 5)

    local sized = io.newBuffer(4096)
    assert(sized:length() == 0 and sized:capacity() == 4096)
end

Arguments

NameTypeDescription
initialinteger | string?

the starting bytes, a starting capacity, or nothing

Returns

TypeDescription
affine(Buffer, destroyOwner)

the buffer, owned by the caller

Raises

  • when initial is neither bytes nor a capacity

newLinesconstructor#

function newLines(takes source: Reader, limit: integer?): affine(Lines)

Takes a reader apart into lines.

The source is taken rather than borrowed, and closing the result closes it: a line reader holds the bytes it read past a terminator, so a second reader over the same source would be reading a stream with holes in it.

Arguments

NameTypeDescription
takes sourceReader

the reader to take

limitinteger?

the most bytes one line may hold, or 65536

Returns

TypeDescription
affine(Lines)

the line reader, owned by the caller

Raises

  • when limit is not a positive integer

newQueueReaderconstructor#

function newQueueReader(source: ByteQueue): Reader

Opens a reader over a byte queue, which it does not own.

Closing this reader leaves the queue behind it open, because the queue was never transferred. A process stream and an HTTP response body are both byte queues without naming this module.

Arguments

NameTypeDescription
sourceByteQueue

the queue to read from

Returns

TypeDescription
Reader

the reader, owned by the caller

Raises

  • when source is not a byte queue

newScalarReaderconstructor#

const newScalarReader: function(source: string | ByteQueue): affine(ScalarReader, destroyOwner)
& function(borrows source: ByteView | Buffer): affine(ScalarReader, destroyOwner)
& function(takes source: Reader): affine(ScalarReader, destroyOwner)

Opens a cursor for sized integer and float reads.

A string, snapshot or buffer is read from a copy of its bytes taken here, and reports its remaining count. A reader or a queue is consumed in place: the reader answers no count, the queue answers what it still holds.

What the source is decides what happens to it. A snapshot or a buffer is borrowed and outlives the cursor; a reader is taken, and closing the cursor closes it; a string or a queue carries no obligation either way.

do
    local cursor = io.newScalarReader(header)
    local magic = cursor:readUint32()
    local count = cursor:readUint16()
end

Arguments

NameTypeDescription
sourcestring | ByteQueue

the bytes, snapshot, buffer, reader or queue to read

Returns

TypeDescription
affine(ScalarReader, destroyOwner)

the new reader, owned by the caller

Raises

  • when source is none of those

newScalarWriterconstructor#

const newScalarWriter: function(): affine(ScalarWriter, destroyOwner)
& function(borrows destination: Buffer): affine(ScalarWriter, destroyOwner) borrows (destination)
& function(takes destination: Writer): affine(ScalarWriter, destroyOwner)

Opens a cursor for sized integer and float writes.

A buffer is appended to, keeping what it already holds. A writer receives the bytes as they are written. Omitting the destination starts a fresh buffer, which ScalarWriter:buffer hands back.

do
    local cursor = io.newScalarWriter()
    cursor:writeUint32(0x4d495355):writeUint16(2)
    local bytes = cursor:buffer()
    assert(bytes ~= nil and bytes:getString() ~= "")
end

Arguments

NameTypeDescription
borrows destinationBuffer

the buffer or writer to append to, or nothing for a fresh buffer

Returns

TypeDescription
affine(ScalarWriter, destroyOwner) borrows (destination)

the new writer, owned by the caller

Raises

  • when destination is neither a buffer nor a writer

newStringReaderconstructor#

function newStringReader(text: string): Reader

Opens a reader over bytes held as a string.

The reader is a Reader like any other, so a parser written against the contract reads a string here and a file or a response body elsewhere without changing:

local reader = io.newStringReader("hello")
assert(reader:read(5) == "hello")
assert(reader:read(1) == "")

Arguments

NameTypeDescription
textstring

the bytes to read

Returns

TypeDescription
Reader

the reader, owned by the caller

Raises

  • when text is not a string

Types#

Bufferinterface#

sealed interface Buffer
    drop: nosuspend function(takes self: Buffer): nil
    length: function(borrows self: Buffer): integer
    capacity: function(borrows self: Buffer): integer
    clear: function(exclusive self: Buffer): nil
    ensureCapacity: function(exclusive self: Buffer, minimum: integer): nil
    resize: function(exclusive self: Buffer, length: integer): nil
    getString: function(borrows self: Buffer, offset: integer?, count: integer?): string
    setString: function(exclusive self: Buffer, bytes: string, offset: integer?): nil
    readSpan: function(borrows self: Buffer): span.ByteSpan borrows (self)
    reserveWrite: function(
        exclusive self: Buffer,
        offset: integer,
        count: integer
    ): affine(BufferWriteLease, destroyOwner) borrows (self)
    view: function(self: Buffer, offset: integer?, count: integer?): affine(ByteView, destroyOwner)
    newReader: function(self: Buffer): Reader
    newWriter: function(exclusive self: Buffer): affine(Writer) borrows (self)
    isReleased: function(borrows self: Buffer): boolean
    close: function(takes self: Buffer): (boolean, string?)
end

Growable byte storage whose pointer and logical length never escape separately.

Methods

drop#
drop: nosuspend function(takes self: Buffer): nil
Arguments
NameTypeDescription
takes selfBuffer
Returns
TypeDescription
nil
length#
length: function(borrows self: Buffer): integer
Arguments
NameTypeDescription
borrows selfBuffer
Returns
TypeDescription
integer
capacity#
capacity: function(borrows self: Buffer): integer
Arguments
NameTypeDescription
borrows selfBuffer
Returns
TypeDescription
integer
clear#
clear: function(exclusive self: Buffer): nil
Arguments
NameTypeDescription
exclusive selfBuffer
Returns
TypeDescription
nil
ensureCapacity#
ensureCapacity: function(exclusive self: Buffer, minimum: integer): nil
Arguments
NameTypeDescription
exclusive selfBuffer
minimuminteger
Returns
TypeDescription
nil
resize#
resize: function(exclusive self: Buffer, length: integer): nil
Arguments
NameTypeDescription
exclusive selfBuffer
lengthinteger
Returns
TypeDescription
nil
getString#
getString: function(borrows self: Buffer, offset: integer?, count: integer?): string
Arguments
NameTypeDescription
borrows selfBuffer
offsetinteger?
countinteger?
Returns
TypeDescription
string
setString#
setString: function(exclusive self: Buffer, bytes: string, offset: integer?): nil
Arguments
NameTypeDescription
exclusive selfBuffer
bytesstring
offsetinteger?
Returns
TypeDescription
nil
readSpan#
readSpan: function(borrows self: Buffer): span.ByteSpan borrows (self)
Arguments
NameTypeDescription
borrows selfBuffer
Returns
TypeDescription
span.ByteSpan borrows (self)
reserveWrite#
reserveWrite: function(
    exclusive self: Buffer,
    offset: integer,
    count: integer
): affine(BufferWriteLease, destroyOwner) borrows (self)
Arguments
NameTypeDescription
exclusive selfBuffer
offsetinteger
countinteger
Returns
TypeDescription
affine(BufferWriteLease, destroyOwner) borrows (self)
view#
view: function(self: Buffer, offset: integer?, count: integer?): affine(ByteView, destroyOwner)
Arguments
NameTypeDescription
selfBuffer
offsetinteger?
countinteger?
Returns
TypeDescription
affine(ByteView, destroyOwner)
newReader#
newReader: function(self: Buffer): Reader
Arguments
NameTypeDescription
selfBuffer
Returns
TypeDescription
Reader
newWriter#
newWriter: function(exclusive self: Buffer): affine(Writer) borrows (self)
Arguments
NameTypeDescription
exclusive selfBuffer
Returns
TypeDescription
affine(Writer) borrows (self)
isReleased#
isReleased: function(borrows self: Buffer): boolean
Arguments
NameTypeDescription
borrows selfBuffer
Returns
TypeDescription
boolean
close#
close: function(takes self: Buffer): (boolean, string?)
Arguments
NameTypeDescription
takes selfBuffer
Returns
TypeDescription
boolean
string?

BufferWriteLeaseinterface#

sealed interface BufferWriteLease
    drop: nosuspend function(takes self: BufferWriteLease): nil
    span: function(exclusive self: BufferWriteLease): span.Writable<uint8> borrows (self)
    commit: function(takes self: BufferWriteLease, written: integer): nil
end

An exclusive prepared region of a buffer. Its span may be written, then the lease is committed with the number of bytes that actually landed. Dropping a lease aborts it without changing the buffer's logical length.

Methods

drop#
drop: nosuspend function(takes self: BufferWriteLease): nil
Arguments
NameTypeDescription
takes selfBufferWriteLease
Returns
TypeDescription
nil
span#
span: function(exclusive self: BufferWriteLease): span.Writable<uint8> borrows (self)
Arguments
NameTypeDescription
exclusive selfBufferWriteLease
Returns
TypeDescription
span.Writable<uint8> borrows (self)
commit#
commit: function(takes self: BufferWriteLease, written: integer): nil
Arguments
NameTypeDescription
takes selfBufferWriteLease
writteninteger
Returns
TypeDescription
nil

ByteQueueinterface#

interface ByteQueue
    metamethod __len: function(self): integer
    get: function(self: ByteQueue, ...: integer?): string
end

A queue of bytes something else is filling, which a reader may be opened over without owning it.

A process stream and an HTTP response body are both one of these without either knowing about this module.

Methods

__len#
__len: function(self): integer
Arguments
NameTypeDescription
?self
Returns
TypeDescription
integer
get#
get: function(self: ByteQueue, ...: integer?): string

Consumes bytes from the front and returns them, or all of them when given no count.

Arguments
NameTypeDescription
selfByteQueue

this queue

...integer?

how many bytes each returned string takes

Returns
TypeDescription
string

the consumed bytes

ByteViewinterface#

sealed interface ByteView
    drop: nosuspend function(takes self: ByteView): nil
    length: function(borrows self: ByteView): integer
    getString: function(borrows self: ByteView): string
    readSpan: function(borrows self: ByteView): span.ByteSpan borrows (self)
    newReader: function(self: ByteView): Reader
    view: function(self: ByteView, offset: integer?, count: integer?): affine(ByteView, destroyOwner)
    isReleased: function(self: ByteView): boolean
    close: function(takes self: ByteView): (boolean, string?)
end

A snapshot of bytes that cannot be written through.

Methods

drop#
drop: nosuspend function(takes self: ByteView): nil
Arguments
NameTypeDescription
takes selfByteView
Returns
TypeDescription
nil
length#
length: function(borrows self: ByteView): integer
Arguments
NameTypeDescription
borrows selfByteView
Returns
TypeDescription
integer
getString#
getString: function(borrows self: ByteView): string
Arguments
NameTypeDescription
borrows selfByteView
Returns
TypeDescription
string
readSpan#
readSpan: function(borrows self: ByteView): span.ByteSpan borrows (self)
Arguments
NameTypeDescription
borrows selfByteView
Returns
TypeDescription
span.ByteSpan borrows (self)
newReader#
newReader: function(self: ByteView): Reader
Arguments
NameTypeDescription
selfByteView
Returns
TypeDescription
Reader
view#
view: function(self: ByteView, offset: integer?, count: integer?): affine(ByteView, destroyOwner)
Arguments
NameTypeDescription
selfByteView
offsetinteger?
countinteger?
Returns
TypeDescription
affine(ByteView, destroyOwner)
isReleased#
isReleased: function(self: ByteView): boolean
Arguments
NameTypeDescription
selfByteView
Returns
TypeDescription
boolean
close#
close: function(takes self: ByteView): (boolean, string?)
Arguments
NameTypeDescription
takes selfByteView
Returns
TypeDescription
boolean
string?

Linesinterface#

sealed interface Lines is nupp.Closeable
    drop: nosuspend function(takes self: Lines): nil
    read: function(self: Lines, ...: integer?): (string?, string?)
    isReleased: function(self: Lines): boolean
    close: nosuspend function(takes self: Lines): nil
end

A reader taken apart into lines.

Reading a line means reading past its end, so something has to hold what came after it. That is what this owns, and it is why a line reader takes its source rather than borrowing one: two things reading the same bytes would each see half of them.

Methods

drop#
drop: nosuspend function(takes self: Lines): nil
Arguments
NameTypeDescription
takes selfLines
Returns
TypeDescription
nil
read#
read: function(self: Lines, ...: integer?): (string?, string?)

Reads the next line, without its terminator.

Both \n and \r\n end a line. Bytes left after the last terminator are a line, because a stream that ended without one still said something.

Arguments
NameTypeDescription
selfLines

this line reader

...integer?
Returns
TypeDescription
string?

the line, or nil at the end

string?

why it could not read, when unsuccessful

isReleased#
isReleased: function(self: Lines): boolean

Whether this reader has been released.

Arguments
NameTypeDescription
selfLines
Returns
TypeDescription
boolean
close#
close: nosuspend function(takes self: Lines): nil

Releases this reader and the source it took.

Arguments
NameTypeDescription
takes selfLines
Returns
TypeDescription
nil

Readerinterface#

affine interface Reader is nupp.Closeable
    read: function(self: Reader, count: integer): (string?, string?)
    readSpan: function(self: Reader, exclusive destination: span.Writable<uint8>): (integer?, string?)
    readInto: function(
        self: Reader,
        exclusive destination: Buffer,
        offset: integer?,
        count: integer?
    ): (integer?, string?)
    transferTo: function(self: Reader, exclusive destination: Writer): (integer?, string?)

end

A forward-only byte source.

An interface rather than the concrete things that satisfy it. A buffer's reader, a file's reader and an HTTP response body are all one of these, so code written against the contract works over any of them without knowing which it has.

Methods

read#
read: function(self: Reader, count: integer): (string?, string?)

Reads up to count bytes.

An empty answer is the end.

Arguments
NameTypeDescription
selfReader

this reader

countinteger

the most bytes to read

Returns
TypeDescription
string?

the bytes, or nil when the reader is closed

string?

why it could not read, when unsuccessful

Raises
  • when count is not a positive integer

readSpan#
readSpan: function(self: Reader, exclusive destination: span.Writable<uint8>): (integer?, string?)

Reads directly into a checked writable span. A zero answer is the end.

Arguments
NameTypeDescription
selfReader

this reader

exclusive destinationspan.Writable<uint8>

the positive-sized range to fill

Returns
TypeDescription
integer?

how many bytes were read, or nil when the reader is closed

string?

why it could not read, when unsuccessful

readInto#
readInto: function(
    self: Reader,
    exclusive destination: Buffer,
    offset: integer?,
    count: integer?
): (integer?, string?)

Reads into a buffer.

A zero answer is the end.

Arguments
NameTypeDescription
selfReader

this reader

exclusive destinationBuffer

the buffer to write into

offsetinteger?

where in the destination to start, or the beginning

countinteger?

the most bytes to read

Returns
TypeDescription
integer?

how many bytes were read, or nil when the reader is closed

string?

why it could not read, when unsuccessful

transferTo#
transferTo: function(self: Reader, exclusive destination: Writer): (integer?, string?)

Writes everything left to a writer.

Arguments
NameTypeDescription
selfReader

this reader

exclusive destinationWriter

the writer to fill

Returns
TypeDescription
integer?

how many bytes moved, or nil on failure

string?

why it could not, when unsuccessful

ScalarReaderinterface#

interface ScalarReader
    drop: nosuspend function(takes self: ScalarReader): nil
    remaining: function(self: ScalarReader): integer?
    atEnd: function(self: ScalarReader): boolean
    skip: function(self: ScalarReader, count: integer): ScalarReader
    readBytes: function(self: ScalarReader, count: integer): string
    readUint8: function(self: ScalarReader): uint32
    readInt8: function(self: ScalarReader): int32
    readUint16: function(self: ScalarReader): uint32
    readInt16: function(self: ScalarReader): int32
    readUint32: function(self: ScalarReader): uint32
    readInt32: function(self: ScalarReader): int32
    readUint64: function(self: ScalarReader): uint64
    readInt64: function(self: ScalarReader): int64
    readFloat32: function(self: ScalarReader): float
    readFloat64: function(self: ScalarReader): number
    close: function(takes self: ScalarReader): (boolean, string?)
end

Reads fixed-width numbers from a source of bytes.

Methods

drop#
drop: nosuspend function(takes self: ScalarReader): nil

Releases it as an ownership terminal.

Repeated calls are safe. Declared rather than written inline: a cleanup contract is nosuspend, and an inline method cannot carry one.

Arguments
NameTypeDescription
takes selfScalarReader

this owner, spent by the call

Returns
TypeDescription
nil
remaining#
remaining: function(self: ScalarReader): integer?

How many bytes are left, when that is knowable.

Arguments
NameTypeDescription
selfScalarReader

this reader

Returns
TypeDescription
integer?

the remaining byte count, or nil over an open-ended source

Raises
  • when the reader is closed

atEnd#
atEnd: function(self: ScalarReader): boolean

Whether the source has nothing left.

Arguments
NameTypeDescription
selfScalarReader

this reader

Returns
TypeDescription
boolean

whether it is at the end

Raises
  • when the reader is closed, or its source failed

skip#
skip: function(self: ScalarReader, count: integer): ScalarReader

Discards bytes.

Arguments
NameTypeDescription
selfScalarReader

this reader

countinteger

how many to discard

Returns
TypeDescription
ScalarReader

this reader

Raises
  • when there are not that many

readBytes#
readBytes: function(self: ScalarReader, count: integer): string

Reads bytes as they are.

Arguments
NameTypeDescription
selfScalarReader

this reader

countinteger

how many to read

Returns
TypeDescription
string

the bytes

Raises
  • when there are not that many

readUint8#
readUint8: function(self: ScalarReader): uint32

Reads one unsigned 8-bit integer.

Arguments
NameTypeDescription
selfScalarReader

this reader

Returns
TypeDescription
uint32

the value, 0 through 255

Raises
  • when the source has no bytes left

readInt8#
readInt8: function(self: ScalarReader): int32

Reads one signed 8-bit integer.

Arguments
NameTypeDescription
selfScalarReader

this reader

Returns
TypeDescription
int32

the value, -128 through 127

Raises
  • when the source has no bytes left

readUint16#
readUint16: function(self: ScalarReader): uint32

Reads one unsigned 16-bit integer in the machine's byte order.

Arguments
NameTypeDescription
selfScalarReader

this reader

Returns
TypeDescription
uint32

the value, 0 through 65535

Raises
  • when fewer than 2 bytes are left

readInt16#
readInt16: function(self: ScalarReader): int32

Reads one signed 16-bit integer in the machine's byte order.

Arguments
NameTypeDescription
selfScalarReader

this reader

Returns
TypeDescription
int32

the value, -32768 through 32767

Raises
  • when fewer than 2 bytes are left

readUint32#
readUint32: function(self: ScalarReader): uint32

Reads one unsigned 32-bit integer in the machine's byte order.

Arguments
NameTypeDescription
selfScalarReader

this reader

Returns
TypeDescription
uint32

the value, 0 through 4294967295

Raises
  • when fewer than 4 bytes are left

readInt32#
readInt32: function(self: ScalarReader): int32

Reads one signed 32-bit integer in the machine's byte order.

Arguments
NameTypeDescription
selfScalarReader

this reader

Returns
TypeDescription
int32

the value

Raises
  • when fewer than 4 bytes are left

readUint64#
readUint64: function(self: ScalarReader): uint64

Reads one unsigned 64-bit integer in the machine's byte order.

Arguments
NameTypeDescription
selfScalarReader

this reader

Returns
TypeDescription
uint64

the value, as uint64 cdata

Raises
  • when fewer than 8 bytes are left

readInt64#
readInt64: function(self: ScalarReader): int64

Reads one signed 64-bit integer in the machine's byte order.

Arguments
NameTypeDescription
selfScalarReader

this reader

Returns
TypeDescription
int64

the value, as int64 cdata

Raises
  • when fewer than 8 bytes are left

readFloat32#
readFloat32: function(self: ScalarReader): float

Reads one 32-bit float in the machine's byte order.

Arguments
NameTypeDescription
selfScalarReader

this reader

Returns
TypeDescription
float

the value, widened to a Lua number

Raises
  • when fewer than 4 bytes are left

readFloat64#
readFloat64: function(self: ScalarReader): number

Reads one 64-bit float in the machine's byte order.

Arguments
NameTypeDescription
selfScalarReader

this reader

Returns
TypeDescription
number

the value

Raises
  • when fewer than 8 bytes are left

close#
close: function(takes self: ScalarReader): (boolean, string?)

Closes the reader, and the reader it was built over when there was one.

Arguments
NameTypeDescription
takes selfScalarReader

this reader, spent by the call

Returns
TypeDescription
boolean

whether the underlying source closed cleanly

string?

why it did not, when it did not

ScalarWriterinterface#

interface ScalarWriter
    drop: nosuspend function(takes self: ScalarWriter): nil
    writeBytes: function(self: ScalarWriter, bytes: string): ScalarWriter
    writeUint8: function(self: ScalarWriter, value: uint32): ScalarWriter
    writeInt8: function(self: ScalarWriter, value: int32): ScalarWriter
    writeUint16: function(self: ScalarWriter, value: uint32): ScalarWriter
    writeInt16: function(self: ScalarWriter, value: int32): ScalarWriter
    writeUint32: function(self: ScalarWriter, value: uint32): ScalarWriter
    writeInt32: function(self: ScalarWriter, value: int32): ScalarWriter
    writeUint64: function(self: ScalarWriter, value: uint64): ScalarWriter
    writeInt64: function(self: ScalarWriter, value: int64): ScalarWriter
    writeFloat32: function(self: ScalarWriter, value: float): ScalarWriter
    writeFloat64: function(self: ScalarWriter, value: number): ScalarWriter
    buffer: function(borrows self: ScalarWriter): Buffer? borrows (self)
    flush: function(self: ScalarWriter): (boolean, string?)
    close: function(takes self: ScalarWriter): (boolean, string?)
end

Writes fixed-width numbers into a destination.

Methods

drop#
drop: nosuspend function(takes self: ScalarWriter): nil

Releases it as an ownership terminal.

Repeated calls are safe. Declared rather than written inline: a cleanup contract is nosuspend, and an inline method cannot carry one.

Arguments
NameTypeDescription
takes selfScalarWriter

this owner, spent by the call

Returns
TypeDescription
nil
writeBytes#
writeBytes: function(self: ScalarWriter, bytes: string): ScalarWriter

Writes bytes as they are.

Arguments
NameTypeDescription
selfScalarWriter

this writer

bytesstring

the bytes to write

Returns
TypeDescription
ScalarWriter

this writer

Raises
  • when bytes is not a string

writeUint8#
writeUint8: function(self: ScalarWriter, value: uint32): ScalarWriter

Writes one unsigned 8-bit integer.

Arguments
NameTypeDescription
selfScalarWriter

this writer

valueuint32

the number to write

Returns
TypeDescription
ScalarWriter

this writer

writeInt8#
writeInt8: function(self: ScalarWriter, value: int32): ScalarWriter

Writes one signed 8-bit integer.

Arguments
NameTypeDescription
selfScalarWriter

this writer

valueint32

the number to write

Returns
TypeDescription
ScalarWriter

this writer

writeUint16#
writeUint16: function(self: ScalarWriter, value: uint32): ScalarWriter

Writes one unsigned 16-bit integer in the machine's byte order.

Arguments
NameTypeDescription
selfScalarWriter

this writer

valueuint32

the number to write

Returns
TypeDescription
ScalarWriter

this writer

writeInt16#
writeInt16: function(self: ScalarWriter, value: int32): ScalarWriter

Writes one signed 16-bit integer in the machine's byte order.

Arguments
NameTypeDescription
selfScalarWriter

this writer

valueint32

the number to write

Returns
TypeDescription
ScalarWriter

this writer

writeUint32#
writeUint32: function(self: ScalarWriter, value: uint32): ScalarWriter

Writes one unsigned 32-bit integer in the machine's byte order.

Arguments
NameTypeDescription
selfScalarWriter

this writer

valueuint32

the number to write

Returns
TypeDescription
ScalarWriter

this writer

writeInt32#
writeInt32: function(self: ScalarWriter, value: int32): ScalarWriter

Writes one signed 32-bit integer in the machine's byte order.

Arguments
NameTypeDescription
selfScalarWriter

this writer

valueint32

the number to write

Returns
TypeDescription
ScalarWriter

this writer

writeUint64#
writeUint64: function(self: ScalarWriter, value: uint64): ScalarWriter

Writes one unsigned 64-bit integer in the machine's byte order.

Arguments
NameTypeDescription
selfScalarWriter

this writer

valueuint64

the number to write

Returns
TypeDescription
ScalarWriter

this writer

writeInt64#
writeInt64: function(self: ScalarWriter, value: int64): ScalarWriter

Writes one signed 64-bit integer in the machine's byte order.

Arguments
NameTypeDescription
selfScalarWriter

this writer

valueint64

the number to write

Returns
TypeDescription
ScalarWriter

this writer

writeFloat32#
writeFloat32: function(self: ScalarWriter, value: float): ScalarWriter

Writes one 32-bit float in the machine's byte order.

Arguments
NameTypeDescription
selfScalarWriter

this writer

valuefloat

the number to write

Returns
TypeDescription
ScalarWriter

this writer

writeFloat64#
writeFloat64: function(self: ScalarWriter, value: number): ScalarWriter

Writes one 64-bit float in the machine's byte order.

Arguments
NameTypeDescription
selfScalarWriter

this writer

valuenumber

the number to write

Returns
TypeDescription
ScalarWriter

this writer

buffer#
buffer: function(borrows self: ScalarWriter): Buffer? borrows (self)

The buffer this writer fills, when it has one of its own.

Arguments
NameTypeDescription
borrows selfScalarWriter

this writer

Returns
TypeDescription
Buffer? borrows (self)

the buffer, or nil when writing through a writer

flush#
flush: function(self: ScalarWriter): (boolean, string?)

Flushes the destination, when there is one to flush.

Arguments
NameTypeDescription
selfScalarWriter

this writer

Returns
TypeDescription
boolean

whether it flushed

string?

why it did not, when it did not

close#
close: function(takes self: ScalarWriter): (boolean, string?)

Closes the writer, and the writer it was built over when there was one.

Arguments
NameTypeDescription
takes selfScalarWriter

this writer, spent by the call

Returns
TypeDescription
boolean

whether the underlying destination closed cleanly

string?

why it did not, when it did not

Writerinterface#

affine interface Writer is nupp.Closeable
    write: function(exclusive self: Writer, bytes: string): (boolean, string?)
    writeSpan: function(exclusive self: Writer, borrows source: span.ByteSpan): (integer?, string?)
    flush: function(self: Writer): (boolean, string?)

end

An appending byte sink.

The counterpart of Reader, and an interface for the same reason: a buffer, an open file and a request body all satisfy it, and a producer written against the contract fills any of them.

Methods

write#
write: function(exclusive self: Writer, bytes: string): (boolean, string?)

Appends bytes.

Arguments
NameTypeDescription
exclusive selfWriter

this writer

bytesstring

the bytes to append

Returns
TypeDescription
boolean

whether they were written

string?

why they were not, when unsuccessful

Raises
  • when bytes is not a string

writeSpan#
writeSpan: function(exclusive self: Writer, borrows source: span.ByteSpan): (integer?, string?)

Appends bytes from a checked shared span.

Arguments
NameTypeDescription
exclusive selfWriter

this writer

borrows sourcespan.ByteSpan

bytes valid for the duration of the call

Returns
TypeDescription
integer?

how many bytes moved, or nil on failure

string?

why it could not, when unsuccessful

flush#
flush: function(self: Writer): (boolean, string?)

Nothing is buffered behind this writer, so this only reports whether it is open.

Arguments
NameTypeDescription
selfWriter

this writer

Returns
TypeDescription
boolean

whether the writer is open

string?

why it is not, when it is not