Compare commits

...

43 Commits

Author SHA1 Message Date
Joel Wetzell 7a5ef4c56b Merge pull request #27 from jwetzell/dependabot/github_actions/actions/checkout-7
Bump actions/checkout from 6 to 7
2026-07-13 13:27:51 -05:00
Joel Wetzell 113cb622e8 Merge pull request #28 from jwetzell/dependabot/go_modules/github.com/urfave/cli/v3-3.10.1
Bump github.com/urfave/cli/v3 from 3.9.0 to 3.10.1
2026-07-13 13:25:51 -05:00
dependabot[bot] 1994949841 Bump github.com/urfave/cli/v3 from 3.9.0 to 3.10.1
Bumps [github.com/urfave/cli/v3](https://github.com/urfave/cli) from 3.9.0 to 3.10.1.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v3.9.0...v3.10.1)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v3
  dependency-version: 3.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-29 02:53:22 +00:00
dependabot[bot] fe499f0c21 Bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-22 02:53:16 +00:00
Joel Wetzell 2278ad7a1c Merge pull request #25 from jwetzell/dependabot/github_actions/codecov/codecov-action-7
Bump codecov/codecov-action from 6 to 7
2026-06-10 11:37:11 -05:00
Joel Wetzell d4a56fcd11 rename variables 2026-06-08 12:23:40 -05:00
dependabot[bot] e8626ab230 Bump codecov/codecov-action from 6 to 7
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 6 to 7.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 02:53:29 +00:00
Joel Wetzell 58672c9ab3 mess around with benchmarks and fuzzing 2026-05-20 17:41:57 -05:00
Joel Wetzell 8b8129b2e2 add size check to bundle content 2026-05-20 17:41:37 -05:00
Joel Wetzell f7850ce70e Merge pull request #24 from jwetzell/fix/float-64-arg-size-check
fix size check on readOSCFloat64
2026-05-20 17:39:59 -05:00
Joel Wetzell 8ed1ab065c fix size check on readOSCFloat64 2026-05-20 08:38:26 -05:00
Joel Wetzell 82e9c7ed24 Merge pull request #23 from jwetzell/dependabot/go_modules/github.com/urfave/cli/v3-3.9.0
Bump github.com/urfave/cli/v3 from 3.8.0 to 3.9.0
2026-05-19 14:34:06 -05:00
dependabot[bot] 9f34a57141 Bump github.com/urfave/cli/v3 from 3.8.0 to 3.9.0
Bumps [github.com/urfave/cli/v3](https://github.com/urfave/cli) from 3.8.0 to 3.9.0.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v3.8.0...v3.9.0)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v3
  dependency-version: 3.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 03:23:16 +00:00
Joel Wetzell 6aadaedb5d some linting things 2026-05-17 09:22:14 -05:00
Joel Wetzell d6d4ac4419 stop using destination 2026-05-09 14:06:33 -05:00
Joel Wetzell a86a9b3121 chore: go fix 2026-05-09 13:44:28 -05:00
Joel Wetzell 216f84a2d9 Merge pull request #22 from jwetzell/error-handling-cleanup
clean out error return from funcs that don't return error
2026-04-29 13:56:47 -05:00
Joel Wetzell bd6a548e34 Add Codecov badge to README 2026-04-29 13:56:32 -05:00
Joel Wetzell d8dff0794a clean out error return from funcs that don't return error
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 13:46:43 -05:00
Joel Wetzell 0c4d36b376 cleanup printing 2026-04-29 13:41:50 -05:00
Joel Wetzell a5ac419e3e return on unsupported arg 2026-04-29 13:41:42 -05:00
Joel Wetzell 3b66c38d18 Merge pull request #21 from jwetzell/dependabot/github_actions/codecov/codecov-action-6
Bump codecov/codecov-action from 5 to 6
2026-04-27 08:35:42 -05:00
Joel Wetzell 4d8ba2a47b get rid of bytes.Buffer use
Co-authored-by: Copilot <copilot@github.com>
2026-04-26 13:37:48 -05:00
dependabot[bot] 0caf1b02a7 Bump codecov/codecov-action from 5 to 6
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 02:55:47 +00:00
Joel Wetzell d82919e9f1 remove codecov badge 2026-04-13 22:59:44 -05:00
Joel Wetzell 4078efa090 update readme 2026-04-13 22:56:57 -05:00
Joel Wetzell 53864a35dc Merge pull request #20 from jwetzell/error-handling
add error handling to arg encoding and message/bundle ToBytes()
2026-04-13 22:53:07 -05:00
Joel Wetzell 2ec4be5426 more tests for bad arg parsing 2026-04-13 22:51:52 -05:00
Joel Wetzell 5f5c34c4ea error message formatting 2026-04-13 22:51:43 -05:00
Joel Wetzell 16093f2509 add support for OSC timetag arg 2026-04-13 22:51:29 -05:00
Joel Wetzell 191d74a20a error message formatting 2026-04-13 22:42:39 -05:00
Joel Wetzell 738b87b03f add tests for PacketFromBytes 2026-04-13 22:42:30 -05:00
Joel Wetzell 42b1f9cf8a add error tests for arg decoding in message 2026-04-13 22:42:15 -05:00
Joel Wetzell 16e1ad039e add test for nested bundle 2026-04-13 22:41:56 -05:00
Joel Wetzell eb539dd319 add test for bundle with bad contents 2026-04-13 22:41:43 -05:00
Joel Wetzell 1465663a63 add tests just for arg array to bytes 2026-04-13 22:22:57 -05:00
Joel Wetzell dd430dd380 add layout for testing bundle error cases 2026-04-13 22:22:44 -05:00
Joel Wetzell 9452268042 add layout for testing OSCBundle error cases 2026-04-13 19:11:11 -05:00
Joel Wetzell b621944dc5 error tests for arg values when encoding 2026-04-13 18:59:43 -05:00
Joel Wetzell 8fa8aa3a55 return errors instead of printing 2026-04-13 18:53:35 -05:00
Joel Wetzell 1d2684f446 cleanup test layout 2026-04-13 18:53:21 -05:00
Joel Wetzell 2548771fe3 add error handling for OSC address and bad arg type encoding 2026-04-13 18:48:50 -05:00
Joel Wetzell 4744321d71 add error handling to arg encoding and message/bundle ToBytes() 2026-04-13 18:38:25 -05:00
22 changed files with 990 additions and 252 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
- goarch: "386"
goos: darwin
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: wangyoucao577/go-release-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
- goarch: "386"
goos: darwin
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: wangyoucao577/go-release-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
- goarch: "386"
goos: darwin
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: wangyoucao577/go-release-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
+2 -2
View File
@@ -9,7 +9,7 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v6
with:
@@ -18,7 +18,7 @@ jobs:
run: go test -v -coverprofile=coverage.txt .
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.txt
+2 -1
View File
@@ -1 +1,2 @@
build
build
coverage*
+8 -4
View File
@@ -1,5 +1,9 @@
A collection of command line OSC utilities written in Go. Mainly an exercise in learning Go.
[![codecov](https://codecov.io/gh/jwetzell/osc-go/branch/main/graph/badge.svg?token=DW3CCZELGI)](https://codecov.io/gh/jwetzell/osc-go)
# `sendosc`
# `makeosc`
# `receiveosc`
A mostly complete OSC implementation and collection of command line OSC utilities written in Go. Mainly an exercise in learning Go.
## Utilities
### `sendosc`
### `makeosc`
### `receiveosc`
+22 -12
View File
@@ -4,30 +4,35 @@ import (
"errors"
)
func (b *OSCBundle) ToBytes() []byte {
func (b *OSCBundle) ToBytes() ([]byte, error) {
bytes := stringToOSCBytes("#bundle")
bytes = append(bytes, timeTagToOSCBytes(b.TimeTag)...)
timeTagBytes := timeTagToOSCBytes(b.TimeTag)
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 := int32ToOSCBytes(int32(packetLength))
bytes = append(bytes, packetLengthBytes...)
bytes = append(bytes, packetBytes...)
}
return bytes
return bytes, nil
}
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)
@@ -37,7 +42,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)
@@ -61,25 +66,30 @@ func BundleFromBytes(bytes []byte) (*OSCBundle, []byte, error) {
remainingBytes = bytesAfterContentSize
if contentSize <= 0 {
return nil, remainingBytes, errors.New("bundle content size must be positive")
}
if len(remainingBytes) < int(contentSize) {
return nil, remainingBytes, errors.New("bundle doesn't have enough bytes for the content size it specifies")
}
bundleContentBytes := remainingBytes[0:contentSize]
if bundleContentBytes[0] == 35 {
switch bundleContentBytes[0] {
case 35: // #
content, _, err := BundleFromBytes(bundleContentBytes)
if err != nil {
return nil, remainingBytes, err
}
bundleContents = append(bundleContents, content)
} else if bundleContentBytes[0] == 47 {
case 47: // /
content, err := MessageFromBytes(bundleContentBytes)
if err != nil {
return nil, remainingBytes, err
}
bundleContents = append(bundleContents, content)
} else {
default:
return nil, remainingBytes, errors.New("bundle contents does not look a bundle or message")
}
remainingBytes = bytesAfterContentSize[contentSize:]
+192 -41
View File
@@ -1,89 +1,240 @@
package osc
import (
"fmt"
"reflect"
"testing"
)
func TestOSCBundleEncoding(t *testing.T) {
func TestGoodOSCBundleEncoding(t *testing.T) {
testCases := []struct {
description string
name string
bundle *OSCBundle
expected []byte
}{
{
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)}}}},
},
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,
44, 102, 0, 0, 67, 220, 0, 0},
},
}
for _, testCase := range testCases {
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)
}
})
}
}
func TestBadOSCBundleEncoding(t *testing.T) {
testCases := []struct {
name string
bundle *OSCBundle
expected []byte
errorString string
}{
{
"simple contents single message",
&OSCBundle{
name: "bundle contains message with bad address",
bundle: &OSCBundle{
TimeTag: OSCTimeTag{
seconds: 32,
fractionalSeconds: 0,
},
Contents: []OSCPacket{&OSCMessage{Address: "/oscillator/4/frequency", Args: []OSCArg{{Type: "f", Value: float32(440)}}}},
Contents: []OSCPacket{&OSCMessage{Address: "hello", Args: []OSCArg{}}},
},
[]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},
errorString: "OSC Message address must start with /",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
actual := testCase.bundle.ToBytes()
got, err := testCase.bundle.ToBytes()
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)
}
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 TestOSCBundleDecoding(t *testing.T) {
func TestGoodOSCBundleDecoding(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,
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 {
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)
}
})
}
}
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, 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",
},
}
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)
}
})
}
}
func FuzzBundleFromBytes(f *testing.F) {
seedBytes := [][]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},
{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 _, seed := range seedBytes {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, data []byte) {
_, _, _ = BundleFromBytes(data)
})
}
+26 -30
View File
@@ -12,41 +12,37 @@ import (
)
func main() {
var Address string
var Args []string
var Types []string
var Slip bool
cmd := &cli.Command{
Name: "makeosc",
Usage: "make osc bytes",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "address",
Value: "",
Usage: "OSC address",
Destination: &Address,
Required: true,
Name: "address",
Value: "",
Usage: "OSC address",
Required: true,
},
&cli.StringSliceFlag{
Name: "arg",
Usage: "OSC args",
Destination: &Args,
Name: "arg",
Usage: "OSC args",
},
&cli.StringSliceFlag{
Name: "type",
Usage: "OSC types",
Destination: &Types,
Name: "type",
Usage: "OSC types",
},
&cli.BoolFlag{
Name: "slip",
Value: false,
Usage: "whether to slip encode the OSC Message bytes",
Destination: &Slip,
Name: "slip",
Value: false,
Usage: "whether to slip encode the OSC Message bytes",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
make(Address, Args, Types, Slip)
address := cmd.String("address")
args := cmd.StringSlice("arg")
types := cmd.StringSlice("type")
slip := cmd.Bool("slip")
makeMsg(address, args, types, slip)
return nil
},
}
@@ -130,8 +126,7 @@ func argToTypedArg(rawArg string, oscType string) osc.OSCArg {
Type: "N",
}
default:
fmt.Print("unhandled osc type: ")
fmt.Printf("%s.\n", oscType)
fmt.Printf("unsupported OSC arg type: %s\n", oscType)
// TODO(jwetzell): something better than this like actual nil, err thing
return osc.OSCArg{}
}
@@ -146,11 +141,12 @@ func slipEncode(bytes []byte) []byte {
var encodedBytes = []byte{END}
for _, byteToEncode := range bytes {
if byteToEncode == END {
switch byteToEncode {
case END:
encodedBytes = append(encodedBytes, ESC, ESC_END)
} else if byteToEncode == ESC {
case ESC:
encodedBytes = append(encodedBytes, ESC, ESC_ESC)
} else {
default:
encodedBytes = append(encodedBytes, byteToEncode)
}
}
@@ -159,7 +155,7 @@ func slipEncode(bytes []byte) []byte {
return encodedBytes
}
func make(address string, args []string, types []string, slip bool) {
func makeMsg(address string, args []string, types []string, slip bool) {
oscMessage := osc.OSCMessage{
Address: address,
@@ -175,13 +171,13 @@ 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)
}
//TODO write buffer to stdout
os.Stdout.Write(oscMessageBuffer)
}
+33 -36
View File
@@ -12,33 +12,25 @@ import (
)
func main() {
var IP string
var Port int32
var Protocol string
var Format string
var Slip bool
cmd := &cli.Command{
Name: "receiveosc",
Usage: "receive OSC messages via UDP or TCP",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "ip",
Usage: "ip to receive OSC messages on",
Value: "0.0.0.0",
Destination: &IP,
Name: "ip",
Usage: "ip to receive OSC messages on",
Value: "0.0.0.0",
},
&cli.Int32Flag{
Name: "port",
Usage: "port to receive OSC messages on",
Destination: &Port,
Value: 8888,
Name: "port",
Usage: "port to receive OSC messages on",
Value: 8888,
},
&cli.StringFlag{
Name: "protocol",
Usage: "protocol to use to receive (tcp or udp)",
Value: "udp",
Destination: &Protocol,
Name: "protocol",
Usage: "protocol to use to receive (tcp or udp)",
Value: "udp",
Validator: func(flag string) error {
if flag != "udp" && flag != "tcp" {
return fmt.Errorf("protocol must be either 'udp' or 'tcp'")
@@ -47,10 +39,9 @@ func main() {
},
},
&cli.StringFlag{
Name: "format",
Usage: "format for messages to be output in ('json')",
Value: "json",
Destination: &Format,
Name: "format",
Usage: "format for messages to be output in ('json')",
Value: "json",
Validator: func(flag string) error {
if flag != "json" {
return fmt.Errorf("format must be 'json'")
@@ -59,22 +50,27 @@ func main() {
},
},
&cli.BoolFlag{
Name: "slip",
Value: false,
Usage: "whether to slip encode the OSC Message bytes",
Destination: &Slip,
Name: "slip",
Value: false,
Usage: "whether to slip encode the OSC Message bytes",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
netAddress := fmt.Sprintf("%s:%d", IP, Port)
switch Protocol {
ip := cmd.String("ip")
port := cmd.Int32("port")
protocol := cmd.String("protocol")
format := cmd.String("format")
slip := cmd.Bool("slip")
netAddress := fmt.Sprintf("%s:%d", ip, port)
switch protocol {
case "udp":
listenUDP(netAddress, Format)
listenUDP(netAddress, format)
case "tcp":
if !Slip {
if !slip {
return fmt.Errorf("OSC 1.0 over TCP is not supported yet")
}
listenTCP(netAddress, Slip, Format)
listenTCP(netAddress, slip, format)
}
return nil
},
@@ -124,9 +120,10 @@ func (s *SLIP) decode(bytes []byte) {
}
if escapeNext {
if packetByte == ESC_END {
switch packetByte {
case ESC_END:
s.pendingBytes = append(s.pendingBytes, END)
} else if packetByte == ESC_ESC {
case ESC_ESC:
s.pendingBytes = append(s.pendingBytes, ESC)
}
escapeNext = false
@@ -207,23 +204,23 @@ func handleBundle(bundle *osc.OSCBundle, format string) {
func listenUDP(netAddress string, format string) {
s, err := net.ResolveUDPAddr("udp4", netAddress)
laddr, err := net.ResolveUDPAddr("udp4", netAddress)
if err != nil {
fmt.Println(err)
return
}
connection, err := net.ListenUDP("udp4", s)
conn, err := net.ListenUDP("udp4", laddr)
if err != nil {
fmt.Println(err)
return
}
defer connection.Close()
defer conn.Close()
buffer := make([]byte, 1024)
for {
bytesRead, _, err := connection.ReadFromUDP(buffer)
bytesRead, _, err := conn.ReadFromUDP(buffer)
if err != nil {
panic(err)
+39 -43
View File
@@ -15,35 +15,25 @@ import (
)
func main() {
var Host string
var Port int32
var Address string
var Protocol string
var Args []string
var Types []string
var Slip bool
cmd := &cli.Command{
Name: "sendosc",
Usage: "send OSC messages via UDP or TCP",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "host",
Usage: "host to send OSC message to",
Destination: &Host,
Required: true,
Name: "host",
Usage: "host to send OSC message to",
Required: true,
},
&cli.Int32Flag{
Name: "port",
Usage: "port to send OSC message to",
Destination: &Port,
Required: true,
Name: "port",
Usage: "port to send OSC message to",
Required: true,
},
&cli.StringFlag{
Name: "protocol",
Usage: "protocol to use to send (tcp or udp)",
Value: "udp",
Destination: &Protocol,
Name: "protocol",
Usage: "protocol to use to send (tcp or udp)",
Value: "udp",
Validator: func(flag string) error {
if flag != "udp" && flag != "tcp" {
return fmt.Errorf("protocol must be either 'udp' or 'tcp'")
@@ -52,32 +42,35 @@ func main() {
},
},
&cli.StringFlag{
Name: "address",
Usage: "OSC address",
Destination: &Address,
Required: true,
Name: "address",
Usage: "OSC address",
Required: true,
},
&cli.StringSliceFlag{
Name: "arg",
Usage: "OSC args",
Value: []string{},
Destination: &Args,
Name: "arg",
Usage: "OSC args",
Value: []string{},
},
&cli.StringSliceFlag{
Name: "type",
Usage: "OSC types",
Value: []string{},
Destination: &Types,
Name: "type",
Usage: "OSC types",
Value: []string{},
},
&cli.BoolFlag{
Name: "slip",
Value: false,
Usage: "whether to slip encode the OSC Message bytes",
Destination: &Slip,
Name: "slip",
Value: false,
Usage: "whether to slip encode the OSC Message bytes",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
send(Host, Port, Address, Args, Types, Protocol, Slip)
host := cmd.String("host")
port := cmd.Int32("port")
address := cmd.String("address")
args := cmd.StringSlice("arg")
types := cmd.StringSlice("type")
protocol := cmd.String("protocol")
slip := cmd.Bool("slip")
send(host, port, address, args, types, protocol, slip)
return nil
},
}
@@ -161,8 +154,7 @@ func argToTypedArg(rawArg string, oscType string) osc.OSCArg {
Type: "N",
}
default:
fmt.Print("unhandled osc type: ")
fmt.Printf("%s.\n", oscType)
fmt.Printf("unsupported OSC arg type: %s\n", oscType)
// TODO(jwetzell): something better than this like actual nil, err thing
return osc.OSCArg{}
}
@@ -177,11 +169,12 @@ func slipEncode(bytes []byte) []byte {
var encodedBytes = []byte{END}
for _, byteToEncode := range bytes {
if byteToEncode == END {
switch byteToEncode {
case END:
encodedBytes = append(encodedBytes, ESC, ESC_END)
} else if byteToEncode == ESC {
case ESC:
encodedBytes = append(encodedBytes, ESC, ESC_ESC)
} else {
default:
encodedBytes = append(encodedBytes, byteToEncode)
}
}
@@ -208,7 +201,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)
@@ -220,7 +216,7 @@ func send(host string, port int32, address string, args []string, types []string
oscMessageBuffer = append(sizeBytes, oscMessageBuffer...)
}
netAddress := fmt.Sprintf("%s:%d", host, port)
netAddress := net.JoinHostPort(host, fmt.Sprintf("%d", port))
conn, err := net.Dial(protocol, netAddress)
if err != nil {
fmt.Printf("Dial err %v", err)
+1 -1
View File
@@ -2,4 +2,4 @@ module github.com/jwetzell/osc-go
go 1.25.1
require github.com/urfave/cli/v3 v3.8.0
require github.com/urfave/cli/v3 v3.10.1
+2 -2
View File
@@ -4,7 +4,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/urfave/cli/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY=
github.com/urfave/cli/v3 v3.10.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+16 -5
View File
@@ -5,8 +5,16 @@ import (
"strings"
)
func (m *OSCMessage) ToBytes() []byte {
//TODO(jwetzell): add error handling
func (m *OSCMessage) ToBytes() ([]byte, error) {
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,11 +26,14 @@ func (m *OSCMessage) ToBytes() []byte {
for _, arg := range m.Args {
sb.WriteString(arg.Type)
}
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) {
+217 -3
View File
@@ -128,16 +128,117 @@ 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)
}
})
}
}
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",
},
{
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 {
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
@@ -324,6 +425,76 @@ 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",
},
{
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 {
@@ -340,3 +511,46 @@ func TestBadOSCMessageDecoding(t *testing.T) {
})
}
}
func BenchmarkMessageToBytes(b *testing.B) {
message := &OSCMessage{
Address: "/hello",
Args: []OSCArg{
{Type: "i", Value: 35},
},
}
for b.Loop() {
_, err := message.ToBytes()
if err != nil {
b.Fatalf("failed to encode properly: %s", err.Error())
}
}
}
func BenchmarkMessageFromBytes(b *testing.B) {
bytes := []byte{47, 104, 101, 108, 108, 111, 0, 0, 44, 105, 0, 0, 0, 0, 0, 35}
for b.Loop() {
_, err := MessageFromBytes(bytes)
if err != nil {
b.Fatalf("failed to decode properly: %s", err.Error())
}
}
}
func FuzzMessageFromBytes(f *testing.F) {
seedBytes := [][]byte{
{},
{47, 104, 101, 108, 108, 111, 0, 0},
{47, 104, 101, 108, 108, 111, 0, 0, 44, 105, 0, 0, 0, 0, 0, 35},
}
for _, seed := range seedBytes {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, data []byte) {
_, _ = MessageFromBytes(data)
})
}
+100 -68
View File
@@ -2,7 +2,6 @@ package osc
// TODO(jwetzell): split things up
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
@@ -18,7 +17,7 @@ func stringToOSCBytes(rawString string) []byte {
padLength := 4 - (len(sb.String()) % 4)
if padLength < 4 {
for i := 0; i < padLength; i++ {
for range padLength {
sb.WriteString("\u0000")
}
}
@@ -27,51 +26,50 @@ func stringToOSCBytes(rawString string) []byte {
}
func int32ToOSCBytes(number int32) []byte {
var buf bytes.Buffer
err := binary.Write(&buf, binary.BigEndian, number)
if err != nil {
panic(err)
}
return buf.Bytes()
bytes := make([]byte, 4)
bytes[0] = byte((number >> 24) & 0xFF)
bytes[1] = byte((number >> 16) & 0xFF)
bytes[2] = byte((number >> 8) & 0xFF)
bytes[3] = byte(number & 0xFF)
return bytes
}
func int64ToOSCBytes(number int64) []byte {
var buf bytes.Buffer
err := binary.Write(&buf, binary.BigEndian, number)
if err != nil {
panic(err)
}
return buf.Bytes()
bytes := make([]byte, 8)
bytes[0] = byte((number >> 56) & 0xFF)
bytes[1] = byte((number >> 48) & 0xFF)
bytes[2] = byte((number >> 40) & 0xFF)
bytes[3] = byte((number >> 32) & 0xFF)
bytes[4] = byte((number >> 24) & 0xFF)
bytes[5] = byte((number >> 16) & 0xFF)
bytes[6] = byte((number >> 8) & 0xFF)
bytes[7] = byte(number & 0xFF)
return bytes
}
func float32ToOSCBytes(number float32) []byte {
var buf bytes.Buffer
err := binary.Write(&buf, binary.BigEndian, number)
if err != nil {
panic(err)
}
return buf.Bytes()
bytes := make([]byte, 4)
binary.BigEndian.PutUint32(bytes, math.Float32bits(number))
return bytes
}
func float64ToOSCBytes(number float64) []byte {
var buf bytes.Buffer
err := binary.Write(&buf, binary.BigEndian, number)
if err != nil {
panic(err)
}
return buf.Bytes()
bytes := make([]byte, 8)
binary.BigEndian.PutUint64(bytes, math.Float64bits(number))
return bytes
}
func byteArrayToOSCBytes(bytes []byte) []byte {
oscBytes := []byte{}
bytesSize := len(bytes)
oscBytes = append(oscBytes, int32ToOSCBytes(int32(bytesSize))...)
bytesSizeBytes := int32ToOSCBytes(int32(bytesSize))
oscBytes = append(oscBytes, bytesSizeBytes...)
oscBytes = append(oscBytes, bytes...)
padLength := 4 - (bytesSize % 4)
if padLength < 4 {
for i := 0; i < padLength; i++ {
for range padLength {
oscBytes = append(oscBytes, 0)
}
}
@@ -81,50 +79,59 @@ func byteArrayToOSCBytes(bytes []byte) []byte {
func timeTagToOSCBytes(timeTag OSCTimeTag) []byte {
timeTagBytes := int32ToOSCBytes(timeTag.seconds)
timeTagBytes = append(timeTagBytes, int32ToOSCBytes(timeTag.fractionalSeconds)...)
fractionalSecondsBytes := int32ToOSCBytes(timeTag.fractionalSeconds)
timeTagBytes = append(timeTagBytes, fractionalSecondsBytes...)
return timeTagBytes
}
func argsToBuffer(args []OSCArg) []byte {
func argsToBuffer(args []OSCArg) ([]byte, error) {
//TODO(jwetzell): add error handling
var argBuffers = []byte{}
for _, arg := range args {
switch oscType := arg.Type; oscType {
switch arg.Type {
case "s":
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 {
argBuffers = append(argBuffers, int32ToOSCBytes(int32(value))...)
valueBytes := int32ToOSCBytes(int32(value))
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int32); ok {
argBuffers = append(argBuffers, int32ToOSCBytes(value)...)
valueBytes := int32ToOSCBytes(int32(value))
argBuffers = append(argBuffers, valueBytes...)
} else {
fmt.Println("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 {
argBuffers = append(argBuffers, float32ToOSCBytes(float32(value))...)
valueBytes := float32ToOSCBytes(value)
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(float64); ok {
argBuffers = append(argBuffers, float32ToOSCBytes(float32(value))...)
valueBytes := float32ToOSCBytes(float32(value))
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int); ok {
argBuffers = append(argBuffers, float32ToOSCBytes(float32(value))...)
valueBytes := float32ToOSCBytes(float32(value))
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int32); ok {
argBuffers = append(argBuffers, float32ToOSCBytes(float32(value))...)
valueBytes := float32ToOSCBytes(float32(value))
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int64); ok {
argBuffers = append(argBuffers, float32ToOSCBytes(float32(value))...)
valueBytes := float32ToOSCBytes(float32(value))
argBuffers = append(argBuffers, valueBytes...)
} else {
fmt.Println("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 {
argBuffers = append(argBuffers, byteArrayToOSCBytes(value)...)
valueBytes := byteArrayToOSCBytes(value)
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)...)
@@ -136,39 +143,50 @@ func argsToBuffer(args []OSCArg) []byte {
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...)
}
case "h":
if value, ok := arg.Value.(int); ok {
argBuffers = append(argBuffers, int64ToOSCBytes(int64(value))...)
valueBytes := int64ToOSCBytes(int64(value))
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int32); ok {
argBuffers = append(argBuffers, int64ToOSCBytes(int64(value))...)
valueBytes := int64ToOSCBytes(int64(value))
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int64); ok {
argBuffers = append(argBuffers, int64ToOSCBytes(value)...)
valueBytes := int64ToOSCBytes(value)
argBuffers = append(argBuffers, valueBytes...)
} else {
fmt.Println("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 {
argBuffers = append(argBuffers, float64ToOSCBytes(float64(value))...)
valueBytes := float64ToOSCBytes(float64(value))
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(float64); ok {
argBuffers = append(argBuffers, float64ToOSCBytes(float64(value))...)
valueBytes := float64ToOSCBytes(value)
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int); ok {
argBuffers = append(argBuffers, float64ToOSCBytes(float64(value))...)
valueBytes := float64ToOSCBytes(float64(value))
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int32); ok {
argBuffers = append(argBuffers, float64ToOSCBytes(float64(value))...)
valueBytes := float64ToOSCBytes(float64(value))
argBuffers = append(argBuffers, valueBytes...)
} else if value, ok := arg.Value.(int64); ok {
argBuffers = append(argBuffers, float64ToOSCBytes(float64(value))...)
valueBytes := float64ToOSCBytes(float64(value))
argBuffers = append(argBuffers, valueBytes...)
} else {
fmt.Println("OSC arg had float type but non-float value.")
return nil, errors.New("OSC arg had float64 type but non-number value")
}
default:
fmt.Printf("unhandled osc type: %s.\n", oscType)
return nil, fmt.Errorf("unsupported OSC argument type: %s", arg.Type)
}
}
return argBuffers
return argBuffers, nil
}
func readOSCString(bytes []byte) (string, []byte, error) {
@@ -206,7 +224,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
@@ -214,7 +232,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
@@ -222,15 +240,15 @@ 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
}
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")
if len(bytes) < 8 {
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
@@ -240,11 +258,15 @@ 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 blobLength < 0 {
return []byte{}, bytes, errors.New("OSC blob arg size not valid: size cannot be negative")
}
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)
@@ -253,12 +275,15 @@ func readOSCBlob(bytes []byte) ([]byte, []byte, error) {
if blobLengthPadding < 4 {
blobEnd = blobEnd + blobLengthPadding
}
if int(blobEnd) > len(bytes) {
return []byte{}, bytes, errors.New("OSC blob arg size not valid: size specified is larger than remaining bytes when accounting for padding")
}
return bytes[4 : 4+blobLength], bytes[blobEnd:], nil
}
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],
@@ -268,14 +293,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{
@@ -292,7 +318,7 @@ func readOSCArg(bytes []byte, oscType string) (OSCArg, []byte, error) {
oscArg := OSCArg{}
oscArg.Type = oscType
remainingBytes := []byte{}
var remainingBytes []byte
//TODO(jwetzell): add error handling
switch oscType {
case "s":
@@ -356,9 +382,15 @@ 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)
return OSCArg{}, bytes, fmt.Errorf("unsupported OSC argument type: %s", oscType)
}
return oscArg, remainingBytes, readArgError
}
+318
View File
@@ -0,0 +1,318 @@
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)
}
})
}
}
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)
}
})
}
}
+2
View File
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("#bundle\x0000000000\x00\x00\x00\x00")
+2
View File
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("/00\x00,b0\x00\xff000")
+2
View File
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("/00\x00,b0\x00\x00\x00\x00\x010")
+2
View File
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("/00\x00,d\x0000000")
+1 -1
View File
@@ -1,7 +1,7 @@
package osc
type OSCPacket interface {
ToBytes() []byte
ToBytes() ([]byte, error)
}
type OSCBundle struct {