Compare commits
10 Commits
fbb68e6ff7
...
81d95dcd90
| Author | SHA1 | Date | |
|---|---|---|---|
| 81d95dcd90 | |||
| 2f2b1a4d2f | |||
| 899f98043f | |||
| 6b7dfce2f3 | |||
| 17201a4c48 | |||
| 50ca98f3c6 | |||
| 77a4d7893f | |||
| 7bebc8c5eb | |||
| 70fb106b48 | |||
| 6b9db4c2a1 |
@ -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>] "}"
|
||||||
|
|||||||
@ -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
|
||||||
|
if field.Option {
|
||||||
|
destination := this.newTemporaryVar("destination")
|
||||||
|
nn, err = this.iprintf("var %s ", destination)
|
||||||
|
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(
|
nn, err = this.generateDecodeValue(
|
||||||
field.Type, "",
|
field.Type, "",
|
||||||
fmt.Sprintf("(&(this.%s))", field.Name), fieldTagVar)
|
fmt.Sprintf("(&(this.%s))", field.Name), fieldTagVar)
|
||||||
n += nn; if err != nil { return n, err }
|
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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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, ""),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -98,6 +98,7 @@ 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
2
go.mod
@ -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
|
||||||
|
|||||||
@ -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 { }
|
||||||
|
|||||||
27
option.go
27
option.go
@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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)),
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user