Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c554cac00d | ||
|
|
d1d00d6472 | ||
|
|
03e14a9ae3 | ||
|
|
995f63a27f | ||
|
|
30bb1a2fa3 | ||
|
|
021ecf425f |
@@ -20,8 +20,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||||
- uses: actions/setup-go@v7
|
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||||
with:
|
with:
|
||||||
go-version: "1.26.5"
|
go-version: "1.26.5"
|
||||||
cache: true
|
cache: true
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
runs-on: macos-26
|
runs-on: macos-26
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||||
- name: Assert pinned toolchain
|
- name: Assert pinned toolchain
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: verify generate proto-lint proto-breaking source-verify scope-verify conformance frame-verify go-test binding-compile strict-contracts clean-generated
|
.PHONY: verify generate proto-lint proto-breaking source-verify scope-verify ci-verify conformance frame-verify go-test binding-compile strict-contracts clean-generated
|
||||||
|
|
||||||
PYTHON ?= python3
|
PYTHON ?= python3
|
||||||
PROTOC ?= protoc
|
PROTOC ?= protoc
|
||||||
@@ -23,6 +23,9 @@ scope-verify:
|
|||||||
$(PYTHON) -B tools/test_check_scope.py
|
$(PYTHON) -B tools/test_check_scope.py
|
||||||
$(PYTHON) -B tools/check_scope.py
|
$(PYTHON) -B tools/check_scope.py
|
||||||
|
|
||||||
|
ci-verify:
|
||||||
|
$(PYTHON) -B tools/check_ci_actions.py
|
||||||
|
|
||||||
go-test:
|
go-test:
|
||||||
go test ./gen/go/... ./tests/go
|
go test ./gen/go/... ./tests/go
|
||||||
|
|
||||||
@@ -45,4 +48,4 @@ frame-verify:
|
|||||||
clean-generated:
|
clean-generated:
|
||||||
$(PYTHON) tools/generate.py --check
|
$(PYTHON) tools/generate.py --check
|
||||||
|
|
||||||
verify: generate proto-lint proto-breaking source-verify scope-verify go-test binding-compile strict-contracts conformance frame-verify clean-generated
|
verify: generate proto-lint proto-breaking source-verify scope-verify ci-verify go-test binding-compile strict-contracts conformance frame-verify clean-generated
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id version kind input expected
|
id version kind input expected
|
||||||
valid-empty-control 1 datagram hex=564401010000000000000000000000000000010000 valid
|
valid-empty-control 1 datagram hex=564401010000000000000000000000000000010000 valid
|
||||||
invalid-short 1 datagram hex=564401 invalid:truncated
|
invalid-short 1 datagram hex=564401 invalid:truncated
|
||||||
invalid-version 1 datagram hex=564402010000000000000000000000000000010000 invalid:unsupported_version
|
invalid-version 1 datagram hex=564403010000000000000000000000000000010000 invalid:unsupported_version
|
||||||
invalid-channel 1 datagram hex=564401990000000000000000000000000000010000 invalid:unknown_channel
|
invalid-channel 1 datagram hex=564401990000000000000000000000000000010000 invalid:unknown_channel
|
||||||
invalid-length 1 datagram hex=564401010000000000000000000000000000010001 invalid:length_mismatch
|
invalid-length 1 datagram hex=564401010000000000000000000000000000010001 invalid:length_mismatch
|
||||||
valid-video-empty 1 datagram hex=5644010a0000000000000000000000000000010000 valid
|
valid-video-empty 1 datagram hex=5644010a0000000000000000000000000000010000 valid
|
||||||
|
|||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
id version kind input expected
|
||||||
|
v2-valid-video-single 2 datagram hex=5644020a00000000010000000000000002000000010003010203 valid
|
||||||
|
v2-valid-video-last-fragment 2 datagram hex=5644020a00000000010000000000000002037a037b0000 valid
|
||||||
|
v2-invalid-short 2 datagram hex=564402 invalid:truncated
|
||||||
|
v2-invalid-version 2 datagram hex=5644030a00000000010000000000000002000000010000 invalid:unsupported_version
|
||||||
|
v2-invalid-channel 2 datagram hex=5644020d00000000010000000000000002000000010000 invalid:unknown_channel
|
||||||
|
v2-invalid-fragment-zero 2 datagram hex=5644020a00000000010000000000000002000000000000 invalid:fragment
|
||||||
|
v2-invalid-fragment-index 2 datagram hex=5644020a00000000010000000000000002000100010000 invalid:fragment
|
||||||
|
v2-invalid-fragment-count-limit 2 datagram hex=5644020a000000000100000000000000020000037c0000 invalid:fragment_limit
|
||||||
|
v2-invalid-length 2 datagram hex=5644020a00000000010000000000000002000000010001 invalid:length_mismatch
|
||||||
|
@@ -9,6 +9,7 @@ valid-controller 1 gateway_input hex=5647493105110200030004ffff00010002000300040
|
|||||||
valid-controller-release 1 gateway_input hex=5647493105110200000000000000000000000000000000 valid
|
valid-controller-release 1 gateway_input hex=5647493105110200000000000000000000000000000000 valid
|
||||||
valid-idr 1 gateway_feedback hex=5647463100010000 valid
|
valid-idr 1 gateway_feedback hex=5647463100010000 valid
|
||||||
valid-fec 1 gateway_feedback hex=56474631000200150000002a000500030002000a000200080002140001 valid
|
valid-fec 1 gateway_feedback hex=56474631000200150000002a000500030002000a000200080002140001 valid
|
||||||
|
valid-terminal-receipt 1 gateway_feedback hex=5647463100030000 valid
|
||||||
valid-termination 1 gateway_feedback hex=564746310110000400000001 valid
|
valid-termination 1 gateway_feedback hex=564746310110000400000001 valid
|
||||||
valid-rumble 1 gateway_feedback hex=56474631011100050112345678 valid
|
valid-rumble 1 gateway_feedback hex=56474631011100050112345678 valid
|
||||||
valid-hdr 1 gateway_feedback hex=564746310112000101 valid
|
valid-hdr 1 gateway_feedback hex=564746310112000101 valid
|
||||||
@@ -18,5 +19,9 @@ invalid-input-reserved 1 gateway_input hex=564749310203010101 invalid:reserved
|
|||||||
invalid-input-utf8 1 gateway_input hex=564749310402c328 invalid:utf8
|
invalid-input-utf8 1 gateway_input hex=564749310402c328 invalid:utf8
|
||||||
invalid-input-length 1 gateway_input hex=564749310104010200 invalid:length
|
invalid-input-length 1 gateway_input hex=564749310104010200 invalid:length
|
||||||
invalid-feedback-direction 1 gateway_feedback hex=5647463101020000 invalid:direction
|
invalid-feedback-direction 1 gateway_feedback hex=5647463101020000 invalid:direction
|
||||||
invalid-feedback-type 1 gateway_feedback hex=5647463100030000 invalid:type
|
invalid-terminal-receipt-direction 1 gateway_feedback hex=5647463101030000 invalid:direction
|
||||||
|
invalid-terminal-receipt-body 1 gateway_feedback hex=5647463100030001ff invalid:length
|
||||||
|
invalid-terminal-receipt-truncated 1 gateway_feedback hex=56474631000300 invalid:truncated
|
||||||
|
invalid-terminal-receipt-length 1 gateway_feedback hex=5647463100030001 invalid:length
|
||||||
|
invalid-feedback-type 1 gateway_feedback hex=5647463100040000 invalid:type
|
||||||
invalid-feedback-length 1 gateway_feedback hex=5647463101100003000000 invalid:length
|
invalid-feedback-length 1 gateway_feedback hex=5647463101100003000000 invalid:length
|
||||||
|
|||||||
|
@@ -3,11 +3,12 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"fixtures/conformance/control-v1.tsv",
|
"fixtures/conformance/control-v1.tsv",
|
||||||
"fixtures/conformance/datagram-v1.tsv",
|
"fixtures/conformance/datagram-v1.tsv",
|
||||||
|
"fixtures/conformance/datagram-v2.tsv",
|
||||||
"fixtures/conformance/events-v1.tsv",
|
"fixtures/conformance/events-v1.tsv",
|
||||||
"fixtures/conformance/gateway-clipboard-audit-v1.tsv",
|
"fixtures/conformance/gateway-clipboard-audit-v1.tsv",
|
||||||
"fixtures/conformance/gateway-clipboard-v1.tsv",
|
"fixtures/conformance/gateway-clipboard-v1.tsv",
|
||||||
"fixtures/conformance/gateway-input-feedback-v1.tsv",
|
"fixtures/conformance/gateway-input-feedback-v1.tsv",
|
||||||
"fixtures/conformance/tunnel-v1.tsv"
|
"fixtures/conformance/tunnel-v1.tsv"
|
||||||
],
|
],
|
||||||
"corpus_sha256": "69d5b12a533ff0d9786784b99aecc8a74a7ec2c6855b75c52e46ecff5bd3e6c5"
|
"corpus_sha256": "544ba8765b4ddcfbe807d1d6e6788fe03d06b8e50d1424b82e4e512312e51248"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# VerseVDI complete media datagram v2
|
||||||
|
|
||||||
|
Datagram v2 carries one complete encoded video or audio unit under one sequence.
|
||||||
|
It is negotiated explicitly as `datagram-v2`; v1 bytes are never reinterpreted.
|
||||||
|
The gateway relays encoded bytes without codec processing or provider exposure.
|
||||||
|
|
||||||
|
All multi-byte integers are unsigned big-endian. The fixed header is 23 bytes:
|
||||||
|
|
||||||
|
| Offset | Size | Field | Rule |
|
||||||
|
|---:|---:|---|---|
|
||||||
|
| 0 | 2 | magic | ASCII `VD` (`0x56 0x44`) |
|
||||||
|
| 2 | 1 | version | `2` only |
|
||||||
|
| 3 | 1 | channel | `media.video.v1` (10) or `media.audio.v1` (11) |
|
||||||
|
| 4 | 1 | flags | zero; unknown bits reject |
|
||||||
|
| 5 | 4 | sequence | session-local wrapping complete-unit sequence |
|
||||||
|
| 9 | 8 | timestamp_ms | sender timestamp, bounded by transport skew policy |
|
||||||
|
| 17 | 2 | fragment_index | zero-based |
|
||||||
|
| 19 | 2 | fragment_count | 1 through 891; index less than count |
|
||||||
|
| 21 | 2 | payload_length | exact payload byte count, at most 1,177 |
|
||||||
|
|
||||||
|
Each QUIC datagram is at most 1,200 bytes. One complete unit is at most
|
||||||
|
1,048,576 encoded bytes and 891 fragments. A sender rejects a larger unit
|
||||||
|
before fragmentation.
|
||||||
|
|
||||||
|
A receiver retains at most four incomplete media units and only received
|
||||||
|
fragment bytes. Fragments for one unit must agree on channel, sequence,
|
||||||
|
timestamp, flags, and count. Exact duplicates are ignored; conflicting
|
||||||
|
duplicates reject that unit. Bounded reorder is accepted. An incomplete unit
|
||||||
|
expires after 250 milliseconds, and accepting a fifth incomplete unit evicts
|
||||||
|
the oldest. Reassembly checks the 1,048,576-byte ceiling before appending and
|
||||||
|
emits only after every fragment is present.
|
||||||
@@ -53,8 +53,9 @@ is the following exact envelope:
|
|||||||
| 6 | 2 | payload length | exact payload byte count |
|
| 6 | 2 | payload length | exact payload byte count |
|
||||||
| 8 | N | payload | exact type-specific body |
|
| 8 | N | payload | exact type-specific body |
|
||||||
|
|
||||||
The client-to-gateway types are `0x01` IDR request (empty) and `0x02` FEC
|
The client-to-gateway types are `0x01` IDR request (empty), `0x02` FEC
|
||||||
status: `frame_index` (u32), `highest_received_sequence` (u16),
|
status, and `0x03` terminal receipt (empty). FEC status contains
|
||||||
|
`frame_index` (u32), `highest_received_sequence` (u16),
|
||||||
`next_contiguous_sequence` (u16), `missing_before_highest` (u16),
|
`next_contiguous_sequence` (u16), `missing_before_highest` (u16),
|
||||||
`total_data_packets` (u16), `total_parity_packets` (u16),
|
`total_data_packets` (u16), `total_parity_packets` (u16),
|
||||||
`received_data_packets` (u16), `received_parity_packets` (u16),
|
`received_data_packets` (u16), `received_parity_packets` (u16),
|
||||||
@@ -63,6 +64,10 @@ status: `frame_index` (u32), `highest_received_sequence` (u16),
|
|||||||
the provider's unsequenced ENet FEC delivery; it does not put it on the reliable
|
the provider's unsequenced ENet FEC delivery; it does not put it on the reliable
|
||||||
provider input path.
|
provider input path.
|
||||||
|
|
||||||
|
The terminal receipt is valid only from client to gateway with an exact
|
||||||
|
zero-byte payload. Session-state authorization remains a gateway responsibility;
|
||||||
|
the Protocol grammar defines only its fixed wire shape.
|
||||||
|
|
||||||
The gateway-to-client types are `0x10` host termination (`exit_code` u32),
|
The gateway-to-client types are `0x10` host termination (`exit_code` u32),
|
||||||
`0x11` rumble (`controller` u8, `low_frequency` u16,
|
`0x11` rumble (`controller` u8, `low_frequency` u16,
|
||||||
`high_frequency` u16), and `0x12` HDR mode (`enabled` exactly `0` or `1`). The
|
`high_frequency` u16), and `0x12` HDR mode (`enabled` exactly `0` or `1`). The
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"version": "1",
|
"version": "1",
|
||||||
|
"framing_profiles": ["datagram-v1", "datagram-v2"],
|
||||||
"header_bytes": 21,
|
"header_bytes": 21,
|
||||||
"maximum_frame_bytes": 65536,
|
"maximum_frame_bytes": 65536,
|
||||||
"channels": [
|
"channels": [
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const SchemaSHA256 = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
|
const SchemaSHA256 = "a86cbdaf2cfb884e3d98467968007e731ca55c6f6eb6dbd5cd6b95e062a9b058"
|
||||||
const ProtocolVersion = "1.0.0"
|
const ProtocolVersion = "1.0.0"
|
||||||
const CurrentWireVersion = "1"
|
const CurrentWireVersion = "1"
|
||||||
const NMinus1WireVersion = "0"
|
const NMinus1WireVersion = "0"
|
||||||
@@ -851,11 +851,8 @@ func (v CapabilityProfile) Validate() error {
|
|||||||
if v.Framing == "" {
|
if v.Framing == "" {
|
||||||
violations = append(violations, FieldViolation{Field: "framing", Code: "required"})
|
violations = append(violations, FieldViolation{Field: "framing", Code: "required"})
|
||||||
}
|
}
|
||||||
if len(v.Framing) < 1 && v.Framing != "" {
|
if v.Framing != "" && !(v.Framing == "datagram-v1" || v.Framing == "datagram-v2") {
|
||||||
violations = append(violations, FieldViolation{Field: "framing", Code: "min_length"})
|
violations = append(violations, FieldViolation{Field: "framing", Code: "invalid_value"})
|
||||||
}
|
|
||||||
if len(v.Framing) > 64 {
|
|
||||||
violations = append(violations, FieldViolation{Field: "framing", Code: "max_length"})
|
|
||||||
}
|
}
|
||||||
if v.Media == "" {
|
if v.Media == "" {
|
||||||
violations = append(violations, FieldViolation{Field: "media", Code: "required"})
|
violations = append(violations, FieldViolation{Field: "media", Code: "required"})
|
||||||
|
|||||||
+1
-1
@@ -14,5 +14,5 @@
|
|||||||
},
|
},
|
||||||
"generator_sha256": "00fdba050eb924a54dd3d63aac0a38560341b675f0de4e3e9631ee895057a9b6",
|
"generator_sha256": "00fdba050eb924a54dd3d63aac0a38560341b675f0de4e3e9631ee895057a9b6",
|
||||||
"protocol_version": "1.0.0",
|
"protocol_version": "1.0.0",
|
||||||
"schema_sha256": "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
|
"schema_sha256": "a86cbdaf2cfb884e3d98467968007e731ca55c6f6eb6dbd5cd6b95e062a9b058"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by tools/generate.py; DO NOT EDIT.
|
// Code generated by tools/generate.py; DO NOT EDIT.
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
pub const SCHEMA_SHA256: &str = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716";
|
pub const SCHEMA_SHA256: &str = "a86cbdaf2cfb884e3d98467968007e731ca55c6f6eb6dbd5cd6b95e062a9b058";
|
||||||
pub const CURRENT_WIRE_VERSION: &str = "1";
|
pub const CURRENT_WIRE_VERSION: &str = "1";
|
||||||
pub const N_MINUS_1_WIRE_VERSION: &str = "0";
|
pub const N_MINUS_1_WIRE_VERSION: &str = "0";
|
||||||
pub const N_MINUS_2_WIRE_VERSION: &str = "-1";
|
pub const N_MINUS_2_WIRE_VERSION: &str = "-1";
|
||||||
@@ -223,9 +223,7 @@ impl CapabilityProfile {
|
|||||||
if self.transport.is_empty() { return Err(ValidationError::new("transport", "required")); }
|
if self.transport.is_empty() { return Err(ValidationError::new("transport", "required")); }
|
||||||
if !self.transport.is_empty() && self.transport.len() < 1 { return Err(ValidationError::new("transport", "min_length")); }
|
if !self.transport.is_empty() && self.transport.len() < 1 { return Err(ValidationError::new("transport", "min_length")); }
|
||||||
if self.transport.len() > 64 { return Err(ValidationError::new("transport", "max_length")); }
|
if self.transport.len() > 64 { return Err(ValidationError::new("transport", "max_length")); }
|
||||||
if self.framing.is_empty() { return Err(ValidationError::new("framing", "required")); }
|
if self.framing != "datagram-v1" && self.framing != "datagram-v2" { return Err(ValidationError::new("framing", "invalid_value")); }
|
||||||
if !self.framing.is_empty() && self.framing.len() < 1 { return Err(ValidationError::new("framing", "min_length")); }
|
|
||||||
if self.framing.len() > 64 { return Err(ValidationError::new("framing", "max_length")); }
|
|
||||||
if self.media.is_empty() { return Err(ValidationError::new("media", "required")); }
|
if self.media.is_empty() { return Err(ValidationError::new("media", "required")); }
|
||||||
if !self.media.is_empty() && self.media.len() < 1 { return Err(ValidationError::new("media", "min_length")); }
|
if !self.media.is_empty() && self.media.len() < 1 { return Err(ValidationError::new("media", "min_length")); }
|
||||||
if self.media.len() > 64 { return Err(ValidationError::new("media", "max_length")); }
|
if self.media.len() > 64 { return Err(ValidationError::new("media", "max_length")); }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by tools/generate.py; DO NOT EDIT.
|
// Code generated by tools/generate.py; DO NOT EDIT.
|
||||||
import Foundation
|
import Foundation
|
||||||
public typealias JSONObject = [String: String]
|
public typealias JSONObject = [String: String]
|
||||||
public let schemaSHA256 = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
|
public let schemaSHA256 = "a86cbdaf2cfb884e3d98467968007e731ca55c6f6eb6dbd5cd6b95e062a9b058"
|
||||||
public let currentWireVersion = "1"
|
public let currentWireVersion = "1"
|
||||||
public let nMinus1WireVersion = "0"
|
public let nMinus1WireVersion = "0"
|
||||||
public let nMinus2WireVersion = "-1"
|
public let nMinus2WireVersion = "-1"
|
||||||
@@ -278,9 +278,7 @@ public struct CapabilityProfile: Codable, Equatable {
|
|||||||
if self.transport.isEmpty { throw ContractValidationError(field: "transport", code: "required") }
|
if self.transport.isEmpty { throw ContractValidationError(field: "transport", code: "required") }
|
||||||
if !self.transport.isEmpty && self.transport.utf8.count < 1 { throw ContractValidationError(field: "transport", code: "min_length") }
|
if !self.transport.isEmpty && self.transport.utf8.count < 1 { throw ContractValidationError(field: "transport", code: "min_length") }
|
||||||
if self.transport.utf8.count > 64 { throw ContractValidationError(field: "transport", code: "max_length") }
|
if self.transport.utf8.count > 64 { throw ContractValidationError(field: "transport", code: "max_length") }
|
||||||
if self.framing.isEmpty { throw ContractValidationError(field: "framing", code: "required") }
|
if !["datagram-v1", "datagram-v2"].contains(self.framing) { throw ContractValidationError(field: "framing", code: "invalid_value") }
|
||||||
if !self.framing.isEmpty && self.framing.utf8.count < 1 { throw ContractValidationError(field: "framing", code: "min_length") }
|
|
||||||
if self.framing.utf8.count > 64 { throw ContractValidationError(field: "framing", code: "max_length") }
|
|
||||||
if self.media.isEmpty { throw ContractValidationError(field: "media", code: "required") }
|
if self.media.isEmpty { throw ContractValidationError(field: "media", code: "required") }
|
||||||
if !self.media.isEmpty && self.media.utf8.count < 1 { throw ContractValidationError(field: "media", code: "min_length") }
|
if !self.media.isEmpty && self.media.utf8.count < 1 { throw ContractValidationError(field: "media", code: "min_length") }
|
||||||
if self.media.utf8.count > 64 { throw ContractValidationError(field: "media", code: "max_length") }
|
if self.media.utf8.count > 64 { throw ContractValidationError(field: "media", code: "max_length") }
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
RC8's canonical requirement names a terminal receipt, but its frame grammar, fixed fixture, and all Protocol validators reject the exact receipt accepted by Data Plane. The existing VGF1 envelope and generated conformance machinery already cover the required boundary.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Make one fixed empty client-direction type `0x03` receipt valid in every Protocol validator.
|
||||||
|
- Preserve all existing VGF1 direction, type, size, and payload validation.
|
||||||
|
- Make independent clients able to construct the receipt from Protocol-owned evidence.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- No new envelope, feedback framework, transport, or acknowledgement protocol.
|
||||||
|
- No change to gateway-to-client termination, rumble, HDR, client IDR, or FEC payloads.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- Extend the existing VGF1 grammar and fixed TSV corpus; generated and native validators remain consumers of that single contract.
|
||||||
|
- Reserve type `0x03` only for client direction with a zero-length payload. Direction and exact-length checks remain prior trust-boundary requirements.
|
||||||
|
- Publish the verified change as a new immutable version; RC8 remains unchanged.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [A validator diverges from the fixed corpus] → Require normalized cross-language conformance in `make verify`.
|
||||||
|
- [Receipt handling is accepted outside terminal state] → Keep state authorization in Data Plane; Protocol validates only the wire shape.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
The canonical terminal-receipt requirement contradicts the fixed VGF1 grammar and every generated validator, so RC8 cannot provide an executable cross-language contract for gateway-owned terminal closure.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Define client-direction VGF1 type `0x03` as an empty terminal receipt in the existing frame grammar.
|
||||||
|
- Add fixed conformance vectors for valid receipt handling and invalid direction, body, truncation, length, and unknown-type cases.
|
||||||
|
- Generate consistent Go, Rust, Swift, and Python validation behavior from the Protocol source.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `gateway-input-feedback`: Make the already-required terminal receipt executable and cross-language conformant without weakening other VGF1 validation.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Protocol frame documentation, conformance fixtures, generators, generated bindings, native validator tools, and immutable Protocol consumers. RC8 remains unchanged and a new immutable Protocol version is required.
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Bounded provider feedback control envelope
|
||||||
|
The registered bidirectional reliable `control.ack.v1` flow SHALL define an ASCII `VGF1` envelope
|
||||||
|
with a direction byte, type byte, big-endian payload length, and exact payload
|
||||||
|
bytes. Only host termination, rumble, and HDR feedback SHALL be valid from the
|
||||||
|
gateway to the client. Only IDR, FEC/loss feedback, and client-direction type
|
||||||
|
`0x03` with an empty payload as terminal receipt SHALL be valid from the client
|
||||||
|
to the gateway. The fixed conformance corpus and every generated or native
|
||||||
|
Protocol validator SHALL accept that exact receipt and reject unknown types,
|
||||||
|
wrong direction, nonempty receipt bodies, truncation, and length mismatch. The
|
||||||
|
terminal receipt SHALL be valid only while the same session awaits receipt of
|
||||||
|
its one terminal event and MUST NOT be forwarded to the provider. The envelope
|
||||||
|
SHALL contain no provider address, certificate, credential, or opaque provider
|
||||||
|
packet.
|
||||||
|
|
||||||
|
#### Scenario: Host termination forwarding
|
||||||
|
- **WHEN** the Apollo adapter receives an authenticated host termination packet
|
||||||
|
- **THEN** the gateway forwards a bounded `VGF1` termination envelope over reliable Verse control and reports the provider state separately
|
||||||
|
|
||||||
|
#### Scenario: Terminal event receipt
|
||||||
|
- **WHEN** a client receives the reliable typed terminal event
|
||||||
|
- **THEN** it sends the fixed empty client-direction type `0x03` receipt and the gateway owns bounded tunnel closure without forwarding the receipt to the provider
|
||||||
|
|
||||||
|
#### Scenario: Unauthorized or malformed feedback
|
||||||
|
- **WHEN** feedback is disabled by policy, has an invalid direction/type/length, contains a forbidden provider field, or sends a terminal receipt outside the awaiting-terminal state
|
||||||
|
- **THEN** the gateway rejects it without forwarding or provider mutation
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
## 1. Red Conformance
|
||||||
|
|
||||||
|
- [x] 1.1 Add fixed valid and invalid terminal-receipt vectors and prove Python, Go, Rust, and Swift reject the required valid vector
|
||||||
|
- [x] 1.2 Add a Data Plane independent-client regression that consumes the Protocol fixed vector rather than the production encoder
|
||||||
|
|
||||||
|
## 2. Contract Repair
|
||||||
|
|
||||||
|
- [x] 2.1 Update the VGF1 grammar and Protocol validator sources for the exact empty client-direction type `0x03` receipt
|
||||||
|
- [x] 2.2 Regenerate bindings normally and prove deterministic generation has no drift
|
||||||
|
- [x] 2.3 Run complete Protocol verification and strict OpenSpec validation
|
||||||
|
|
||||||
|
## 3. Immutable Release
|
||||||
|
|
||||||
|
- [x] 3.1 Verify the next version is unused locally and remotely, publish one immutable annotated tag, and verify its object and peeled commit
|
||||||
|
- [x] 3.2 Resolve the version from separate empty caches and pin exact fetched checksums in Data Plane and Connection Server
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-07-30
|
||||||
+2
-2
@@ -5,5 +5,5 @@
|
|||||||
|
|
||||||
## 2. Immutable release
|
## 2. Immutable release
|
||||||
|
|
||||||
- [ ] 2.1 Publish one new never-reused immutable Protocol version after final contract verification
|
- [x] 2.1 Publish one new never-reused immutable Protocol version after final contract verification
|
||||||
- [ ] 2.2 Resolve the version from separate empty consumer caches and record exact checksums
|
- [x] 2.2 Resolve the version from separate empty consumer caches and record exact checksums
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-07-30
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
RC9 datagram-v1 uses one-byte fragment fields and caps a media unit at 16 path-MTU fragments. The reviewed Apollo assembler can recover an encoded frame up to 1,028,152 bytes, so the replacement contract must carry at least that size while rejecting proportional allocation from untrusted header claims.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Preserve datagram-v1 byte-for-byte and add explicitly negotiated datagram-v2.
|
||||||
|
- Keep each QUIC datagram at 1,200 bytes or less.
|
||||||
|
- Bound one complete encoded unit to 1 MiB, at most 891 fragments, four incomplete units, and a 250 ms reassembly lifetime.
|
||||||
|
- Define duplicate, reorder, timeout, and malformed behavior for every validator.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- Provider packet exposure, codec parsing, decode/transcode, retransmission, or a generic transport framework.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- Datagram-v2 widens fragment index and count to unsigned 16-bit network-order fields, making a 23-byte header and 1,177-byte media payload. This is the smallest header change that covers the reviewed provider maximum without unrelated sequences.
|
||||||
|
- Sequence identifies one complete unit. All fragments must agree on channel, sequence, timestamp, count, and flags.
|
||||||
|
- Reassembly stores only received fragment bytes, rejects conflicting duplicates, permits exact duplicates and bounded reorder, evicts oldest state above four units, and expires state after 250 ms.
|
||||||
|
- Capability validation registers `datagram-v1` and `datagram-v2`; exact profile equality keeps downgrade behavior fail closed.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [More fragments increase per-frame overhead] → retain the 1,200-byte PMTU envelope and the 1 MiB frame ceiling.
|
||||||
|
- [Four concurrent 1 MiB units can consume bounded memory] → allocate only received bytes and evict/expire incomplete state.
|
||||||
|
- [RC9 peers cannot validate datagram-v2] → require a new immutable Protocol release and exact consumer pins; never mutate RC9.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
The RC9 datagram contract limits one encoded media unit to 18,864 bytes, below the bounded complete frames required by the reviewed Apollo adapter and the fixed Phase 3C profiles. A new immutable Protocol release must define complete-frame transport and reassembly bounds before consumers can relay realistic encoded frames without mutation.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Add a versioned PMTU-safe media datagram header with wider fragment fields.
|
||||||
|
- Bound complete encoded frames, fragments, reassembly state, duplicates, reorder, timeout, and allocation.
|
||||||
|
- Preserve RC9 datagram-v1 unchanged and require explicit negotiation of the new framing profile.
|
||||||
|
- Add fixed Python, Go, Rust, and Swift conformance vectors for valid and malformed framing.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `gateway-media-framing`: Versioned complete encoded-frame fragmentation and bounded reassembly contract.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Protocol framing documentation, registries, capability schema, fixed conformance fixtures, validators, generated Go/Rust/Swift bindings, and immutable consumer versioning. Requirements: SYS-002, SYS-003, P3C-006, P3C-007, P3C-008, P3C-030, P3C-038, VER-001, VER-002, VER-028.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Versioned complete encoded-frame datagrams
|
||||||
|
The Protocol SHALL register `datagram-v2` with a 23-byte network-order header containing magic, version, channel, flags, sequence, timestamp, unsigned 16-bit fragment index, unsigned 16-bit fragment count, and unsigned 16-bit payload length. Media datagrams MUST remain at most 1,200 bytes with at most 1,177 payload bytes, and one sequence MUST represent exactly one complete encoded unit of at most 1,048,576 bytes and 891 fragments.
|
||||||
|
|
||||||
|
#### Scenario: Bounded large encoded frame
|
||||||
|
- **WHEN** an encoded media frame is larger than the datagram-v1 18,864-byte limit but no larger than 1,048,576 bytes
|
||||||
|
- **THEN** datagram-v2 carries it under one sequence in at most 891 independently bounded fragments
|
||||||
|
|
||||||
|
#### Scenario: Oversized encoded frame
|
||||||
|
- **WHEN** a sender attempts more than 1,048,576 complete bytes or 891 fragments
|
||||||
|
- **THEN** framing rejects the unit before sending or allocating proportional state
|
||||||
|
|
||||||
|
### Requirement: Bounded complete-frame reassembly
|
||||||
|
Datagram-v2 reassembly SHALL retain at most four incomplete media units and only the bytes actually received, permit bounded fragment reorder and exact duplicate fragments, reject conflicting duplicates or inconsistent metadata, expire incomplete state after 250 milliseconds, and emit a payload only after every fragment is present exactly once.
|
||||||
|
|
||||||
|
#### Scenario: Reordered frame completes
|
||||||
|
- **WHEN** every valid fragment for one unit arrives out of order within the reassembly bounds
|
||||||
|
- **THEN** the receiver emits exactly one byte-identical complete encoded frame with its original boundary
|
||||||
|
|
||||||
|
#### Scenario: Malformed or stale reassembly
|
||||||
|
- **WHEN** fragments conflict, metadata changes, an index or count is invalid, state exceeds four incomplete units, or a unit exceeds its timeout
|
||||||
|
- **THEN** the receiver rejects or evicts that incomplete unit without proportional allocation or payload emission
|
||||||
|
|
||||||
|
### Requirement: Explicit framing negotiation
|
||||||
|
`CapabilityProfile.framing` SHALL accept only registered `datagram-v1` or `datagram-v2` values. A peer MUST use datagram-v2 only after exact capability intersection and MUST NOT reinterpret datagram-v1 bytes as datagram-v2.
|
||||||
|
|
||||||
|
#### Scenario: Independent datagram-v2 peers overlap
|
||||||
|
- **WHEN** both peers advertise the registered datagram-v2 framing profile
|
||||||
|
- **THEN** generated Go, Rust, and Swift validation and fixed conformance accept the profile
|
||||||
|
|
||||||
|
#### Scenario: Unknown or downgraded framing
|
||||||
|
- **WHEN** a peer advertises an unknown framing value or the peers advertise different versions
|
||||||
|
- **THEN** capability validation or intersection rejects the session before media forwarding
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
## 1. Red conformance
|
||||||
|
|
||||||
|
- [x] 1.1 Add fixed datagram-v2 and capability vectors that fail current Python, Go, Rust, and Swift validators
|
||||||
|
- [x] 1.2 Prove unknown version, malformed length, invalid fragment, and oversized complete-unit cases remain rejected
|
||||||
|
|
||||||
|
## 2. Contract implementation
|
||||||
|
|
||||||
|
- [x] 2.1 Add the versioned datagram-v2 frame grammar and registered framing values
|
||||||
|
- [x] 2.2 Update generator/native validators and regenerate Go, Rust, and Swift bindings normally
|
||||||
|
|
||||||
|
## 3. Verification
|
||||||
|
|
||||||
|
- [x] 3.1 Run focused cross-language conformance and strict OpenSpec validation
|
||||||
|
- [x] 3.2 Run complete `make verify` and prove a second generation has no drift
|
||||||
|
|
||||||
|
## 4. Immutable boundary
|
||||||
|
|
||||||
|
- [ ] 4.1 Publish one new never-reused immutable Protocol version and resolve it from empty consumer caches
|
||||||
@@ -40,28 +40,27 @@ an implementation-specific release-all provider command.
|
|||||||
The registered bidirectional reliable `control.ack.v1` flow SHALL define an ASCII `VGF1` envelope
|
The registered bidirectional reliable `control.ack.v1` flow SHALL define an ASCII `VGF1` envelope
|
||||||
with a direction byte, type byte, big-endian payload length, and exact payload
|
with a direction byte, type byte, big-endian payload length, and exact payload
|
||||||
bytes. Only host termination, rumble, and HDR feedback SHALL be valid from the
|
bytes. Only host termination, rumble, and HDR feedback SHALL be valid from the
|
||||||
gateway to the client. Only IDR, FEC/loss feedback, and an empty terminal receipt
|
gateway to the client. Only IDR, FEC/loss feedback, and client-direction type
|
||||||
SHALL be valid from the client to the gateway. The terminal receipt SHALL be
|
`0x03` with an empty payload as terminal receipt SHALL be valid from the client
|
||||||
valid only while the same session awaits receipt of its one terminal event and
|
to the gateway. The fixed conformance corpus and every generated or native
|
||||||
MUST NOT be forwarded to the provider. The envelope SHALL contain no provider
|
Protocol validator SHALL accept that exact receipt and reject unknown types,
|
||||||
address, certificate, credential, or opaque provider packet.
|
wrong direction, nonempty receipt bodies, truncation, and length mismatch. The
|
||||||
|
terminal receipt SHALL be valid only while the same session awaits receipt of
|
||||||
|
its one terminal event and MUST NOT be forwarded to the provider. The envelope
|
||||||
|
SHALL contain no provider address, certificate, credential, or opaque provider
|
||||||
|
packet.
|
||||||
|
|
||||||
#### Scenario: Host termination forwarding
|
#### Scenario: Host termination forwarding
|
||||||
- **WHEN** the Apollo adapter receives an authenticated host termination
|
- **WHEN** the Apollo adapter receives an authenticated host termination packet
|
||||||
packet
|
- **THEN** the gateway forwards a bounded `VGF1` termination envelope over reliable Verse control and reports the provider state separately
|
||||||
- **THEN** the gateway forwards a bounded `VGF1` termination envelope over
|
|
||||||
reliable Verse control and reports the provider state separately.
|
|
||||||
|
|
||||||
#### Scenario: Terminal event receipt
|
#### Scenario: Terminal event receipt
|
||||||
- **WHEN** a client receives the reliable typed terminal event
|
- **WHEN** a client receives the reliable typed terminal event
|
||||||
- **THEN** it sends the empty terminal receipt and the gateway owns bounded
|
- **THEN** it sends the fixed empty client-direction type `0x03` receipt and the gateway owns bounded tunnel closure without forwarding the receipt to the provider
|
||||||
tunnel closure without forwarding the receipt to the provider.
|
|
||||||
|
|
||||||
#### Scenario: Unauthorized or malformed feedback
|
#### Scenario: Unauthorized or malformed feedback
|
||||||
- **WHEN** feedback is disabled by policy, has an invalid direction/type/length,
|
- **WHEN** feedback is disabled by policy, has an invalid direction/type/length, contains a forbidden provider field, or sends a terminal receipt outside the awaiting-terminal state
|
||||||
contains a forbidden provider field, or sends a terminal receipt outside the
|
- **THEN** the gateway rejects it without forwarding or provider mutation
|
||||||
awaiting-terminal state
|
|
||||||
- **THEN** the gateway rejects it without forwarding or provider mutation.
|
|
||||||
|
|
||||||
### Requirement: Policy-bound text clipboard envelope
|
### Requirement: Policy-bound text clipboard envelope
|
||||||
The reliable `clipboard.text.v1` flow SHALL carry only a typed UTF-8 text
|
The reliable `clipboard.text.v1` flow SHALL carry only a typed UTF-8 text
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"max_frame_bytes": 65536,
|
"max_frame_bytes": 65536,
|
||||||
|
"framing_profiles": [
|
||||||
|
{"id": "datagram-v1", "header_bytes": 21, "media_payload_bytes": 1179, "max_fragments": 16, "max_complete_bytes": 18864},
|
||||||
|
{"id": "datagram-v2", "header_bytes": 23, "media_payload_bytes": 1177, "max_fragments": 891, "max_complete_bytes": 1048576}
|
||||||
|
],
|
||||||
"datagrams": [
|
"datagrams": [
|
||||||
{"id": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024, "payload_profile": "gateway-feedback-v1"},
|
{"id": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024, "payload_profile": "gateway-feedback-v1"},
|
||||||
{"id": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048},
|
{"id": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048},
|
||||||
|
|||||||
@@ -372,7 +372,7 @@
|
|||||||
"required": ["transport", "framing", "media", "audio", "source_rate_control", "client_decode"],
|
"required": ["transport", "framing", "media", "audio", "source_rate_control", "client_decode"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"transport": {"type": "string", "minLength": 1, "maxLength": 64},
|
"transport": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
"framing": {"type": "string", "minLength": 1, "maxLength": 64},
|
"framing": {"type": "string", "enum": ["datagram-v1", "datagram-v2"]},
|
||||||
"media": {"type": "string", "minLength": 1, "maxLength": 64},
|
"media": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
"audio": {"type": "string", "minLength": 1, "maxLength": 64},
|
"audio": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
"source_rate_control": {"type": "string", "minLength": 1, "maxLength": 64},
|
"source_rate_control": {"type": "string", "minLength": 1, "maxLength": 64},
|
||||||
|
|||||||
@@ -122,6 +122,21 @@ func TestCapabilityIntersectionSelectsRegisteredOrderedProfiles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCapabilityProfileRequiresRegisteredFraming(t *testing.T) {
|
||||||
|
valid := protocol.CapabilityProfile{
|
||||||
|
Transport: "quic-tls13", Framing: "datagram-v2", Media: "encoded",
|
||||||
|
Audio: "encoded", SourceRateControl: "server", ClientDecode: []string{"h264-opus"},
|
||||||
|
}
|
||||||
|
if err := valid.Validate(); err != nil {
|
||||||
|
t.Fatalf("registered datagram-v2 rejected: %v", err)
|
||||||
|
}
|
||||||
|
invalid := valid
|
||||||
|
invalid.Framing = "datagram-v3"
|
||||||
|
if err := invalid.Validate(); err == nil {
|
||||||
|
t.Fatal("unregistered framing accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTunnelAdmissionRequiresDeviceSignature(t *testing.T) {
|
func TestTunnelAdmissionRequiresDeviceSignature(t *testing.T) {
|
||||||
request := protocol.TunnelAdmissionRequest{
|
request := protocol.TunnelAdmissionRequest{
|
||||||
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway",
|
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway",
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Require immutable commits for third-party Gitea workflow actions."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
USE = re.compile(r"^\s*-\s+uses:\s+([^@\s]+)@([^\s#]+)", re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
for workflow in sorted((ROOT / ".gitea/workflows").glob("*.y*ml")):
|
||||||
|
for action, revision in USE.findall(workflow.read_text(encoding="utf-8")):
|
||||||
|
if not action.startswith("./") and not re.fullmatch(r"[0-9a-f]{40}", revision):
|
||||||
|
raise SystemExit(f"{workflow.relative_to(ROOT)}: mutable action {action}@{revision}")
|
||||||
|
|
||||||
|
print("Protocol CI action references are immutable")
|
||||||
@@ -87,6 +87,7 @@ def check_text_boundaries() -> None:
|
|||||||
ROOT / "openapi/control-v1.yaml",
|
ROOT / "openapi/control-v1.yaml",
|
||||||
ROOT / "proto/versevdi/control/v1/control.proto",
|
ROOT / "proto/versevdi/control/v1/control.proto",
|
||||||
ROOT / "frames/datagram-v1.md",
|
ROOT / "frames/datagram-v1.md",
|
||||||
|
ROOT / "frames/datagram-v2.md",
|
||||||
ROOT / "frames/registry.json",
|
ROOT / "frames/registry.json",
|
||||||
ROOT / "registries/features.json",
|
ROOT / "registries/features.json",
|
||||||
ROOT / "registries/datagrams.json",
|
ROOT / "registries/datagrams.json",
|
||||||
|
|||||||
@@ -14,11 +14,6 @@ import (
|
|||||||
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
datagramHeaderBytes = 21
|
|
||||||
maximumFrameBytes = 65536
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
entries, err := os.ReadDir("fixtures/conformance")
|
entries, err := os.ReadDir("fixtures/conformance")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -270,12 +265,16 @@ func classifyGatewayFeedback(encoded string) string {
|
|||||||
return "valid"
|
return "valid"
|
||||||
}
|
}
|
||||||
return "invalid:field"
|
return "invalid:field"
|
||||||
|
case 3:
|
||||||
|
if len(body) == 0 {
|
||||||
|
return "valid"
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return "invalid:type"
|
return "invalid:type"
|
||||||
}
|
}
|
||||||
return "invalid:length"
|
return "invalid:length"
|
||||||
}
|
}
|
||||||
if kind == 1 || kind == 2 {
|
if kind == 1 || kind == 2 || kind == 3 {
|
||||||
return "invalid:direction"
|
return "invalid:direction"
|
||||||
}
|
}
|
||||||
switch kind {
|
switch kind {
|
||||||
@@ -316,16 +315,24 @@ func classifyDatagram(encoded string) string {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "invalid:hex"
|
return "invalid:hex"
|
||||||
}
|
}
|
||||||
if len(raw) < datagramHeaderBytes {
|
if len(raw) < 3 {
|
||||||
return "invalid:truncated"
|
return "invalid:truncated"
|
||||||
}
|
}
|
||||||
if string(raw[:2]) != "VD" {
|
if string(raw[:2]) != "VD" {
|
||||||
return "invalid:magic"
|
return "invalid:magic"
|
||||||
}
|
}
|
||||||
if raw[2] != 1 {
|
if raw[2] != 1 && raw[2] != 2 {
|
||||||
return "invalid:unsupported_version"
|
return "invalid:unsupported_version"
|
||||||
}
|
}
|
||||||
|
headerBytes := 21
|
||||||
limits := map[byte]int{1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
|
limits := map[byte]int{1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
|
||||||
|
if raw[2] == 2 {
|
||||||
|
headerBytes = 23
|
||||||
|
limits = map[byte]int{10: 1177, 11: 1177}
|
||||||
|
}
|
||||||
|
if len(raw) < headerBytes {
|
||||||
|
return "invalid:truncated"
|
||||||
|
}
|
||||||
limit, ok := limits[raw[3]]
|
limit, ok := limits[raw[3]]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "invalid:unknown_channel"
|
return "invalid:unknown_channel"
|
||||||
@@ -333,17 +340,27 @@ func classifyDatagram(encoded string) string {
|
|||||||
if raw[4] != 0 {
|
if raw[4] != 0 {
|
||||||
return "invalid:flags"
|
return "invalid:flags"
|
||||||
}
|
}
|
||||||
if raw[18] == 0 || raw[17] >= raw[18] {
|
fragmentIndex, fragmentCount := int(raw[17]), int(raw[18])
|
||||||
|
payloadOffset := 19
|
||||||
|
if raw[2] == 2 {
|
||||||
|
fragmentIndex = int(raw[17])<<8 | int(raw[18])
|
||||||
|
fragmentCount = int(raw[19])<<8 | int(raw[20])
|
||||||
|
payloadOffset = 21
|
||||||
|
if fragmentCount > 891 {
|
||||||
|
return "invalid:fragment_limit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fragmentCount == 0 || fragmentIndex >= fragmentCount {
|
||||||
return "invalid:fragment"
|
return "invalid:fragment"
|
||||||
}
|
}
|
||||||
payloadLength := int(raw[19])<<8 | int(raw[20])
|
payloadLength := int(raw[payloadOffset])<<8 | int(raw[payloadOffset+1])
|
||||||
if payloadLength > limit {
|
if payloadLength > limit {
|
||||||
return "invalid:payload_limit"
|
return "invalid:payload_limit"
|
||||||
}
|
}
|
||||||
if len(raw) != datagramHeaderBytes+payloadLength {
|
if len(raw) != headerBytes+payloadLength {
|
||||||
return "invalid:length_mismatch"
|
return "invalid:length_mismatch"
|
||||||
}
|
}
|
||||||
if len(raw) > maximumFrameBytes {
|
if raw[2] == 1 && len(raw) > 65536 || raw[2] == 2 && len(raw) > 1200 {
|
||||||
return "invalid:frame_limit"
|
return "invalid:frame_limit"
|
||||||
}
|
}
|
||||||
return "valid"
|
return "valid"
|
||||||
|
|||||||
+28
-12
@@ -143,10 +143,12 @@ fn classify_gateway_feedback(encoded: &str) -> &'static str {
|
|||||||
1 => "invalid:length",
|
1 => "invalid:length",
|
||||||
2 if valid_fec_status(body) => "valid",
|
2 if valid_fec_status(body) => "valid",
|
||||||
2 => "invalid:field",
|
2 => "invalid:field",
|
||||||
|
3 if body.is_empty() => "valid",
|
||||||
|
3 => "invalid:length",
|
||||||
_ => "invalid:type",
|
_ => "invalid:type",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if kind == 1 || kind == 2 {
|
if kind == 1 || kind == 2 || kind == 3 {
|
||||||
return "invalid:direction";
|
return "invalid:direction";
|
||||||
}
|
}
|
||||||
match kind {
|
match kind {
|
||||||
@@ -187,36 +189,50 @@ fn classify_datagram(encoded: &str) -> &'static str {
|
|||||||
Some(raw) => raw,
|
Some(raw) => raw,
|
||||||
None => return "invalid:hex",
|
None => return "invalid:hex",
|
||||||
};
|
};
|
||||||
if raw.len() < 21 {
|
if raw.len() < 3 {
|
||||||
return "invalid:truncated";
|
return "invalid:truncated";
|
||||||
}
|
}
|
||||||
if raw[0..2] != *b"VD" {
|
if raw[0..2] != *b"VD" {
|
||||||
return "invalid:magic";
|
return "invalid:magic";
|
||||||
}
|
}
|
||||||
if raw[2] != 1 {
|
if raw[2] != 1 && raw[2] != 2 {
|
||||||
return "invalid:unsupported_version";
|
return "invalid:unsupported_version";
|
||||||
}
|
}
|
||||||
let limit = match raw[3] {
|
let header_bytes = if raw[2] == 1 { 21 } else { 23 };
|
||||||
1 => 1024,
|
if raw.len() < header_bytes {
|
||||||
2 => 2048,
|
return "invalid:truncated";
|
||||||
3 => 65515,
|
}
|
||||||
10 | 11 | 12 => 1179,
|
let limit = match (raw[2], raw[3]) {
|
||||||
|
(1, 1) => 1024,
|
||||||
|
(1, 2) => 2048,
|
||||||
|
(1, 3) => 65515,
|
||||||
|
(1, 10 | 11 | 12) => 1179,
|
||||||
|
(2, 10 | 11) => 1177,
|
||||||
_ => return "invalid:unknown_channel",
|
_ => return "invalid:unknown_channel",
|
||||||
};
|
};
|
||||||
if raw[4] != 0 {
|
if raw[4] != 0 {
|
||||||
return "invalid:flags";
|
return "invalid:flags";
|
||||||
}
|
}
|
||||||
if raw[18] == 0 || raw[17] >= raw[18] {
|
let (fragment_index, fragment_count, payload_offset) = if raw[2] == 1 {
|
||||||
|
(raw[17] as usize, raw[18] as usize, 19)
|
||||||
|
} else {
|
||||||
|
let count = ((raw[19] as usize) << 8) | raw[20] as usize;
|
||||||
|
if count > 891 {
|
||||||
|
return "invalid:fragment_limit";
|
||||||
|
}
|
||||||
|
((((raw[17] as usize) << 8) | raw[18] as usize), count, 21)
|
||||||
|
};
|
||||||
|
if fragment_count == 0 || fragment_index >= fragment_count {
|
||||||
return "invalid:fragment";
|
return "invalid:fragment";
|
||||||
}
|
}
|
||||||
let payload_length = ((raw[19] as usize) << 8) | raw[20] as usize;
|
let payload_length = ((raw[payload_offset] as usize) << 8) | raw[payload_offset + 1] as usize;
|
||||||
if payload_length > limit {
|
if payload_length > limit {
|
||||||
return "invalid:payload_limit";
|
return "invalid:payload_limit";
|
||||||
}
|
}
|
||||||
if raw.len() != 21 + payload_length {
|
if raw.len() != header_bytes + payload_length {
|
||||||
return "invalid:length_mismatch";
|
return "invalid:length_mismatch";
|
||||||
}
|
}
|
||||||
if raw.len() > 65536 {
|
if raw[2] == 1 && raw.len() > 65536 || raw[2] == 2 && raw.len() > 1200 {
|
||||||
return "invalid:frame_limit";
|
return "invalid:frame_limit";
|
||||||
}
|
}
|
||||||
"valid"
|
"valid"
|
||||||
|
|||||||
@@ -101,10 +101,11 @@ func classifyGatewayFeedback(_ encoded: String) -> String {
|
|||||||
case 1: return body.isEmpty ? "valid" : "invalid:length"
|
case 1: return body.isEmpty ? "valid" : "invalid:length"
|
||||||
case 2:
|
case 2:
|
||||||
return validFECStatus(body) ? "valid" : "invalid:field"
|
return validFECStatus(body) ? "valid" : "invalid:field"
|
||||||
|
case 3: return body.isEmpty ? "valid" : "invalid:length"
|
||||||
default: return "invalid:type"
|
default: return "invalid:type"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if kind == 1 || kind == 2 { return "invalid:direction" }
|
if kind == 1 || kind == 2 || kind == 3 { return "invalid:direction" }
|
||||||
switch kind {
|
switch kind {
|
||||||
case 0x10: return body.count == 4 ? "valid" : "invalid:length"
|
case 0x10: return body.count == 4 ? "valid" : "invalid:length"
|
||||||
case 0x11:
|
case 0x11:
|
||||||
@@ -128,23 +129,30 @@ func validFECStatus(_ body: [UInt8]) -> Bool {
|
|||||||
|
|
||||||
func classifyDatagram(_ encoded: String) -> String {
|
func classifyDatagram(_ encoded: String) -> String {
|
||||||
guard let raw = decodeHex(encoded) else { return "invalid:hex" }
|
guard let raw = decodeHex(encoded) else { return "invalid:hex" }
|
||||||
guard raw.count >= 21 else { return "invalid:truncated" }
|
guard raw.count >= 3 else { return "invalid:truncated" }
|
||||||
guard raw[0] == 0x56 && raw[1] == 0x44 else { return "invalid:magic" }
|
guard raw[0] == 0x56 && raw[1] == 0x44 else { return "invalid:magic" }
|
||||||
guard raw[2] == 1 else { return "invalid:unsupported_version" }
|
guard raw[2] == 1 || raw[2] == 2 else { return "invalid:unsupported_version" }
|
||||||
|
let headerBytes = raw[2] == 1 ? 21 : 23
|
||||||
|
guard raw.count >= headerBytes else { return "invalid:truncated" }
|
||||||
let limit: Int
|
let limit: Int
|
||||||
switch raw[3] {
|
switch (raw[2], raw[3]) {
|
||||||
case 1: limit = 1024
|
case (1, 1): limit = 1024
|
||||||
case 2: limit = 2048
|
case (1, 2): limit = 2048
|
||||||
case 3: limit = 65515
|
case (1, 3): limit = 65515
|
||||||
case 10, 11, 12: limit = 1179
|
case (1, 10), (1, 11), (1, 12): limit = 1179
|
||||||
|
case (2, 10), (2, 11): limit = 1177
|
||||||
default: return "invalid:unknown_channel"
|
default: return "invalid:unknown_channel"
|
||||||
}
|
}
|
||||||
guard raw[4] == 0 else { return "invalid:flags" }
|
guard raw[4] == 0 else { return "invalid:flags" }
|
||||||
guard raw[18] > 0 && raw[17] < raw[18] else { return "invalid:fragment" }
|
let fragmentIndex = raw[2] == 1 ? Int(raw[17]) : Int(raw[17]) * 256 + Int(raw[18])
|
||||||
let payloadLength = Int(raw[19]) * 256 + Int(raw[20])
|
let fragmentCount = raw[2] == 1 ? Int(raw[18]) : Int(raw[19]) * 256 + Int(raw[20])
|
||||||
|
if raw[2] == 2 && fragmentCount > 891 { return "invalid:fragment_limit" }
|
||||||
|
guard fragmentCount > 0 && fragmentIndex < fragmentCount else { return "invalid:fragment" }
|
||||||
|
let payloadOffset = raw[2] == 1 ? 19 : 21
|
||||||
|
let payloadLength = Int(raw[payloadOffset]) * 256 + Int(raw[payloadOffset + 1])
|
||||||
guard payloadLength <= limit else { return "invalid:payload_limit" }
|
guard payloadLength <= limit else { return "invalid:payload_limit" }
|
||||||
guard raw.count == 21 + payloadLength else { return "invalid:length_mismatch" }
|
guard raw.count == headerBytes + payloadLength else { return "invalid:length_mismatch" }
|
||||||
guard raw.count <= 65536 else { return "invalid:frame_limit" }
|
guard raw[2] == 1 ? raw.count <= 65536 : raw.count <= 1200 else { return "invalid:frame_limit" }
|
||||||
return "valid"
|
return "valid"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ TEXT_PATHS = (
|
|||||||
"proto/versevdi/control/v1/control.proto",
|
"proto/versevdi/control/v1/control.proto",
|
||||||
"proto/versevdi/tunnel/v1/tunnel.proto",
|
"proto/versevdi/tunnel/v1/tunnel.proto",
|
||||||
"frames/datagram-v1.md",
|
"frames/datagram-v1.md",
|
||||||
|
"frames/datagram-v2.md",
|
||||||
"frames/registry.json",
|
"frames/registry.json",
|
||||||
"registries/features.json",
|
"registries/features.json",
|
||||||
"registries/datagrams.json",
|
"registries/datagrams.json",
|
||||||
|
|||||||
@@ -35,6 +35,17 @@ let capability = try CapabilityProfile(
|
|||||||
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||||
)
|
)
|
||||||
|
_ = try CapabilityProfile(
|
||||||
|
transport: "quic-tls13", framing: "datagram-v2", media: "encoded",
|
||||||
|
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||||
|
)
|
||||||
|
do {
|
||||||
|
_ = try CapabilityProfile(
|
||||||
|
transport: "quic-tls13", framing: "datagram-v3", media: "encoded",
|
||||||
|
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||||
|
)
|
||||||
|
fatalError("unregistered framing was accepted")
|
||||||
|
} catch { }
|
||||||
let request = try TunnelAdmissionRequest(
|
let request = try TunnelAdmissionRequest(
|
||||||
version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience",
|
version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience",
|
||||||
grant: String(repeating: "g", count: 43), reconnectSequence: 0,
|
grant: String(repeating: "g", count: 43), reconnectSequence: 0,
|
||||||
@@ -162,6 +173,14 @@ fn main() {
|
|||||||
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||||
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
assert!(CapabilityProfile::new(
|
||||||
|
"quic-tls13".into(), "datagram-v2".into(), "encoded".into(),
|
||||||
|
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||||
|
).is_ok());
|
||||||
|
assert!(CapabilityProfile::new(
|
||||||
|
"quic-tls13".into(), "datagram-v3".into(), "encoded".into(),
|
||||||
|
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||||
|
).is_err());
|
||||||
let request = TunnelAdmissionRequest::new(
|
let request = TunnelAdmissionRequest::new(
|
||||||
"1".into(), "session".into(), "gateway".into(), "audience".into(),
|
"1".into(), "session".into(), "gateway".into(), "audience".into(),
|
||||||
"g".repeat(43), 0, "n".repeat(16), "s".repeat(86), capabilities.clone(),
|
"g".repeat(43), 0, "n".repeat(16), "s".repeat(86), capabilities.clone(),
|
||||||
|
|||||||
+2
-1
@@ -31,9 +31,10 @@ def main() -> int:
|
|||||||
value = json.loads((ROOT / registry).read_text(encoding="utf-8"))
|
value = json.loads((ROOT / registry).read_text(encoding="utf-8"))
|
||||||
entries = value.get("features", value.get("datagrams"))
|
entries = value.get("features", value.get("datagrams"))
|
||||||
assert entries and len({entry["id"] for entry in entries}) == len(entries)
|
assert entries and len({entry["id"] for entry in entries}) == len(entries)
|
||||||
|
maximum_bound = 1_048_576 if registry.endswith("datagrams.json") else 65_536
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
maximum = entry.get("max_frame_bytes", entry.get("max_payload_bytes"))
|
maximum = entry.get("max_frame_bytes", entry.get("max_payload_bytes"))
|
||||||
assert isinstance(maximum, int) and 1 <= maximum <= 65536
|
assert isinstance(maximum, int) and 1 <= maximum <= maximum_bound
|
||||||
|
|
||||||
manifest = json.loads((ROOT / "fixtures/valid/manifest.json").read_text(encoding="utf-8"))
|
manifest = json.loads((ROOT / "fixtures/valid/manifest.json").read_text(encoding="utf-8"))
|
||||||
assert set(manifest).issubset(set(defs["ConnectionManifest"]["properties"]))
|
assert set(manifest).issubset(set(defs["ConnectionManifest"]["properties"]))
|
||||||
|
|||||||
+33
-22
@@ -7,47 +7,58 @@ import binascii
|
|||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
HEADER_BYTES = 21
|
V1_CHANNEL_LIMITS = {1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
|
||||||
MAX_FRAME_BYTES = 65536
|
V2_CHANNEL_LIMITS = {10: 1177, 11: 1177}
|
||||||
CHANNEL_LIMITS = {1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
|
|
||||||
|
|
||||||
|
|
||||||
def classify(raw: bytes) -> str:
|
def classify(raw: bytes) -> str:
|
||||||
if len(raw) < HEADER_BYTES:
|
if len(raw) < 3:
|
||||||
return "invalid:truncated"
|
return "invalid:truncated"
|
||||||
if raw[:2] != b"VD":
|
if raw[:2] != b"VD":
|
||||||
return "invalid:magic"
|
return "invalid:magic"
|
||||||
if raw[2] != 1:
|
if raw[2] not in (1, 2):
|
||||||
return "invalid:unsupported_version"
|
return "invalid:unsupported_version"
|
||||||
if raw[3] not in CHANNEL_LIMITS:
|
header_bytes = 21 if raw[2] == 1 else 23
|
||||||
|
if len(raw) < header_bytes:
|
||||||
|
return "invalid:truncated"
|
||||||
|
limits = V1_CHANNEL_LIMITS if raw[2] == 1 else V2_CHANNEL_LIMITS
|
||||||
|
if raw[3] not in limits:
|
||||||
return "invalid:unknown_channel"
|
return "invalid:unknown_channel"
|
||||||
if raw[4] != 0:
|
if raw[4] != 0:
|
||||||
return "invalid:flags"
|
return "invalid:flags"
|
||||||
fragment_index, fragment_count = raw[17], raw[18]
|
if raw[2] == 1:
|
||||||
|
fragment_index, fragment_count = raw[17], raw[18]
|
||||||
|
payload_length = int.from_bytes(raw[19:21], "big")
|
||||||
|
else:
|
||||||
|
fragment_index = int.from_bytes(raw[17:19], "big")
|
||||||
|
fragment_count = int.from_bytes(raw[19:21], "big")
|
||||||
|
payload_length = int.from_bytes(raw[21:23], "big")
|
||||||
|
if fragment_count > 891:
|
||||||
|
return "invalid:fragment_limit"
|
||||||
if fragment_count == 0 or fragment_index >= fragment_count:
|
if fragment_count == 0 or fragment_index >= fragment_count:
|
||||||
return "invalid:fragment"
|
return "invalid:fragment"
|
||||||
payload_length = int.from_bytes(raw[19:21], "big")
|
if payload_length > limits[raw[3]]:
|
||||||
if payload_length > CHANNEL_LIMITS[raw[3]]:
|
|
||||||
return "invalid:payload_limit"
|
return "invalid:payload_limit"
|
||||||
if len(raw) != HEADER_BYTES + payload_length:
|
if len(raw) != header_bytes + payload_length:
|
||||||
return "invalid:length_mismatch"
|
return "invalid:length_mismatch"
|
||||||
if len(raw) > MAX_FRAME_BYTES:
|
if raw[2] == 1 and len(raw) > 65536 or raw[2] == 2 and len(raw) > 1200:
|
||||||
return "invalid:frame_limit"
|
return "invalid:frame_limit"
|
||||||
return "valid"
|
return "valid"
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
lines = (ROOT / "fixtures/conformance/datagram-v1.tsv").read_text(encoding="utf-8").splitlines()
|
for fixture in ("datagram-v1.tsv", "datagram-v2.tsv"):
|
||||||
assert lines[0] == "id\tversion\tkind\tinput\texpected"
|
lines = (ROOT / "fixtures/conformance" / fixture).read_text(encoding="utf-8").splitlines()
|
||||||
for line in lines[1:]:
|
assert lines[0] == "id\tversion\tkind\tinput\texpected"
|
||||||
identifier, version, kind, input_value, expected = line.split("\t")
|
for line in lines[1:]:
|
||||||
assert kind == "datagram" and version == "1"
|
identifier, version, kind, input_value, expected = line.split("\t")
|
||||||
encoded = input_value.removeprefix("hex=")
|
assert kind == "datagram" and version in ("1", "2")
|
||||||
try:
|
encoded = input_value.removeprefix("hex=")
|
||||||
actual = classify(binascii.unhexlify(encoded))
|
try:
|
||||||
except binascii.Error:
|
actual = classify(binascii.unhexlify(encoded))
|
||||||
actual = "invalid:hex"
|
except binascii.Error:
|
||||||
assert actual == expected, f"{identifier}: {actual} != {expected}"
|
actual = "invalid:hex"
|
||||||
|
assert actual == expected, f"{fixture}:{identifier}: {actual} != {expected}"
|
||||||
print("Datagram frame validation passed")
|
print("Datagram frame validation passed")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -59,8 +59,10 @@ def classify_feedback(raw: bytes) -> str:
|
|||||||
return "valid" if not body else "invalid:length"
|
return "valid" if not body else "invalid:length"
|
||||||
if kind == 2:
|
if kind == 2:
|
||||||
return "valid" if valid_fec_status(body) else "invalid:field"
|
return "valid" if valid_fec_status(body) else "invalid:field"
|
||||||
|
if kind == 3:
|
||||||
|
return "valid" if not body else "invalid:length"
|
||||||
return "invalid:type"
|
return "invalid:type"
|
||||||
if kind in (1, 2):
|
if kind in (1, 2, 3):
|
||||||
return "invalid:direction"
|
return "invalid:direction"
|
||||||
if kind == 0x10:
|
if kind == 0x10:
|
||||||
return "valid" if len(body) == 4 else "invalid:length"
|
return "valid" if len(body) == 4 else "invalid:length"
|
||||||
|
|||||||
Reference in New Issue
Block a user