Compare commits

...

10 Commits

14 changed files with 275 additions and 92 deletions

View File

@ -33,6 +33,11 @@ PDL allows defining a protocol using HOPP and TAPE.
| {...} | KTV | * | Table with defined schema | {...} | KTV | * | Table with defined schema
| Any | * | * | Value of an undefined type | Any | * | * | Value of an undefined type
Tables with a defined schema can specify some fields as optional using a
question mark before the type. This will wrap the field the go-util
ucontainer.Option type. When encoding, void fields will not be included in the
output, and when decoding, unspecified fields are left void.
[^1]: Excluding SI and SBA. I5 and U5 cannot be used in an array, but String and [^1]: Excluding SI and SBA. I5 and U5 cannot be used in an array, but String and
Buffer are simply forced to use their "long" variant. Buffer are simply forced to use their "long" variant.
@ -48,6 +53,7 @@ structures. They are separated by whitespace.
| Method | `M[0-9A-Fa-f]{4}` | A 16-bit hexadecimal method code. | Method | `M[0-9A-Fa-f]{4}` | A 16-bit hexadecimal method code.
| Key | `[0-9A-Fa-f]{4}` | A 16-bit hexadecimal table key. | Key | `[0-9A-Fa-f]{4}` | A 16-bit hexadecimal table key.
| Ident | `[A-Z][A-Za-z0-9]` | An identifier. | Ident | `[A-Z][A-Za-z0-9]` | An identifier.
| Option | `?` | A question mark.
| Comma | `,` | A comma separator. | Comma | `,` | A comma separator.
| LBrace | `{` | A left curly brace. | LBrace | `{` | A left curly brace.
| RBrace | `}` | A right curly brace. | RBrace | `}` | A right curly brace.
@ -107,7 +113,7 @@ Below is an EBNF description of the language.
<method> -> /M[0-9A-Fa-f]{4}/ <method> -> /M[0-9A-Fa-f]{4}/
<key> -> /[0-9A-Fa-f]{4}/ <key> -> /[0-9A-Fa-f]{4}/
<ident> -> /[A-Z][A-Za-z0-9]/ <ident> -> /[A-Z][A-Za-z0-9]/
<field> -> <key> <ident> <type> <field> -> <key> <ident> ["?"] <type>
<type> -> <ident> <type> -> <ident>
| "[" "]" <type> | "[" "]" <type>
| "{" (<comment>* <field> ",")* [<comment>* <field>] "}" | "{" (<comment>* <field> ",")* [<comment>* <field>] "}"

View File

