Skip to content

Add consistent validation to protocol message parsing#77

Merged
kahrendt merged 8 commits into
mainfrom
pr/protocol
Jul 9, 2026
Merged

Add consistent validation to protocol message parsing#77
kahrendt merged 8 commits into
mainfrom
pr/protocol

Conversation

@kahrendt

@kahrendt kahrendt commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Centralizes scalar/enum field parsing in protocol.cpp behind three helpers (read_uint_field, read_bool_field, read_enum_field) that enforce strict JSON types, range bounds, and warn-and-drop on malformed input. This replaces ad-hoc as<T>() casts that silently truncated out-of-range values (e.g. volume: 300 → 44).

  • Narrow integer and boolean fields are now range- and type-checked; floats and int-as-bool are rejected.
  • Enum fields (playback_state, artwork source/format, supported_commands) drop unrecognized values instead of clearing the field.
  • Parsed objects are moved into message structs rather than copied.
  • Adds seek / seek_relative to the controller command enum (recognition/round-trip only).

New tests cover range validation, type strictness, float rejection, required-scalar rejection, and element-wise array validation.

kahrendt added 5 commits July 9, 2026 07:52
Assign locally-built parse results (metadata, controller, player, artwork, visualizer, and stream end/clear roles) into their destination message structs with std::move, avoiding a copy of their heap-allocated vectors and strings. Trivially copyable results are left as plain copies.
Add read_uint_field<T> and read_bool_field helpers that read optional scalar fields with a strict is<T>() check (rejecting floats and numeric strings), optional [min, max] range bounds, and warn-and-drop on malformed values. Apply them across the parser (player, artwork, command, controller, visualizer, metadata progress, hello version), clamping volume to the protocol maximum of 100. Route the metadata tri-state string/uint16 helpers through the same warn-on-malformed path.

Malformed values of known fields are dropped with a log instead of silently coerced; required fields that fail validation reject the message via the existing completeness checks.
Refactor parse_color_field to read each RGB component with read_uint_field<uint8_t> instead of grabbing it as an int and manually range-checking 0-255. Since uint8_t's range is exactly 0-255, the strict type check is also the range check, and malformed components now log through the same uniform format as the other fields.
Add read_enum_field<E> that parses an optional enum from a wire string via a from_string function, warning and dropping a present non-string or an unrecognized value. Apply it to the fields whose policy is apply-if-valid-else-leave: artwork source/format, controller repeat, group playback_state, and each supported_commands element. The controller role is frozen at v1, so an unrecognized command is a non-compliant value rather than a forward-compatible one.

Fields with different policies keep their handling: codec maps unknown values to the UNSUPPORTED sentinel; required command and connection_reason reject the message; visualizer types (a draft role) skip unknowns silently for forward compatibility.
This doesn't add full support where we can properly send these commands. It just adds them to the known list so the newly added enum validation fields won't fire on a newer server.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors protocol message parsing to centralize scalar/enum decoding behind helper functions that enforce stricter JSON typing and bounds checking, and updates controller command handling to recognize new seek-related commands.

Changes:

  • Introduces read_uint_field, read_bool_field, and read_enum_field helpers to validate JSON types/ranges and drop malformed optional fields with warnings.
  • Adjusts multiple message parsers to use the new helpers and moves parsed objects into message structs to avoid copies.
  • Adds seek and seek_relative to the controller command enum and round-trip parsing/formatting, with new tests covering stricter validation behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
tests/test_protocol.cpp Adds tests for strict scalar/enum validation (range/type/float rejection) and controller command round-trip updates.
src/protocol.cpp Centralizes scalar/enum parsing via helpers; applies warn-and-drop behavior and some move optimizations in message processing.
src/protocol_messages.h Extends controller command string conversions to include seek / seek_relative.
include/sendspin/controller_role.h Extends public controller command enum with SEEK and SEEK_RELATIVE.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/protocol.cpp Outdated
Comment thread src/protocol.cpp
Comment on lines 466 to 474
// Parse optional playback_state
JsonVariantConst playback_state_var = root["payload"]["playback_state"];
if (playback_state_var.is<const char*>()) {
std::string state_str = playback_state_var.as<std::string>();
group_msg->group.playback_state = playback_state_from_string(state_str);
} else if (!playback_state_var.isUnbound() && playback_state_var.isNull()) {
if (!playback_state_var.isUnbound() && playback_state_var.isNull()) {
// Field set to null - clear from state
group_msg->group.playback_state = std::nullopt;
} else if (auto state = read_enum_field(playback_state_var, "playback_state",
playback_state_from_string)) {
group_msg->group.playback_state = *state;
}

@kahrendt kahrendt Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is a gap in the spec. Every client is always part of one group, so these fields should never be cleared.

Comment thread src/protocol.cpp
@kahrendt kahrendt enabled auto-merge (squash) July 9, 2026 13:34
@kahrendt kahrendt merged commit 6d2cc4d into main Jul 9, 2026
5 checks passed
@kahrendt kahrendt deleted the pr/protocol branch July 9, 2026 13:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants