Skip to content
Merged
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
2 changes: 2 additions & 0 deletions trimsock.gd/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@

# Only for local use
sh/ensure-uids.sh

.vscode/
3 changes: 3 additions & 0 deletions trimsock.gd/addons/trimsock.gd/line_parser.gd
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ func parse(p_line: String) -> TrimsockCommand:
return command

func read_name() -> String:
if is_eol():
return ""

if chr() == "\"":
return read_quoted()
else:
Expand Down
24 changes: 19 additions & 5 deletions trimsock.gd/addons/trimsock.gd/line_reader.gd
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@ var at := 0
var is_quote := false
var is_escape := false


func reset() -> void:
buffer.clear()
at = 0
is_quote = false
is_escape = false

func ingest(data: PackedByteArray) -> Error:
var new_size := buffer.size() + data.size()
if new_size > max_size:
buffer.clear()
reset()
return ERR_OUT_OF_MEMORY

buffer.append_array(data)
Expand All @@ -30,20 +37,27 @@ func read_text() -> String:
return ""

func has_data(size: int) -> bool:
return buffer.size() >= size
return buffer.size() > size

func read_data(size: int) -> PackedByteArray:
func read_data(size: int) -> TrimsockResult.Data:
assert(has_data(size), "Trying to read more bytes than available!")

# Grab result
var result := buffer.slice(0, size)
buffer = buffer.slice(size)
var is_terminated := String.chr(buffer[size]) == "\n"

buffer = buffer.slice(size + 1)
at = 0

# Reset flags
is_escape = false
is_quote = false

return result
if not is_terminated:
return TrimsockResult.Data.of_error(ERR_PARSE_ERROR,
"Expected newline after %d bytes of raw data!" % [size])

return TrimsockResult.Data.of_value(result)

func chr() -> String:
return String.chr(buffer[at])
Expand Down
2 changes: 1 addition & 1 deletion trimsock.gd/addons/trimsock.gd/plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
name="trimsock.gd"
description="Godot implementation of the trimsock protocol"
author="Tamás Gálffy"
version="0.13.2"
version="0.13.3"
script="trimsock.gd"
16 changes: 14 additions & 2 deletions trimsock.gd/addons/trimsock.gd/reactor.gd
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ var _id_generator: TrimsockIDGenerator = RandomTrimsockIDGenerator.new(12)
signal on_attach(source: Variant)
## Emitted when a known source is detached from the reactor
signal on_detach(source: Variant)
## Emitted when data received from a source can't be parsed
signal on_ingest_error(source: Variant, error: Error, message: String)


## Poll all sources and process incoming data
Expand All @@ -46,8 +48,12 @@ func poll() -> void:
for source in _sources:
var reader := _readers[source] as TrimsockReader
while true:
var command := reader.read()
var result := reader.read()
var command := result.value()
if not command:
if not result.is_success():
var error := result.error()
on_ingest_error.emit(source, error.code, error.message)
break

_handle(command, source)
Expand Down Expand Up @@ -155,7 +161,13 @@ func _write(target: Variant, command: TrimsockCommand) -> void:
func _ingest(source: Variant, data: PackedByteArray) -> Error:
assert(_readers.has(source), "Ingesting data from unknown source! Did you call `attach()`?")
var reader := _readers[source] as TrimsockReader
return reader.ingest_bytes(data)
var result := reader.ingest_bytes(data)
if not result.is_success():
var error := result.error()
on_ingest_error.emit(source, error.code, error.message)
return error.code

return OK

func _handle(command: TrimsockCommand, source: Variant) -> void:
var xchg := _get_exchange_for(command, source)
Expand Down
109 changes: 83 additions & 26 deletions trimsock.gd/addons/trimsock.gd/reader.gd
Original file line number Diff line number Diff line change
Expand Up @@ -9,63 +9,120 @@ class_name TrimsockReader
## [br][br]
## After ingestion, call [method read] to extract commands from the ingested
## data. Data that is parsed is immediately freed from the internal buffer.
## [br][br]
## If the incoming data is malformed, it is discarded and the error is reported
## in the returned [TrimsockResult]. The reader continues parsing with the data
## following the offending command.