@ -11,6 +11,7 @@ import "git.tebibyte.media/sashakoshka/hopp/tape"
const imports = const imports =
` `
import "git.tebibyte.media/sashakoshka/hopp"
import "git.tebibyte.media/sashakoshka/hopp/tape" import "git.tebibyte.media/sashakoshka/hopp/tape"
` `
@ -56,6 +57,8 @@ func boolInt(input bool) int {
return 0 return 0
} }
} }
var _ hopp.Option[int]
` `
// Generator converts protocols into Go code. // Generator converts protocols into Go code.
@ -479,19 +482,33 @@ func (this *Generator) generateEncodeValue(typ Type, valueSource, tagSource stri
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
this.push() this.push()
for key, field := range typ.Fields { for key, field := range typ.Fields {
fieldSource := fmt.Sprintf("%s.%s", valueSource, field.Name)
if field.Option {
nn, err = this.iprintf("if value, ok := %s.Value(); ok {\n", fieldSource)
n += nn; if err != nil { return n, err }
fieldSource = "value"
this.push()
}
nn, err = this.iprintf("nn, err = encoder.WriteUint16(0x%04X)\n", key) nn, err = this.iprintf("nn, err = encoder.WriteUint16(0x%04X)\n", key)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck() nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
fieldSource := fmt.Sprintf("%s.%s", valueSource, field.Name)
tagVar, nn, err := this.generateTag(field.Type, fieldSource) tagVar, nn, err := this.generateTag(field.Type, fieldSource)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
nn, err = this.iprintf("nn, err = encoder.WriteUint8(uint8(%s))\n", tagVar) nn, err = this.iprintf("nn, err = encoder.WriteUint8(uint8(%s))\n", tagVar)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck() nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
nn, err = this.generateEncodeValue(field.Type, fieldSource, tagVar) nn, err = this.generateEncodeValue(field.Type, fieldSource, tagVar)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
if field.Option {
this.pop()
nn, err = this.iprintf("}\n")
n += nn; if err != nil { return n, err }
}
} }
this.pop() this.pop()
nn, err = this.iprintf("}\n") nn, err = this.iprintf("}\n")
@ -554,13 +571,7 @@ func (this *Generator) generateDecodeValue(typ Type, typeName, valueSource, tagS
prefix = "ReadInt" prefix = "ReadInt"
} }
destinationVar := this.newTemporaryVar("destination") destinationVar := this.newTemporaryVar("destination")
nn, err := this.iprintf("var %s ", destinationVar) nn, err := this.iprintf("%s, nn, err := decoder.%s%d()\n", destinationVar, prefix, typ.Bits)
n += nn; if err != nil { return n, err }
nn, err = this.generateType(typ)
n += nn; if err != nil { return n, err }
nn, err = this.print("\n")
n += nn; if err != nil { return n, err }
nn, err = this.iprintf("%s, nn, err = decoder.%s%d()\n", destinationVar, prefix, typ.Bits)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck() nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
@ -574,13 +585,7 @@ func (this *Generator) generateDecodeValue(typ Type, typeName, valueSource, tagS
case TypeFloat: case TypeFloat:
// FP: <value: FloatN> // FP: <value: FloatN>
destinationVar := this.newTemporaryVar("destination") destinationVar := this.newTemporaryVar("destination")
nn, err := this.iprintf("var %s ", destinationVar) nn, err := this.iprintf("%s, nn, err := decoder.ReadFloat%d()\n", destinationVar, typ.Bits)
n += nn; if err != nil { return n, err }
nn, err = this.generateType(typ)
n += nn; if err != nil { return n, err }
nn, err = this.print("\n")
n += nn; if err != nil { return n, err }
nn, err = this.iprintf("%s, nn, err = decoder.ReadFloat%d()\n", destinationVar, typ.Bits)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
nn, err = this.generateErrorCheck() nn, err = this.generateErrorCheck()
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
@ -803,6 +808,7 @@ func (this *Generator) generateDecodeBranch(hash [16]byte, typ Type, typeName st
nn, err = this.iprintf("}\n") nn, err = this.iprintf("}\n")
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
case TypeTableDefined: case TypeTableDefined:
// TODO: options
// KTV: <length: UN> (<key: U16> <tag: Tag> <value>)* // KTV: <length: UN> (<key: U16> <tag: Tag> <value>)*
// read header // read header
lengthVar := this.newTemporaryVar("length") lengthVar := this.newTemporaryVar("length")
@ -879,10 +885,25 @@ func (this *Generator) generateDecodeBranch(hash [16]byte, typ Type, typeName st
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
// decode payload // decode payload
nn, err = this.generateDecodeValue( if field.Option {
field.Type, "", destination := this.newTemporaryVar("destination")
fmt.Sprintf("(&(this.%s))", field.Name), fieldTagVar) nn, err = this.iprintf("var %s ", destination)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
nn, err = this.generateType(field.Type)
n += nn; if err != nil { return n, err }
nn, err = this.printf("\n")
n += nn; if err != nil { return n, err }
nn, err = this.generateDecodeValue(
field.Type, "", fmt.Sprintf("(&%s)", destination), fieldTagVar)
n += nn; if err != nil { return n, err }
nn, err = this.iprintf("this.%s = hopp.O(%s)\n", field.Name, destination)
n += nn; if err != nil { return n, err }
} else {
nn, err = this.generateDecodeValue(
field.Type, "",
fmt.Sprintf("(&(this.%s))", field.Name), fieldTagVar)
n += nn; if err != nil { return n, err }
}
this.pop() this.pop()
} }
nn, err = this.iprintf("default:\n") nn, err = this.iprintf("default:\n")
@ -895,16 +916,6 @@ func (this *Generator) generateDecodeBranch(hash [16]byte, typ Type, typeName st
this.pop() this.pop()
nn, err = this.iprintf("}\n") nn, err = this.iprintf("}\n")
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
// TODO once options are implemented, have a set of
// bools for each non-optional field, and check here
// that they are all true. a counter will not work
// because if someone specifies a non-optional field
// twice, they can neglect to specify another
// non-optional field and we won't even know because the
// count will still be even. we shouldn't use a map
// either because its an allocation and its way more
// memory than just, like 5 bools (on the stack no less)
default: return n, fmt.Errorf("unexpected type: %T", typ) default: return n, fmt.Errorf("unexpected type: %T", typ)
} }
@ -1133,8 +1144,16 @@ func (this *Generator) generateTypeTableDefined(typ TypeTableDefined) (n int, er
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
nn, err = this.iprintf("%s ", field.Name) nn, err = this.iprintf("%s ", field.Name)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
if field.Option {
nn, err = this.print("hopp.Option[")
n += nn; if err != nil { return n, err }
}
nn, err = this.generateType(field.Type) nn, err = this.generateType(field.Type)
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
if field.Option {
nn, err = this.print("]")
n += nn; if err != nil { return n, err }
}
nn, err = this.print("\n") nn, err = this.print("\n")
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
} }

