Selaa lähdekoodia

wire: add optional struct tag

This commit adds a new "optional" struct tag.

struct {
        Val0     uint16
        Optional *struct {
                Val1 uint16
        } `oscar:"optional"`
}

The marshaler skips marshaling pointer struct fields marked optional if
the pointer is nil. No bytes are written to the output byte array.

The unmarshaler skips unmarshaling pointer struct fields marked
optional if the pointer is nil. It leaves the pointer nil and does not
throw an EOF error, which is the default behavior for non-optional
fields.

The optional field must be the -last- field in the struct, otherwise the
marshaler/unmarshaler throws an error.

In addition, this commit aligns the struct tag format more closely with
convention. All the marshaling options now belong under the "oscar" key.
Mike 2 vuotta sitten
vanhempi
commit
ef3b784a0c

+ 3 - 3
foodgroup/buddy_test.go

@@ -56,7 +56,7 @@ func TestBuddyService_AddBuddies(t *testing.T) {
 			sess: newTestSession("user_screen_name", sessOptSignonComplete),
 			bodyIn: wire.SNAC_0x03_0x04_BuddyAddBuddies{
 				Buddies: []struct {
-					ScreenName string `len_prefix:"uint8"`
+					ScreenName string `oscar:"len_prefix=uint8"`
 				}{
 					{
 						ScreenName: "buddy_1_online",
@@ -119,7 +119,7 @@ func TestBuddyService_AddBuddies(t *testing.T) {
 			sess: newTestSession("user_screen_name"),
 			bodyIn: wire.SNAC_0x03_0x04_BuddyAddBuddies{
 				Buddies: []struct {
-					ScreenName string `len_prefix:"uint8"`
+					ScreenName string `oscar:"len_prefix=uint8"`
 				}{
 					{
 						ScreenName: "buddy_1_online",
@@ -198,7 +198,7 @@ func TestBuddyService_DelBuddies(t *testing.T) {
 			sess: newTestSession("user_screen_name", sessOptSignonComplete),
 			bodyIn: wire.SNAC_0x03_0x05_BuddyDelBuddies{
 				Buddies: []struct {
-					ScreenName string `len_prefix:"uint8"`
+					ScreenName string `oscar:"len_prefix=uint8"`
 				}{
 					{
 						ScreenName: "buddy_1_online",

+ 3 - 3
foodgroup/oservice.go

@@ -75,7 +75,7 @@ var rateLimitSNAC = wire.SNAC_0x01_0x07_OServiceRateParamsReply{
 		Pairs []struct {
 			FoodGroup uint16
 			SubGroup  uint16
-		} `count_prefix:"uint16"`
+		} `oscar:"count_prefix=uint16"`
 	}{
 		{
 			ID: 1,
@@ -553,8 +553,8 @@ type OServiceServiceForBOS struct {
 // chatLoginCookie represents credentials used to authenticate a user chat
 // session.
 type chatLoginCookie struct {
-	ChatCookie string                  `len_prefix:"uint8"`
-	ScreenName state.DisplayScreenName `len_prefix:"uint8"`
+	ChatCookie string                  `oscar:"len_prefix=uint8"`
+	ScreenName state.DisplayScreenName `oscar:"len_prefix=uint8"`
 }
 
 // ServiceRequest handles service discovery, providing a host name and metadata

+ 1 - 1
foodgroup/oservice_test.go

@@ -503,7 +503,7 @@ func TestOServiceService_RateParamsQuery(t *testing.T) {
 				Pairs []struct {
 					FoodGroup uint16
 					SubGroup  uint16
-				} `count_prefix:"uint16"`
+				} `oscar:"count_prefix=uint16"`
 			}{
 				{
 					ID: 1,

+ 2 - 2
server/oscar/handler/buddy_test.go

@@ -65,7 +65,7 @@ func TestBuddyHandler_AddBuddies(t *testing.T) {
 		},
 		Body: wire.SNAC_0x03_0x04_BuddyAddBuddies{
 			Buddies: []struct {
-				ScreenName string `len_prefix:"uint8"`
+				ScreenName string `oscar:"len_prefix=uint8"`
 			}{
 				{
 					ScreenName: "user1",
@@ -97,7 +97,7 @@ func TestBuddyHandler_DelBuddies(t *testing.T) {
 		},
 		Body: wire.SNAC_0x03_0x05_BuddyDelBuddies{
 			Buddies: []struct {
-				ScreenName string `len_prefix:"uint8"`
+				ScreenName string `oscar:"len_prefix=uint8"`
 			}{
 				{
 					ScreenName: "user1",

+ 1 - 1
server/oscar/handler/oservice_test.go

@@ -221,7 +221,7 @@ func TestOServiceHandler_RateParamsQuery(t *testing.T) {
 				Pairs []struct {
 					FoodGroup uint16
 					SubGroup  uint16
-				} `count_prefix:"uint16"`
+				} `oscar:"count_prefix=uint16"`
 			}{
 				{
 					ID: 1,

+ 3 - 3
state/cookie.go

@@ -87,12 +87,12 @@ func (c HMACCookieBaker) Crack(data []byte) ([]byte, error) {
 
 type hmacTokenPayload struct {
 	Expiry uint32
-	Data   []byte `len_prefix:"uint16"`
+	Data   []byte `oscar:"len_prefix=uint16"`
 }
 
 type hmacToken struct {
-	Data []byte `len_prefix:"uint16"`
-	Sig  []byte `len_prefix:"uint16"`
+	Data []byte `oscar:"len_prefix=uint16"`
+	Sig  []byte `oscar:"len_prefix=uint16"`
 }
 
 func (h *hmacToken) hash(key []byte) {

+ 127 - 89
wire/decode.go

@@ -12,50 +12,38 @@ import (
 var ErrUnmarshalFailure = errors.New("failed to unmarshal")
 
 func Unmarshal(v any, r io.Reader) error {
-	return unmarshal(reflect.TypeOf(v).Elem(), reflect.ValueOf(v).Elem(), "", r)
+	if err := unmarshal(reflect.TypeOf(v).Elem(), reflect.ValueOf(v).Elem(), "", r); err != nil {
+		return fmt.Errorf("%w: %w", ErrUnmarshalFailure, err)
+	}
+	return nil
 }
 
 func unmarshal(t reflect.Type, v reflect.Value, tag reflect.StructTag, r io.Reader) error {
-	switch v.Kind() {
-	case reflect.Struct:
-		if lenTag, ok := tag.Lookup("len_prefix"); ok {
-			bufLen, err := readUnsignedInt(lenTag, r)
-			if err != nil {
-				return err
-			}
-			b := make([]byte, bufLen)
-			if bufLen > 0 {
-				if _, err := io.ReadFull(r, b); err != nil {
-					return err
-				}
-			}
-			r = bytes.NewBuffer(b)
-		}
-		for i := 0; i < v.NumField(); i++ {
-			if err := unmarshal(t.Field(i).Type, v.Field(i), t.Field(i).Tag, r); err != nil {
-				return err
-			}
+	oscTag, err := parseOSCARTag(tag)
+	if err != nil {
+		return fmt.Errorf("error parsing tag: %w", err)
+	}
+
+	if oscTag.optional {
+		v.Set(reflect.New(t.Elem()))
+		err := unmarshalStruct(t.Elem(), v.Elem(), oscTag, r)
+		if errors.Is(err, io.EOF) {
+			// no values to read, but that's ok since this struct is optional
+			v.Set(reflect.Zero(t))
+			err = nil
 		}
-		return nil
+		return err
+	} else if v.Kind() == reflect.Ptr {
+		return errNonOptionalPointer
+	}
+
+	switch v.Kind() {
+	case reflect.Slice:
+		return unmarshalSlice(v, oscTag, r)
 	case reflect.String:
-		var bufLen int
-		if lenTag, ok := tag.Lookup("len_prefix"); ok {
-			var err error
-			if bufLen, err = readUnsignedInt(lenTag, r); err != nil {
-				return err
-			}
-		} else {
-			return fmt.Errorf("%w: missing len_prefix tag", ErrUnmarshalFailure)
-		}
-		buf := make([]byte, bufLen)
-		if bufLen > 0 {
-			if _, err := io.ReadFull(r, buf); err != nil {
-				return err
-			}
-		}
-		// todo is there a more efficient way?
-		v.SetString(string(buf))
-		return nil
+		return unmarshalString(v, oscTag, r)
+	case reflect.Struct:
+		return unmarshalStruct(t, v, oscTag, r)
 	case reflect.Uint8:
 		var l uint8
 		if err := binary.Read(r, binary.BigEndian, &l); err != nil {
@@ -84,82 +72,132 @@ func unmarshal(t reflect.Type, v reflect.Value, tag reflect.StructTag, r io.Read
 		}
 		v.Set(reflect.ValueOf(l))
 		return nil
-	case reflect.Slice:
-		if lenTag, ok := tag.Lookup("len_prefix"); ok {
-			bufLen, err := readUnsignedInt(lenTag, r)
-			if err != nil {
+	default:
+		return fmt.Errorf("unsupported type %v", t.Kind())
+	}
+}
+
+func unmarshalSlice(v reflect.Value, oscTag oscarTag, r io.Reader) error {
+	slice := reflect.New(v.Type()).Elem()
+	elemType := v.Type().Elem()
+
+	if oscTag.hasLenPrefix {
+		bufLen, err := unmarshalUnsignedInt(oscTag.lenPrefix, r)
+		if err != nil {
+			return err
+		}
+		b := make([]byte, bufLen)
+		if bufLen > 0 {
+			if _, err := io.ReadFull(r, b); err != nil {
 				return err
 			}
-			buf := make([]byte, bufLen)
-			if bufLen > 0 {
-				if _, err := io.ReadFull(r, buf); err != nil {
-					return err
-				}
+		}
+		buf := bytes.NewBuffer(b)
+		for buf.Len() > 0 {
+			elem := reflect.New(elemType).Elem()
+			if err := unmarshal(elemType, elem, "", buf); err != nil {
+				return err
+			}
+			slice = reflect.Append(slice, elem)
+		}
+	} else if oscTag.hasCountPrefix {
+		count, err := unmarshalUnsignedInt(oscTag.countPrefix, r)
+		if err != nil {
+			return err
+		}
+
+		for i := 0; i < count; i++ {
+			elem := reflect.New(elemType).Elem()
+			if err := unmarshal(elemType, elem, "", r); err != nil {
+				return err
 			}
-			b := bytes.NewBuffer(buf)
-			slice := reflect.New(v.Type()).Elem()
-			// todo: if this is a slice of scalars, there should be no need to
-			//  call Unmarshal on each element. it should be possible to just
-			//  call binary.Read(r, binary.BigEndian, []byte)
-			for b.Len() > 0 {
-				v1 := reflect.New(v.Type().Elem()).Interface()
-				if err := Unmarshal(v1, b); err != nil {
-					return err
+			slice = reflect.Append(slice, elem)
+		}
+	} else {
+		for {
+			elem := reflect.New(elemType).Elem()
+			if err := unmarshal(elemType, elem, "", r); err != nil {
+				if errors.Is(err, io.EOF) {
+					break
 				}
-				slice = reflect.Append(slice, reflect.ValueOf(v1).Elem())
+				return err
 			}
-			v.Set(slice)
-		} else if countTag, ok := tag.Lookup("count_prefix"); ok {
-			count, err := readUnsignedInt(countTag, r)
-			if err != nil {
+			slice = reflect.Append(slice, elem)
+		}
+	}
+	v.Set(slice)
+	return nil
+}
+
+func unmarshalString(v reflect.Value, oscTag oscarTag, r io.Reader) error {
+	if !oscTag.hasLenPrefix {
+		return fmt.Errorf("missing len_prefix tag")
+	}
+	bufLen, err := unmarshalUnsignedInt(oscTag.lenPrefix, r)
+	if err != nil {
+		return err
+	}
+	buf := make([]byte, bufLen)
+	if bufLen > 0 {
+		if _, err := io.ReadFull(r, buf); err != nil {
+			return err
+		}
+	}
+	// todo is there a more efficient way?
+	v.SetString(string(buf))
+	return nil
+}
+
+func unmarshalStruct(t reflect.Type, v reflect.Value, oscTag oscarTag, r io.Reader) error {
+	if oscTag.hasLenPrefix {
+		bufLen, err := unmarshalUnsignedInt(oscTag.lenPrefix, r)
+		if err != nil {
+			return err
+		}
+		b := make([]byte, bufLen)
+		if bufLen > 0 {
+			if _, err := io.ReadFull(r, b); err != nil {
 				return err
 			}
-			slice := reflect.New(v.Type()).Elem()
-			for i := 0; i < count; i++ {
-				v1 := reflect.New(v.Type().Elem()).Interface()
-				if err := Unmarshal(v1, r); err != nil {
-					return err
-				}
-				slice = reflect.Append(slice, reflect.ValueOf(v1).Elem())
+		}
+		r = bytes.NewBuffer(b)
+	}
+	for i := 0; i < v.NumField(); i++ {
+		field := t.Field(i)
+		value := v.Field(i)
+		if field.Type.Kind() == reflect.Ptr {
+			if i != v.NumField()-1 {
+				return fmt.Errorf("pointer type found at non-final field %s", field.Name)
 			}
-			v.Set(slice)
-		} else {
-			slice := reflect.New(v.Type()).Elem()
-			for {
-				v1 := reflect.New(v.Type().Elem()).Interface()
-				if err := Unmarshal(v1, r); err != nil {
-					if err == io.EOF {
-						break
-					}
-					return err
-				}
-				slice = reflect.Append(slice, reflect.ValueOf(v1).Elem())
+			if field.Type.Elem().Kind() != reflect.Struct {
+				return fmt.Errorf("%w: field %s must point to a struct, got %v instead",
+					errNonOptionalPointer, field.Name, field.Type.Elem().Kind())
 			}
-			v.Set(slice)
 		}
-		return nil
-	default:
-		return fmt.Errorf("%w: unsupported type %v", ErrUnmarshalFailure, t.Kind())
+		if err := unmarshal(field.Type, value, field.Tag, r); err != nil {
+			return err
+		}
 	}
+	return nil
 }
 
-func readUnsignedInt(intType string, r io.Reader) (int, error) {
+func unmarshalUnsignedInt(intType reflect.Kind, r io.Reader) (int, error) {
 	var bufLen int
 	switch intType {
-	case "uint8":
+	case reflect.Uint8:
 		var l uint8
 		if err := binary.Read(r, binary.BigEndian, &l); err != nil {
 			return 0, err
 		}
 		bufLen = int(l)
-	case "uint16":
+	case reflect.Uint16:
 		var l uint16
 		if err := binary.Read(r, binary.BigEndian, &l); err != nil {
 			return 0, err
 		}
 		bufLen = int(l)
 	default:
-		return 0, fmt.Errorf("%w: unsupported len_prefix type %s. allowed types: uint8, uint16", ErrUnmarshalFailure, intType)
+		panic(fmt.Sprintf("unsupported type %s. allowed types: uint8, uint16", intType))
 	}
 	return bufLen, nil
 }

+ 195 - 32
wire/decode_test.go

@@ -99,10 +99,10 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "string8",
 			prototype: &struct {
-				Val string `len_prefix:"uint8"`
+				Val string `oscar:"len_prefix=uint8"`
 			}{},
 			want: &struct {
-				Val string `len_prefix:"uint8"`
+				Val string `oscar:"len_prefix=uint8"`
 			}{
 				Val: "test-value",
 			},
@@ -113,7 +113,7 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "string8 read error",
 			prototype: &struct {
-				Val string `len_prefix:"uint8"`
+				Val string `oscar:"len_prefix=uint8"`
 			}{},
 			given:   []byte{},
 			wantErr: io.EOF,
@@ -121,10 +121,10 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "string16",
 			prototype: &struct {
-				Val string `len_prefix:"uint16"`
+				Val string `oscar:"len_prefix=uint16"`
 			}{},
 			want: &struct {
-				Val string `len_prefix:"uint16"`
+				Val string `oscar:"len_prefix=uint16"`
 			}{
 				Val: "test-value",
 			},
@@ -135,7 +135,7 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "string16 read error",
 			prototype: &struct {
-				Val string `len_prefix:"uint16"`
+				Val string `oscar:"len_prefix=uint16"`
 			}{},
 			given:   []byte{},
 			wantErr: io.EOF,
@@ -143,7 +143,7 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "unsupported string prefix type",
 			prototype: &struct {
-				Val string `len_prefix:"uint128"`
+				Val string `oscar:"len_prefix=uint128"`
 			}{},
 			wantErr: ErrUnmarshalFailure,
 			given: append(
@@ -163,7 +163,7 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "partial string8",
 			prototype: &struct {
-				Val string `len_prefix:"uint8"`
+				Val string `oscar:"len_prefix=uint8"`
 			}{},
 			wantErr: io.EOF,
 			given: append(
@@ -173,10 +173,10 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "byte slice with uint8 len_prefix",
 			prototype: &struct {
-				Val []byte `len_prefix:"uint8"`
+				Val []byte `oscar:"len_prefix=uint8"`
 			}{},
 			want: &struct {
-				Val []byte `len_prefix:"uint8"`
+				Val []byte `oscar:"len_prefix=uint8"`
 			}{
 				Val: []byte(`hello`),
 			},
@@ -187,7 +187,7 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "slice of invalid type with uint8 len_prefix",
 			prototype: &struct {
-				Val []int `len_prefix:"uint8"`
+				Val []int `oscar:"len_prefix=uint8"`
 			}{},
 			wantErr: ErrUnmarshalFailure,
 			given:   []byte{0x04, 0x65, 0x6c, 0x6c, 0x6f},
@@ -195,7 +195,7 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "byte slice with uint8 len_prefix with read error",
 			prototype: &struct {
-				Val []byte `len_prefix:"uint8"`
+				Val []byte `oscar:"len_prefix=uint8"`
 			}{},
 			wantErr: io.EOF,
 			given: append(
@@ -205,7 +205,7 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "byte slice with uint8 len_prefix read error",
 			prototype: &struct {
-				Val []byte `len_prefix:"uint8"`
+				Val []byte `oscar:"len_prefix=uint8"`
 			}{},
 			wantErr: io.EOF,
 			given:   []byte{},
@@ -213,10 +213,10 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "byte slice with uint16 len_prefix",
 			prototype: &struct {
-				Val []byte `len_prefix:"uint16"`
+				Val []byte `oscar:"len_prefix=uint16"`
 			}{},
 			want: &struct {
-				Val []byte `len_prefix:"uint16"`
+				Val []byte `oscar:"len_prefix=uint16"`
 			}{
 				Val: []byte(`hello`),
 			},
@@ -227,7 +227,7 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "byte slice with uint16 len_prefix read error",
 			prototype: &struct {
-				Val []byte `len_prefix:"uint16"`
+				Val []byte `oscar:"len_prefix=uint16"`
 			}{},
 			wantErr: io.EOF,
 			given:   []byte{},
@@ -235,7 +235,7 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "byte slice with invalid len_prefix",
 			prototype: &struct {
-				Val []byte `len_prefix:"uint128"`
+				Val []byte `oscar:"len_prefix=uint128"`
 			}{},
 			wantErr: ErrUnmarshalFailure,
 			given: append(
@@ -268,10 +268,10 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "struct slice with uint8 count_prefix",
 			prototype: &struct {
-				Val []TLV `count_prefix:"uint8"`
+				Val []TLV `oscar:"count_prefix=uint8"`
 			}{},
 			want: &struct {
-				Val []TLV `count_prefix:"uint8"`
+				Val []TLV `oscar:"count_prefix=uint8"`
 			}{
 				Val: []TLV{
 					NewTLV(10, uint16(1234)),
@@ -287,7 +287,7 @@ func TestUnmarshal(t *testing.T) {
 			prototype: &struct {
 				Val []struct {
 					Val int16
-				} `count_prefix:"uint8"`
+				} `oscar:"count_prefix=uint8"`
 			}{},
 			wantErr: ErrUnmarshalFailure,
 			given: append(
@@ -297,7 +297,7 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "struct slice with uint8 count_prefix read error",
 			prototype: &struct {
-				Val []TLV `count_prefix:"uint8"`
+				Val []TLV `oscar:"count_prefix=uint8"`
 			}{},
 			wantErr: io.EOF,
 			given:   []byte{},
@@ -305,10 +305,10 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "struct slice with uint16 count_prefix",
 			prototype: &struct {
-				Val []TLV `count_prefix:"uint16"`
+				Val []TLV `oscar:"count_prefix=uint16"`
 			}{},
 			want: &struct {
-				Val []TLV `count_prefix:"uint16"`
+				Val []TLV `oscar:"count_prefix=uint16"`
 			}{
 				Val: []TLV{
 					NewTLV(10, uint16(1234)),
@@ -324,7 +324,7 @@ func TestUnmarshal(t *testing.T) {
 			prototype: &struct {
 				Val []struct {
 					Val int16
-				} `count_prefix:"uint16"`
+				} `oscar:"count_prefix=uint16"`
 			}{},
 			wantErr: ErrUnmarshalFailure,
 			given: append(
@@ -334,7 +334,7 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "struct slice with uint16 count_prefix read error",
 			prototype: &struct {
-				Val []TLV `count_prefix:"uint16"`
+				Val []TLV `oscar:"count_prefix=uint16"`
 			}{},
 			wantErr: io.EOF,
 			given:   []byte{},
@@ -342,7 +342,7 @@ func TestUnmarshal(t *testing.T) {
 		{
 			name: "struct slice with invalid count_prefix",
 			prototype: &struct {
-				Val []TLV `count_prefix:"uint128"`
+				Val []TLV `oscar:"count_prefix=uint128"`
 			}{},
 			wantErr: ErrUnmarshalFailure,
 			given: append(
@@ -356,7 +356,7 @@ func TestUnmarshal(t *testing.T) {
 				Val1 struct {
 					Val2 uint16
 					Val3 uint8
-				} `len_prefix:"uint8"`
+				} `oscar:"len_prefix=uint8"`
 				Val4 uint16
 			}{},
 			want: &struct {
@@ -364,7 +364,7 @@ func TestUnmarshal(t *testing.T) {
 				Val1 struct {
 					Val2 uint16
 					Val3 uint8
-				} `len_prefix:"uint8"`
+				} `oscar:"len_prefix=uint8"`
 				Val4 uint16
 			}{
 				Val0: 34,
@@ -392,7 +392,7 @@ func TestUnmarshal(t *testing.T) {
 				Val1 struct {
 					Val2 uint16
 					Val3 uint8
-				} `len_prefix:"uint16"`
+				} `oscar:"len_prefix=uint16"`
 				Val4 uint16
 			}{},
 			want: &struct {
@@ -400,7 +400,7 @@ func TestUnmarshal(t *testing.T) {
 				Val1 struct {
 					Val2 uint16
 					Val3 uint8
-				} `len_prefix:"uint16"`
+				} `oscar:"len_prefix=uint16"`
 				Val4 uint16
 			}{
 				Val0: 34,
@@ -426,7 +426,7 @@ func TestUnmarshal(t *testing.T) {
 			prototype: &struct {
 				Val1 struct {
 					Val2 uint16
-				} `len_prefix:"uint16"`
+				} `oscar:"len_prefix=uint16"`
 			}{},
 			given: []byte{
 				0x00, 0x10, // 16 byte len, but the body is truncated
@@ -438,10 +438,173 @@ func TestUnmarshal(t *testing.T) {
 			prototype: &struct {
 				Val1 struct {
 					Val2 uint16
-				} `len_prefix:"uint128"`
+				} `oscar:"len_prefix=uint128"`
 			}{},
 			wantErr: ErrUnmarshalFailure,
 		},
+		{
+			name: "optional struct has value",
+			prototype: &struct {
+				Val0     uint16
+				Optional *struct {
+					Val1 uint16
+				} `oscar:"optional"`
+			}{},
+			want: &struct {
+				Val0     uint16
+				Optional *struct {
+					Val1 uint16
+				} `oscar:"optional"`
+			}{
+				Val0: 34,
+				Optional: &struct {
+					Val1 uint16
+				}{
+					Val1: 100,
+				},
+			},
+			given: []byte{
+				0x00, 0x22, // Val0
+				0x00, 0x64, // Val1
+			},
+		},
+		{
+			name: "optional struct with value missing `optional` struct tag",
+			prototype: &struct {
+				Val0     uint16
+				Optional *struct {
+					Val1 uint16
+				}
+			}{},
+			given: []byte{
+				0x00, 0x22, // Val0
+				0x00, 0x64, // Val1
+			},
+			wantErr: ErrUnmarshalFailure,
+		},
+		{
+			name: "optional struct doesn't have value",
+			prototype: &struct {
+				Val0     uint16
+				Optional *struct {
+					Val1 uint16
+				} `oscar:"optional"`
+			}{},
+			want: &struct {
+				Val0     uint16
+				Optional *struct {
+					Val1 uint16
+				} `oscar:"optional"`
+			}{
+				Val0:     34,
+				Optional: nil,
+			},
+			given: []byte{
+				0x00, 0x22, // Val0
+			},
+		},
+		{
+			name: "optional struct followed by value throws error",
+			prototype: &struct {
+				Optional *struct {
+					Val0 uint16
+				} `oscar:"optional"`
+				Val1 uint16
+			}{},
+			wantErr: ErrUnmarshalFailure,
+			given: []byte{
+				0x00, 0x22, // Val0
+				0x00, 0x22, // Val1
+			},
+		},
+		{
+			name: "optional non-struct field throws error",
+			prototype: &struct {
+				Optional *uint16 `oscar:"optional"`
+			}{},
+			wantErr: ErrUnmarshalFailure,
+			given: []byte{
+				0x00, 0x22, // Val0
+			},
+		},
+		{
+			name: "non-struct pointer value throws error",
+			prototype: func() any {
+				val := 10
+				ptr1 := &val
+				return &ptr1
+			}(),
+			wantErr: ErrUnmarshalFailure,
+			given: []byte{
+				0x00, 0x22, // Val0
+			},
+		},
+		{
+			name: "optional struct with uint16 len_prefix and value",
+			prototype: &struct {
+				Val0 uint8
+				Val1 *struct {
+					Val2 uint16
+					Val3 uint8
+				} `oscar:"len_prefix=uint16,optional"`
+			}{},
+			want: &struct {
+				Val0 uint8
+				Val1 *struct {
+					Val2 uint16
+					Val3 uint8
+				} `oscar:"len_prefix=uint16,optional"`
+			}{
+				Val0: 34,
+				Val1: &struct {
+					Val2 uint16
+					Val3 uint8
+				}{
+					Val2: 16,
+					Val3: 10,
+				},
+			},
+			given: []byte{
+				0x22,       // Val0
+				0x00, 0x03, // Val1 struct len
+				0x00, 0x10, // Val2
+				0x0A,       // Val3
+				0x00, 0x20, // Val2
+			},
+		},
+		{
+			name: "optional struct with uint16 len_prefix and no value",
+			prototype: &struct {
+				Val0 uint8
+				Val1 *struct {
+					Val2 uint16
+					Val3 uint8
+				} `oscar:"len_prefix=uint16,optional"`
+			}{},
+			want: &struct {
+				Val0 uint8
+				Val1 *struct {
+					Val2 uint16
+					Val3 uint8
+				} `oscar:"len_prefix=uint16,optional"`
+			}{
+				Val0: 34,
+				Val1: nil,
+			},
+			given: []byte{
+				0x22, // Val0
+			},
+		},
+		{
+			name: "optional field that isn't a pointer to a struct is unsupported",
+			prototype: &struct {
+				Val1 *string `oscar:"optional"`
+			}{},
+			given: []byte{
+				0x00,
+			},
+			wantErr: errNonOptionalPointer,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {

+ 163 - 64
wire/encode.go

@@ -7,108 +7,207 @@ import (
 	"fmt"
 	"io"
 	"reflect"
+	"strings"
 )
 
-var ErrMarshalFailure = errors.New("failed to marshal")
-var ErrMarshalFailureNilSNAC = errors.New("attempting to marshal a nil SNAC")
+var (
+	ErrMarshalFailure        = errors.New("failed to marshal")
+	errMarshalFailureNilSNAC = errors.New("attempting to marshal a nil SNAC")
+	errNonOptionalPointer    = errors.New("pointer fields must reference structs and have an `optional` struct tag")
+	errOptionalNonPointer    = errors.New("optional fields must be pointers")
+	errInvalidStructTag      = errors.New("invalid struct tag")
+)
 
 func Marshal(v any, w io.Writer) error {
-	return marshal(reflect.TypeOf(v), reflect.ValueOf(v), "", w)
+	if err := marshal(reflect.TypeOf(v), reflect.ValueOf(v), "", w); err != nil {
+		return fmt.Errorf("%w: %w", ErrMarshalFailure, err)
+	}
+	return nil
 }
 
 func marshal(t reflect.Type, v reflect.Value, tag reflect.StructTag, w io.Writer) error {
 	if t == nil {
-		return ErrMarshalFailureNilSNAC
+		return errMarshalFailureNilSNAC
 	}
-	switch t.Kind() {
-	case reflect.Struct:
-		marshalEachField := func(w io.Writer) error {
-			for i := 0; i < t.NumField(); i++ {
-				if err := marshal(t.Field(i).Type, v.Field(i), t.Field(i).Tag, w); err != nil {
-					return err
-				}
-			}
-			return nil
+
+	oscTag, err := parseOSCARTag(tag)
+	if err != nil {
+		return err
+	}
+
+	if oscTag.optional {
+		if t.Kind() != reflect.Ptr {
+			return fmt.Errorf("%w: got %v", errOptionalNonPointer, t.Kind())
 		}
-		if lenTag, ok := tag.Lookup("len_prefix"); ok {
-			buf := &bytes.Buffer{}
-			if err := marshalEachField(buf); err != nil {
-				return err
-			}
-			// write struct length
-			if err := writeUnsignedInt(lenTag, buf.Len(), w); err != nil {
-				return err
-			}
-			// write struct bytes
-			if buf.Len() > 0 {
-				_, err := w.Write(buf.Bytes())
-				return err
-			}
-			return nil
+		if v.IsNil() {
+			return nil // nil value
 		}
-		return marshalEachField(w)
+		// dereference pointer
+		return marshalStruct(t.Elem(), v.Elem(), oscTag, w)
+	} else if t.Kind() == reflect.Ptr {
+		return errNonOptionalPointer
+	}
+
+	switch t.Kind() {
+	case reflect.Slice:
+		return marshalSlice(t, v, oscTag, w)
 	case reflect.String:
-		if lenTag, ok := tag.Lookup("len_prefix"); ok {
-			if err := writeUnsignedInt(lenTag, len(v.String()), w); err != nil {
+		return marshalString(oscTag, v, w)
+	case reflect.Struct:
+		return marshalStruct(t, v, oscTag, w)
+	case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		return binary.Write(w, binary.BigEndian, v.Interface())
+	default:
+		return fmt.Errorf("unsupported type %v", t.Kind())
+	}
+}
+
+func marshalSlice(t reflect.Type, v reflect.Value, oscTag oscarTag, w io.Writer) error {
+	// todo: only write to temporary buffer if len_prefix is set
+	buf := &bytes.Buffer{}
+	if t.Elem().Kind() == reflect.Struct {
+		for j := 0; j < v.Len(); j++ {
+			if err := marshalStruct(t.Elem(), v.Index(j), oscarTag{}, buf); err != nil {
 				return err
 			}
 		}
-		return binary.Write(w, binary.BigEndian, []byte(v.String()))
-	case reflect.Slice:
-		// todo: only write to temporary buffer if len_prefix is set
-		buf := &bytes.Buffer{}
-		if t.Elem().Kind() == reflect.Struct {
-			for j := 0; j < v.Len(); j++ {
-				element := v.Index(j)
-				if err := Marshal(element.Interface(), buf); err != nil {
-					return err
-				}
-			}
-		} else {
-			if err := binary.Write(buf, binary.BigEndian, v.Interface()); err != nil {
-				return fmt.Errorf("%w: error marshalling %s", ErrMarshalFailure, t.Elem().Kind())
-			}
+	} else {
+		if err := binary.Write(buf, binary.BigEndian, v.Interface()); err != nil {
+			return fmt.Errorf("error marshalling %s", t.Elem().Kind())
 		}
+	}
 
-		var hasLenPrefix bool
-		if l, ok := tag.Lookup("len_prefix"); ok {
-			hasLenPrefix = true
-			if err := writeUnsignedInt(l, buf.Len(), w); err != nil {
-				return err
-			}
+	if oscTag.hasLenPrefix {
+		if err := marshalUnsignedInt(oscTag.lenPrefix, buf.Len(), w); err != nil {
+			return err
+		}
+	} else if oscTag.hasCountPrefix {
+		if err := marshalUnsignedInt(oscTag.countPrefix, v.Len(), w); err != nil {
+			return err
+		}
+	}
+	if buf.Len() > 0 {
+		_, err := w.Write(buf.Bytes())
+		return err
+	}
+	return nil
+}
+
+func marshalString(oscTag oscarTag, v reflect.Value, w io.Writer) error {
+	if oscTag.hasLenPrefix {
+		if err := marshalUnsignedInt(oscTag.lenPrefix, len(v.String()), w); err != nil {
+			return err
 		}
-		if l, ok := tag.Lookup("count_prefix"); ok {
-			if hasLenPrefix {
-				return fmt.Errorf("%w: struct elem has both len_prefix and count_prefix: ", ErrMarshalFailure)
+	}
+	return binary.Write(w, binary.BigEndian, []byte(v.String()))
+}
+
+func marshalStruct(t reflect.Type, v reflect.Value, oscTag oscarTag, w io.Writer) error {
+	marshalEachField := func(w io.Writer) error {
+		for i := 0; i < t.NumField(); i++ {
+			field := t.Field(i)
+			value := v.Field(i)
+			if field.Type.Kind() == reflect.Ptr {
+				if i != t.NumField()-1 {
+					return fmt.Errorf("pointer type found at non-final field %s", field.Name)
+				}
+				if field.Type.Elem().Kind() != reflect.Struct {
+					return fmt.Errorf("field %s must point to a struct, got %v instead", field.Name,
+						field.Type.Elem().Kind())
+				}
 			}
-			if err := writeUnsignedInt(l, v.Len(), w); err != nil {
+			if err := marshal(field.Type, value, field.Tag, w); err != nil {
 				return err
 			}
 		}
+		return nil
+	}
+	if oscTag.hasLenPrefix {
+		buf := &bytes.Buffer{}
+		if err := marshalEachField(buf); err != nil {
+			return err
+		}
+		// write struct length
+		if err := marshalUnsignedInt(oscTag.lenPrefix, buf.Len(), w); err != nil {
+			return err
+		}
+		// write struct bytes
 		if buf.Len() > 0 {
 			_, err := w.Write(buf.Bytes())
 			return err
 		}
 		return nil
-	case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
-		return binary.Write(w, binary.BigEndian, v.Interface())
-	default:
-		return fmt.Errorf("%w: unsupported type %v", ErrMarshalFailure, t.Kind())
 	}
+	return marshalEachField(w)
 }
 
-func writeUnsignedInt(intType string, intVal int, w io.Writer) error {
+func marshalUnsignedInt(intType reflect.Kind, intVal int, w io.Writer) error {
 	switch intType {
-	case "uint8":
+	case reflect.Uint8:
 		if err := binary.Write(w, binary.BigEndian, uint8(intVal)); err != nil {
 			return err
 		}
-	case "uint16":
+	case reflect.Uint16:
 		if err := binary.Write(w, binary.BigEndian, uint16(intVal)); err != nil {
 			return err
 		}
 	default:
-		return fmt.Errorf("%w: unsupported type %s. allowed types: uint8, uint16", ErrMarshalFailure, intType)
+		panic(fmt.Sprintf("unsupported type %s. allowed types: uint8, uint16", intType))
 	}
 	return nil
 }
+
+type oscarTag struct {
+	hasCountPrefix bool
+	countPrefix    reflect.Kind
+	hasLenPrefix   bool
+	lenPrefix      reflect.Kind
+	optional       bool
+}
+
+func parseOSCARTag(tag reflect.StructTag) (oscarTag, error) {
+	var oscTag oscarTag
+
+	val, ok := tag.Lookup("oscar")
+	if !ok {
+		return oscTag, nil
+	}
+
+	for _, kv := range strings.Split(val, ",") {
+		kvSplit := strings.SplitN(kv, "=", 2)
+		if len(kvSplit) == 2 {
+			switch kvSplit[0] {
+			case "len_prefix":
+				oscTag.hasLenPrefix = true
+				switch kvSplit[1] {
+				case "uint8":
+					oscTag.lenPrefix = reflect.Uint8
+				case "uint16":
+					oscTag.lenPrefix = reflect.Uint16
+				default:
+					return oscTag, fmt.Errorf("%w: unsupported type %s. allowed types: uint8, uint16",
+						errInvalidStructTag, kvSplit[1])
+				}
+			case "count_prefix":
+				oscTag.hasCountPrefix = true
+				switch kvSplit[1] {
+				case "uint8":
+					oscTag.countPrefix = reflect.Uint8
+				case "uint16":
+					oscTag.countPrefix = reflect.Uint16
+				default:
+					return oscTag, fmt.Errorf("%w: unsupported type %s. allowed types: uint8, uint16",
+						errInvalidStructTag, kvSplit[1])
+				}
+			}
+		} else {
+			oscTag.optional = kvSplit[0] == "optional"
+		}
+	}
+
+	var err error
+	if oscTag.hasCountPrefix && oscTag.hasLenPrefix {
+		err = fmt.Errorf("%w: struct elem has both len_prefix and count_prefix", errInvalidStructTag)
+	}
+	return oscTag, err
+}

+ 183 - 23
wire/encode_test.go

@@ -77,7 +77,7 @@ func TestMarshal(t *testing.T) {
 			name: "string8",
 			w:    &bytes.Buffer{},
 			given: struct {
-				Val string `len_prefix:"uint8"`
+				Val string `oscar:"len_prefix=uint8"`
 			}{
 				Val: "test-value",
 			},
@@ -89,7 +89,7 @@ func TestMarshal(t *testing.T) {
 			name: "string8 write error",
 			w:    errWriter{},
 			given: struct {
-				Val string `len_prefix:"uint8"`
+				Val string `oscar:"len_prefix=uint8"`
 			}{
 				Val: "test-value",
 			},
@@ -99,7 +99,7 @@ func TestMarshal(t *testing.T) {
 			name: "string16",
 			w:    &bytes.Buffer{},
 			given: struct {
-				Val string `len_prefix:"uint16"`
+				Val string `oscar:"len_prefix=uint16"`
 			}{
 				Val: "test-value",
 			},
@@ -111,7 +111,7 @@ func TestMarshal(t *testing.T) {
 			name: "string16 write error",
 			w:    errWriter{},
 			given: struct {
-				Val string `len_prefix:"uint16"`
+				Val string `oscar:"len_prefix=uint16"`
 			}{
 				Val: "test-value",
 			},
@@ -121,7 +121,7 @@ func TestMarshal(t *testing.T) {
 			name: "string with unknown prefix type",
 			w:    &bytes.Buffer{},
 			given: struct {
-				Val string `len_prefix:"uint128"`
+				Val string `oscar:"len_prefix=uint128"`
 			}{
 				Val: "test-value",
 			},
@@ -171,7 +171,7 @@ func TestMarshal(t *testing.T) {
 			name: "byte slice with uint8 len_prefix",
 			w:    &bytes.Buffer{},
 			given: struct {
-				Val []byte `len_prefix:"uint8"`
+				Val []byte `oscar:"len_prefix=uint8"`
 			}{
 				Val: []byte(`hello`),
 			},
@@ -183,7 +183,7 @@ func TestMarshal(t *testing.T) {
 			name: "byte slice with uint8 len_prefix with error",
 			w:    errWriter{},
 			given: struct {
-				Val []byte `len_prefix:"uint8"`
+				Val []byte `oscar:"len_prefix=uint8"`
 			}{
 				Val: []byte(`hello`),
 			},
@@ -193,7 +193,7 @@ func TestMarshal(t *testing.T) {
 			name: "byte slice with uint16 len_prefix",
 			w:    &bytes.Buffer{},
 			given: struct {
-				Val []byte `len_prefix:"uint16"`
+				Val []byte `oscar:"len_prefix=uint16"`
 			}{
 				Val: []byte(`hello`),
 			},
@@ -205,7 +205,7 @@ func TestMarshal(t *testing.T) {
 			name: "byte slice with uint16 len_prefix with error",
 			w:    errWriter{},
 			given: struct {
-				Val []byte `len_prefix:"uint16"`
+				Val []byte `oscar:"len_prefix=uint16"`
 			}{
 				Val: []byte(`hello`),
 			},
@@ -243,7 +243,7 @@ func TestMarshal(t *testing.T) {
 			name: "struct slice with uint8 count_prefix",
 			w:    &bytes.Buffer{},
 			given: struct {
-				Val []TLV `count_prefix:"uint8"`
+				Val []TLV `oscar:"count_prefix=uint8"`
 			}{
 				Val: []TLV{
 					NewTLV(10, uint16(1234)),
@@ -258,7 +258,7 @@ func TestMarshal(t *testing.T) {
 			name: "struct slice with uint8 count_prefix with error",
 			w:    errWriter{},
 			given: struct {
-				Val []TLV `count_prefix:"uint8"`
+				Val []TLV `oscar:"count_prefix=uint8"`
 			}{
 				Val: []TLV{
 					NewTLV(10, uint16(1234)),
@@ -270,7 +270,7 @@ func TestMarshal(t *testing.T) {
 			name: "struct slice with uint16 count_prefix",
 			w:    &bytes.Buffer{},
 			given: struct {
-				Val []TLV `count_prefix:"uint16"`
+				Val []TLV `oscar:"count_prefix=uint16"`
 			}{
 				Val: []TLV{
 					NewTLV(10, uint16(1234)),
@@ -285,7 +285,7 @@ func TestMarshal(t *testing.T) {
 			name: "struct slice with uint16 count_prefix with error",
 			w:    errWriter{},
 			given: struct {
-				Val []TLV `count_prefix:"uint16"`
+				Val []TLV `oscar:"count_prefix=uint16"`
 			}{
 				Val: []TLV{
 					NewTLV(10, uint16(1234)),
@@ -297,7 +297,7 @@ func TestMarshal(t *testing.T) {
 			name: "byte slice with uint16 len_prefix and uint16 count_prefix",
 			w:    &bytes.Buffer{},
 			given: struct {
-				Val []byte `len_prefix:"uint16" count_prefix:"uint16"`
+				Val []byte `oscar:"len_prefix=uint16,count_prefix=uint16"`
 			}{
 				Val: []byte(`hello`),
 			},
@@ -307,7 +307,7 @@ func TestMarshal(t *testing.T) {
 			name: "byte slice with unknown len_prefix type",
 			w:    &bytes.Buffer{},
 			given: struct {
-				Val []byte `len_prefix:"uint128"`
+				Val []byte `oscar:"len_prefix=uint128"`
 			}{
 				Val: []byte(`hello`),
 			},
@@ -317,17 +317,17 @@ func TestMarshal(t *testing.T) {
 			name: "byte slice with unknown count_prefix type",
 			w:    &bytes.Buffer{},
 			given: struct {
-				Val []byte `count_prefix:"uint128"`
+				Val []byte `oscar:"count_prefix=uint128"`
 			}{
 				Val: []byte(`hello`),
 			},
-			wantErr: ErrMarshalFailure,
+			wantErr: errInvalidStructTag,
 		},
 		{
 			name:    "empty snac",
 			w:       &bytes.Buffer{},
 			given:   nil,
-			wantErr: ErrMarshalFailureNilSNAC,
+			wantErr: errMarshalFailureNilSNAC,
 		},
 		{
 			name: "struct with uint8 len_prefix",
@@ -337,7 +337,7 @@ func TestMarshal(t *testing.T) {
 				Val1 struct {
 					Val2 uint16
 					Val3 uint8
-				} `len_prefix:"uint8"`
+				} `oscar:"len_prefix=uint8"`
 				Val4 uint16
 			}{
 				Val0: 34,
@@ -366,7 +366,7 @@ func TestMarshal(t *testing.T) {
 				Val1 struct {
 					Val2 uint16
 					Val3 uint8
-				} `len_prefix:"uint16"`
+				} `oscar:"len_prefix=uint16"`
 				Val4 uint16
 			}{
 				Val0: 34,
@@ -393,7 +393,7 @@ func TestMarshal(t *testing.T) {
 			given: struct {
 				Val1 struct {
 					Val2 int
-				} `len_prefix:"uint16"`
+				} `oscar:"len_prefix=uint16"`
 			}{
 				Val1: struct {
 					Val2 int
@@ -408,7 +408,7 @@ func TestMarshal(t *testing.T) {
 			w:    &bytes.Buffer{},
 			given: struct {
 				Val1 struct {
-				} `len_prefix:"uint16"`
+				} `oscar:"len_prefix=uint16"`
 			}{
 				Val1: struct {
 				}{},
@@ -423,7 +423,7 @@ func TestMarshal(t *testing.T) {
 			given: struct {
 				Val1 struct {
 					Val2 uint16
-				} `len_prefix:"uint128"`
+				} `oscar:"len_prefix=uint128"`
 			}{
 				Val1: struct {
 					Val2 uint16
@@ -431,6 +431,166 @@ func TestMarshal(t *testing.T) {
 					Val2: 16,
 				},
 			},
+			wantErr: errInvalidStructTag,
+		},
+		{
+			name: "optional struct has value",
+			w:    &bytes.Buffer{},
+			given: struct {
+				Val0     uint16
+				Optional *struct {
+					Val1 uint16
+				} `oscar:"optional"`
+			}{
+				Val0: 34,
+				Optional: &struct {
+					Val1 uint16
+				}{
+					Val1: 100,
+				},
+			},
+			want: []byte{
+				0x00, 0x22, // Val0
+				0x00, 0x64, // Val1
+			},
+		},
+		{
+			name: "optional struct with value missing `optional` struct tag",
+			w:    &bytes.Buffer{},
+			given: struct {
+				Val0     uint16
+				Optional *struct {
+					Val1 uint16
+				}
+			}{
+				Val0: 34,
+				Optional: &struct {
+					Val1 uint16
+				}{
+					Val1: 100,
+				},
+			},
+			wantErr: errNonOptionalPointer,
+		},
+		{
+			name: "optional struct doesn't have value",
+			w:    &bytes.Buffer{},
+			given: struct {
+				Val0     uint16
+				Optional *struct {
+					Val1 uint16
+				} `oscar:"optional"`
+			}{
+				Val0:     34,
+				Optional: nil,
+			},
+			want: []byte{
+				0x00, 0x22, // Val0
+			},
+		},
+		{
+			name: "optional struct not last field throws error",
+			w:    &bytes.Buffer{},
+			given: struct {
+				Optional *struct {
+					Val1 uint16
+				} `oscar:"optional"`
+				Val0 uint16
+			}{
+				Optional: nil,
+				Val0:     34,
+			},
+			wantErr: ErrMarshalFailure,
+		},
+		{
+			name: "optional non-pointer struct field throws error",
+			w:    &bytes.Buffer{},
+			given: struct {
+				Optional *string `oscar:"optional"`
+			}{
+				Optional: func() *string {
+					v := "blahblah"
+					return &v
+				}(),
+			},
+			wantErr: ErrMarshalFailure,
+		},
+		{
+			name: "non-struct pointer value throws error",
+			w:    &bytes.Buffer{},
+			given: func() *string {
+				v := "blahblah"
+				return &v
+			}(),
+			wantErr: ErrMarshalFailure,
+		},
+		{
+			name: "optional struct with uint16 len_prefix and value",
+			w:    &bytes.Buffer{},
+			given: struct {
+				Val0     uint16
+				Optional *struct {
+					Val1 uint16
+				} `oscar:"len_prefix=uint16,optional"`
+			}{
+				Val0: 34,
+				Optional: &struct {
+					Val1 uint16
+				}{
+					Val1: 100,
+				},
+			},
+			want: []byte{
+				0x00, 0x22, // Val0
+				0x00, 0x02, // Val0
+				0x00, 0x64, // Val1
+			},
+		},
+		{
+			name: "optional struct with uint16 len_prefix and no value",
+			w:    &bytes.Buffer{},
+			given: struct {
+				Val0     uint16
+				Optional *struct {
+					Val1 uint16
+				} `oscar:"len_prefix=uint16,optional"`
+			}{
+				Val0:     34,
+				Optional: nil,
+			},
+			want: []byte{
+				0x00, 0x22, // Val0
+			},
+		},
+		{
+			name: "optional field that isn't a pointer is unsupported",
+			w:    &bytes.Buffer{},
+			given: struct {
+				Val0     uint16
+				Optional struct {
+					Val1 uint16
+				} `oscar:"len_prefix=uint16,optional"`
+			}{
+				Val0: 34,
+				Optional: struct {
+					Val1 uint16
+				}{
+					Val1: 100,
+				},
+			},
+			wantErr: errOptionalNonPointer,
+		},
+		{
+			name: "optional field that isn't a pointer to a struct is unsupported",
+			w:    &bytes.Buffer{},
+			given: struct {
+				Optional *string `oscar:"len_prefix=uint16,optional"`
+			}{
+				Optional: func() *string {
+					v := "blahblah"
+					return &v
+				}(),
+			},
 			wantErr: ErrMarshalFailure,
 		},
 	}

+ 1 - 1
wire/frames.go

@@ -23,7 +23,7 @@ type FLAPFrame struct {
 	StartMarker uint8
 	FrameType   uint8
 	Sequence    uint16
-	Payload     []byte `len_prefix:"uint16"`
+	Payload     []byte `oscar:"len_prefix=uint16"`
 }
 
 type SNACFrame struct {

+ 25 - 25
wire/snacs.go

@@ -205,7 +205,7 @@ type SNAC_0x01_0x04_OServiceServiceRequest struct {
 
 type SNAC_0x01_0x04_TLVRoomInfo struct {
 	Exchange       uint16
-	Cookie         string `len_prefix:"uint8"`
+	Cookie         string `oscar:"len_prefix=uint8"`
 	InstanceNumber uint16
 }
 
@@ -248,13 +248,13 @@ type SNAC_0x01_0x07_OServiceRateParamsReply struct {
 		MaxLevel        uint32
 		LastTime        uint32
 		CurrentState    uint8
-	} `count_prefix:"uint16"`
+	} `oscar:"count_prefix=uint16"`
 	RateGroups []struct {
 		ID    uint16
 		Pairs []struct {
 			FoodGroup uint16
 			SubGroup  uint16
-		} `count_prefix:"uint16"`
+		} `oscar:"count_prefix=uint16"`
 	}
 }
 
@@ -390,7 +390,7 @@ type SNAC_0x02_0x0A_LocateSetDirReply struct {
 }
 
 type SNAC_0x02_0x0B_LocateGetDirInfo struct {
-	WatcherScreenNames string `len_prefix:"uint8"`
+	WatcherScreenNames string `oscar:"len_prefix=uint8"`
 }
 
 type SNAC_0x02_0x0F_LocateSetKeywordInfo struct {
@@ -404,7 +404,7 @@ type SNAC_0x02_0x10_LocateSetKeywordReply struct {
 
 type SNAC_0x02_0x05_LocateUserInfoQuery struct {
 	Type       uint16
-	ScreenName string `len_prefix:"uint8"`
+	ScreenName string `oscar:"len_prefix=uint8"`
 }
 
 func (s SNAC_0x02_0x05_LocateUserInfoQuery) RequestProfile() bool {
@@ -417,7 +417,7 @@ func (s SNAC_0x02_0x05_LocateUserInfoQuery) RequestAwayMessage() bool {
 
 type SNAC_0x02_0x15_LocateUserInfoQuery2 struct {
 	Type2      uint32
-	ScreenName string `len_prefix:"uint8"`
+	ScreenName string `oscar:"len_prefix=uint8"`
 }
 
 //
@@ -456,13 +456,13 @@ type SNAC_0x03_0x03_BuddyRightsReply struct {
 
 type SNAC_0x03_0x04_BuddyAddBuddies struct {
 	Buddies []struct {
-		ScreenName string `len_prefix:"uint8"`
+		ScreenName string `oscar:"len_prefix=uint8"`
 	}
 }
 
 type SNAC_0x03_0x05_BuddyDelBuddies struct {
 	Buddies []struct {
-		ScreenName string `len_prefix:"uint8"`
+		ScreenName string `oscar:"len_prefix=uint8"`
 	}
 }
 
@@ -529,7 +529,7 @@ const (
 type ICBMFragment struct {
 	ID      uint8
 	Version uint8
-	Payload []byte `len_prefix:"uint16"`
+	Payload []byte `oscar:"len_prefix=uint16"`
 }
 
 // ICBMMessage represents the text component of an ICBM message.
@@ -560,7 +560,7 @@ type SNAC_0x04_0x05_ICBMParameterReply struct {
 type SNAC_0x04_0x06_ICBMChannelMsgToHost struct {
 	Cookie     uint64
 	ChannelID  uint16
-	ScreenName string `len_prefix:"uint8"`
+	ScreenName string `oscar:"len_prefix=uint8"`
 	TLVRestBlock
 }
 
@@ -622,7 +622,7 @@ func UnmarshalICBMMessageText(b []byte) (string, error) {
 
 type SNAC_0x04_0x08_ICBMEvilRequest struct {
 	SendAs     uint16
-	ScreenName string `len_prefix:"uint8"`
+	ScreenName string `oscar:"len_prefix=uint8"`
 }
 
 type SNAC_0x04_0x09_ICBMEvilReply struct {
@@ -633,7 +633,7 @@ type SNAC_0x04_0x09_ICBMEvilReply struct {
 type SNAC_0x04_0x0B_ICBMClientErr struct {
 	Cookie     uint64
 	ChannelID  uint16
-	ScreenName string `len_prefix:"uint8"`
+	ScreenName string `oscar:"len_prefix=uint8"`
 	Code       uint16
 	ErrInfo    []byte
 }
@@ -641,13 +641,13 @@ type SNAC_0x04_0x0B_ICBMClientErr struct {
 type SNAC_0x04_0x0C_ICBMHostAck struct {
 	Cookie     uint64
 	ChannelID  uint16
-	ScreenName string `len_prefix:"uint8"`
+	ScreenName string `oscar:"len_prefix=uint8"`
 }
 
 type SNAC_0x04_0x14_ICBMClientEvent struct {
 	Cookie     uint64
 	ChannelID  uint16
-	ScreenName string `len_prefix:"uint8"`
+	ScreenName string `oscar:"len_prefix=uint8"`
 	Event      uint16
 }
 
@@ -808,7 +808,7 @@ type SNAC_0x0D_0x03_ChatNavRequestExchangeInfo struct {
 
 type SNAC_0x0D_0x04_ChatNavRequestRoomInfo struct {
 	Exchange       uint16
-	Cookie         string `len_prefix:"uint8"`
+	Cookie         string `oscar:"len_prefix=uint8"`
 	InstanceNumber uint16
 	DetailLevel    uint8
 }
@@ -894,7 +894,7 @@ const (
 
 type SNAC_0x0E_0x02_ChatRoomInfoUpdate struct {
 	Exchange       uint16
-	Cookie         string `len_prefix:"uint8"`
+	Cookie         string `oscar:"len_prefix=uint8"`
 	InstanceNumber uint16
 	DetailLevel    uint8
 	TLVBlock
@@ -998,7 +998,7 @@ func GetClearIconHash() []byte {
 // BARTInfo represents a BART feedbag item
 type BARTInfo struct {
 	Flags uint8
-	Hash  []byte `len_prefix:"uint8"`
+	Hash  []byte `oscar:"len_prefix=uint8"`
 }
 
 // HasClearIconHash reports whether the BART ID hash contains the
@@ -1014,7 +1014,7 @@ type BARTID struct {
 
 type SNAC_0x10_0x02_BARTUploadQuery struct {
 	Type uint16
-	Data []byte `len_prefix:"uint16"`
+	Data []byte `oscar:"len_prefix=uint16"`
 }
 
 type SNAC_0x10_0x03_BARTUploadReply struct {
@@ -1023,15 +1023,15 @@ type SNAC_0x10_0x03_BARTUploadReply struct {
 }
 
 type SNAC_0x10_0x04_BARTDownloadQuery struct {
-	ScreenName string `len_prefix:"uint8"`
+	ScreenName string `oscar:"len_prefix=uint8"`
 	Command    uint8
 	BARTID
 }
 
 type SNAC_0x10_0x05_BARTDownloadReply struct {
-	ScreenName string `len_prefix:"uint8"`
+	ScreenName string `oscar:"len_prefix=uint8"`
 	BARTID     BARTID
-	Data       []byte `len_prefix:"uint16"`
+	Data       []byte `oscar:"len_prefix=uint16"`
 }
 
 // 0x13: Feedbag
@@ -1202,7 +1202,7 @@ type SNAC_0x13_0x05_FeedbagQueryIfModified struct {
 
 type SNAC_0x13_0x06_FeedbagReply struct {
 	Version    uint8
-	Items      []FeedbagItem `count_prefix:"uint16"`
+	Items      []FeedbagItem `oscar:"count_prefix=uint16"`
 	LastUpdate uint32
 }
 
@@ -1255,7 +1255,7 @@ type SNAC_0x17_0x06_BUCPChallengeRequest struct {
 }
 
 type SNAC_0x17_0x07_BUCPChallengeResponse struct {
-	AuthKey string `len_prefix:"uint16"`
+	AuthKey string `oscar:"len_prefix=uint16"`
 }
 
 //
@@ -1288,13 +1288,13 @@ const (
 )
 
 type TLVUserInfo struct {
-	ScreenName   string `len_prefix:"uint8"`
+	ScreenName   string `oscar:"len_prefix=uint8"`
 	WarningLevel uint16
 	TLVBlock
 }
 
 type FeedbagItem struct {
-	Name    string `len_prefix:"uint16"`
+	Name    string `oscar:"len_prefix=uint16"`
 	GroupID uint16
 	ItemID  uint16
 	ClassID uint16

+ 3 - 3
wire/tlv.go

@@ -11,7 +11,7 @@ import (
 // together in arrays.
 type TLV struct {
 	Tag   uint16
-	Value []byte `len_prefix:"uint16"`
+	Value []byte `oscar:"len_prefix=uint16"`
 }
 
 // NewTLV creates a new instance of TLV.
@@ -41,13 +41,13 @@ type TLVRestBlock struct {
 // TLVBlock is a type of TLV array that has the TLV element count encoded as a
 // 2-byte value at the beginning of the encoded blob.
 type TLVBlock struct {
-	TLVList `count_prefix:"uint16"`
+	TLVList `oscar:"count_prefix=uint16"`
 }
 
 // TLVLBlock is a type of TLV array that has the TLV blob byte-length encoded
 // as a 2-byte value at the beginning of the encoded blob.
 type TLVLBlock struct {
-	TLVList `len_prefix:"uint16"`
+	TLVList `oscar:"len_prefix=uint16"`
 }
 
 // TLVList is a list of TLV elements. It provides methods to append and access