## Upper limit on the internal buffer's size
##
## Ingested data is buffered until [method read] can extract a full command
## from it. If this size limit is exceeded, the buffer's contents are discarded
## and [constant ERR_OUT_OF_MEMORY] is reported.
var max_size: int:
get:
return _line_reader.max_size
set(value):
_line_reader.max_size = value

var _line_reader: _TrimsockLineReader = _TrimsockLineReader.new()
var _line_parser: _TrimsockLineParser = _TrimsockLineParser.new()
var _queued_raw: TrimsockCommand = null
var _queued_raw_size := -1

## Ingest incoming text
## [br][br]
## Returns [constant OK] on success, or [constant ERR_OUT_OF_MEMORY] if the
## internal buffer can't store the data.
func ingest_text(text: String) -> Error:
return _line_reader.ingest(text.to_utf8_buffer())
## Returns a successful result, or an error result with
## [constant ERR_OUT_OF_MEMORY] if the internal buffer can't store the data.
func ingest_text(text: String) -> TrimsockResult:
return ingest_bytes(text.to_utf8_buffer())

## Ingest incoming binary data
## [br][br]
## Returns [constant OK] on success, or [constant ERR_OUT_OF_MEMORY] if the
## internal buffer can't store the data.
func ingest_bytes(bytes: PackedByteArray) -> Error:
return _line_reader.ingest(bytes)
## Returns a successful result, or an error result with
## [constant ERR_OUT_OF_MEMORY] if the internal buffer can't store the data.
func ingest_bytes(bytes: PackedByteArray) -> TrimsockResult:
var error := _line_reader.ingest(bytes)
Comment thread
IZ-sandwich marked this conversation as resolved.
if error != OK:
_dequeue_raw()
return TrimsockResult.failure(error,
"Buffer overflow! Can't ingest %d bytes without exceeding %d!" \
% [bytes.size(), max_size])

return TrimsockResult.success()

## Try and extract a command from the ingested data
## [br][br]
## Returns a parsed command, or [code]null[/code] if no command is available
## yet.
func read() -> TrimsockCommand:
var command := _pop()
## Returns a result holding the parsed command, or a successful result without
## a command if none is available yet. Malformed data is rejected by returning
## an error result.
func read() -> TrimsockResult.Command:
var result := _pop()
var command := result.value()
if command:
_TrimsockConventions.apply(command)
return command
return result

func _pop() -> TrimsockCommand:
func _pop() -> TrimsockResult.Command:
# We read a raw command earlier, waiting to have enough data
if _queued_raw:
var data_size := int(_queued_raw.text)
if not _line_reader.has_data(data_size):
return null
if not _line_reader.has_data(_queued_raw_size):
return TrimsockResult.Command.of_success()

var raw_command := _queued_raw
var read_result := _line_reader.read_data(_queued_raw_size)

_dequeue_raw()

if not read_result.is_success():
var error := read_result.error()
return TrimsockResult.Command.of_error(error.code, error.message)

_queued_raw.raw = _line_reader.read_data(data_size)
_queued_raw.text = ""
_queued_raw.chunks.clear()
raw_command.raw = read_result.value()
raw_command.text = ""
raw_command.chunks.clear()

var result := _queued_raw
_queued_raw = null
return result
return TrimsockResult.Command.of_value(raw_command)

# No queued command, try to read a new one
var line := _line_reader.read_text()
if not line:
return null
return TrimsockResult.Command.of_success()

var command := _line_parser.parse(line)
if command.is_raw:
var data_size := _parse_data_size(command.text)
if data_size < 0:
return TrimsockResult.Command.of_error(ERR_INVALID_DATA,
"Invalid raw command size: \"%s\"!" % [command.text])

# Command is raw, we'll keep it in the queue until we read the binary
# data for it
_queued_raw = command
_queue_raw(command, data_size)

# Try getting it immediately, in case we already have the data in buffer
return _pop()