View File

@ -84,6 +84,34 @@ func init() {
}, },
}, },
} }
exampleProtocol.Messages[0x0006] = Message {
Name: "Option",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "OU5", Option: true, Type: TypeInt { Bits: 5 } },
0x0001: Field { Name: "OU8", Option: true, Type: TypeInt { Bits: 8 } },
0x0002: Field { Name: "OU16", Option: true, Type: TypeInt { Bits: 16 } },
0x0003: Field { Name: "OU32", Option: true, Type: TypeInt { Bits: 32 } },
0x0004: Field { Name: "OU64", Option: true, Type: TypeInt { Bits: 64 } },
0x0005: Field { Name: "OI8", Option: true, Type: TypeInt { Bits: 8, Signed: true } },
0x0006: Field { Name: "OI16", Option: true, Type: TypeInt { Bits: 16, Signed: true } },
0x0007: Field { Name: "OI32", Option: true, Type: TypeInt { Bits: 32, Signed: true } },
0x0008: Field { Name: "OI64", Option: true, Type: TypeInt { Bits: 64, Signed: true } },
0x0009: Field { Name: "OF32", Option: true, Type: TypeFloat { Bits: 32 } },
0x000A: Field { Name: "OF64", Option: true, Type: TypeFloat { Bits: 64 } },
0x000B: Field { Name: "OBool", Option: true, Type: TypeBool { } },
0x000C: Field { Name: "OString", Option: true, Type: TypeString { } },
0x000D: Field { Name: "OArray", Option: true, Type: TypeArray {
Element: TypeInt { Bits: 16, Signed: true } } },
0x000E: Field { Name: "OTable", Option: true, Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0001: Field { Name: "A", Type: TypeInt { Bits: 8, Signed: true }, },
0x0002: Field { Name: "B", Type: TypeFloat { Bits: 64 }, },
} } },
0x000F: Field { Name: "T0", Option: true, Type: TypeTable { } },
},
},
}
exampleProtocol.Types["User"] = Typedef { exampleProtocol.Types["User"] = Typedef {
Type: TypeTableDefined { Type: TypeTableDefined {
Fields: map[uint16] Field { Fields: map[uint16] Field {

View File

@ -10,6 +10,7 @@ const (
TokenMethod parse.TokenKind = iota TokenMethod parse.TokenKind = iota
TokenKey TokenKey
TokenIdent TokenIdent
TokenOption
TokenComma TokenComma
TokenLBrace TokenLBrace
TokenRBrace TokenRBrace
@ -22,6 +23,7 @@ var tokenNames = map[parse.TokenKind] string {
TokenMethod: "Method", TokenMethod: "Method",
TokenKey: "Key", TokenKey: "Key",
TokenIdent: "Ident", TokenIdent: "Ident",
TokenOption: "Option",
TokenComma: "Comma", TokenComma: "Comma",
TokenLBrace: "LBrace", TokenLBrace: "LBrace",
TokenRBrace: "RBrace", TokenRBrace: "RBrace",
@ -122,6 +124,11 @@ func (this *lexer) nextInternal() (token parse.Token, err error) {
if this.eof { err = nil; return } if this.eof { err = nil; return }
if err != nil { return } if err != nil { return }
} }
// Option
case this.rune == '?':
token.Kind = TokenOption
appendRune()
if this.eof { err = nil; return }
// Comma // Comma
case this.rune == ',': case this.rune == ',':
token.Kind = TokenComma token.Kind = TokenComma

View File

@ -16,6 +16,7 @@ func TestLex(test *testing.T) {
// wow // wow
0001 Users []User, 0001 Users []User,
0002 Followers U32, 0002 Followers U32,
0003 Wings ?Int,
}`)) }`))
if err != nil { test.Fatal(parse.Format(err)) } if err != nil { test.Fatal(parse.Format(err)) }
@ -42,6 +43,11 @@ func TestLex(test *testing.T) {
tok(TokenIdent, "Followers"), tok(TokenIdent, "Followers"),
tok(TokenIdent, "U32"), tok(TokenIdent, "U32"),
tok(TokenComma, ","), tok(TokenComma, ","),
tok(TokenKey, "0003"),
tok(TokenIdent, "Wings"),
tok(TokenOption, "?"),
tok(TokenIdent, "Int"),
tok(TokenComma, ","),
tok(TokenRBrace, "}"), tok(TokenRBrace, "}"),
tok(parse.EOF, ""), tok(parse.EOF, ""),
} }

View File

@ -214,12 +214,19 @@ func (this *parser) parseField(doc string) (uint16, Field, error) {
name := this.Value() name := this.Value()
err = this.Next() err = this.Next()
if err != nil { return 0, Field { }, err } if err != nil { return 0, Field { }, err }
option := false
if this.Kind() == TokenOption {
option = true
err = this.Next()
if err != nil { return 0, Field { }, err }
}
typ, err := this.parseType() typ, err := this.parseType()
if err != nil { return 0, Field { }, err } if err != nil { return 0, Field { }, err }
return uint16(key), Field { return uint16(key), Field {
Name: name, Name: name,
Doc: doc, Doc: doc,
Type: typ, Type: typ,
Option: option,
}, nil }, nil
} }

View File

@ -34,6 +34,7 @@ func TestParse(test *testing.T) {
0x0001: Field { Name: "Bio", Type: TypeString { } }, 0x0001: Field { Name: "Bio", Type: TypeString { } },
0x0002: Field { Name: "Followers", Type: TypeInt { Bits: 32 } }, 0x0002: Field { Name: "Followers", Type: TypeInt { Bits: 32 } },
0x0003: Field { Name: "Bouncy", Type: TypeBool { } }, 0x0003: Field { Name: "Bouncy", Type: TypeBool { } },
0x0004: Field { Name: "Wings", Type: TypeInt { Bits: 32 } },
}, },
}, },
} }
@ -63,6 +64,7 @@ func TestParse(test *testing.T) {
0001 Bio String, 0001 Bio String,
0002 Followers U32, 0002 Followers U32,
0003 Bouncy Bool, 0003 Bouncy Bool,
0004 Wings ?U32,
} }
Anything Any Anything Any

