From 4744321d7144047beda560f99de081d597fc97cc Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 13 Apr 2026 18:38:25 -0500 Subject: [PATCH 01/16] add error handling to arg encoding and message/bundle ToBytes() --- .gitignore | 3 +- bundle.go | 23 +++++-- bundle_test.go | 24 ++++--- cmd/makeosc/makeosc.go | 5 +- cmd/sendosc/sendosc.go | 5 +- message.go | 10 ++- message_test.go | 10 ++- osc.go | 149 +++++++++++++++++++++++++++++++---------- types.go | 2 +- 9 files changed, 169 insertions(+), 62 deletions(-) diff --git a/.gitignore b/.gitignore index c795b05..0664ec5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -build \ No newline at end of file +build +coverage* \ No newline at end of file diff --git a/bundle.go b/bundle.go index 68eb56b..022d8e7 100644 --- a/bundle.go +++ b/bundle.go @@ -4,21 +4,32 @@ import ( "errors" ) -func (b *OSCBundle) ToBytes() []byte { +func (b *OSCBundle) ToBytes() ([]byte, error) { bytes := stringToOSCBytes("#bundle") - bytes = append(bytes, timeTagToOSCBytes(b.TimeTag)...) + timeTagBytes, err := timeTagToOSCBytes(b.TimeTag) + if err != nil { + return nil, err + } + bytes = append(bytes, timeTagBytes...) for _, packet := range b.Contents { - packetBytes := packet.ToBytes() - packetLength := len(packet.ToBytes()) + packetBytes, err := packet.ToBytes() + if err != nil { + return nil, err + } + packetLength := len(packetBytes) - bytes = append(bytes, int32ToOSCBytes(int32(packetLength))...) + packetLengthBytes, err := int32ToOSCBytes(int32(packetLength)) + if err != nil { + return nil, err + } + bytes = append(bytes, packetLengthBytes...) bytes = append(bytes, packetBytes...) } - return bytes + return bytes, nil } func BundleFromBytes(bytes []byte) (*OSCBundle, []byte, error) { diff --git a/bundle_test.go b/bundle_test.go index 8a808eb..66e31ce 100644 --- a/bundle_test.go +++ b/bundle_test.go @@ -9,9 +9,9 @@ import ( func TestOSCBundleEncoding(t *testing.T) { testCases := []struct { - description string - bundle *OSCBundle - expected []byte + name string + bundle *OSCBundle + expected []byte }{ { "simple contents single message", @@ -32,13 +32,19 @@ func TestOSCBundleEncoding(t *testing.T) { for _, testCase := range testCases { - actual := testCase.bundle.ToBytes() + t.Run(testCase.name, func(t *testing.T) { + + got, err := testCase.bundle.ToBytes() + + if err != nil { + t.Fatalf("failed to encode properly: %s", err.Error()) + } + + if !reflect.DeepEqual(got, testCase.expected) { + t.Fatalf("failed to encode properly got '%v', expected '%v'", got, testCase.expected) + } + }) - if !reflect.DeepEqual(actual, testCase.expected) { - t.Errorf("Test '%s' failed to encode properly", testCase.description) - fmt.Printf("expected: %v\n", testCase.expected) - fmt.Printf("actual: %v\n", actual) - } } } diff --git a/cmd/makeosc/makeosc.go b/cmd/makeosc/makeosc.go index 5772cea..76c26a1 100644 --- a/cmd/makeosc/makeosc.go +++ b/cmd/makeosc/makeosc.go @@ -175,7 +175,10 @@ func make(address string, args []string, types []string, slip bool) { oscMessage.Args = append(oscMessage.Args, argToTypedArg(arg, oscType)) } - oscMessageBuffer := oscMessage.ToBytes() + oscMessageBuffer, err := oscMessage.ToBytes() + if err != nil { + panic(err) + } if slip { oscMessageBuffer = slipEncode(oscMessageBuffer) diff --git a/cmd/sendosc/sendosc.go b/cmd/sendosc/sendosc.go index 6e45d2f..88e4643 100644 --- a/cmd/sendosc/sendosc.go +++ b/cmd/sendosc/sendosc.go @@ -208,7 +208,10 @@ func send(host string, port int32, address string, args []string, types []string } - oscMessageBuffer := oscMessage.ToBytes() + oscMessageBuffer, err := oscMessage.ToBytes() + if err != nil { + panic(err) + } if slip { oscMessageBuffer = slipEncode(oscMessageBuffer) diff --git a/message.go b/message.go index bd0051f..f37e0e3 100644 --- a/message.go +++ b/message.go @@ -5,7 +5,7 @@ import ( "strings" ) -func (m *OSCMessage) ToBytes() []byte { +func (m *OSCMessage) ToBytes() ([]byte, error) { //TODO(jwetzell): add error handling oscBuffer := []byte{} @@ -20,9 +20,13 @@ func (m *OSCMessage) ToBytes() []byte { } oscBuffer = append(oscBuffer, stringToOSCBytes(sb.String())...) - oscBuffer = append(oscBuffer, argsToBuffer(m.Args)...) + argsBuffer, err := argsToBuffer(m.Args) + if err != nil { + return nil, err + } + oscBuffer = append(oscBuffer, argsBuffer...) - return oscBuffer + return oscBuffer, nil } func MessageFromBytes(bytes []byte) (*OSCMessage, error) { diff --git a/message_test.go b/message_test.go index 0a5fb44..e163f0e 100644 --- a/message_test.go +++ b/message_test.go @@ -128,10 +128,14 @@ func TestGoodOSCMessageEncoding(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - actual := testCase.message.ToBytes() + got, err := testCase.message.ToBytes() - if !reflect.DeepEqual(actual, testCase.expected) { - t.Fatalf("failed to encode properly got '%v', expected '%v'", actual, testCase.expected) + if err != nil { + t.Fatalf("failed to encode properly: %s", err.Error()) + } + + if !reflect.DeepEqual(got, testCase.expected) { + t.Fatalf("failed to encode properly got '%v', expected '%v'", got, testCase.expected) } }) } diff --git a/osc.go b/osc.go index 3b80921..e0be749 100644 --- a/osc.go +++ b/osc.go @@ -26,47 +26,51 @@ func stringToOSCBytes(rawString string) []byte { return []byte(sb.String()) } -func int32ToOSCBytes(number int32) []byte { +func int32ToOSCBytes(number int32) ([]byte, error) { var buf bytes.Buffer err := binary.Write(&buf, binary.BigEndian, number) if err != nil { - panic(err) + return nil, err } - return buf.Bytes() + return buf.Bytes(), nil } -func int64ToOSCBytes(number int64) []byte { +func int64ToOSCBytes(number int64) ([]byte, error) { var buf bytes.Buffer err := binary.Write(&buf, binary.BigEndian, number) if err != nil { - panic(err) + return nil, err } - return buf.Bytes() + return buf.Bytes(), nil } -func float32ToOSCBytes(number float32) []byte { +func float32ToOSCBytes(number float32) ([]byte, error) { var buf bytes.Buffer err := binary.Write(&buf, binary.BigEndian, number) if err != nil { - panic(err) + return nil, err } - return buf.Bytes() + return buf.Bytes(), nil } -func float64ToOSCBytes(number float64) []byte { +func float64ToOSCBytes(number float64) ([]byte, error) { var buf bytes.Buffer err := binary.Write(&buf, binary.BigEndian, number) if err != nil { - panic(err) + return nil, err } - return buf.Bytes() + return buf.Bytes(), nil } -func byteArrayToOSCBytes(bytes []byte) []byte { +func byteArrayToOSCBytes(bytes []byte) ([]byte, error) { oscBytes := []byte{} bytesSize := len(bytes) - oscBytes = append(oscBytes, int32ToOSCBytes(int32(bytesSize))...) + bytesSizeBytes, err := int32ToOSCBytes(int32(bytesSize)) + if err != nil { + return nil, err + } + oscBytes = append(oscBytes, bytesSizeBytes...) oscBytes = append(oscBytes, bytes...) padLength := 4 - (bytesSize % 4) @@ -76,17 +80,24 @@ func byteArrayToOSCBytes(bytes []byte) []byte { } } - return oscBytes + return oscBytes, nil } -func timeTagToOSCBytes(timeTag OSCTimeTag) []byte { - timeTagBytes := int32ToOSCBytes(timeTag.seconds) - timeTagBytes = append(timeTagBytes, int32ToOSCBytes(timeTag.fractionalSeconds)...) +func timeTagToOSCBytes(timeTag OSCTimeTag) ([]byte, error) { + timeTagBytes, err := int32ToOSCBytes(timeTag.seconds) + if err != nil { + return nil, err + } + fractionalSecondsBytes, err := int32ToOSCBytes(timeTag.fractionalSeconds) + if err != nil { + return nil, err + } + timeTagBytes = append(timeTagBytes, fractionalSecondsBytes...) - return timeTagBytes + return timeTagBytes, nil } -func argsToBuffer(args []OSCArg) []byte { +func argsToBuffer(args []OSCArg) ([]byte, error) { //TODO(jwetzell): add error handling var argBuffers = []byte{} @@ -100,29 +111,61 @@ func argsToBuffer(args []OSCArg) []byte { } case "i": if value, ok := arg.Value.(int); ok { - argBuffers = append(argBuffers, int32ToOSCBytes(int32(value))...) + valueBytes, err := int32ToOSCBytes(int32(value)) + if err != nil { + return nil, err + } + argBuffers = append(argBuffers, valueBytes...) } else if value, ok := arg.Value.(int32); ok { - argBuffers = append(argBuffers, int32ToOSCBytes(value)...) + valueBytes, err := int32ToOSCBytes(int32(value)) + if err != nil { + return nil, err + } + argBuffers = append(argBuffers, valueBytes...) } else { fmt.Println("OSC arg had integer type but non-integer value.") } case "f": if value, ok := arg.Value.(float32); ok { - argBuffers = append(argBuffers, float32ToOSCBytes(float32(value))...) + valueBytes, err := float32ToOSCBytes(value) + if err != nil { + return nil, err + } + argBuffers = append(argBuffers, valueBytes...) } else if value, ok := arg.Value.(float64); ok { - argBuffers = append(argBuffers, float32ToOSCBytes(float32(value))...) + valueBytes, err := float32ToOSCBytes(float32(value)) + if err != nil { + return nil, err + } + argBuffers = append(argBuffers, valueBytes...) } else if value, ok := arg.Value.(int); ok { - argBuffers = append(argBuffers, float32ToOSCBytes(float32(value))...) + valueBytes, err := float32ToOSCBytes(float32(value)) + if err != nil { + return nil, err + } + argBuffers = append(argBuffers, valueBytes...) } else if value, ok := arg.Value.(int32); ok { - argBuffers = append(argBuffers, float32ToOSCBytes(float32(value))...) + valueBytes, err := float32ToOSCBytes(float32(value)) + if err != nil { + return nil, err + } + argBuffers = append(argBuffers, valueBytes...) } else if value, ok := arg.Value.(int64); ok { - argBuffers = append(argBuffers, float32ToOSCBytes(float32(value))...) + valueBytes, err := float32ToOSCBytes(float32(value)) + if err != nil { + return nil, err + } + argBuffers = append(argBuffers, valueBytes...) } else { fmt.Println("OSC arg had float type but non-float value.") } case "b": if value, ok := arg.Value.([]byte); ok { - argBuffers = append(argBuffers, byteArrayToOSCBytes(value)...) + valueBytes, err := byteArrayToOSCBytes(value) + if err != nil { + return nil, err + } + argBuffers = append(argBuffers, valueBytes...) } else { fmt.Println("OSC arg had blob type but non-blob value.") } @@ -142,25 +185,57 @@ func argsToBuffer(args []OSCArg) []byte { } case "h": if value, ok := arg.Value.(int); ok { - argBuffers = append(argBuffers, int64ToOSCBytes(int64(value))...) + valueBytes, err := int64ToOSCBytes(int64(value)) + if err != nil { + return nil, err + } + argBuffers = append(argBuffers, valueBytes...) } else if value, ok := arg.Value.(int32); ok { - argBuffers = append(argBuffers, int64ToOSCBytes(int64(value))...) + valueBytes, err := int64ToOSCBytes(int64(value)) + if err != nil { + return nil, err + } + argBuffers = append(argBuffers, valueBytes...) } else if value, ok := arg.Value.(int64); ok { - argBuffers = append(argBuffers, int64ToOSCBytes(value)...) + valueBytes, err := int64ToOSCBytes(value) + if err != nil { + return nil, err + } + argBuffers = append(argBuffers, valueBytes...) } else { fmt.Println("OSC arg had integer type but non-integer value.") } case "d": if value, ok := arg.Value.(float32); ok { - argBuffers = append(argBuffers, float64ToOSCBytes(float64(value))...) + valueBytes, err := float64ToOSCBytes(float64(value)) + if err != nil { + return nil, err + } + argBuffers = append(argBuffers, valueBytes...) } else if value, ok := arg.Value.(float64); ok { - argBuffers = append(argBuffers, float64ToOSCBytes(float64(value))...) + valueBytes, err := float64ToOSCBytes(value) + if err != nil { + return nil, err + } + argBuffers = append(argBuffers, valueBytes...) } else if value, ok := arg.Value.(int); ok { - argBuffers = append(argBuffers, float64ToOSCBytes(float64(value))...) + valueBytes, err := float64ToOSCBytes(float64(value)) + if err != nil { + return nil, err + } + argBuffers = append(argBuffers, valueBytes...) } else if value, ok := arg.Value.(int32); ok { - argBuffers = append(argBuffers, float64ToOSCBytes(float64(value))...) + valueBytes, err := float64ToOSCBytes(float64(value)) + if err != nil { + return nil, err + } + argBuffers = append(argBuffers, valueBytes...) } else if value, ok := arg.Value.(int64); ok { - argBuffers = append(argBuffers, float64ToOSCBytes(float64(value))...) + valueBytes, err := float64ToOSCBytes(float64(value)) + if err != nil { + return nil, err + } + argBuffers = append(argBuffers, valueBytes...) } else { fmt.Println("OSC arg had float type but non-float value.") } @@ -168,7 +243,7 @@ func argsToBuffer(args []OSCArg) []byte { fmt.Printf("unhandled osc type: %s.\n", oscType) } } - return argBuffers + return argBuffers, nil } func readOSCString(bytes []byte) (string, []byte, error) { diff --git a/types.go b/types.go index 482fe7d..c6f47fa 100644 --- a/types.go +++ b/types.go @@ -1,7 +1,7 @@ package osc type OSCPacket interface { - ToBytes() []byte + ToBytes() ([]byte, error) } type OSCBundle struct { From 2548771fe3a62c3feee6ab0f5f9b552ac9b627bb Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 13 Apr 2026 18:48:50 -0500 Subject: [PATCH 02/16] add error handling for OSC address and bad arg type encoding --- message.go | 11 +++++++++-- message_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ osc.go | 2 +- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/message.go b/message.go index f37e0e3..9942f06 100644 --- a/message.go +++ b/message.go @@ -6,7 +6,15 @@ import ( ) func (m *OSCMessage) ToBytes() ([]byte, error) { - //TODO(jwetzell): add error handling + + if len(m.Address) == 0 { + return nil, errors.New("OSC Message must have an address") + } + + if m.Address[0] != '/' { + return nil, errors.New("OSC Message address must start with /") + } + oscBuffer := []byte{} oscBuffer = append(oscBuffer, stringToOSCBytes(m.Address)...) @@ -18,7 +26,6 @@ func (m *OSCMessage) ToBytes() ([]byte, error) { for _, arg := range m.Args { sb.WriteString(arg.Type) } - oscBuffer = append(oscBuffer, stringToOSCBytes(sb.String())...) argsBuffer, err := argsToBuffer(m.Args) if err != nil { diff --git a/message_test.go b/message_test.go index e163f0e..5000488 100644 --- a/message_test.go +++ b/message_test.go @@ -142,6 +142,47 @@ func TestGoodOSCMessageEncoding(t *testing.T) { } +func TestBadOSCMessageEncoding(t *testing.T) { + testCases := []struct { + name string + message *OSCMessage + errorString string + }{ + { + name: "empty message", + message: &OSCMessage{}, + errorString: "OSC Message must have an address", + }, + { + name: "address does not start with /", + message: &OSCMessage{Address: "hello"}, + errorString: "OSC Message address must start with /", + }, + { + name: "arg with unsupported type", + message: &OSCMessage{ + Address: "/hello", + Args: []OSCArg{{Type: "x", Value: "unsupported"}}, + }, + errorString: "unsupported OSC argument type: x", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got, err := testCase.message.ToBytes() + + if err == nil { + t.Fatalf("OSCMessage.ToBytes() expected to fail but got: %+v", got) + } + + if err.Error() != testCase.errorString { + t.Fatalf("OSCMessage.ToBytes() got error '%s', expected '%s'", err.Error(), testCase.errorString) + } + }) + } +} + func TestGoodOSCMessageDecoding(t *testing.T) { testCases := []struct { name string diff --git a/osc.go b/osc.go index e0be749..4e403cd 100644 --- a/osc.go +++ b/osc.go @@ -240,7 +240,7 @@ func argsToBuffer(args []OSCArg) ([]byte, error) { fmt.Println("OSC arg had float type but non-float value.") } default: - fmt.Printf("unhandled osc type: %s.\n", oscType) + return nil, fmt.Errorf("unsupported OSC argument type: %s", oscType) } } return argBuffers, nil From 1d2684f446d569701eea9d95fdc87583df26f890 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 13 Apr 2026 18:53:21 -0500 Subject: [PATCH 03/16] cleanup test layout --- bundle_test.go | 43 ++++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/bundle_test.go b/bundle_test.go index 66e31ce..cef3236 100644 --- a/bundle_test.go +++ b/bundle_test.go @@ -1,7 +1,6 @@ package osc import ( - "fmt" "reflect" "testing" ) @@ -31,7 +30,6 @@ func TestOSCBundleEncoding(t *testing.T) { } for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { got, err := testCase.bundle.ToBytes() @@ -44,27 +42,25 @@ func TestOSCBundleEncoding(t *testing.T) { t.Fatalf("failed to encode properly got '%v', expected '%v'", got, testCase.expected) } }) - } - } func TestOSCBundleDecoding(t *testing.T) { testCases := []struct { - description string - expected *OSCBundle - bytes []byte + name string + expected *OSCBundle + bytes []byte }{ { - "simple contents single message", - &OSCBundle{ + name: "simple contents single message", + expected: &OSCBundle{ TimeTag: OSCTimeTag{ seconds: 32, fractionalSeconds: 0, }, Contents: []OSCPacket{&OSCMessage{Address: "/oscillator/4/frequency", Args: []OSCArg{{Type: "f", Value: float32(440)}}}}, }, - []byte{35, 98, 117, 110, 100, 108, 101, 0, 0, 0, 0, + bytes: []byte{35, 98, 117, 110, 100, 108, 101, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 32, 47, 111, 115, 99, 105, 108, 108, 97, 116, 111, 114, 47, 52, 47, 102, 114, 101, 113, 117, 101, 110, 99, 121, 0, @@ -73,23 +69,20 @@ func TestOSCBundleDecoding(t *testing.T) { } for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + actual, remainingBytes, error := BundleFromBytes(testCase.bytes) - actual, remainingBytes, error := BundleFromBytes(testCase.bytes) + if error != nil { + t.Fatalf("failed to decode properly: %s", error.Error()) + } - if error != nil { - fmt.Println(error) - t.Errorf("Test '%s' failed to encode properly", testCase.description) - } - - if len(remainingBytes) > 0 { - t.Errorf("Test '%s' should not have any remaining bytes", testCase.description) - } - - if !reflect.DeepEqual(actual, testCase.expected) { - t.Errorf("Test '%s' failed to encode bundle properly", testCase.description) - fmt.Printf("expected: %v\n", testCase.expected) - fmt.Printf("actual: %v\n", actual) - } + if len(remainingBytes) > 0 { + t.Fatalf("should not have any remaining bytes") + } + if !reflect.DeepEqual(actual, testCase.expected) { + t.Fatalf("failed to decode properly got '%v', expected '%v'", actual, testCase.expected) + } + }) } } From 8fa8aa3a55c63418f4254370c27a48223b8fef47 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 13 Apr 2026 18:53:35 -0500 Subject: [PATCH 04/16] return errors instead of printing --- osc.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osc.go b/osc.go index 4e403cd..fc81414 100644 --- a/osc.go +++ b/osc.go @@ -107,7 +107,7 @@ func argsToBuffer(args []OSCArg) ([]byte, error) { if value, ok := arg.Value.(string); ok { argBuffers = append(argBuffers, stringToOSCBytes(value)...) } else { - fmt.Println("OSC arg had string type but non-string value.") + return nil, errors.New("OSC arg had string type but non-string value") } case "i": if value, ok := arg.Value.(int); ok { @@ -123,7 +123,7 @@ func argsToBuffer(args []OSCArg) ([]byte, error) { } argBuffers = append(argBuffers, valueBytes...) } else { - fmt.Println("OSC arg had integer type but non-integer value.") + return nil, errors.New("OSC arg had integer type but non-integer value") } case "f": if value, ok := arg.Value.(float32); ok { @@ -157,7 +157,7 @@ func argsToBuffer(args []OSCArg) ([]byte, error) { } argBuffers = append(argBuffers, valueBytes...) } else { - fmt.Println("OSC arg had float type but non-float value.") + return nil, errors.New("OSC arg had float type but non-float value.") } case "b": if value, ok := arg.Value.([]byte); ok { @@ -167,7 +167,7 @@ func argsToBuffer(args []OSCArg) ([]byte, error) { } argBuffers = append(argBuffers, valueBytes...) } else { - fmt.Println("OSC arg had blob type but non-blob value.") + return nil, errors.New("OSC arg had blob type but non-blob value.") } case "T": argBuffers = append(argBuffers, make([]byte, 0)...) @@ -203,7 +203,7 @@ func argsToBuffer(args []OSCArg) ([]byte, error) { } argBuffers = append(argBuffers, valueBytes...) } else { - fmt.Println("OSC arg had integer type but non-integer value.") + return nil, errors.New("OSC arg had integer type but non-integer value.") } case "d": if value, ok := arg.Value.(float32); ok { @@ -237,7 +237,7 @@ func argsToBuffer(args []OSCArg) ([]byte, error) { } argBuffers = append(argBuffers, valueBytes...) } else { - fmt.Println("OSC arg had float type but non-float value.") + return nil, errors.New("OSC arg had float type but non-float value.") } default: return nil, fmt.Errorf("unsupported OSC argument type: %s", oscType) From b621944dc5f3c3e8f2080d640420e273717dd3ce Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 13 Apr 2026 18:59:43 -0500 Subject: [PATCH 05/16] error tests for arg values when encoding --- message_test.go | 56 +++++++++++++++++++++++++++++++++++++++++++++++++ osc.go | 13 +++++++----- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/message_test.go b/message_test.go index 5000488..c64ae05 100644 --- a/message_test.go +++ b/message_test.go @@ -166,6 +166,62 @@ func TestBadOSCMessageEncoding(t *testing.T) { }, errorString: "unsupported OSC argument type: x", }, + { + name: "string arg that is not a string", + message: &OSCMessage{ + Address: "/hello", + Args: []OSCArg{{Type: "s", Value: 123}}, + }, + errorString: "OSC arg had string type but non-string value", + }, + { + name: "int32 arg that is not a number", + message: &OSCMessage{ + Address: "/hello", + Args: []OSCArg{{Type: "i", Value: "not an int"}}, + }, + errorString: "OSC arg had int32 type but non-number value", + }, + { + name: "float32 arg that is not a number", + message: &OSCMessage{ + Address: "/hello", + Args: []OSCArg{{Type: "f", Value: "not a float"}}, + }, + errorString: "OSC arg had float32 type but non-number value", + }, + { + name: "int64 arg that is not a number", + message: &OSCMessage{ + Address: "/hello", + Args: []OSCArg{{Type: "h", Value: "not an int"}}, + }, + errorString: "OSC arg had int64 type but non-number value", + }, + { + name: "float64 arg that is not a number", + message: &OSCMessage{ + Address: "/hello", + Args: []OSCArg{{Type: "d", Value: "not a float"}}, + }, + errorString: "OSC arg had float64 type but non-number value", + }, + { + name: "blob arg that is not a byte array", + message: &OSCMessage{ + Address: "/hello", + Args: []OSCArg{{Type: "b", Value: "not a blob"}}, + }, + errorString: "OSC arg had blob type but non-blob value", + }, + { + name: "color arg that is not an OSCColor", + message: &OSCMessage{ + Address: "/hello", + Args: []OSCArg{{Type: "r", Value: "not a color"}}, + }, + errorString: "OSC arg had color type but non-color value", + }, } for _, testCase := range testCases { diff --git a/osc.go b/osc.go index fc81414..7b7bbbf 100644 --- a/osc.go +++ b/osc.go @@ -123,7 +123,7 @@ func argsToBuffer(args []OSCArg) ([]byte, error) { } argBuffers = append(argBuffers, valueBytes...) } else { - return nil, errors.New("OSC arg had integer type but non-integer value") + return nil, errors.New("OSC arg had int32 type but non-number value") } case "f": if value, ok := arg.Value.(float32); ok { @@ -157,7 +157,7 @@ func argsToBuffer(args []OSCArg) ([]byte, error) { } argBuffers = append(argBuffers, valueBytes...) } else { - return nil, errors.New("OSC arg had float type but non-float value.") + return nil, errors.New("OSC arg had float32 type but non-number value") } case "b": if value, ok := arg.Value.([]byte); ok { @@ -167,7 +167,7 @@ func argsToBuffer(args []OSCArg) ([]byte, error) { } argBuffers = append(argBuffers, valueBytes...) } else { - return nil, errors.New("OSC arg had blob type but non-blob value.") + return nil, errors.New("OSC arg had blob type but non-blob value") } case "T": argBuffers = append(argBuffers, make([]byte, 0)...) @@ -179,6 +179,9 @@ func argsToBuffer(args []OSCArg) ([]byte, error) { argBuffers = append(argBuffers, make([]byte, 0)...) case "r": color, ok := arg.Value.(OSCColor) + if !ok { + return nil, errors.New("OSC arg had color type but non-color value") + } if ok { colorBytes := []byte{color.r, color.g, color.b, color.a} argBuffers = append(argBuffers, colorBytes...) @@ -203,7 +206,7 @@ func argsToBuffer(args []OSCArg) ([]byte, error) { } argBuffers = append(argBuffers, valueBytes...) } else { - return nil, errors.New("OSC arg had integer type but non-integer value.") + return nil, errors.New("OSC arg had int64 type but non-number value") } case "d": if value, ok := arg.Value.(float32); ok { @@ -237,7 +240,7 @@ func argsToBuffer(args []OSCArg) ([]byte, error) { } argBuffers = append(argBuffers, valueBytes...) } else { - return nil, errors.New("OSC arg had float type but non-float value.") + return nil, errors.New("OSC arg had float64 type but non-number value") } default: return nil, fmt.Errorf("unsupported OSC argument type: %s", oscType) From 9452268042e0a6487752cbb1a04772ed50849f88 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 13 Apr 2026 19:11:11 -0500 Subject: [PATCH 06/16] add layout for testing OSCBundle error cases --- bundle.go | 6 ++-- bundle_test.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/bundle.go b/bundle.go index 022d8e7..da1c644 100644 --- a/bundle.go +++ b/bundle.go @@ -34,11 +34,11 @@ func (b *OSCBundle) ToBytes() ([]byte, error) { func BundleFromBytes(bytes []byte) (*OSCBundle, []byte, error) { if len(bytes) < 20 { - return nil, bytes, errors.New("bundle has to be at least 20 bytes") + return nil, bytes, errors.New("OSC Bundle has to be at least 20 bytes") } if bytes[0] != 35 { - return nil, bytes, errors.New("bundle must start with a #") + return nil, bytes, errors.New("OSC Bundle must start with a #") } bundleHeader, bytesAfterBundleHeader, err := readOSCString(bytes) @@ -48,7 +48,7 @@ func BundleFromBytes(bytes []byte) (*OSCBundle, []byte, error) { } if bundleHeader != "#bundle" { - return nil, bytesAfterBundleHeader, errors.New("bundle must start with #bundle string") + return nil, bytesAfterBundleHeader, errors.New("OSC Bundle must start with #bundle string") } timeTag, bytesAfterTimeTag, err := readOSCTimeTag(bytesAfterBundleHeader) diff --git a/bundle_test.go b/bundle_test.go index cef3236..10f8a40 100644 --- a/bundle_test.go +++ b/bundle_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestOSCBundleEncoding(t *testing.T) { +func TestGoodOSCBundleEncoding(t *testing.T) { testCases := []struct { name string @@ -13,15 +13,15 @@ func TestOSCBundleEncoding(t *testing.T) { expected []byte }{ { - "simple contents single message", - &OSCBundle{ + name: "simple contents single message", + bundle: &OSCBundle{ TimeTag: OSCTimeTag{ seconds: 32, fractionalSeconds: 0, }, Contents: []OSCPacket{&OSCMessage{Address: "/oscillator/4/frequency", Args: []OSCArg{{Type: "f", Value: float32(440)}}}}, }, - []byte{35, 98, 117, 110, 100, 108, 101, 0, 0, 0, 0, + expected: []byte{35, 98, 117, 110, 100, 108, 101, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 32, 47, 111, 115, 99, 105, 108, 108, 97, 116, 111, 114, 47, 52, 47, 102, 114, 101, 113, 117, 101, 110, 99, 121, 0, @@ -45,7 +45,31 @@ func TestOSCBundleEncoding(t *testing.T) { } } -func TestOSCBundleDecoding(t *testing.T) { +func TestBadOSCBundleEncoding(t *testing.T) { + + testCases := []struct { + name string + bundle *OSCBundle + errorString string + }{} + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + + got, err := testCase.bundle.ToBytes() + + if err == nil { + t.Fatalf("OSCBundle.ToBytes() expected to fail but got: %+v", got) + } + + if err.Error() != testCase.errorString { + t.Fatalf("OSCBundle.ToBytes() got error '%s', expected '%s'", err.Error(), testCase.errorString) + } + }) + } +} + +func TestGoodOSCBundleDecoding(t *testing.T) { testCases := []struct { name string expected *OSCBundle @@ -86,3 +110,48 @@ func TestOSCBundleDecoding(t *testing.T) { }) } } + +func TestBadOSCBundleDecoding(t *testing.T) { + testCases := []struct { + name string + bytes []byte + errorString string + }{ + { + name: "empty byte array", + bytes: []byte{}, + errorString: "OSC Bundle has to be at least 20 bytes", + }, + { + name: "does not start with #", + bytes: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + errorString: "OSC Bundle must start with a #", + }, + { + name: "does not start with #bundle", + bytes: []byte{35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + errorString: "OSC Bundle must start with #bundle string", + }, + { + name: "bundle header not properly null terminated", + bytes: []byte{35, 98, 117, 110, 100, 108, 101, 35, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0}, + errorString: "OSC Bundle must start with #bundle string", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got, _, err := BundleFromBytes(testCase.bytes) + + if err == nil { + t.Fatalf("BundleFromBytes expected to fail but got: %+v", got) + } + + if err.Error() != testCase.errorString { + t.Fatalf("BundleFromBytes got error '%s', expected '%s'", err.Error(), testCase.errorString) + } + }) + } +} From dd430dd3801c8def3bc789ca4d3cd5fd5b899f1b Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 13 Apr 2026 22:22:44 -0500 Subject: [PATCH 07/16] add layout for testing bundle error cases --- bundle_test.go | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/bundle_test.go b/bundle_test.go index 10f8a40..d190d21 100644 --- a/bundle_test.go +++ b/bundle_test.go @@ -134,10 +134,28 @@ func TestBadOSCBundleDecoding(t *testing.T) { }, { name: "bundle header not properly null terminated", - bytes: []byte{35, 98, 117, 110, 100, 108, 101, 35, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0}, - errorString: "OSC Bundle must start with #bundle string", + bytes: []byte{ + 35, 98, 117, 110, 100, 108, 101, + 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35}, + errorString: "OSC string must be null-terminated", + }, + { + name: "bundle contains incorrect size", + bytes: []byte{ + 35, 98, 117, 110, 100, 108, 101, 0, // #bundle + 0, 0, 0, 0, 0, 0, 0, 0, // time tag + 0, 0, 0, 100, // content size of 100 but only 10 bytes of content + 35, 35, 35, 35, 35, 35, 35, 35, 35, 35}, + errorString: "bundle doesn't have enough bytes for the content size it specifies", + }, + { + name: "bundle doesn't contain message or bundle", + bytes: []byte{ + 35, 98, 117, 110, 100, 108, 101, 0, // #bundle + 0, 0, 0, 0, 0, 0, 0, 0, // time tag + 0, 0, 0, 10, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + errorString: "bundle contents does not look a bundle or message", }, } From 1465663a63d4de0cea1203dede8b055ec35acf32 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 13 Apr 2026 22:22:57 -0500 Subject: [PATCH 08/16] add tests just for arg array to bytes --- osc_test.go | 235 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 osc_test.go diff --git a/osc_test.go b/osc_test.go new file mode 100644 index 0000000..e118fb5 --- /dev/null +++ b/osc_test.go @@ -0,0 +1,235 @@ +package osc + +import ( + "reflect" + "testing" +) + +func TestGoodOSCArgsToBuffer(t *testing.T) { + + testCases := []struct { + name string + args []OSCArg + expected []byte + }{ + { + name: "int arg", + args: []OSCArg{ + { + Type: "i", + Value: int(123), + }, + }, + expected: []byte{0, 0, 0, 123}, + }, + { + name: "int32 arg", + args: []OSCArg{ + { + Type: "i", + Value: int32(123), + }, + }, + expected: []byte{0, 0, 0, 123}, + }, + { + name: "float32 arg", + args: []OSCArg{ + { + Type: "f", + Value: float32(123), + }, + }, + expected: []byte{66, 246, 0, 0}, + }, + { + name: "float32 arg with int value", + args: []OSCArg{ + { + Type: "f", + Value: int(123), + }, + }, + expected: []byte{66, 246, 0, 0}, + }, + { + name: "float32 arg with int32 value", + args: []OSCArg{ + { + Type: "f", + Value: int32(123), + }, + }, + expected: []byte{66, 246, 0, 0}, + }, + { + name: "float32 arg with int64 value", + args: []OSCArg{ + { + Type: "f", + Value: int64(123), + }, + }, + expected: []byte{66, 246, 0, 0}, + }, + { + name: "float64 arg", + args: []OSCArg{ + { + Type: "d", + Value: float64(123), + }, + }, + expected: []byte{64, 94, 192, 0, 0, 0, 0, 0}, + }, + { + name: "float64 arg with float32 value", + args: []OSCArg{ + { + Type: "d", + Value: float32(123), + }, + }, + expected: []byte{64, 94, 192, 0, 0, 0, 0, 0}, + }, + { + name: "float64 arg with int value", + args: []OSCArg{ + { + Type: "d", + Value: int(123), + }, + }, + expected: []byte{64, 94, 192, 0, 0, 0, 0, 0}, + }, + { + name: "float64 arg with int32 value", + args: []OSCArg{ + { + Type: "d", + Value: int32(123), + }, + }, + expected: []byte{64, 94, 192, 0, 0, 0, 0, 0}, + }, + { + name: "float64 arg with int64 value", + args: []OSCArg{ + { + Type: "d", + Value: int64(123), + }, + }, + expected: []byte{64, 94, 192, 0, 0, 0, 0, 0}, + }, + { + name: "int64 arg", + args: []OSCArg{ + { + Type: "h", + Value: int64(123), + }, + }, + expected: []byte{0, 0, 0, 0, 0, 0, 0, 123}, + }, + { + name: "int64 arg with int32 value", + args: []OSCArg{ + { + Type: "h", + Value: int32(123), + }, + }, + expected: []byte{0, 0, 0, 0, 0, 0, 0, 123}, + }, + { + name: "blob arg", + args: []OSCArg{ + { + Type: "b", + Value: []byte{1, 2, 3}, + }, + }, + expected: []byte{0, 0, 0, 3, 1, 2, 3, 0}, + }, + { + name: "true arg", + args: []OSCArg{ + { + Type: "T", + Value: true, + }, + }, + expected: []byte{}, + }, + { + name: "false arg", + args: []OSCArg{ + { + Type: "F", + Value: false, + }, + }, + expected: []byte{}, + }, + { + name: "nil arg", + args: []OSCArg{ + { + Type: "N", + Value: nil, + }, + }, + expected: []byte{}, + }, + { + name: "inifinitum arg", + args: []OSCArg{ + { + Type: "I", + Value: nil, + }, + }, + expected: []byte{}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + + got, err := argsToBuffer(testCase.args) + + if err != nil { + t.Fatalf("failed to encode properly: %s", err.Error()) + } + + if !reflect.DeepEqual(got, testCase.expected) { + t.Fatalf("failed to encode properly got '%v', expected '%v'", got, testCase.expected) + } + }) + } +} + +func TestBadOSCArgsToBuffer(t *testing.T) { + + testCases := []struct { + name string + args []OSCArg + errorString string + }{} + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + + got, err := argsToBuffer(testCase.args) + + if err == nil { + t.Fatalf("argsToBuffer expected to fail but got: %+v", got) + } + + if err.Error() != testCase.errorString { + t.Fatalf("argsToBuffer got error '%s', expected '%s'", err.Error(), testCase.errorString) + } + }) + } +} From eb539dd3199566145962a2ce03da79a9f2bda83a Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 13 Apr 2026 22:41:43 -0500 Subject: [PATCH 09/16] add test for bundle with bad contents --- bundle_test.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/bundle_test.go b/bundle_test.go index d190d21..f50b75f 100644 --- a/bundle_test.go +++ b/bundle_test.go @@ -51,7 +51,19 @@ func TestBadOSCBundleEncoding(t *testing.T) { name string bundle *OSCBundle errorString string - }{} + }{ + { + name: "bundle contains message with bad address", + bundle: &OSCBundle{ + TimeTag: OSCTimeTag{ + seconds: 32, + fractionalSeconds: 0, + }, + Contents: []OSCPacket{&OSCMessage{Address: "hello", Args: []OSCArg{}}}, + }, + errorString: "OSC Message address must start with /", + }, + } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { From 16e1ad039e859b8fe148260f92e39aed83cc73ef Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 13 Apr 2026 22:41:56 -0500 Subject: [PATCH 10/16] add test for nested bundle --- bundle_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/bundle_test.go b/bundle_test.go index f50b75f..44e033a 100644 --- a/bundle_test.go +++ b/bundle_test.go @@ -102,6 +102,31 @@ func TestGoodOSCBundleDecoding(t *testing.T) { 47, 102, 114, 101, 113, 117, 101, 110, 99, 121, 0, 44, 102, 0, 0, 67, 220, 0, 0}, }, + { + name: "simple contents nested bundle", + expected: &OSCBundle{ + TimeTag: OSCTimeTag{ + seconds: 32, + fractionalSeconds: 0, + }, + Contents: []OSCPacket{&OSCBundle{ + TimeTag: OSCTimeTag{ + seconds: 64, + fractionalSeconds: 0, + }, + Contents: []OSCPacket{&OSCMessage{Address: "/oscillator/4/frequency", Args: []OSCArg{{Type: "f", Value: float32(440)}}}}, + }}, + }, + bytes: []byte{35, 98, 117, 110, 100, 108, 101, 0, // #bundle + 0, 0, 0, 32, 0, 0, 0, 0, // time tag + 0, 0, 0, 52, // content size + 35, 98, 117, 110, 100, 108, 101, 0, // #bundle + 0, 0, 0, 64, 0, 0, 0, 0, // time tag + 0, 0, 0, 32, // content size + 47, 111, 115, 99, 105, 108, 108, 97, 116, 111, 114, 47, 52, + 47, 102, 114, 101, 113, 117, 101, 110, 99, 121, 0, + 44, 102, 0, 0, 67, 220, 0, 0}, + }, } for _, testCase := range testCases { From 42b1f9cf8a7d0313ca823128ea7426a15d21cee5 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 13 Apr 2026 22:42:15 -0500 Subject: [PATCH 11/16] add error tests for arg decoding in message --- message_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/message_test.go b/message_test.go index c64ae05..9777a98 100644 --- a/message_test.go +++ b/message_test.go @@ -425,6 +425,48 @@ func TestBadOSCMessageDecoding(t *testing.T) { }, errorString: "OSC string is not properly padded", }, + { + name: "int32 arg not 4 bytes", + bytes: []byte{ + 47, 104, 101, 108, 108, 111, 0, 0, 44, 105, 0, 0, 0, + }, + errorString: "OSC int32 arg is not 4 bytes", + }, + { + name: "int64 arg not 8 bytes", + bytes: []byte{ + 47, 104, 101, 108, 108, 111, 0, 0, 44, 104, 0, 0, 0, 0, 0, 0, + }, + errorString: "OSC int64 arg is not 8 bytes", + }, + { + name: "float32 arg not 4 bytes", + bytes: []byte{ + 47, 104, 101, 108, 108, 111, 0, 0, 44, 102, 0, 0, 66, + }, + errorString: "OSC float32 arg is not 4 bytes", + }, + { + name: "float64 arg not 8 bytes", + bytes: []byte{ + 47, 104, 101, 108, 108, 111, 0, 0, 44, 100, 0, 0, 0, + }, + errorString: "OSC float64 arg is not 8 bytes", + }, + { + name: "blob arg size not valid", + bytes: []byte{ + 47, 104, 101, 108, 108, 111, 0, 0, 44, 98, 0, 0, 0, 0, 0, + }, + errorString: "OSC blob arg size not valid: OSC int32 arg is not 4 bytes", + }, + { + name: "blob arg size mismatch", + bytes: []byte{ + 47, 104, 101, 108, 108, 111, 0, 0, 44, 98, 0, 0, 0, 0, 0, 4, 98, 108, 111, + }, + errorString: "OSC blob arg size not valid: size specified is larger than remaining bytes", + }, } for _, testCase := range testCases { From 738b87b03f79236d0217fe5c980eb38bdb3df12f Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 13 Apr 2026 22:42:30 -0500 Subject: [PATCH 12/16] add tests for PacketFromBytes --- osc_test.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/osc_test.go b/osc_test.go index e118fb5..af24c9e 100644 --- a/osc_test.go +++ b/osc_test.go @@ -233,3 +233,86 @@ func TestBadOSCArgsToBuffer(t *testing.T) { }) } } + +func TestGoodPacketFromBytes(t *testing.T) { + + testCases := []struct { + name string + expected OSCPacket + bytes []byte + }{ + { + name: "message with no args", + expected: &OSCMessage{ + Address: "/hello", + Args: []OSCArg{}, + }, + bytes: []byte{47, 104, 101, 108, 108, 111, 0, 0}, + }, + { + name: "bundle with one message with no args", + expected: &OSCBundle{ + TimeTag: OSCTimeTag{ + seconds: 32, + fractionalSeconds: 0, + }, + Contents: []OSCPacket{&OSCMessage{Address: "/oscillator/4/frequency", Args: []OSCArg{{Type: "f", Value: float32(440)}}}}, + }, + bytes: []byte{35, 98, 117, 110, 100, 108, 101, 0, 0, 0, 0, + 32, 0, 0, 0, 0, 0, 0, 0, 32, 47, 111, + 115, 99, 105, 108, 108, 97, 116, 111, 114, 47, 52, + 47, 102, 114, 101, 113, 117, 101, 110, 99, 121, 0, + 44, 102, 0, 0, 67, 220, 0, 0}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got, remainingBytes, err := PacketFromBytes(testCase.bytes) + + if err != nil { + t.Fatalf("failed to decode properly: %s", err.Error()) + } + + if len(remainingBytes) != 0 { + t.Fatalf("failed to decode properly, expected no remaining bytes but got: %v", remainingBytes) + } + + if !reflect.DeepEqual(got, testCase.expected) { + t.Fatalf("failed to decode properly got '%v', expected '%v'", got, testCase.expected) + } + }) + } +} + +func TestBadPacketFromBytes(t *testing.T) { + + testCases := []struct { + name string + bytes []byte + errorString string + }{ + {name: "empty bytes", + bytes: []byte{}, + errorString: "cannot create OSC Packet from empty byte array", + }, + {name: "packet that does not start with / or #", + bytes: []byte{0, 1, 2, 3}, + errorString: "OSC Packet must start with # for bundle or / for message", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got, _, err := PacketFromBytes(testCase.bytes) + + if err == nil { + t.Fatalf("PacketFromBytes expected to fail but got: %+v", got) + } + + if err.Error() != testCase.errorString { + t.Fatalf("PacketFromBytes got error '%s', expected '%s'", err.Error(), testCase.errorString) + } + }) + } +} From 191d74a20a9d7104552f131cede027e3245d9703 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 13 Apr 2026 22:42:39 -0500 Subject: [PATCH 13/16] error message formatting --- osc.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osc.go b/osc.go index 7b7bbbf..7f749c1 100644 --- a/osc.go +++ b/osc.go @@ -284,7 +284,7 @@ func readOSCString(bytes []byte) (string, []byte, error) { func readOSCInt32(bytes []byte) (int32, []byte, error) { if len(bytes) < 4 { - return 0, bytes, errors.New("int data must be at least 4 bytes large") + return 0, bytes, errors.New("OSC int32 arg is not 4 bytes") } bits := binary.BigEndian.Uint32(bytes[0:4]) return int32(bits), bytes[4:], nil @@ -292,7 +292,7 @@ func readOSCInt32(bytes []byte) (int32, []byte, error) { func readOSCInt64(bytes []byte) (int64, []byte, error) { if len(bytes) < 8 { - return 0, bytes, errors.New("int data must be at least 4 bytes large") + return 0, bytes, errors.New("OSC int64 arg is not 8 bytes") } bits := binary.BigEndian.Uint64(bytes[0:8]) return int64(bits), bytes[8:], nil @@ -300,7 +300,7 @@ func readOSCInt64(bytes []byte) (int64, []byte, error) { func readOSCFloat32(bytes []byte) (float32, []byte, error) { if len(bytes) < 4 { - return 0, bytes, errors.New("float data must be at least 4 bytes large") + return 0, bytes, errors.New("OSC float32 arg is not 4 bytes") } bits := binary.BigEndian.Uint32(bytes[0:4]) return math.Float32frombits(bits), bytes[4:], nil @@ -308,7 +308,7 @@ func readOSCFloat32(bytes []byte) (float32, []byte, error) { func readOSCFloat64(bytes []byte) (float64, []byte, error) { if len(bytes) < 4 { - return 0, bytes, errors.New("float data must be at least 4 bytes large") + return 0, bytes, errors.New("OSC float64 arg is not 8 bytes") } bits := binary.BigEndian.Uint64(bytes[0:8]) return math.Float64frombits(bits), bytes[8:], nil @@ -318,11 +318,11 @@ func readOSCBlob(bytes []byte) ([]byte, []byte, error) { blobLength, remainingBytes, err := readOSCInt32(bytes) if err != nil { - return []byte{}, bytes, errors.New("problem reading blob data size") + return []byte{}, bytes, errors.New("OSC blob arg size not valid: " + err.Error()) } if len(remainingBytes) < int(blobLength) { - return []byte{}, bytes, errors.New("blob data specified a size larger than the remaining message data") + return []byte{}, bytes, errors.New("OSC blob arg size not valid: size specified is larger than remaining bytes") } blobLengthPadding := 4 - (blobLength % 4) From 16093f250911d6a9c6b7243bcd975f30568981d9 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 13 Apr 2026 22:51:29 -0500 Subject: [PATCH 14/16] add support for OSC timetag arg --- osc.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osc.go b/osc.go index 7f749c1..d00620f 100644 --- a/osc.go +++ b/osc.go @@ -434,9 +434,16 @@ func readOSCArg(bytes []byte, oscType string) (OSCArg, []byte, error) { } oscArg.Value = argFloat remainingBytes = bytesLeft + case "t": + argTimeTag, bytesLeft, err := readOSCTimeTag(bytes) + if err != nil { + readArgError = err + } + oscArg.Value = argTimeTag + remainingBytes = bytesLeft default: fmt.Printf("unsupported osc type: %s\n", oscType) - readArgError = errors.New("unsupported osc type: " + oscType) + readArgError = errors.New("unsupported OSC argument type: " + oscType) } return oscArg, remainingBytes, readArgError } From 5f5c34c4eabf7f9717f61fca7ba4d45fd51638a7 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 13 Apr 2026 22:51:43 -0500 Subject: [PATCH 15/16] error message formatting --- osc.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osc.go b/osc.go index d00620f..29d9718 100644 --- a/osc.go +++ b/osc.go @@ -336,7 +336,7 @@ func readOSCBlob(bytes []byte) ([]byte, []byte, error) { func readOSCColor(bytes []byte) (OSCColor, []byte, error) { if len(bytes) < 4 { - return OSCColor{0, 0, 0, 0}, bytes, errors.New("color data must be at least 4 bytes large") + return OSCColor{0, 0, 0, 0}, bytes, errors.New("OSC color arg is not 4 bytes") } oscColor := OSCColor{ r: bytes[0], @@ -346,14 +346,15 @@ func readOSCColor(bytes []byte) (OSCColor, []byte, error) { } return oscColor, bytes[4:], nil } + func readOSCTimeTag(bytes []byte) (OSCTimeTag, []byte, error) { seconds, bytesAfterSeconds, err := readOSCInt32(bytes) if err != nil { - return OSCTimeTag{}, bytes, err + return OSCTimeTag{}, bytes, fmt.Errorf("OSC time tag seconds are not valid: %s", err) } fractionalSeconds, remainingBytes, err := readOSCInt32(bytesAfterSeconds) if err != nil { - return OSCTimeTag{}, bytes, err + return OSCTimeTag{}, bytes, fmt.Errorf("OSC time tag fractional seconds are not valid: %s", err) } return OSCTimeTag{ From 2ec4be542698b7ef5ceb91d6f55e098b0e8c6fca Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 13 Apr 2026 22:51:52 -0500 Subject: [PATCH 16/16] more tests for bad arg parsing --- message_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/message_test.go b/message_test.go index 9777a98..1ffb987 100644 --- a/message_test.go +++ b/message_test.go @@ -467,6 +467,34 @@ func TestBadOSCMessageDecoding(t *testing.T) { }, errorString: "OSC blob arg size not valid: size specified is larger than remaining bytes", }, + { + name: "color arg not 4 bytes", + bytes: []byte{ + 47, 104, 101, 108, 108, 111, 0, 0, 44, 114, 0, 0, 20, 21, + }, + errorString: "OSC color arg is not 4 bytes", + }, + { + name: "time tag arg seconds not complete", + bytes: []byte{ + 47, 104, 101, 108, 108, 111, 0, 0, 44, 116, 0, 0, 0, + }, + errorString: "OSC time tag seconds are not valid: OSC int32 arg is not 4 bytes", + }, + { + name: "time tag arg fractional seconds not complete", + bytes: []byte{ + 47, 104, 101, 108, 108, 111, 0, 0, 44, 116, 0, 0, 0, 32, 0, 0, 0, + }, + errorString: "OSC time tag fractional seconds are not valid: OSC int32 arg is not 4 bytes", + }, + { + name: "unknown arg type", + bytes: []byte{ + 47, 104, 101, 108, 108, 111, 0, 0, 44, 120, 0, 0, + }, + errorString: "unsupported OSC argument type: x", + }, } for _, testCase := range testCases {