From 857c97223cc35f821f68196ed24f87143a7e6ce4 Mon Sep 17 00:00:00 2001 From: Aashish <145881415+aashish254@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:52:04 +0545 Subject: [PATCH] refuse to encode a 1033 frame the package cannot parse --- cmd/ntripper/main.go | 5 ++- cmd/ntripper/main_test.go | 6 ++-- rtcm/encode.go | 38 +++++++++++++++++++--- rtcm/encode_test.go | 64 ++++++++++++++++++++++++++++++++++++-- rtcm/mixed_scanner_test.go | 8 ++--- rtcm/rtcm.go | 9 +++--- 6 files changed, 113 insertions(+), 17 deletions(-) diff --git a/cmd/ntripper/main.go b/cmd/ntripper/main.go index 0e2f50da..15dff476 100644 --- a/cmd/ntripper/main.go +++ b/cmd/ntripper/main.go @@ -413,13 +413,16 @@ func streamFrames( var rawxCount uint64 // Inject RTCM 1033 on connect and every 30s. - msg1033 := rtcm.Encode1033(rtcm.AntennaDescriptor{ + msg1033, err := rtcm.Encode1033(rtcm.AntennaDescriptor{ StationID: stationID, AntennaType: "ADVNULLANTENNA", AntennaSetupID: 0, ReceiverType: "u-blox F9T", ReceiverFW: "2.20", }) + if err != nil { + return fmt.Errorf("encoding 1033: %w", err) + } if _, err := client.Write(msg1033); err != nil { return casterWrite("initial 1033", err) } diff --git a/cmd/ntripper/main_test.go b/cmd/ntripper/main_test.go index d505f53f..a8434db8 100644 --- a/cmd/ntripper/main_test.go +++ b/cmd/ntripper/main_test.go @@ -91,8 +91,10 @@ func TestSetupLogger(t *testing.T) { func TestParseStreamCleanFile(t *testing.T) { // A file of back-to-back valid RTCM3 frames parses without error. - a := rtcm.Encode1033(rtcm.AntennaDescriptor{StationID: 1, AntennaType: "A"}) - b := rtcm.Encode1033(rtcm.AntennaDescriptor{StationID: 2, AntennaType: "BB"}) + a, err := rtcm.Encode1033(rtcm.AntennaDescriptor{StationID: 1, AntennaType: "A"}) + require.NoError(t, err) + b, err := rtcm.Encode1033(rtcm.AntennaDescriptor{StationID: 2, AntennaType: "BB"}) + require.NoError(t, err) path := filepath.Join(t.TempDir(), "stream.rtcm3") require.NoError(t, os.WriteFile(path, append(append([]byte{}, a...), b...), 0o644)) require.NoError(t, parseStream(path)) diff --git a/rtcm/encode.go b/rtcm/encode.go index e4b45176..9718ee98 100644 --- a/rtcm/encode.go +++ b/rtcm/encode.go @@ -16,7 +16,11 @@ limitations under the License. package rtcm -import "encoding/binary" +import "fmt" + +// maxDescriptorFieldLen is the width of the DF227/DF230/DF232/DF234/DF236 +// counters that prefix each descriptor string. +const maxDescriptorFieldLen = 255 // AntennaDescriptor holds parameters for generating RTCM 1033 // (Receiver and Antenna Descriptor) messages. @@ -31,7 +35,10 @@ type AntennaDescriptor struct { } // Encode1033 generates a complete RTCM 1033 frame (header + payload + CRC). -func Encode1033(desc AntennaDescriptor) []byte { +// It reports ErrDescriptorTooLong for a string the 8-bit counters cannot carry, +// and ErrPayloadTooLarge for a frame whose length does not fit the 10-bit header +// field, rather than emitting a frame the parser and scanner both reject. +func Encode1033(desc AntennaDescriptor) ([]byte, error) { // Build the bit-packed payload. // RTCM 1033 fields: // DF002: Message Number (12 bits) = 1033 @@ -48,6 +55,23 @@ func Encode1033(desc AntennaDescriptor) []byte { // DF236: Serial Counter K (8 bits) // DF237: Receiver Serial (K bytes) + fields := []struct { + name string + value string + }{ + {"AntennaType", desc.AntennaType}, + {"AntennaSerial", desc.AntennaSerial}, + {"ReceiverType", desc.ReceiverType}, + {"ReceiverFW", desc.ReceiverFW}, + {"ReceiverSerial", desc.ReceiverSerial}, + } + for _, f := range fields { + if len(f.value) > maxDescriptorFieldLen { + return nil, fmt.Errorf("%w: %s is %d bytes, max %d", + ErrDescriptorTooLong, f.name, len(f.value), maxDescriptorFieldLen) + } + } + payloadSize := 3 + // 12+12 bits = 24 bits = 3 bytes for msg number + station ID 1 + len(desc.AntennaType) + 1 + // setup ID @@ -56,6 +80,11 @@ func Encode1033(desc AntennaDescriptor) []byte { 1 + len(desc.ReceiverFW) + 1 + len(desc.ReceiverSerial) + if payloadSize > MaxPayloadLen { + return nil, fmt.Errorf("%w: %d > %d for 1033", + ErrPayloadTooLarge, payloadSize, MaxPayloadLen) + } + payload := make([]byte, payloadSize) offset := 0 @@ -107,11 +136,12 @@ func Encode1033(desc AntennaDescriptor) []byte { // Header: preamble + 6 reserved bits (0) + 10-bit length frame[0] = Preamble - binary.BigEndian.PutUint16(frame[1:3], uint16(len(payload)&0xFFFF)) + frame[1] = byte((len(payload) >> 8) & 0x03) + frame[2] = byte(len(payload) & 0xFF) // Payload copy(frame[HeaderSize:], payload) putCRC(frame) - return frame + return frame, nil } diff --git a/rtcm/encode_test.go b/rtcm/encode_test.go index 02edf149..a4e262fe 100644 --- a/rtcm/encode_test.go +++ b/rtcm/encode_test.go @@ -17,6 +17,8 @@ limitations under the License. package rtcm import ( + "bytes" + "strings" "testing" "github.com/stretchr/testify/require" @@ -27,6 +29,13 @@ func stationIDOf(payload []byte) uint16 { return uint16(payload[1]&0x0F)<<8 | uint16(payload[2]) } +func mustEncode1033(t *testing.T, desc AntennaDescriptor) []byte { + t.Helper() + frame, err := Encode1033(desc) + require.NoError(t, err) + return frame +} + func TestEncode1033RoundTrip(t *testing.T) { desc := AntennaDescriptor{ StationID: 1234, @@ -34,7 +43,7 @@ func TestEncode1033RoundTrip(t *testing.T) { ReceiverType: "u-blox F9T", ReceiverFW: "2.20", } - frame := Encode1033(desc) + frame := mustEncode1033(t, desc) f, err := ParseFrame(frame) // also validates CRC-24Q require.NoError(t, err) @@ -45,8 +54,59 @@ func TestEncode1033RoundTrip(t *testing.T) { require.Contains(t, string(f.Payload), "2.20") } +func TestEncode1033RejectsUnrepresentableFrames(t *testing.T) { + t.Run("total exceeds the 10-bit header length", func(t *testing.T) { + // Every field is individually legal under its 8-bit counter, but the + // payload they add up to does not fit the header's length field. + long := strings.Repeat("A", 250) + frame, err := Encode1033(AntennaDescriptor{ + StationID: 7, + AntennaType: long, + AntennaSerial: long, + ReceiverType: long, + ReceiverFW: long, + ReceiverSerial: long, + }) + require.Nil(t, frame) + require.ErrorIs(t, err, ErrPayloadTooLarge) + }) + + t.Run("field exceeds its 8-bit counter", func(t *testing.T) { + frame, err := Encode1033(AntennaDescriptor{ + StationID: 7, + AntennaType: strings.Repeat("A", maxDescriptorFieldLen+1), + }) + require.Nil(t, frame) + require.ErrorIs(t, err, ErrDescriptorTooLong) + }) +} + +func TestEncode1033LargestRepresentableFrame(t *testing.T) { + // 9 bytes of fixed overhead plus 255+255+255+249 descriptor bytes is exactly + // MaxPayloadLen, the longest frame the header can still describe. + frame, err := Encode1033(AntennaDescriptor{ + StationID: 7, + AntennaType: strings.Repeat("A", maxDescriptorFieldLen), + AntennaSerial: strings.Repeat("B", maxDescriptorFieldLen), + ReceiverType: strings.Repeat("C", maxDescriptorFieldLen), + ReceiverFW: strings.Repeat("D", maxDescriptorFieldLen-6), + }) + require.NoError(t, err) + require.Len(t, frame, HeaderSize+MaxPayloadLen+CRCSize) + require.Zero(t, frame[1]&0xFC, "reserved bits must stay zero") + + f, err := ParseFrame(frame) + require.NoError(t, err) + require.Equal(t, uint16(1033), f.MessageType) + + s := NewScanner(bytes.NewReader(frame)) + require.True(t, s.Scan(), "the scanner must sync on what the encoder emitted") + require.Equal(t, uint16(1033), s.Frame().MessageType) + require.NoError(t, s.Err()) +} + func TestPatchStationID(t *testing.T) { - orig := Encode1033(AntennaDescriptor{StationID: 1, AntennaType: "X"}) + orig := mustEncode1033(t, AntennaDescriptor{StationID: 1, AntennaType: "X"}) patched := PatchStationID(orig, 4090) f, err := ParseFrame(patched) // CRC must still validate after the patch diff --git a/rtcm/mixed_scanner_test.go b/rtcm/mixed_scanner_test.go index 7d06e219..756b6598 100644 --- a/rtcm/mixed_scanner_test.go +++ b/rtcm/mixed_scanner_test.go @@ -41,9 +41,9 @@ func TestScannerUBXFrame(t *testing.T) { } func TestScannerMixedUBXAndRTCM(t *testing.T) { - rtcmA := Encode1033(AntennaDescriptor{StationID: 1, AntennaType: "A"}) + rtcmA := mustEncode1033(t, AntennaDescriptor{StationID: 1, AntennaType: "A"}) ubx := buildUBXFrame(UBXMsgRAWX, []byte{1, 2, 3, 4, 5}) - rtcmB := Encode1033(AntennaDescriptor{StationID: 2, AntennaType: "BB"}) + rtcmB := mustEncode1033(t, AntennaDescriptor{StationID: 2, AntennaType: "BB"}) var buf bytes.Buffer buf.Write(rtcmA) @@ -73,7 +73,7 @@ func TestScannerMixedUBXAndRTCM(t *testing.T) { func TestScannerUBXBadChecksumResyncs(t *testing.T) { bad := buildUBXFrame(UBXMsgRAWX, []byte{0x11, 0x22, 0x33}) bad[len(bad)-1] ^= 0xFF // corrupt the checksum - good := Encode1033(AntennaDescriptor{StationID: 3, AntennaType: "CCC"}) + good := mustEncode1033(t, AntennaDescriptor{StationID: 3, AntennaType: "CCC"}) var buf bytes.Buffer buf.Write(bad) @@ -145,7 +145,7 @@ func TestScannerRecordsNonEOFReadError(t *testing.T) { }) t.Run("RTCM", func(t *testing.T) { - frame := Encode1033(AntennaDescriptor{StationID: 1, AntennaType: "A"}) + frame := mustEncode1033(t, AntennaDescriptor{StationID: 1, AntennaType: "A"}) s := NewMixedScanner(&shortThenErrReader{data: frame[:4], err: readErr}) require.False(t, s.Scan()) require.ErrorIs(t, s.Err(), readErr) diff --git a/rtcm/rtcm.go b/rtcm/rtcm.go index 592f33cf..3d442c47 100644 --- a/rtcm/rtcm.go +++ b/rtcm/rtcm.go @@ -50,10 +50,11 @@ const ( ) var ( - ErrInvalidPreamble = errors.New("invalid preamble") - ErrPayloadTooLarge = errors.New("payload length exceeds maximum") - ErrFrameTooShort = errors.New("frame data too short") - ErrCRCMismatch = errors.New("CRC-24Q mismatch") + ErrInvalidPreamble = errors.New("invalid preamble") + ErrPayloadTooLarge = errors.New("payload length exceeds maximum") + ErrDescriptorTooLong = errors.New("descriptor length exceeds 8-bit counter") + ErrFrameTooShort = errors.New("frame data too short") + ErrCRCMismatch = errors.New("CRC-24Q mismatch") ) // Frame represents a parsed RTCM3 frame.