View File

@ -95,9 +95,10 @@ func (typ TypeTableDefined) String() string {
} }
type Field struct { type Field struct {
Name string Name string
Doc string Doc string
Type Type Type Type
Option bool
} }
func (field Field) String() string { func (field Field) String() string {

2
go.mod
View File

@ -1,6 +1,6 @@
module git.tebibyte.media/sashakoshka/hopp module git.tebibyte.media/sashakoshka/hopp
go 1.23.0 go 1.24.0
require ( require (
git.tebibyte.media/sashakoshka/go-cli v0.1.3 git.tebibyte.media/sashakoshka/go-cli v0.1.3

View File

@ -97,6 +97,32 @@ func (sn Snake) String() string {
return out.String() return out.String()
} }
func (sn Snake) CharsString() string {
if len(sn) == 0 || len(sn[0]) == 0 || len(sn[0][0]) == 0 {
return "EMPTY"
}
out := strings.Builder { }
for index, sector := range sn {
if index > 0 { out.WriteString(" : ") }
out.WriteRune('[')
for index, variation := range sector {
if index > 0 { out.WriteString(" / ") }
for _, byt := range variation {
run := rune(byt)
if unicode.IsPrint(run) && run < 0x7F {
out.WriteRune(run)
} else {
out.WriteRune('.')
}
out.WriteRune(' ')
}
}
out.WriteRune(']')
}
return out.String()
}
// HexBytes formats bytes into a hexadecimal string. // HexBytes formats bytes into a hexadecimal string.
func HexBytes(data []byte) string { func HexBytes(data []byte) string {
if len(data) == 0 { return "EMPTY" } if len(data) == 0 { return "EMPTY" }
@ -107,6 +133,24 @@ func HexBytes(data []byte) string {
return out.String() return out.String()
} }
// HexChars returns all printable bytes in the string, with non-printable ones
// replaced with a dot. Each character has an extra space after it for placing
// underneath the result of HexBytes.
func HexChars(data []byte) string {
if len(data) == 0 { return "EMPTY" }
out := strings.Builder { }
for _, byt := range data {
run := rune(byt)
if unicode.IsPrint(run) && run < 0x7F {
out.WriteRune(run)
} else {
out.WriteRune('.')
}
out.WriteRune(' ')
}
return out.String()
}
// Describe returns a string representing the type and data of the given value. // Describe returns a string representing the type and data of the given value.
func Describe(value any) string { func Describe(value any) string {
desc := describer { } desc := describer { }

View File

@ -2,29 +2,10 @@ package hopp
import "git.tebibyte.media/sashakoshka/go-util/container" import "git.tebibyte.media/sashakoshka/go-util/container"
// Option allows an optional value to be defined without using a pointer. // Option is an alias for ucontainer.Option, defined here for convenience
// TODO make generic alias once go 1.24 releases type Option[T any] = ucontainer.Option[T]
type Option[T any] ucontainer.Optional[T]
// O is an alias for ucontainer.O, defined here for convenience
func O[T any](value T) Option[T] { func O[T any](value T) Option[T] {
return Option[T](ucontainer.O(value)) return ucontainer.O(value)
}
func Void[T any]() Option[T] {
return Option[T](ucontainer.Void[T]())
}
func (option Option[T]) Ok() bool {
return (ucontainer.Optional[T])(option).Exists()
}
func (option Option[T]) Get() (T, bool) {
return (ucontainer.Optional[T])(option).Value()
}
func (option Option[T]) Default(defaul T) T {
if value, ok := option.Get(); ok {
return value
}
return defaul
} }

View File

@ -6,6 +6,10 @@ package tape
// TODO: add support for struct tags: `tape:"0000"`, tape:"0001"` so they can get // TODO: add support for struct tags: `tape:"0000"`, tape:"0001"` so they can get
// transformed into tables with a defined schema // transformed into tables with a defined schema
// TODO: support special behavior for options in structs: don't just write a
// zero value if the option is void, write no field at all. also consider doing
// this for maps, and maybe slices.
// TODO: test all of these smaller functions individually // TODO: test all of these smaller functions individually
// For an explanation as to why this package always treats LBA/SBA as strings, // For an explanation as to why this package always treats LBA/SBA as strings,
@ -79,6 +83,12 @@ func EncodeAny(encoder *Encoder, value any, tag Tag) (n int, err error) {
return n, nil return n, nil
} }
// option
if isTypeOption(reflectValue.Type()) {
elemValue, _ := optionValue(reflectValue) // zero value for free
return EncodeAny(encoder, elemValue, tag)
}
// aggregates // aggregates
reflectType := reflect.TypeOf(value) reflectType := reflect.TypeOf(value)
switch reflectType.Kind() { switch reflectType.Kind() {
@ -311,6 +321,12 @@ func tagAny(reflectValue reflect.Value) (Tag, error) {
return bufferLenTag(reflectValue.Len()), nil return bufferLenTag(reflectValue.Len()), nil
} }
// option
if isTypeOption(reflectValue.Type()) {
elem, _ := optionValue(reflectValue) // zero value for free
return tagAny(elem)
}
// aggregates // aggregates
reflectType := reflectValue.Type() reflectType := reflectValue.Type()
switch reflectType.Kind() { switch reflectType.Kind() {
@ -336,9 +352,14 @@ func encodeAnySlice(encoder *Encoder, value any, tag Tag) (n int, err error) {
for index := 0; index < reflectValue.Len(); index += 1 { for index := 0; index < reflectValue.Len(); index += 1 {
itemTag, err := tagAny(reflectValue.Index(index)) itemTag, err := tagAny(reflectValue.Index(index))
if err != nil { return n, err } if err != nil { return n, err }
if itemTag.Is(SBA) {
// SBA data in an LBA will always have the tag LBA:0,
// because 32 <= 256
continue
}
if itemTag.CN() > oneTag.CN() { oneTag = itemTag } if itemTag.CN() > oneTag.CN() { oneTag = itemTag }
} }
if oneTag.Is(SBA) { oneTag += 1 << 5 } if oneTag.Is(SBA) { oneTag = LBA.WithCN(oneTag.CN()) }
nn, err = encoder.WriteUint8(uint8(oneTag)) nn, err = encoder.WriteUint8(uint8(oneTag))
n += nn; if err != nil { return n, err } n += nn; if err != nil { return n, err }
for index := 0; index < reflectValue.Len(); index += 1 { for index := 0; index < reflectValue.Len(); index += 1 {
@ -576,6 +597,21 @@ func isTypeAny(typ reflect.Type) bool {
return typ.Kind() == reflect.Interface && typ.NumMethod() == 0 return typ.Kind() == reflect.Interface && typ.NumMethod() == 0
} }
// isTypeOption returns whether the given reflect.Type is a ucontainer.Option,
// and returns the element type if true.
func isTypeOption(typ reflect.Type) bool {
// TODO: change when needed
goutilPath := "git.tebibyte.media/sashakoshka/go-util"
return typ.Name() == "Option" && typ.PkgPath() == goutilPath + "/container"
}
// optionValue returns the value of an option. The value MUST be an option, or
// this function will panic.
func optionValue(value reflect.Value) (elem reflect.Value, ok bool) {
result := value.MethodByName("Value").Call([]reflect.Value { })
return result[0], result[1].Bool()
}
// peekSlice returns the element tag and dimension count of the OTA currently // peekSlice returns the element tag and dimension count of the OTA currently
// being decoded. It does not use up the decoder, it only peeks. // being decoded. It does not use up the decoder, it only peeks.
func peekSlice(decoder *Decoder, tag Tag) (Tag, int, error) { func peekSlice(decoder *Decoder, tag Tag) (Tag, int, error) {

View File

@ -3,39 +3,10 @@ package tape
import "bytes" import "bytes"
import "testing" import "testing"
import "reflect" import "reflect"
import "git.tebibyte.media/sashakoshka/go-util/container"
import tu "git.tebibyte.media/sashakoshka/hopp/internal/testutil" import tu "git.tebibyte.media/sashakoshka/hopp/internal/testutil"
var samplePayloads = [][]byte { var samplePayloads [][]byte
/* int8 */ []byte { byte(LSI.WithCN(0)), 0x45 },
/* int16 */ []byte { byte(LSI.WithCN(1)), 0x45, 0x67 },
/* int32 */ []byte { byte(LSI.WithCN(3)), 0x45, 0x67, 0x89, 0xAB },
/* int64 */ []byte { byte(LSI.WithCN(7)), 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23 },
/* uint5 */ []byte { byte(SI.WithCN(12)) },
/* uint8 */ []byte { byte(LI.WithCN(0)), 0x45 },
/* uint16 */ []byte { byte(LI.WithCN(1)), 0x45, 0x67 },
/* uint32 */ []byte { byte(LI.WithCN(3)), 0x45, 0x67, 0x89, 0xAB },
/* uint64 */ []byte { byte(LI.WithCN(7)), 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23 },
/* bool */ []byte { byte(SI.WithCN(0)) },
/* bool */ []byte { byte(SI.WithCN(1)) },
/* string */ []byte { byte(SBA.WithCN(7)), 'p', 'u', 'p', 'e', 'v', 'e', 'r' },
/* []byte */ []byte { byte(SBA.WithCN(5)), 'b', 'l', 'a', 'r', 'g' },
/* []string */ []byte {
byte(OTA.WithCN(0)), 2, byte(LBA.WithCN(0)),
0x08, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23,
0x05, 0x11, 0x11, 0x11, 0x11, 0x11,
},
/* map[uint16] any */ []byte {
byte(KTV.WithCN(0)), 2,
0x02, 0x23, byte(LSI.WithCN(1)), 0x45, 0x67,
0x02, 0x24, byte(LI.WithCN(3)), 0x45, 0x67, 0x89, 0xAB,
},
/* map[uint16] any */ []byte {
byte(KTV.WithCN(0)), 3,
0x00, 0x01, 0x63, 0x43, 0xF4, 0xC0, 0x00,
0x00, 0x02, 0x82, 'h', 'i',
0x00, 0x03, 0x21, 0x39, 0x92,
},
}
var sampleValues = []any { var sampleValues = []any {
/* int8 */ int8(0x45), /* int8 */ int8(0x45),
@ -64,6 +35,64 @@ var sampleValues = []any {
0x0002: "hi", 0x0002: "hi",
0x0003: uint16(0x3992), 0x0003: uint16(0x3992),
}, },
// IMPORTANT: ONLY ADD AT THE END!!!! DO NOT MOVE WHAT IS ALREADY HERE!
// IMPORTANT: ONLY ADD AT THE END!!!! DO NOT MOVE WHAT IS ALREADY HERE!
}
type sample struct {
t Tag
v any
s tu.Snake
}
var samples = []sample {
/* int8 */ sample { t: LSI.WithCN(0), s: tu.S(0x45) },
/* int16 */ sample { t: LSI.WithCN(1), s: tu.S(0x45, 0x67) },
/* int32 */ sample { t: LSI.WithCN(3), s: tu.S(0x45, 0x67, 0x89, 0xAB) },
/* int64 */ sample { t: LSI.WithCN(7), s: tu.S(0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23) },
/* uint5 */ sample { t: SI.WithCN(12), s: tu.S() },
/* uint8 */ sample { t: LI.WithCN(0), s: tu.S(0x45) },
/* uint16 */ sample { t: LI.WithCN(1), s: tu.S(0x45, 0x67) },
/* uint32 */ sample { t: LI.WithCN(3), s: tu.S(0x45, 0x67, 0x89, 0xAB) },
/* uint64 */ sample { t: LI.WithCN(7), s: tu.S(0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23) },
/* bool */ sample { t: SI.WithCN(0), s: tu.S() },
/* bool */ sample { t: SI.WithCN(1), s: tu.S() },
/* string */ sample { t: SBA.WithCN(7), s: tu.S('p', 'u', 'p', 'e', 'v', 'e', 'r') },
/* []byte */ sample { t: SBA.WithCN(5), s: tu.S('b', 'l', 'a', 'r', 'g') },
/* []string */ sample {
t: OTA.WithCN(0),
s: tu.S(2, byte(LBA.WithCN(0)),
0x08, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23,
0x05, 0x11, 0x11, 0x11, 0x11, 0x11),
},
/* map[uint16] any */ sample {
t: KTV.WithCN(0),
s: tu.S(2).AddVar(
[]byte { 0x02, 0x23, byte(LSI.WithCN(1)), 0x45, 0x67 },
[]byte { 0x02, 0x24, byte(LI.WithCN(3)), 0x45, 0x67, 0x89, 0xAB }),
},
/* map[uint16] any */ sample {
t: KTV.WithCN(0),
s: tu.S(3).AddVar(
[]byte { 0x00, 0x01, 0x63, 0x43, 0xF4, 0xC0, 0x00 },
[]byte { 0x00, 0x02, 0x82, 'h', 'i' },
[]byte { 0x00, 0x03, 0x21, 0x39, 0x92 }),
},
}
var sampleOptionValues []any
func init() {
sampleOptionValues = make([]any, len(sampleValues))
for index, value := range sampleValues {
sampleOptionValues[index] = ucontainer.O(value)
samples[index].v = value
}
samplePayloads = make([][]byte, len(samples))
for index, sample := range samples {
item := append([]byte { byte(sample.t) }, sample.s.Flatten()...)
samplePayloads[index] = item
}
} }
type userDefinedInteger int16 type userDefinedInteger int16
@ -271,6 +300,20 @@ func TestEncodeDecodeAnyDestination(test *testing.T) {
} }
} }
func TestEncodeOption(test *testing.T) {
for _, sample := range samples {
snake := sample.s
tag := sample.t
value := sample.v
if _, ok := value.(bool); tag.Is(SI) && !ok {
// we will never encode an SI unless its a bool
continue
}
err := testEncodeAny(test, value, tag, snake)
if err != nil { test.Fatal(err) }
}
}
func TestPeekSlice(test *testing.T) { func TestPeekSlice(test *testing.T) {
buffer := bytes.NewBuffer([]byte { buffer := bytes.NewBuffer([]byte {
2, byte(OTA.WithCN(3)), 2, byte(OTA.WithCN(3)),

View File

@ -32,7 +32,9 @@ func testEncodeAny(test *testing.T, value any, correctTag Tag, correctBytes tu.S
test.Log("n: ", n) test.Log("n: ", n)
test.Log("tag: ", tag) test.Log("tag: ", tag)
test.Log("got: ", tu.HexBytes(bytes)) test.Log("got: ", tu.HexBytes(bytes))
test.Log(" : ", tu.HexChars(bytes))
test.Log("correct:", correctBytes) test.Log("correct:", correctBytes)
test.Log(" :", correctBytes.CharsString())
if tag != correctTag { if tag != correctTag {
return fmt.Errorf("tag not equal: %v != %v", tag, correctTag) return fmt.Errorf("tag not equal: %v != %v", tag, correctTag)
} }
@ -56,6 +58,7 @@ func testEncodeDecodeAny(test *testing.T, value, correctValue any) error {
test.Log("n: ", n) test.Log("n: ", n)
test.Log("tag:", tag) test.Log("tag:", tag)
test.Log("got:", tu.HexBytes(bytes)) test.Log("got:", tu.HexBytes(bytes))
test.Log(" :", tu.HexChars(bytes))
test.Log("decoding...", tag) test.Log("decoding...", tag)
if n != len(bytes) { if n != len(bytes) {
return fmt.Errorf("n not equal: %d != %d", n, len(bytes)) return fmt.Errorf("n not equal: %d != %d", n, len(bytes))