diff --git a/application_defined.go b/application_defined.go new file mode 100644 index 0000000..77a1193 --- /dev/null +++ b/application_defined.go @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package rtcp + +import ( + "encoding/binary" +) + +// ApplicationDefined represents an RTCP application-defined packet. +type ApplicationDefined struct { + SubType uint8 + SSRC uint32 + Name string + Data []byte +} + +// DestinationSSRC returns the SSRC value for this packet. +func (a ApplicationDefined) DestinationSSRC() []uint32 { + return []uint32{a.SSRC} +} + +// Marshal serializes the application-defined struct into a byte slice with padding. +func (a ApplicationDefined) Marshal() ([]byte, error) { + dataLength := len(a.Data) + if dataLength > 0xFFFF-12 { + return nil, errAppDefinedDataTooLarge + } + if len(a.Name) != 4 { + return nil, errAppDefinedInvalidName + } + // Calculate the padding size to be added to make the packet length a multiple of 4 bytes. + paddingSize := 4 - (dataLength % 4) + if paddingSize == 4 { + paddingSize = 0 + } + + packetSize := a.MarshalSize() + header := Header{ + Type: TypeApplicationDefined, + Length: uint16((packetSize / 4) - 1), + Padding: paddingSize != 0, + Count: a.SubType, + } + + headerBytes, err := header.Marshal() + if err != nil { + return nil, err + } + + rawPacket := make([]byte, packetSize) + copy(rawPacket, headerBytes) + binary.BigEndian.PutUint32(rawPacket[4:8], a.SSRC) + copy(rawPacket[8:12], a.Name) + copy(rawPacket[12:], a.Data) + + // Add padding if necessary. + if paddingSize > 0 { + for i := 0; i < paddingSize; i++ { + rawPacket[12+dataLength+i] = byte(paddingSize) + } + } + + return rawPacket, nil +} + +// Unmarshal parses the given raw packet into an application-defined struct, handling padding. +func (a *ApplicationDefined) Unmarshal(rawPacket []byte) error { + /* + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |V=2|P| subtype | PT=APP=204 | length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | SSRC/CSRC | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | name (ASCII) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | application-dependent data ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + header := Header{} + err := header.Unmarshal(rawPacket) + if err != nil { + return err + } + if len(rawPacket) < 12 { + return errPacketTooShort + } + + if int(header.Length+1)*4 != len(rawPacket) { + return errAppDefinedInvalidLength + } + + a.SubType = header.Count + a.SSRC = binary.BigEndian.Uint32(rawPacket[4:8]) + a.Name = string(rawPacket[8:12]) + + // Check for padding. + paddingSize := 0 + if header.Padding { + paddingSize = int(rawPacket[len(rawPacket)-1]) + if paddingSize > len(rawPacket)-12 { + return errWrongPadding + } + } + + a.Data = rawPacket[12 : len(rawPacket)-paddingSize] + + return nil +} + +// MarshalSize returns the size of the packet once marshaled +func (a *ApplicationDefined) MarshalSize() int { + dataLength := len(a.Data) + // Calculate the padding size to be added to make the packet length a multiple of 4 bytes. + paddingSize := 4 - (dataLength % 4) + if paddingSize == 4 { + paddingSize = 0 + } + return 12 + dataLength + paddingSize +} diff --git a/application_defined_test.go b/application_defined_test.go new file mode 100644 index 0000000..6db4c4f --- /dev/null +++ b/application_defined_test.go @@ -0,0 +1,266 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package rtcp + +import ( + "errors" + "reflect" + "testing" +) + +func TestTApplicationPacketUnmarshal(t *testing.T) { + for _, test := range []struct { + Name string + Data []byte + Want ApplicationDefined + WantError error + }{ + { + Name: "valid", + Data: []byte{ + // Application Packet Type + Length(0x0003) + 0x80, 0xcc, 0x00, 0x03, + // sender=0x4baae1ab + 0x4b, 0xaa, 0xe1, 0xab, + // name='NAME' + 0x4E, 0x41, 0x4D, 0x45, + // data='ABCD' + 0x41, 0x42, 0x43, 0x44, + }, + Want: ApplicationDefined{ + SubType: 0, + SSRC: 0x4baae1ab, + Name: "NAME", + Data: []byte{0x41, 0x42, 0x43, 0x44}, + }, + }, + { + Name: "validCustomSsubType", + Data: []byte{ + // Application Packet Type (SubType 31) + Length(0x0003) + 0x9f, 0xcc, 0x00, 0x03, + // sender=0x4baae1ab + 0x4b, 0xaa, 0xe1, 0xab, + // name='NAME' + 0x4E, 0x41, 0x4D, 0x45, + // data='ABCD' + 0x41, 0x42, 0x43, 0x44, + }, + Want: ApplicationDefined{ + SubType: 31, + SSRC: 0x4baae1ab, + Name: "NAME", + Data: []byte{0x41, 0x42, 0x43, 0x44}, + }, + }, + { + Name: "validWithPadding", + Data: []byte{ + // Application Packet Type + Length(0x0002) (0xA0 has padding bit set) + 0xA0, 0xcc, 0x00, 0x04, + // sender=0x4baae1ab + 0x4b, 0xaa, 0xe1, 0xab, + // name='NAME' + 0x4E, 0x41, 0x4D, 0x45, + // data='ABCDE' + 0x41, 0x42, 0x43, 0x44, 0x45, + // 3 bytes padding as packet length must be a division of 4 + 0x03, 0x03, 0x03, + }, + Want: ApplicationDefined{ + SubType: 0, + SSRC: 0x4baae1ab, + Name: "NAME", + Data: []byte{0x41, 0x42, 0x43, 0x44, 0x45}, + }, + }, + { + Name: "invalidAppPacketLengthField", + Data: []byte{ + // Application Packet Type + invalid Length(0x00FF) + 0x80, 0xcc, 0x00, 0xFF, + // sender=0x4baae1ab + 0x4b, 0xaa, 0xe1, 0xab, + // name='NAME' + 0x4E, 0x41, 0x4D, 0x45, + // data='ABCD' + 0x41, 0x42, 0x43, 0x44, + }, + WantError: errAppDefinedInvalidLength, + }, + { + Name: "invalidPacketLengthTooShort", + Data: []byte{ + // Application Packet Type + Length(0x0002). Total packet length is less than 12 bytes + 0x80, 0xcc, 0x00, 0x2, + // sender=0x4baae1ab + 0x4b, 0xaa, 0xe1, 0xab, + // name='SUI' + 0x53, 0x55, 0x49, + }, + WantError: errPacketTooShort, + }, + { + Name: "wrongPaddingSize", + Data: []byte{ + // Application Packet Type + Length(0x0002) (0xA0 has padding bit set) + 0xA0, 0xcc, 0x00, 0x04, + // sender=0x4baae1ab + 0x4b, 0xaa, 0xe1, 0xab, + // name='NAME' + 0x4E, 0x41, 0x4D, 0x45, + // data='ABCDE' + 0x41, 0x42, 0x43, 0x44, 0x45, + // 3 bytes padding as packet length must be a division of 4 + 0x03, 0x03, 0x09, // last byte has padding size 0x09 which is more than the data + padding bytes + }, + WantError: errWrongPadding, + }, + { + Name: "invalidHeader", + Data: []byte{ + // Application Packet Type + invalid Length(0x00FF) + 0xFF, + }, + WantError: errPacketTooShort, + }, + } { + var apk ApplicationDefined + err := apk.Unmarshal(test.Data) + if got, want := err, test.WantError; !errors.Is(got, want) { + t.Fatalf("Unmarshal %q result: got = %v, want %v", test.Name, got, want) + } + if err != nil { + continue + } + + if got, want := apk, test.Want; !reflect.DeepEqual(got, want) { + t.Fatalf("Unmarshal %q result: got %v, want %v", test.Name, got, want) + } + + // Check SSRC is matching + if apk.SSRC != 0x4baae1ab { + t.Fatalf("SSRC %q result: got packet SSRC %x instead of %x", test.Name, apk.SSRC, 0x4baae1ab) + } + if apk.SSRC != apk.DestinationSSRC()[0] { + t.Fatalf("SSRC %q result: DestinationSSRC() %x doesn't match SSRC field %x", test.Name, apk.DestinationSSRC()[0], apk.SSRC) + } + } +} + +func TestTApplicationPacketMarshal(t *testing.T) { + for _, test := range []struct { + Name string + Want []byte + Packet ApplicationDefined + WantError error + }{ + { + Name: "valid", + Want: []byte{ + // Application Packet Type + Length(0x0003) + 0x80, 0xcc, 0x00, 0x03, + // sender=0x4baae1ab + 0x4b, 0xaa, 0xe1, 0xab, + // name='NAME' + 0x4E, 0x41, 0x4D, 0x45, + // data='ABCD' + 0x41, 0x42, 0x43, 0x44, + }, + Packet: ApplicationDefined{ + SSRC: 0x4baae1ab, + Name: "NAME", + Data: []byte{0x41, 0x42, 0x43, 0x44}, + }, + }, + { + Name: "validCustomSubType", + Want: []byte{ + // Application Packet Type (SubType 31) + Length(0x0003) + 0x9f, 0xcc, 0x00, 0x03, + // sender=0x4baae1ab + 0x4b, 0xaa, 0xe1, 0xab, + // name='NAME' + 0x4E, 0x41, 0x4D, 0x45, + // data='ABCD' + 0x41, 0x42, 0x43, 0x44, + }, + Packet: ApplicationDefined{ + SubType: 31, + SSRC: 0x4baae1ab, + Name: "NAME", + Data: []byte{0x41, 0x42, 0x43, 0x44}, + }, + }, + { + Name: "validWithPadding", + Want: []byte{ + // Application Packet Type + Length(0x0002) (0xA0 has padding bit set) + 0xA0, 0xcc, 0x00, 0x04, + // sender=0x4baae1ab + 0x4b, 0xaa, 0xe1, 0xab, + // name='NAME' + 0x4E, 0x41, 0x4D, 0x45, + // data='ABCDE' + 0x41, 0x42, 0x43, 0x44, 0x45, + // 3 bytes padding as packet length must be a division of 4 + 0x03, 0x03, 0x03, + }, + Packet: ApplicationDefined{ + SSRC: 0x4baae1ab, + Name: "NAME", + Data: []byte{0x41, 0x42, 0x43, 0x44, 0x45}, + }, + }, + { + Name: "invalidDataTooLarge", + WantError: errAppDefinedDataTooLarge, + Packet: ApplicationDefined{ + SSRC: 0x4baae1ab, + Name: "NAME", + Data: make([]byte, 0xFFFF-12+1), // total max packet size is 0xFFFF including header and other fields. + }, + }, + { + Name: "invalidName", + WantError: errAppDefinedInvalidName, + Packet: ApplicationDefined{ + SSRC: 0x4baae1ab, + Name: "NOT4CHARS", + Data: []byte{0x41, 0x42, 0x43, 0x44}, + }, + }, + { + Name: "InvalidSubType", + WantError: errInvalidHeader, + Packet: ApplicationDefined{ + SubType: 32, // Must be up to 31 + SSRC: 0x4baae1ab, + Name: "NAME", + Data: []byte{0x41, 0x42, 0x43, 0x44}, + }, + }, + } { + rawPacket, err := test.Packet.Marshal() + + // Check for expected errors + if got, want := err, test.WantError; !errors.Is(got, want) { + t.Fatalf("Marshal %q result: got = %v, want %v", test.Name, got, want) + } + if err != nil { + continue + } + + // Check for expected successful result + if got, want := rawPacket, test.Want; !reflect.DeepEqual(got, want) { + t.Fatalf("Marshal %q result: got %v, want %v", test.Name, got, want) + } + + // Check if MarshalSize() is matching the marshaled bytes + marshalSize := test.Packet.MarshalSize() + if marshalSize != len(rawPacket) { + t.Fatalf("MarshalSize %q result: got %d bytes instead of %d", test.Name, len(rawPacket), marshalSize) + } + } +} diff --git a/errors.go b/errors.go index d2477af..05e6847 100644 --- a/errors.go +++ b/errors.go @@ -35,4 +35,7 @@ var ( errWrongChunkType = errors.New("rtcp: wrong chunk type") errBadStructMemberType = errors.New("rtcp: struct contains unexpected member type") errBadReadParameter = errors.New("rtcp: cannot read into non-pointer") + errAppDefinedInvalidLength = errors.New("rtcp: application defined type invalid length") + errAppDefinedDataTooLarge = errors.New("rtcp: application defined data is too large") + errAppDefinedInvalidName = errors.New("rtcp: application defined name must be 4 ASCII chars") ) diff --git a/packet.go b/packet.go index cdb83c0..fc3c9a3 100644 --- a/packet.go +++ b/packet.go @@ -114,6 +114,9 @@ func unmarshal(rawData []byte) (packet Packet, bytesprocessed int, err error) { case TypeExtendedReport: packet = new(ExtendedReport) + case TypeApplicationDefined: + packet = new(ApplicationDefined) + default: packet = new(RawPacket) } diff --git a/packet_test.go b/packet_test.go index b39048f..6eb0a83 100644 --- a/packet_test.go +++ b/packet_test.go @@ -71,6 +71,15 @@ func realPacket() []byte { 0x90, 0x2f, 0x9e, 0x2e, // media=0x902f9e2e 0x90, 0x2f, 0x9e, 0x2e, + + // ApplicationDefined (offset=116) + 0x80, 0xcc, 0x00, 0x03, + // sender=0x4baae1ab + 0x4b, 0xaa, 0xe1, 0xab, + // name='NAME' + 0x4E, 0x41, 0x4D, 0x45, + // data='ABCD' + 0x41, 0x42, 0x43, 0x44, } } @@ -106,6 +115,11 @@ func TestUnmarshal(t *testing.T) { SenderSSRC: 0x902f9e2e, MediaSSRC: 0x902f9e2e, }, + &ApplicationDefined{ + SSRC: 0x4baae1ab, + Name: "NAME", + Data: []byte{0x41, 0x42, 0x43, 0x44}, + }, } assert.Equal(t, expected, packet)