return command
return TrimsockResult.Command.of_value(command)

# Parse the data size of a raw command or return -1 if it's invalid
func _parse_data_size(text: String) -> int:
if not text.is_valid_int():
return -1

var size := text.to_int()

# The data and its terminating newline must both fit in the buffer
if size < 0 or size >= max_size:
return -1

return size

func _queue_raw(command: TrimsockCommand, data_size: int) -> void:
_queued_raw = command
_queued_raw_size = data_size

func _dequeue_raw() -> void:
Comment thread
IZ-sandwich marked this conversation as resolved.
_queued_raw = null
_queued_raw_size = -1
133 changes: 133 additions & 0 deletions trimsock.gd/addons/trimsock.gd/result.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
extends RefCounted
class_name TrimsockResult

## Represents the result of a trimsock operation
##
## If the call was successful, this instance will store the resulting
## value, and [method is_success] will return [code]true[/code]. This class has
## multiple specializations, each implementing a [code]value()[/code] method
## that returns a type-safe success result.
## [br][br]
## If the call failed, [method is_success] will return [code]false[/code]. The
## actual error is returned by [method error].

## Represents an error encountered while ingesting or parsing trimsock data
class ErrorData:
## The error code
## [br][br]
## This indicates the error type. Can be checked programmatically to
## intelligently try and recover from an error.
var code: Error

## The error message
## [br][br]
## This gives a further description of the error itself. Should not be
## relied on, error messages may change between releases without notice.
## However, they can be useful to convey the issue to the user.
var message: String

func _init(p_code: Error, p_message: String):
code = p_code
message = p_message

func _to_string() -> String:
return "%s: %s" % [error_string(code), message]


## Stores a [TrimsockCommand] on success
##
## See [TrimsockResult] for details.
class Command extends TrimsockResult:
## Construct a successful result object with the given [param value]
static func of_value(value: TrimsockCommand) -> Command:
var result := Command.new()
result._is_success = true
result._value = value
return result

## Construct a successful result object without any command
## [br][br]
## Used when no command could be extracted yet, without any error.
static func of_success() -> Command:
var result := Command.new()
result._is_success = true
return result

## Construct an error result
static func of_error(error: Error, message: String) -> Command:
var result := Command.new()
result._is_success = false
result._error = ErrorData.new(error, message)
return result

## Get the resulting command
## [br][br]
## Returns [code]null[/code] if the operation failed, or if it succeeded
## without extracting a command.
func value() -> TrimsockCommand:
if _is_success:
return _value as TrimsockCommand
Comment thread
elementbound marked this conversation as resolved.
else:
return null

## Stores binary data on success
##
## See [TrimsockResult] for details.
class Data extends TrimsockResult:
## Construct a successful result object with the given [param value]
static func of_value(value: PackedByteArray) -> Data:
var result := Data.new()
result._is_success = true
result._value = value
return result

## Construct an error result
static func of_error(error: Error, message: String) -> Data:
var result := Data.new()
result._is_success = false
result._error = ErrorData.new(error, message)
return result

## Get the resulting data
## [br][br]
## Returns an empty array if the operation failed.
func value() -> PackedByteArray:
if _is_success:
return _value as PackedByteArray
else:
return PackedByteArray()

var _is_success: bool
var _value: Variant
var _error: ErrorData

## Construct an error result
static func failure(error: Error, message: String) -> TrimsockResult:
var result := TrimsockResult.new()
result._is_success = false
result._error = ErrorData.new(error, message)
return result

## Construct a successful result without any value
static func success() -> TrimsockResult:
var result := TrimsockResult.new()
result._is_success = true
return result


## Return true if the operation was successful
func is_success() -> bool:
return _is_success

## Return the error, or [code]null[/code] if the operation was successful
func error() -> ErrorData:
if _is_success:
return null
else:
return _error

func _to_string() -> String:
if _is_success:
return str(_value)
else:
return str(_error)
1 change: 1 addition & 0 deletions trimsock.gd/addons/trimsock.gd/result.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://b8q6e1o6dw1eh
Loading
Loading