169 Commits

Author SHA1 Message Date
0ac26711ac tape: Fix crash when decoding a bool sometimes 2025-10-15 01:16:03 -04:00
8446ae6186 tape: Implement bools in dymanic system 2025-10-15 01:07:51 -04:00
c511ebcb15 tape: Add bools to tests 2025-10-15 01:07:39 -04:00
00b0f13d3e generate: Implement bool in static system 2025-10-15 00:58:14 -04:00
13d35e54f5 generate: Include bool in tests 2025-10-15 00:58:00 -04:00
770f6b05b4 generate: Parse Bool type 2025-10-15 00:31:54 -04:00
2ee954e18f generate: Add bool data structure 2025-10-15 00:31:30 -04:00
cdfccb0f1c design: Add Bool type 2025-10-14 23:17:59 -04:00
5d5d3fd31c generate: Emit doc comments (in theory) 2025-10-13 17:29:27 -04:00
190a89fbb3 generate: Parse doc comments 2025-10-13 16:16:57 -04:00
e991b5af67 generate: Add comments to protocol data structures, tests 2025-10-13 14:15:59 -04:00
5a3d0e19ea generate: Add comments to lexer 2025-10-13 14:00:48 -04:00
fbc55534f6 design: Add comments to the language spec 2025-10-13 13:42:11 -04:00
b6e180f466 Update go.mod, go.sum 2025-10-13 13:15:06 -04:00
8f5f25780e cmd/hopp-generate: Improve command line interface 2025-10-13 13:14:44 -04:00
f08213cd49 tape: Fix comment 2025-10-13 10:54:40 -04:00
2194198693 Merge pull request 'unify-byte-counts' (#21) from unify-byte-counts into main
Reviewed-on: #21
2025-10-13 08:49:46 -06:00
5c2b8a0582 tape: Correctly decode into a table destination all the time 2025-10-13 10:33:59 -04:00
4575fa229b generate: Make the table type an alias so we don't have a million of em 2025-10-13 10:33:36 -04:00
cbfb513933 tape: canAssign now reports true for named table types 2025-10-12 21:27:35 -04:00
f10327356e generate: Describe more values in tests 2025-10-12 18:50:28 -04:00
f402b46b1c internal/testutil: More fixes for Describe 2025-10-12 18:50:17 -04:00
c3d0f33700 tape: Test troublesome data structure 2025-10-12 18:41:32 -04:00
ba2dc6b53f tape: Properly allocate maps when decoding KTV 2025-10-12 18:28:36 -04:00
2e03867c66 tape: Fix DecodeAny method not converting values properly 2025-10-12 18:09:55 -04:00
7a03d8d6b5 tape: Test DecodeAny method 2025-10-12 18:07:00 -04:00
b2504cda2d internal/testutil: Describe no longer panics on private struct fields 2025-10-12 17:58:10 -04:00
f6b12d43fb generate: Fix off by one errors in generated code and tests 2025-10-12 17:11:44 -04:00
c185f5058f generate: Fix some off by one errors in TestGenerateRunEncodeDecode 2025-10-12 13:43:19 -04:00
813d219580 tape: Fix bufferLenTag off by one error 2025-10-12 13:39:37 -04:00
b44d364f0f tape: Test TagAny function 2025-10-12 13:38:33 -04:00
405b458702 tape: Comment chart for reading tags from hexdumps 2025-10-12 13:37:50 -04:00
5778616965 design: Codify usage of CN + 1 bytes everywhere 2025-10-12 13:11:47 -04:00
f5de450c39 Merge pull request 'any-type' (#20) from any-type into main
Reviewed-on: #20
2025-10-12 11:03:53 -06:00
aebc6972ad tape: Fix TestEncodeDecodeAnyTable 2025-10-12 11:44:14 -04:00
ef3f5cf4bb tape: Decode KTV into any 2025-10-12 11:34:49 -04:00
3f51beddb6 tape: Decoding OTA into any no longer results in a pointer 2025-10-12 11:27:20 -04:00
56c376cd4e tape: Decode OTAs into any, and allow assignment of SBA/LBA to string 2025-10-12 11:17:41 -04:00
19f02d6137 tape: Implement setString 2025-10-12 10:43:04 -04:00
7df18f7d26 tape: Assorted changes i forgor 2025-09-26 00:20:53 -04:00
84b96ed8f3 Add /debug to .gitignore 2025-09-26 00:20:44 -04:00
85a66a3e70 tape: Create test to ensure DecodeAnyInto can receive a pointer to any 2025-09-10 10:21:29 -04:00
81391ef101 testutil: Print <invalid> instead of crashing on invalid value 2025-09-10 10:20:44 -04:00
92040a1bc4 generate: Implement encoding and decoding of Any type 2025-09-10 09:45:25 -04:00
1bb565c6fe generate: Write tests for Any type 2025-09-10 09:45:09 -04:00
c4ab60515b tape: Test that floating point values can be decoded dynamically 2025-09-08 21:39:50 -04:00
8b0915dff1 tape: Test that floating point values can be dynamically encoded 2025-09-08 21:37:39 -04:00
785b48085d tape: Dynamically encode floating point values 2025-09-08 18:24:21 -04:00
419c3651bf generate: Add Any type to parser and syntax tree 2025-09-08 09:58:50 -04:00
8dac25035f generate: Use DecodeAnyInto in generated code 2025-09-08 09:47:37 -04:00
b7bdaba694 tape: Split DecodeAny into two funcs, one autocreates skeleton value 2025-09-08 09:42:39 -04:00
45dfdb255e design: Add Any type to PDL language document 2025-09-07 23:46:22 -04:00
5b1448be3e Merge pull request 'message-size-increase' (#3) from message-size-increase into main
Reviewed-on: #3
2025-09-07 19:27:33 -06:00
12fbfa6293 hopp: Add SetDeadline methods to Conn and Trans 2025-09-05 18:48:12 -04:00
44fb561758 generate: Safely cast in the static decoder 2025-08-29 12:21:10 -04:00
04c352fad6 tape: Safely cast when dynamically encoding/decoding 2025-08-29 12:03:39 -04:00
0ea7e222cc generate: Respect limits when statically decoding 2025-08-28 12:53:58 -04:00
ae79a32309 generate: Respect limits when statically encoding 2025-08-28 12:51:55 -04:00
e28ab4dc6b tape: Respect limits when dynamically decoding 2025-08-28 12:31:49 -04:00
80161b37f7 tape: Respect limits when dynamically encoding
Still need: dynamic decoding, static decoding, static encoding
2025-08-28 09:31:33 -04:00
9d40b81e00 tape: Add limits to the API 2025-08-28 09:31:14 -04:00
80c7d25c73 hopp: Clarify documentation for Conn.SetSizeLimit 2025-08-28 09:03:27 -04:00
743a5d4ae0 generate: Fix float encoding 2025-08-27 22:59:01 -04:00
ea17e354a3 cmd/hopp-generate: Update generate command 2025-08-27 22:54:43 -04:00
4dc8a30ebd generate: Don't hardcode filename as test.pdl in errors 2025-08-27 22:54:18 -04:00
15c5f0b2b8 generate: More fixes for TestGenerateRunDecodeWrongType 2025-08-27 22:22:00 -04:00
087b6b6690 generate: Fix problems with TestGenerateRunDecodeWrongType 2025-08-27 18:33:55 -04:00
77bfc45fea generate: Cast strings and buffers when decoding 2025-08-27 14:55:10 -04:00
de6099fadc generate: cast integers when decoding 2025-08-27 00:43:10 -04:00
0097dbeedd generate: cast certain types when encoding 2025-08-26 06:17:43 -04:00
2db7ff88c2 generate: Import the wrong type test from the dynamic encoder/decoder 2025-08-22 06:10:17 -04:00
4fd15c79a4 generate: Cause MessagePulse test case to pass (float was too precise) 2025-08-21 18:24:44 -04:00
d6f6a3485c generate: Run encoder output through decoder in tests 2025-08-21 06:25:55 -04:00
5d0b95d59a generate: Fix comparisons in generated canAssign function 2025-08-20 15:21:45 -04:00
756bc79c16 hopp: Point METADAPT implementations at new decoding functions 2025-08-20 14:27:46 -04:00
a59870cc69 hopp: Delete message.go 2025-08-20 14:27:38 -04:00
782472aa8f hopp: Bring some old decoding/encoding functions that worked on []byte back 2025-08-20 14:27:03 -04:00
52aa07a98f generate: Add TestGenerateRunDecode 2025-08-20 12:59:10 -04:00
94041f2abc generate: Significantly improve testGenerateRun 2025-08-20 12:43:47 -04:00
423f547da3 testutil: Add Flatten method to Snake 2025-08-20 12:42:54 -04:00
9278bdcb43 generate: Add decoder test function to testGenerateRun 2025-08-20 12:09:54 -04:00
0acf44886a generate: Clean up generate_test.go 2025-08-20 12:05:37 -04:00
a4da33536c tape: Utilize skimming in the dynamic decoder 2025-08-18 22:26:53 -04:00
2180d29615 tape: Implement Skim function 2025-08-12 12:29:50 -04:00
2209763666 tape: Write test for Skim function 2025-08-12 12:29:28 -04:00
96c8d7924f tape: Test that integers of a user-defined type can be encoded 2025-08-12 08:21:19 -04:00
fdf0aa89a4 tape: Use reflection when encoding integers 2025-08-12 08:16:34 -04:00
1bded9852d tape: Dynamic tests put out more information 2025-08-12 08:16:21 -04:00
8beb9de256 Merge pull request 'encode-signedness' (#13) from encode-signedness into message-size-increase
Reviewed-on: #13
2025-08-11 19:10:56 -06:00
dc72cc2010 generate: Support LSI tags 2025-08-11 20:59:20 -04:00
0e03f84b8a generate: Update tests with new TNs 2025-08-11 20:59:10 -04:00
02196edf61 design: Change tag for signed PDL integers 2025-08-11 20:10:03 -04:00
1058615f6f tape: Do something when receiving an LSI tag 2025-08-11 18:40:56 -04:00
024edfa922 tape: Actually test decoding lol 2025-08-11 18:39:38 -04:00
fe973af99c tape: Test dynamic encoding and decoding of signed integers 2025-08-10 23:28:43 -04:00
52f0d6932e tape: Add encoding and decoding of signed integers 2025-08-10 23:28:25 -04:00
8e14a2c3f1 tape: Add LSI to Tag constants 2025-08-07 21:14:40 -04:00
4fbb70081a generate: Finish test sub-case for MessageNestedArray 2025-08-06 22:15:22 -04:00
a108e53cb6 Merge pull request 'branched-generated-encoder' (#9) from branched-generated-encoder into message-size-increase
Reviewed-on: #9
2025-08-06 19:11:08 -06:00
57c30ac669 generate: Generator compiles 2025-08-06 20:19:31 -04:00
a270c22cb9 generate: The generics idea didn't work, use type names instead 2025-08-06 19:39:41 -04:00
a99d4dee66 generate: Fix no return statement, unused variables 2025-08-06 19:07:57 -04:00
c18e251b4a generate: Convert int64 to int to satisfy range
This is a stupid fucking restriction
2025-08-06 18:58:54 -04:00
170f79c914 generate: Fix bad variable names 2025-08-06 18:48:17 -04:00
77c6b67d65 generate: Break line after continue statements 2025-08-06 18:41:37 -04:00
195d0f9725 generate: Pass decoder to branch functions 2025-08-06 18:40:28 -04:00
fa4f591126 generate: make branch functions generic, take in ~ of base type 2025-08-06 18:38:30 -04:00
12142706e1 generate: Fix syntax and formatting errors 2025-08-06 17:59:26 -04:00
30e9ead1ab generate: Do the same for <user-type>.Decode 2025-08-06 17:27:04 -04:00
1118b11bcd generate: Properly check assignment within <message>.Decode 2025-08-06 17:24:51 -04:00
7343cf5853 generate: Fix array element tag variable 2025-08-06 17:03:58 -04:00
a9f583d2e7 generate: Validate OTA tags properly 2025-08-06 17:02:33 -04:00
c4dd129fc5 generate: Decode tables (but don't validate their length yet) 2025-08-06 17:00:39 -04:00
2cbf58d558 generate: Decode arrays (but don't validate their length yet) 2025-08-05 06:22:27 -04:00
7dcfc08678 generate: Add "stub" for actually generating branch functions 2025-08-04 16:01:50 -04:00
711ac30486 generate: Add branch decode function request queue 2025-08-04 12:26:16 -04:00
b15c3aa76c generate: Implement Generator.generateDecodeBranch 2025-08-04 09:36:52 -04:00
a1bfae443c design: Add paragraph about how we need a skimming function in tape 2025-08-03 22:28:06 -04:00
df3fe1280d generate: Remove abort parameter 2025-08-03 22:27:14 -04:00
41b3376fa3 generate: Add Generator.generateDecodeBranch stub 2025-08-03 22:19:06 -04:00
fae702edfd generate: Add String functions, TypeHash function for types 2025-08-03 22:07:31 -04:00
c86f9b03f2 generated: Remove unneeded code 2025-08-03 21:00:24 -04:00
dcbfbe9141 design: Import issue from Tebitea (it is down) 2025-08-03 20:59:59 -04:00
40444ee2f4 generate: Add TODOs about a big problem... 2025-07-25 21:01:48 -04:00
59cc90166f generate: WIP test of nested arrays 2025-07-25 21:01:23 -04:00
f222fb02b7 generate: Gracefully discard unexpected data while decoding 2025-07-22 20:20:47 -04:00
6ecc33a46b generate: Fix TestGenerateRun when testing numbers 2025-07-21 16:51:33 -04:00
5d84636b55 tape: Add functions to encode and decode float16 2025-07-21 15:58:32 -04:00
f009a970cd generate: Fix another syntax error when reading floats 2025-07-21 15:00:57 -04:00
8b63166ba1 generate: Test encoding floating point values 2025-07-21 14:57:34 -04:00
3ef7de118b generate: Fix syntax error when reading floats 2025-07-21 14:56:58 -04:00
51ed6aed9f generate: Fix TestGenerateRun so it snakes around User tables 2025-07-21 14:32:33 -04:00
6017ac1fa3 generate: Encode SI properly 2025-07-21 14:11:40 -04:00
b8047585fb generate: Test integer encoding 2025-07-21 14:10:34 -04:00
ad3973dd9e generate: Test array encoding 2025-07-20 23:21:59 -04:00
0f626b2e93 generate: Fix array encoding not writing length or item tag properly 2025-07-20 23:19:21 -04:00
272e47224d generate: Make output of testGenerateRun easier to compare 2025-07-20 23:18:36 -04:00
2c57423838 generate: Fix encoding of table length field 2025-07-20 10:26:29 -04:00
e2b9e809a8 generate: Fix TestGenerateRun 2025-07-20 10:25:53 -04:00
7e8b272ef0 generate: And now for an actual use of testGenerateRun 2025-07-19 06:24:30 -04:00
a257902705 generate: Flush the buffer before trying to compare it 2025-07-17 14:47:23 -04:00
4955f66ad6 generate: testEncode uses snakes now 2025-07-17 14:06:05 -04:00
f646207ab1 generate: Use newTemporarVar in more places 2025-07-17 11:28:53 -04:00
b50a199842 generate: Fix the testEncode function 2025-07-17 11:19:57 -04:00
b826cbf83e generate: The test now compiles 2025-07-17 10:26:31 -04:00
b73f9fa7ce generate: Implement decoding (untested) 2025-07-16 22:57:12 -04:00
d3d7b07a74 generate: Emit stub for message decoding function 2025-07-12 20:01:42 -04:00
daa6a44179 generate: Generate Method method 2025-07-12 19:37:58 -04:00
af7669c783 generate: Fix more nonsense surrounding named types 2025-07-11 20:08:43 -04:00
2305814e10 tape: Add StringTag 2025-07-10 21:56:38 -04:00
5a3296a842 generate: Parse primitive types into actual types rather than named types 2025-07-10 21:53:21 -04:00
3bf365a7a9 generate: Fix more semantic errors in the generated code 2025-07-08 21:50:29 -04:00
e48be0bc15 generate: Fix more semantic issues with generated code 2025-07-08 21:32:34 -04:00
a210f6112c generate: Fix more semantic issues in generated code 2025-07-08 20:35:11 -04:00
9ff317d443 generate: Change comment so it gets detected by the regex for generated files 2025-07-08 15:44:24 -04:00
cdba8ee601 generate: Fix a bunch of semantic issues with the generated code 2025-07-08 14:52:05 -04:00
e75d7534c1 generate: Fix syntax errors in generated code 2025-07-08 12:01:21 -04:00
8a0ae9b03f generate: Update tests to account for new changes 2025-07-08 11:39:37 -04:00
9bc90b0e17 generate: What the fuck is a teibibyte 2025-07-08 11:39:18 -04:00
c70c23d137 generate: Fix testGenerateRun so that it actually works 2025-07-08 11:38:00 -04:00
a9d5bb83a2 generate: Add framework for testing output of generated code 2025-07-07 16:00:25 -04:00
f1df5fa84d generate: Add stub return to generateDecodeValue so it compiles 2025-07-07 15:13:18 -04:00
76a8f9444a tape: Ignore type names when encoding primitives using reflection 2025-07-05 22:10:55 -04:00
0f20c4cdab tape: Fix TestEncodeDecodeAny using int instead of uint32 2025-07-05 19:14:36 -04:00
1b82f2cd83 tape: Use tu.Describe() in tests 2025-07-05 18:47:57 -04:00
6ba70ed046 internal/testutil: Create function to closely examine any data 2025-07-05 18:47:08 -04:00
c118a4d7ef tape: Change name of test to TestEncodeDecodeAnyTable 2025-07-04 14:30:23 -04:00
877698d402 tape: Remove print statements 2025-07-04 14:18:56 -04:00
5989a82bee tape: Fix negative slice length 2025-07-04 14:18:30 -04:00
33 changed files with 3112 additions and 444 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/generate/test
/debug

View File

@@ -1,43 +1,66 @@
package main
import "os"
import "fmt"
import "strings"
import "path/filepath"
import "git.tebibyte.media/sashakoshka/go-cli"
import "git.tebibyte.media/sashakoshka/goparse"
import "git.tebibyte.media/sashakoshka/hopp/generate"
func main() {
name := os.Args[0]
if len(os.Args) != 3 {
fmt.Fprintf(os.Stderr, "Usage: %s SOURCE DESTINATION\n", name)
flagOutput := cli.NewInputFlag('o', "output", "The output file", "", cli.ValString)
flagPackageName := cli.NewInputFlag('p', "package-name", "The package name of the file", "", cli.ValString)
command := cli.New("Compile PDL files to program source code",
flagOutput,
flagPackageName)
command.Syntax = "FILE [OPTIONS]..."
command.ParseOrExit(os.Args)
if len(command.Args) != 1 {
command.Usage()
os.Exit(2)
}
source := os.Args[1]
destination := os.Args[2]
source := command.Args[0]
destination := flagOutput.Value
if destination == "" {
destination = "protocol.go"
}
input, err := os.Open(source)
handleErr(1, err)
handleErr(command, 1, err)
defer input.Close()
protocol, err := generate.ParseReader(input)
handleErr(1, err)
protocol, err := generate.ParseReader(source, input)
handleErr(command, 1, err)
packageName := flagPackageName.Value
if packageName == "" {
absDestination, err := filepath.Abs(destination)
handleErr(1, err)
packageName := cleanPackageName(strings.ReplaceAll(
strings.ToLower(filepath.Base(absDestination)),
" ", "_"))
destination = filepath.Join(os.Args[2], "generated.go")
handleErr(command, 1, err)
base := filepath.Base(absDestination)
if scrounged, ok := scroungeForPackageName(base); ok {
packageName = scrounged
} else {
packageName = strings.ReplaceAll(
strings.ToLower(base),
" ", "_")
}
}
packageName = cleanPackageName(packageName)
output, err := os.Create(destination)
handleErr(1, err)
err = protocol.Generate(output, packageName)
handleErr(1, err)
fmt.Fprintf(os.Stderr, "%s: OK\n", name)
handleErr(command, 1, err)
generator := generate.Generator {
Output: output,
PackageName: packageName,
}
_, err = generator.Generate(protocol)
handleErr(command, 1, err)
command.Println(output, "OK")
}
func handleErr(code int, err error) {
func handleErr(command *cli.Cli, code int, err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err)
command.Errorln(parse.Format(err))
os.Exit(code)
}
}
@@ -56,3 +79,32 @@ func cleanPackageName(str string) string {
}
return string(buffer[:j])
}
func scroungeForPackageName(dir string) (string, bool) {
entries, err := os.ReadDir(dir)
if err != nil { return "", false}
for _, entry := range entries {
if !entry.Type().IsRegular() { continue }
file, err := os.Open(filepath.Join(dir, entry.Name()))
if err != nil { continue }
defer file.Close()
// FIXME: it is entirely possible that the only file will have
// a shitload of doc comments preceeding the package name, and
// those comments are usually huge so this is bad
buffer := [512]byte { }
n, _ := file.Read(buffer[:])
text := string(buffer[:n])
packageIndex := strings.Index(text, "package")
if packageIndex < 0 { continue }
text = text[packageIndex:]
newlineIndex := strings.Index(text, "\n")
if packageIndex > 0 { text = text[:newlineIndex] }
fields := strings.Fields(text)
if len(fields) < 2 { continue }
return fields[1], true
}
return "", false
}

47
codec.go Normal file
View File

@@ -0,0 +1,47 @@
package hopp
import "fmt"
type anyInt16 interface { ~uint16 | ~int16 }
type anyInt64 interface { ~uint64 | ~int64 }
// decodeI16 decodes a 16 bit integer from the given data.
func decodeI16[T anyInt16](data []byte) (T, error) {
if len(data) != 2 { return 0, fmt.Errorf("decoding int16: %w", ErrWrongBufferLength) }
return T(data[0]) << 8 | T(data[1]), nil
}
// encodeI16 encodes a 16 bit integer into the given buffer.
func encodeI16[T anyInt16](buffer []byte, value T) error {
if len(buffer) != 2 { return fmt.Errorf("encoding int16: %w", ErrWrongBufferLength) }
buffer[0] = byte(value >> 8)
buffer[1] = byte(value)
return nil
}
// decodeI64 decodes a 64 bit integer from the given data.
func decodeI64[T anyInt64](data []byte) (T, error) {
if len(data) != 8 { return 0, fmt.Errorf("decoding int64: %w", ErrWrongBufferLength) }
return T(data[0]) << 56 |
T(data[1]) << 48 |
T(data[2]) << 40 |
T(data[3]) << 32 |
T(data[4]) << 24 |
T(data[5]) << 16 |
T(data[6]) << 8 |
T(data[7]), nil
}
// encodeI64 encodes a 64 bit integer into the given buffer.
func encodeI64[T anyInt64](buffer []byte, value T) error {
if len(buffer) != 8 { return fmt.Errorf("encoding int64: %w", ErrWrongBufferLength) }
buffer[0] = byte(value >> 56)
buffer[1] = byte(value >> 48)
buffer[2] = byte(value >> 40)
buffer[3] = byte(value >> 32)
buffer[4] = byte(value >> 24)
buffer[5] = byte(value >> 16)
buffer[6] = byte(value >> 8)
buffer[7] = byte(value)
return nil
}

View File

@@ -2,7 +2,7 @@ package hopp
import "io"
import "net"
// import "time"
import "time"
const defaultSizeLimit int64 = 1024 * 1024 // 1 megabyte
@@ -23,8 +23,13 @@ type Conn interface {
// be called in a loop to avoid the connection locking up.
AcceptTrans() (Trans, error)
// SetDeadline operates is [net.Conn.SetDeadline] but for OpenTrans
// and AcceptTrans calls.
SetDeadline(t time.Time) error
// SetSizeLimit sets a limit (in bytes) for how large messages can be.
// By default, this limit is 1 megabyte.
// By default, this limit is 1 megabyte. Note that this is only
// enforced when sending and receiving byte slices, and it does not
// apply to [Trans.SendWriter] or [Trans.ReceiveReader].
SetSizeLimit(limit int64)
}
@@ -40,8 +45,6 @@ type Trans interface {
// unique within the connection. This method is safe for concurrent use.
ID() int64
// TODO: add methods for setting send and receive deadlines
// Send sends a message. This method is not safe for concurrent use.
Send(method uint16, data []byte) error
// SendWriter sends data written to an [io.Writer]. The writer must be
@@ -57,4 +60,12 @@ type Trans interface {
// previously opened through this function will be discarded. This
// method is not safe for concurrent use, and neither is its result.
ReceiveReader() (method uint16, data io.Reader, err error)
// See the documentation for [net.Conn.SetDeadline].
SetDeadline(time.Time) error
// TODO
// // See the documentation for [net.Conn.SetReadDeadline].
// SetReadDeadline(t time.Time) error
// // See the documentation for [net.Conn.SetWriteDeadline].
// SetWriteDeadline(t time.Time) error
}

View File

@@ -0,0 +1,128 @@
# Branched Generated Decoder
Pasted here because Tebitea is down
## The problem
TAPE is designed so that the decoder can gloss over data it does not understand.
Technically the protocol allows for this, but I completely forgot to implement
this in the generated decoder, oops. This would be trivial if TAPE messages were
still flat tables, but they aren't, because those aren't useful enough. So,
let's analyze the problem.
## When it happens
There are two reasons something might not match up with the expected data:
The first and most obvious is unrecognized keys. If the key is not in the set of
recognized keys for a KTV, it should leave the corresponding struct field blank.
Once #6 has been implemented, throw an error if the data was not optional.
The second is wrong types. If we are expecting KTV and get SBA, we should leave
the data as empty. The aforementioned concern about #6 also applies here. We
don't need to worry about special cases at the structure root, because it would
be technically possible to make the structure root an option, so it really is
just a normal value. Until #6, we will leave that blank too.
## Preliminary ideas
The first is going to be pretty simple. All we need to do is have a skimmer
function that skims over TAPE data very, and then call that on the KTV value
each time we run into a mystery key. It should only return an error if the
structure of the data is malformed in such a way that it cannot continue to the
next one. This should be stored in the tape package alongside the dynamic
decoding functions, because they will essentially function the same way and
could probably share lots of code.
The second is a bit more complicated because of the existence of KTV and OTA
because they are aggregate types. Go types work a bit differently, as if you
have an array of an array of an array of ints, that information is represented
in one place, whereas TAPE doesn't really do that. All of that information is
sort of buried within the data structure, so we don't know what we will be
decoding before we actually do it. Whenever we encounter a type we don't expect,
we would need to abort decoding of the entire data structure, and then skim over
whatever detritus is left, which would literally be in a half-decoded state. The
fact that the code is generated flat and thus cannot use return or defer
statements contributes to the complexity of this problem. We need to go up, but
we can't. There is no up, only forward.
Of course, the dynamic decoder does not have this problem in the first place
because it doesn't expect anything, and constructs the destination to fit
whatever it sees in the TAPE structure as it is decoding it. KTVs are completely
dynamic because they are implemented as maps, so the only time it needs to
completely comprehend a type is with OTAs. There is a function called typeOf
that gets the type of the current tag and returns it as a reflect.Type, which
necessitates recursion and peeking at OTAs and their elements.
We could try to do the same thing in the generated decoder, comparing the
determined type against the expected type to try to figure out whether we should
decode an array or a table, etc. This is immediately problematic as it requires
memory to be allocated, both for the peek buffer and the resulting tree of type
information. If we end up with some crazy way to keep track of the types, that's
only one half of the allocation problem and we would still be spending extra
cycles going over all of that twice.
## Performance constraints
The generated decoder is supposed to blaze through data, and it can't do that if
it does all the singing and dancing that the dynamic decoder does. It's time for
some performance constraints:
- No allocations, except as required to build the destination for the data
- No redundant work
- So, no freaking peeking
- It should take well under 500 lines of generated code to decode one message of
reasonable size (i.e. be careful not to bloat the binary)
I'm not really going to do my usual thing here of making a slow version and
speeding it up over time based on evidence and experimentation because these
constraints inform the design so much it would be impossible to continue without
them. I am 99% confident that these constraints will allow for an acceptable
baseline of performance (for generated code) and we can still profile and
micro-optimize later. This is good enough for me.
Heavy solution
There is a solution that might work very well which involves completely redoing
the generated decoding code. We could create a function for every source type to
destination type mapping that exists in protocol, and then compose them all
together. The decoding methods for each message or type would be wrappers around
the correct function for their root TAPE -> Go type mapping. The main benefit of
this is it would make this problem a lot more manageable because the interface
points between the data would be represented by function boundaries. This would
allow the use of return and defer statements, and would allow more code sharing,
producing a smaller binary. Go would probably inline these where needed.
Would this work? Probably. More investigation is required to make sure. I want
to stop re-writing things I don't need to. On the other hand, it is just the
decoder.
## Light solution
TODO: find a solution that satisfies the performance constraints, keeps the same
identical interface, and works off the same code. I am convinced this is doable,
and it might even allow us to extract more data from an unexpected structure.
However, continuing this way might introduce unmanageable complexity. It is
already a little unmanageable and I am just one pony (kind of).
## Implementation
Heavy solution is going to work here, applied to only the points of
`Generator.generateDecodeValue` where it decodes an aggregate data structure.
That way, only minimal amounts of code need to be redone.
Whenever a branch needs to happen, a call shall be generated, a deferred
implementation request shall be added to a special FIFO queue within the
generator. After generating data structures and their root decoding functions,
the generator shall pick away at this queue until no requests remain. The
generator shall accept new items during this process, so that recursion is
possible. This is all to ensure it is only ever writing one function at a time
The functions shall take a pointer to a type that accepts any type like (~) the
destination's base type. We should also probably just call
`Generator.generateDecodeValue` directly on user defined types this way, keeping
their public `Decode` methods just for convenience.
The tape package shall contain a skimming function that takes a decoder and a
tag, and recursively consumes the decoder given the context of the tag. This
shall be utilized by the decoder functions to skip over values if their tags
or keys do not match up with what is expected.

View File

@@ -7,12 +7,12 @@ PDL allows defining a protocol using HOPP and TAPE.
| Syntax | TN | CN | Description
| ---------- | ------- | -: | -----------
| I5 | SI | |
| I8 | LI | 0 |
| I16 | LI | 1 |
| I32 | LI | 3 |
| I64 | LI | 7 |
| I128[^2] | LI | 15 |
| I256[^2] | LI | 31 |
| I8 | LSI | 0 |
| I16 | LSI | 1 |
| I32 | LSI | 3 |
| I64 | LSI | 7 |
| I128[^2] | LSI | 15 |
| I256[^2] | LSI | 31 |
| U5 | SI | |
| U8 | LI | 0 |
| U16 | LI | 1 |
@@ -25,11 +25,13 @@ PDL allows defining a protocol using HOPP and TAPE.
| F64 | FP | 7 |
| F128[^2] | FP | 15 |
| F256[^2] | FP | 31 |
| Bool | SI | |
| String | SBA/LBA | * | UTF-8 string
| Buffer | SBA/LBA | * | Byte array
| []\<TYPE\> | OTA | * | Array of any type[^1]
| Table | KTV | * | Table with undefined schema
| {...} | KTV | * | Table with defined schema
| Any | * | * | Value of an undefined type
[^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.
@@ -51,6 +53,7 @@ structures. They are separated by whitespace.
| RBrace | `}` | A right curly brace.
| LBracket | `[` | A left square bracket.
| RBracket | `]` | A right square bracket.
| Comment | `\/\/.*$` | A doc comment starting with a double-slash.
## Syntax
@@ -67,18 +70,27 @@ an Ident token respectively. A message consists of the method code (Method), the
message name (Ident), and the message's root type. This is usually a table, but
can be anything.
Messages, types, and table fields can all have doc comments preceding them,
which are used to generate documentation for the protocol. The syntax is the
same as Go's (for now). Comments aren't allowed anywhere else.
Here is an example of all that:
```
// Connect is sent from the client to the server as the first message of an
// authenticated transaction.
M0000 Connect {
0000 Name String,
0001 Password String,
}
// UserList is sent from the server to the client in response to a Connect
// message.
M0001 UserList {
0000 Users []User,
}
// User holds profile information about a single user.
User {
0000 Name String,
0001 Bio String,
@@ -98,7 +110,7 @@ Below is an EBNF description of the language.
<field> -> <key> <ident> <type>
<type> -> <ident>
| "[" "]" <type>
| "{" (<field> ",")* [<field>] "}"
<message> -> <method> <ident> <type>
<typedef> -> <ident> <type>
| "{" (<comment>* <field> ",")* [<comment>* <field>] "}"
<message> -> <comment>* <method> <ident> <type>
<typedef> -> <comment>* <ident> <type>
```

View File

@@ -75,16 +75,16 @@ connection. Thus, the value may range from 0 to 31 if unsigned, and from -16 to
17 if signed.
#### Large Integer (LI)
LI encodes an integer of up to 256 bits, which are stored in the payload. The CN
determine the length of the payload in bytes. The integer is big-endian. Whether
LI encodes an integer of up to 256 bits, which are stored in the payload. The
length of the payload (in bytes) is CN + 1. The integer is big-endian. Whether
the payload is interpreted as unsigned or as signed two's complement is semantic
information and must be agreed upon by both sides of the connection. Thus, the
value may range from 0 to 31 if unsigned, and from -16 to 17 if signed.
#### Floating Point (FP)
FP encodes an IEEE 754 floating point number of up to 256 bits, which are stored
in the payload. The CN determines the length of the payload in bytes, and it may
only be one of these values: 16, 32, 64, 128, or 256.
in the payload. The length of the payload (in bytes) is CN + 1. The only
supported bit widths for floats are as follows: 16, 32, 64, 128, and 256.
#### Small Byte Array (SBA)
SBA encodes an array of up to 32 bytes, which are stored in the paylod. The
@@ -98,15 +98,16 @@ in bytes is determined by the CN.
#### One-Tag Array (OTA)
OTA encodes an array of up to 2^256 items, which are stored in the payload after
the length field and the item tag, where the length field comes first. Each item
must be the same length, as they all share the same tag. The length of the data
length field in bytes is determined by the CN.
must be the same length, as they all share the same tag. The length of the
length field (in bytes) is CN + 1.
#### Key-Tag-Value Table (KTV)
KTV encodes a table of up to 2^256 key/value pairs, which are stored in the
payload after the length field. The pairs themselves consist of a 16-bit
unsigned big-endian key followed by a tag and then the payload. Pair values can
be of different types and sizes. The order of the pairs is not significant and
should never be treated as such.
should never be treated as such. The length of the length field (in bytes) is
CN + 1.
## Transports
A transport is a protocol that HOPP connections can run on top of. HOPP

View File

@@ -9,6 +9,7 @@ type Error string; const (
ErrIntegerOverflow Error = "integer overflow"
ErrMessageMalformed Error = "message is malformed"
ErrTablePairMissing Error = "required table pair is missing"
ErrWrongBufferLength Error = "wrong buffer length"
)
// Error implements the error interface.

File diff suppressed because it is too large Load Diff

458
generate/generate_test.go Normal file
View File

@@ -0,0 +1,458 @@
package generate
// import "fmt"
import "testing"
// TODO: once everything has been ironed out, test that the public API of the
// generator is equal to something specific
var exampleProtocol = defaultProtocol()
func init() {
exampleProtocol.Messages[0x0000] = Message {
Name: "Connect",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "Name", Type: TypeString { } },
0x0001: Field { Name: "Password", Type: TypeString { } },
},
},
}
exampleProtocol.Messages[0x0001] = Message {
Name: "UserList",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "Users", Type: TypeArray { Element: TypeNamed { Name: "User" } } },
},
},
}
exampleProtocol.Messages[0x0002] = Message {
Name: "Pulse",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "Index", Type: TypeInt { Bits: 5 } },
0x0001: Field { Name: "Offset", Type: TypeInt { Bits: 16, Signed: true }},
0x0002: Field { Name: "X", Type: TypeFloat { Bits: 16 }},
0x0003: Field { Name: "Y", Type: TypeFloat { Bits: 32 }},
0x0004: Field { Name: "Z", Type: TypeFloat { Bits: 64 }},
},
},
}
exampleProtocol.Messages[0x0003] = Message {
Name: "NestedArray",
Type: TypeArray { Element: TypeArray { Element: TypeInt { Bits: 8 } } },
}
exampleProtocol.Messages[0x0004] = Message {
Name: "Integers",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "U5", Type: TypeInt { Bits: 5 } },
0x0001: Field { Name: "U8", Type: TypeInt { Bits: 8 } },
0x0002: Field { Name: "U16", Type: TypeInt { Bits: 16 } },
0x0003: Field { Name: "U32", Type: TypeInt { Bits: 32 } },
0x0004: Field { Name: "U64", Type: TypeInt { Bits: 64 } },
0x0006: Field { Name: "I8", Type: TypeInt { Bits: 8, Signed: true } },
0x0007: Field { Name: "I16", Type: TypeInt { Bits: 16, Signed: true } },
0x0008: Field { Name: "I32", Type: TypeInt { Bits: 32, Signed: true } },
0x0009: Field { Name: "I64", Type: TypeInt { Bits: 64, Signed: true } },
0x000B: Field { Name: "NI8", Type: TypeInt { Bits: 8, Signed: true } },
0x000C: Field { Name: "NI16",Type: TypeInt { Bits: 16, Signed: true } },
0x000D: Field { Name: "NI32",Type: TypeInt { Bits: 32, Signed: true } },
0x000E: Field { Name: "NI64",Type: TypeInt { Bits: 64, Signed: true } },
0x000F: Field { Name: "Bool",Type: TypeBool { } },
},
},
}
exampleProtocol.Messages[0x0005] = Message {
Name: "Dynamic",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "AU8", Type: TypeAny { } },
0x0001: Field { Name: "AU16", Type: TypeAny { } },
0x0002: Field { Name: "AU32", Type: TypeAny { } },
0x0003: Field { Name: "AU64", Type: TypeAny { } },
0x0004: Field { Name: "AI8", Type: TypeAny { } },
0x0005: Field { Name: "AI16", Type: TypeAny { } },
0x0006: Field { Name: "AI32", Type: TypeAny { } },
0x0007: Field { Name: "AI64", Type: TypeAny { } },
0x0008: Field { Name: "AF32", Type: TypeAny { } },
0x0009: Field { Name: "AF64", Type: TypeAny { } },
0x000A: Field { Name: "AString", Type: TypeAny { } },
0x000B: Field { Name: "AArray", Type: TypeAny { } },
0x000C: Field { Name: "ATable", Type: TypeAny { } },
0x000D: Field { Name: "T0", Type: TypeTable { } },
},
},
}
exampleProtocol.Types["User"] = Typedef {
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "Name", Type: TypeString { } },
0x0001: Field { Name: "Bio", Type: TypeString { } },
0x0002: Field { Name: "Followers", Type: TypeInt { Bits: 32 } },
},
},
}
}
func TestGenerateRunEncodeDecode(test *testing.T) {
testGenerateRun(test, &exampleProtocol, "encode-decode", `
// imports
`, `
log.Println("MessageConnect")
messageConnect := MessageConnect {
Name: "rarity",
Password: "gems",
}
testEncodeDecode(
&messageConnect,
tu.S(0xE0, 0x02).AddVar(
[]byte { 0x00, 0x00, 0x86, 'r', 'a', 'r', 'i', 't', 'y' },
[]byte { 0x00, 0x01, 0x84, 'g', 'e', 'm', 's' },
))
log.Println("MessageUserList")
messageUserList := MessageUserList {
Users: []User {
User {
Name: "rarity",
Bio: "asdjads",
Followers: 0x324,
},
User {
Name: "deez nuts",
Bio: "logy",
Followers: 0x8000,
},
User {
Name: "creekflow",
Bio: "im creekflow",
Followers: 0x3894,
},
},
}
testEncodeDecode(
&messageUserList,
tu.S(0xE0, 0x01, 0x00, 0x00,
0xC0, 0x03, 0xE0,
).Add(0x03).AddVar(
[]byte { 0x00, 0x00, 0x86, 'r', 'a', 'r', 'i', 't', 'y' },
[]byte { 0x00, 0x01, 0x87, 'a', 's', 'd', 'j', 'a', 'd', 's' },
[]byte { 0x00, 0x02, 0x23, 0x00, 0x00, 0x03, 0x24 },
).Add(0x03).AddVar(
[]byte { 0x00, 0x00, 0x89, 'd', 'e', 'e', 'z', ' ', 'n', 'u', 't', 's' },
[]byte { 0x00, 0x01, 0x84, 'l', 'o', 'g', 'y' },
[]byte { 0x00, 0x02, 0x23, 0x00, 0x00, 0x80, 0x00 },
).Add(0x03).AddVar(
[]byte { 0x00, 0x00, 0x89, 'c', 'r', 'e', 'e', 'k', 'f', 'l', 'o', 'w' },
[]byte { 0x00, 0x01, 0x8C, 'i', 'm', ' ', 'c', 'r', 'e', 'e', 'k', 'f',
'l', 'o', 'w' },
[]byte { 0x00, 0x02, 0x23, 0x00, 0x00, 0x38, 0x94 },
))
log.Println("MessagePulse")
messagePulse := MessagePulse {
Index: 9,
Offset: -0x3521,
X: 45.375,
Y: 294.1,
Z: 384729384.234892034,
}
testEncodeDecode(
&messagePulse,
tu.S(0xE0, 0x05).AddVar(
[]byte { 0x00, 0x00, 0x09 },
[]byte { 0x00, 0x01, 0x41, 0xCA, 0xDF },
[]byte { 0x00, 0x02, 0x61, 0x51, 0xAC },
[]byte { 0x00, 0x03, 0x63, 0x43, 0x93, 0x0C, 0xCD },
[]byte { 0x00, 0x04, 0x67, 0x41, 0xB6, 0xEE, 0x81, 0x28, 0x3C, 0x21, 0xE2 },
))
log.Println("MessageNestedArray")
uint8s := func(n int) []uint8 {
array := make([]uint8, n)
for index := range array {
array[index] = uint8(index + 1) | 0xF0
}
return array
}
messageNestedArray := MessageNestedArray {
uint8s(6),
uint8s(35),
}
testEncodeDecode(
&messageNestedArray,
tu.S(0xC0, 0x02, 0xC0,
0x06, 0x20, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6,
35, 0x20, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6,
0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC,
0xFD, 0xFE, 0xFF, 0xF0, 0xF1, 0xF2,
0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8,
0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE,
0xFF, 0xF0, 0xF1, 0xF2, 0xF3))
log.Println("MessageIntegers")
messageIntegers := MessageIntegers {
U5: 0x13,
U8: 0xC9,
U16: 0x34C9,
U32: 0x10E134C9,
U64: 0x639109BC10E134C9,
I8: 0x35,
I16: 0x34C9,
I32: 0x10E134C9,
I64: 0x639109BC10E134C9,
NI8: -0x35,
NI16: -0x34C9,
NI32: -0x10E134C9,
NI64: -0x639109BC10E134C9,
Bool: true,
}
testEncodeDecode(
&messageIntegers,
tu.S(0xE0, 14).AddVar(
[]byte { 0x00, 0x00, 0x13 },
[]byte { 0x00, 0x01, 0x20, 0xC9 },
[]byte { 0x00, 0x02, 0x21, 0x34, 0xC9 },
[]byte { 0x00, 0x03, 0x23, 0x10, 0xE1, 0x34, 0xC9 },
[]byte { 0x00, 0x04, 0x27, 0x63, 0x91, 0x09, 0xBC, 0x10, 0xE1, 0x34, 0xC9 },
[]byte { 0x00, 0x06, 0x40, 0x35 },
[]byte { 0x00, 0x07, 0x41, 0x34, 0xC9 },
[]byte { 0x00, 0x08, 0x43, 0x10, 0xE1, 0x34, 0xC9 },
[]byte { 0x00, 0x09, 0x47, 0x63, 0x91, 0x09, 0xBC, 0x10, 0xE1, 0x34, 0xC9 },
[]byte { 0x00, 0x0B, 0x40, 0xCB },
[]byte { 0x00, 0x0C, 0x41, 0xCB, 0x37 },
[]byte { 0x00, 0x0D, 0x43, 0xEF, 0x1E, 0xCB, 0x37 },
[]byte { 0x00, 0x0E, 0x47, 0x9C, 0x6E, 0xF6, 0x43, 0xEF, 0x1E, 0xCB, 0x37 },
[]byte { 0x00, 0x0F, 0x01 },
))
log.Println("MessageDynamic")
messageDynamic := MessageDynamic {
AU8: uint8(0x23),
AU16: uint16(0x3247),
AU32: uint32(0x87324523),
AU64: uint64(0x3284029034098234),
AI8: int8(0x23),
AI16: int16(0x3247),
AI32: int32(0x57324523),
AI64: int64(0x3284029034098234),
AF32: float32(2342.2378),
AF64: float64(324.8899992),
AString: "fox bed",
AArray: []int16 { 0x7, 0x6, 0x5, 0x4 },
ATable: map[uint16] any {
0x0001: int8(0x8),
0x0002: float64(4.4),
},
T0: map[uint16] any {
0x0001: float32(489.5),
0x0002: "hi",
0x0003: uint16(0x3992),
},
}
testEncodeDecode(
&messageDynamic,
tu.S(0xE0, 14).AddVar(
[]byte { 0x00, 0x00, 0x20, 0x23 },
[]byte { 0x00, 0x01, 0x21, 0x32, 0x47 },
[]byte { 0x00, 0x02, 0x23, 0x87, 0x32, 0x45, 0x23 },
[]byte { 0x00, 0x03, 0x27, 0x32, 0x84, 0x02, 0x90, 0x34, 0x09, 0x82, 0x34 },
[]byte { 0x00, 0x04, 0x40, 0x23 },
[]byte { 0x00, 0x05, 0x41, 0x32, 0x47 },
[]byte { 0x00, 0x06, 0x43, 0x57, 0x32, 0x45, 0x23 },
[]byte { 0x00, 0x07, 0x47, 0x32, 0x84, 0x02, 0x90, 0x34, 0x09, 0x82, 0x34 },
[]byte { 0x00, 0x08, 0x63, 0x45, 0x12, 0x63, 0xCE },
[]byte { 0x00, 0x09, 0x67, 0x40, 0x74, 0x4E, 0x3D, 0x6F, 0xCD, 0x17, 0x75 },
[]byte { 0x00, 0x0A, 0x87, 'f', 'o', 'x', ' ', 'b', 'e', 'd' },
[]byte { 0x00, 0x0B, 0xC0, 0x04, 0x41,
0x00, 0x07,
0x00, 0x06,
0x00, 0x05,
0x00, 0x04 },
[]byte { 0x00, 0x0C, 0xE0, 0x02,
0x00, 0x01, 0x40, 0x08,
0x00, 0x02, 0x67, 0x40, 0x11, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A },
[]byte { 0x00, 0x0D, 0xE0, 0x03, // ERR
0x00, 0x01, 0x63, 0x43, 0xF4, 0xC0, 0x00,
0x00, 0x02, 0x82, 'h', 'i',
0x00, 0x03, 0x21, 0x39, 0x92 },
))
`)
}
func TestGenerateRunDecodeWrongType(test *testing.T) {
protocol := defaultProtocol()
protocol.Messages[0x0000] = Message {
Name: "Uint5",
Type: TypeInt { Bits: 5 },
}
protocol.Messages[0x0001] = Message {
Name: "Uint8",
Type: TypeInt { Bits: 8 },
}
protocol.Messages[0x0002] = Message {
Name: "Uint16",
Type: TypeInt { Bits: 16 },
}
protocol.Messages[0x0003] = Message {
Name: "Uint32",
Type: TypeInt { Bits: 32 },
}
protocol.Messages[0x0004] = Message {
Name: "Uint64",
Type: TypeInt { Bits: 64 },
}
protocol.Messages[0x0005] = Message {
Name: "Int8",
Type: TypeInt { Bits: 8 },
}
protocol.Messages[0x0006] = Message {
Name: "Int16",
Type: TypeInt { Bits: 16 },
}
protocol.Messages[0x0007] = Message {
Name: "Int32",
Type: TypeInt { Bits: 32 },
}
protocol.Messages[0x0008] = Message {
Name: "Int64",
Type: TypeInt { Bits: 64 },
}
protocol.Messages[0x0009] = Message {
Name: "String",
Type: TypeString { },
}
protocol.Messages[0x000A] = Message {
Name: "Buffer",
Type: TypeBuffer { },
}
protocol.Messages[0x000B] = Message {
Name: "StringArray",
Type: TypeArray { Element: TypeString { } },
}
protocol.Messages[0x000C] = Message {
Name: "Table",
Type: TypeTable { },
}
protocol.Messages[0x000D] = Message {
Name: "TableDefined",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "Name", Type: TypeString { } },
0x0001: Field { Name: "Password", Type: TypeString { } },
},
},
}
testGenerateRun(test, &protocol, "decode-wrong-type", `
// imports
`, `
datas := [][]byte {
/* int8 */ []byte { byte(tape.LSI.WithCN(0)), 0x45 },
/* int16 */ []byte { byte(tape.LSI.WithCN(1)), 0x45, 0x67 },
/* int32 */ []byte { byte(tape.LSI.WithCN(3)), 0x45, 0x67, 0x89, 0xAB },
/* int64 */ []byte { byte(tape.LSI.WithCN(7)), 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23 },
/* uint5 */ []byte { byte(tape.SI.WithCN(12)) },
/* uint8 */ []byte { byte(tape.LI.WithCN(0)), 0x45 },
/* uint16 */ []byte { byte(tape.LI.WithCN(1)), 0x45, 0x67 },
/* uint32 */ []byte { byte(tape.LI.WithCN(3)), 0x45, 0x67, 0x89, 0xAB },
/* uint64 */ []byte { byte(tape.LI.WithCN(7)), 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23 },
/* string */ []byte { byte(tape.SBA.WithCN(7)), 'p', 'u', 'p', 'e', 'v', 'e', 'r' },
/* []byte */ []byte { byte(tape.SBA.WithCN(5)), 'b', 'l', 'a', 'r', 'g' },
/* []string */ []byte {
byte(tape.OTA.WithCN(0)), 2, byte(tape.LBA.WithCN(0)),
0x08, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23,
0x05, 0x11, 0x11, 0x11, 0x11, 0x11,
},
/* map[uint16] any */ []byte {
byte(tape.KTV.WithCN(0)), 2,
0x02, 0x23, byte(tape.LSI.WithCN(1)), 0x45, 0x67,
0x02, 0x23, byte(tape.LI.WithCN(3)), 0x45, 0x67, 0x89, 0xAB,
},
}
for index, data := range datas {
log.Printf("data %2d %v [%s]", index, tape.Tag(data[0]), tu.HexBytes(data[1:]))
// integers should only assign to other integers
if index > 8 {
cas := func(destination Message) {
n, err := destination.Decode(tape.NewDecoder(bytes.NewBuffer(data)))
if err != nil { log.Fatalf("error: %v | n: %d", err, n) }
reflectValue := reflect.ValueOf(destination).Elem()
if reflectValue.CanInt() {
if reflectValue.Int() != 0 {
log.Fatalf(
"destination not zero: %v",
reflectValue.Elem().Interface())
}
} else {
if reflectValue.Uint() != 0 {
log.Fatalf(
"destination not zero: %v",
reflectValue.Elem().Interface())
}
}
if n != len(data) {
log.Fatalf("n not equal: %d != %d", n, len(data))
}
}
log.Println("- MessageInt8")
{ var dest MessageInt8; cas(&dest) }
log.Println("- MessageInt16")
{ var dest MessageInt16; cas(&dest) }
log.Println("- MessageInt32")
{ var dest MessageInt32; cas(&dest) }
log.Println("- MessageInt64")
{ var dest MessageInt64; cas(&dest) }
log.Println("- MessageUint8")
{ var dest MessageUint8; cas(&dest) }
log.Println("- MessageUint16")
{ var dest MessageUint16; cas(&dest) }
log.Println("- MessageUint32")
{ var dest MessageUint32; cas(&dest) }
log.Println("- MessageUint64")
{ var dest MessageUint64; cas(&dest) }
}
arrayCase := func(destination Message) {
n, err := destination.Decode(tape.NewDecoder(bytes.NewBuffer(data)),)
if err != nil { log.Fatalf("error: %v | n: %d", err, n) }
reflectDestination := reflect.ValueOf(destination)
reflectValue := reflectDestination.Elem()
if reflectValue.Len() != 0 {
log.Fatalf("len(destination) not zero: %v", reflectValue.Interface())
}
if n != len(data) {
log.Fatalf("n not equal: %d != %d", n, len(data))
}
}
anyCase := func(destination Message) {
n, err := destination.Decode(tape.NewDecoder(bytes.NewBuffer(data)),)
if err != nil { log.Fatalf("error: %v | n: %d", err, n) }
reflectDestination := reflect.ValueOf(destination)
reflectValue := reflectDestination.Elem()
if reflectValue == reflect.Zero(reflectValue.Type()) {
log.Fatalf("len(destination) not zero: %v", reflectValue.Interface())
}
if n != len(data) {
log.Fatalf("n not equal: %d != %d", n, len(data))
}
}
// SBA/LBA types should only assign to other SBA/LBA types
if index != 9 && index != 10 {
log.Println("- MessageString")
{ var dest MessageString; arrayCase(&dest) }
log.Println("- MessageBuffer")
{ var dest MessageBuffer; arrayCase(&dest) }
}
// arrays should only assign to other arrays
if index != 11 {
log.Println("- MessageStringArray")
{ var dest MessageStringArray; arrayCase(&dest) }
}
// tables should only assign to other tables
if index != 12 {
log.Println("- MessageTable")
{ var dest = make(MessageTable); arrayCase(&dest) }
log.Println("- MessageTableDefined")
{ var dest MessageTableDefined; anyCase(&dest) }
}
}
`)
}

View File

@@ -15,6 +15,7 @@ const (
TokenRBrace
TokenLBracket
TokenRBracket
TokenComment
)
var tokenNames = map[parse.TokenKind] string {
@@ -26,6 +27,7 @@ var tokenNames = map[parse.TokenKind] string {
TokenRBrace: "RBrace",
TokenLBracket: "LBracket",
TokenRBracket: "RBracket",
TokenComment: "Comment",
}
func Lex(fileName string, reader io.Reader) (parse.Lexer, error) {
@@ -81,6 +83,18 @@ func (this *lexer) nextInternal() (token parse.Token, err error) {
}
}
unexpected := func() error {
if unicode.IsPrint(this.rune) {
return parse.Errorf (
this.pos(), "unexpected rune '%c'",
this.rune)
} else {
return parse.Errorf (
this.pos(), "unexpected rune %U",
this.rune)
}
}
defer func () {
newPos := this.pos()
newPos.End -- // TODO figure out why tf we have to do this
@@ -133,14 +147,21 @@ func (this *lexer) nextInternal() (token parse.Token, err error) {
token.Kind = TokenRBracket
appendRune()
if this.eof { err = nil; return }
case unicode.IsPrint(this.rune):
err = parse.Errorf (
this.pos(), "unexpected rune '%c'",
this.rune)
// Comment
case this.rune == '/':
token.Kind = TokenComment
appendRune()
if this.eof { return }
if this.rune != '/' {
err = unexpected()
return
}
for this.rune != '\n' {
appendRune()
if this.eof { err = nil; return }
}
default:
err = parse.Errorf (
this.pos(), "unexpected rune %U",
this.rune)
err = unexpected()
}
return

View File

@@ -6,14 +6,21 @@ import "git.tebibyte.media/sashakoshka/goparse"
func TestLex(test *testing.T) {
lexer, err := Lex("test.pdl", strings.NewReader(`
// User holds profile information about a single user.
M0001 User {
0000 Name String,
// dog water comment
// Users is asdkjsagkj why
//
// wow
0001 Users []User,
0002 Followers U32,
}`))
if err != nil { test.Fatal(parse.Format(err)) }
correctTokens := []parse.Token {
tok(TokenComment, "// User holds profile information about a single user."),
tok(TokenMethod, "0001"),
tok(TokenIdent, "User"),
tok(TokenLBrace, "{"),
@@ -21,6 +28,10 @@ func TestLex(test *testing.T) {
tok(TokenIdent, "Name"),
tok(TokenIdent, "String"),
tok(TokenComma, ","),
tok(TokenComment, "// dog water comment"),
tok(TokenComment, "// Users is asdkjsagkj why"),
tok(TokenComment, "// "),
tok(TokenComment, "// wow"),
tok(TokenKey, "0001"),
tok(TokenIdent, "Users"),
tok(TokenLBracket, "["),

131
generate/misc_test.go Normal file
View File

@@ -0,0 +1,131 @@
package generate
import "os"
import "fmt"
import "os/exec"
import "testing"
import "path/filepath"
func testGenerateRun(test *testing.T, protocol *Protocol, title, imports, testCase string) {
// reset data directory
dir := filepath.Join("test", title)
err := os.RemoveAll(dir)
if err != nil { test.Fatal(err) }
err = os.MkdirAll(dir, 0750)
if err != nil { test.Fatal(err) }
// open files
sourceFile, err := os.Create(filepath.Join(dir, "protocol.go"))
if err != nil { test.Fatal(err) }
defer sourceFile.Close()
mainFile, err := os.Create(filepath.Join(dir, "main.go"))
if err != nil { test.Fatal(err) }
defer mainFile.Close()
// generate protocol
generator := Generator {
Output: sourceFile,
PackageName: "main",
}
_, err = generator.Generate(protocol)
if err != nil { test.Fatal(err) }
// build static source files
imports = `
import "log"
import "bytes"
import "reflect"
import "git.tebibyte.media/sashakoshka/hopp/tape"
import tu "git.tebibyte.media/sashakoshka/hopp/internal/testutil"
` + imports
setup := `log.Println("*** BEGIN TEST CASE OUTPUT ***")`
teardown := `log.Println("--- END TEST CASE OUTPUT ---")`
static := `
func testEncode(message Message, correct tu.Snake) {
buffer := bytes.Buffer { }
encoder := tape.NewEncoder(&buffer)
n, err := message.Encode(encoder)
if err != nil { log.Fatalf("at %d: %v\n", n, err) }
encoder.Flush()
got := buffer.Bytes()
log.Printf("got: [%s]", tu.HexBytes(got))
log.Println("correct:", correct)
if n != len(got) {
log.Fatalf("n incorrect: %d != %d\n", n, len(got))
}
if ok, n := correct.Check(got); !ok {
log.Fatalln("not equal at", n)
}
}
func testDecode(correct Message, data any) {
var flat []byte
switch data := data.(type) {
case []byte: flat = data
case tu.Snake: flat = data.Flatten()
}
message := reflect.New(reflect.ValueOf(correct).Elem().Type()).Interface().(Message)
log.Println("before: ", message)
decoder := tape.NewDecoder(bytes.NewBuffer(flat))
n, err := message.Decode(decoder)
if err != nil { log.Fatalf("at %d: %v\n", n, err) }
log.Println("got: ", message)
log.Println("correct:", correct)
if n != len(flat) {
log.Fatalf("n incorrect: %d != %d\n", n, len(flat))
}
if !reflect.DeepEqual(message, correct) {
log.Fatalln("not equal")
}
}
// TODO: possibly combine the two above functions into this one,
// also take a data parameter here (snake)
func testEncodeDecode(message Message, data tu.Snake) {buffer := bytes.Buffer { }
log.Println("encoding:")
encoder := tape.NewEncoder(&buffer)
n, err := message.Encode(encoder)
if err != nil { log.Fatalf("at %d: %v\n", n, err) }
encoder.Flush()
got := buffer.Bytes()
log.Printf("got: [%s]", tu.HexBytes(got))
log.Println("correct:", data)
if n != len(got) {
log.Fatalf("n incorrect: %d != %d\n", n, len(got))
}
if ok, n := data.Check(got); !ok {
log.Fatalln("not equal at", n)
}
log.Println("decoding:")
destination := reflect.New(reflect.ValueOf(message).Elem().Type()).Interface().(Message)
flat := data.Flatten()
log.Println("before: ", tu.Describe(destination))
decoder := tape.NewDecoder(bytes.NewBuffer(flat))
n, err = destination.Decode(decoder)
if err != nil { log.Fatalf("at %d: %v\n", n, err) }
log.Println("got: ", tu.Describe(destination))
log.Println("correct:", tu.Describe(message))
if n != len(flat) {
log.Fatalf("n incorrect: %d != %d\n", n, len(flat))
}
if !reflect.DeepEqual(destination, message) {
log.Fatalln("not equal")
}
}
`
fmt.Fprintf(
mainFile, "package main\n%s\nfunc main() {\n%s\n%s\n%s\n}\n%s",
imports, setup, testCase, teardown, static)
// build and run test
command := exec.Command("go", "run", "./" + filepath.Join("generate", dir))
workingDirAbs, err := filepath.Abs("..")
if err != nil { test.Fatal(err) }
command.Dir = workingDirAbs
command.Env = os.Environ()
output, err := command.CombinedOutput()
test.Logf("output of %v:\n%s", command, output)
if err != nil { test.Fatal(err) }
}

View File

@@ -1,6 +1,7 @@
package generate
import "io"
import "strings"
import "strconv"
import "git.tebibyte.media/sashakoshka/goparse"
@@ -21,12 +22,12 @@ func Parse(lx parse.Lexer) (*Protocol, error) {
func defaultProtocol() Protocol {
return Protocol {
Messages: make(map[uint16] Message),
Types: map[string] Type { },
Types: map[string] Typedef { },
}
}
func ParseReader(reader io.Reader) (*Protocol, error) {
lx, err := Lex("test.pdl", reader)
func ParseReader(fileName string, reader io.Reader) (*Protocol, error) {
lx, err := Lex(fileName, reader)
if err != nil { return nil, err }
return Parse(lx)
}
@@ -47,18 +48,28 @@ func (this *parser) parse() error {
}
func (this *parser) parseTopLevel() error {
err := this.ExpectDesc("message or typedef", TokenMethod, TokenIdent)
doc := ""
for {
err := this.ExpectDesc("message or typedef", TokenMethod, TokenIdent, TokenComment)
if err != nil { return err }
if this.EOF() { return nil }
if this.Kind() == TokenComment {
if doc != "" { doc += "\n" }
doc += this.parseComment(this.Value())
this.Next()
} else {
break
}
}
switch this.Kind() {
case TokenMethod: return this.parseMessage()
case TokenIdent: return this.parseTypedef()
case TokenMethod: return this.parseMessage(doc)
case TokenIdent: return this.parseTypedef(doc)
}
panic("bug")
}
func (this *parser) parseMessage() error {
func (this *parser) parseMessage(doc string) error {
err := this.Expect(TokenMethod)
if err != nil { return err }
method, err := this.parseHexNumber(this.Value(), 0xFFFF)
@@ -72,12 +83,13 @@ func (this *parser) parseMessage() error {
if err != nil { return err }
this.protocol.Messages[uint16(method)] = Message {
Name: name,
Doc: doc,
Type: typ,
}
return nil
}
func (this *parser) parseTypedef() error {
func (this *parser) parseTypedef(doc string) error {
err := this.Expect(TokenIdent)
if err != nil { return err }
name := this.Value()
@@ -85,7 +97,10 @@ func (this *parser) parseTypedef() error {
if err != nil { return err }
typ, err := this.parseType()
if err != nil { return err }
this.protocol.Types[name] = typ
this.protocol.Types[name] = Typedef {
Doc: doc,
Type: typ,
}
return nil
}
@@ -95,6 +110,30 @@ func (this *parser) parseType() (Type, error) {
switch this.Kind() {
case TokenIdent:
switch this.Value() {
case "U8": return TypeInt { Bits: 8 }, this.Next()
case "U16": return TypeInt { Bits: 16 }, this.Next()
case "U32": return TypeInt { Bits: 32 }, this.Next()
case "U64": return TypeInt { Bits: 64 }, this.Next()
case "U128": return TypeInt { Bits: 128 }, this.Next()
case "U256": return TypeInt { Bits: 256 }, this.Next()
case "I8": return TypeInt { Bits: 8, Signed: true }, this.Next()
case "I16": return TypeInt { Bits: 16, Signed: true }, this.Next()
case "I32": return TypeInt { Bits: 32, Signed: true }, this.Next()
case "I64": return TypeInt { Bits: 64, Signed: true }, this.Next()
case "I128": return TypeInt { Bits: 128, Signed: true }, this.Next()
case "I256": return TypeInt { Bits: 256, Signed: true }, this.Next()
case "F16": return TypeFloat { Bits: 16 }, this.Next()
case "F32": return TypeFloat { Bits: 32 }, this.Next()
case "F64": return TypeFloat { Bits: 64 }, this.Next()
case "F128": return TypeFloat { Bits: 128 }, this.Next()
case "F256": return TypeFloat { Bits: 256 }, this.Next()
case "String": return TypeString { }, this.Next()
case "Buffer": return TypeBuffer { }, this.Next()
case "Table": return TypeTable { }, this.Next()
case "Any": return TypeAny { }, this.Next()
case "Bool": return TypeBool { }, this.Next()
}
return this.parseTypeNamed()
case TokenLBracket:
return this.parseTypeArray()
@@ -134,12 +173,22 @@ func (this *parser) parseTypeTable() (TypeTableDefined, error) {
Fields: make(map[uint16] Field),
}
for {
err := this.ExpectDesc("table field", TokenKey, TokenRBrace)
doc := ""
for {
err := this.ExpectDesc("table field", TokenKey, TokenRBrace, TokenComment)
if err != nil { return TypeTableDefined { }, err }
if this.Kind() == TokenComment {
if doc != "" { doc += "\n" }
doc += this.parseComment(this.Value())
this.Next()
} else {
break
}
}
if this.Is(TokenRBrace) {
break
}
key, field, err := this.parseField()
key, field, err := this.parseField(doc)
if err != nil { return TypeTableDefined { }, err }
typ.Fields[key] = field
err = this.Expect(TokenComma, TokenRBrace)
@@ -155,7 +204,7 @@ func (this *parser) parseTypeTable() (TypeTableDefined, error) {
return typ, nil
}
func (this *parser) parseField() (uint16, Field, error) {
func (this *parser) parseField(doc string) (uint16, Field, error) {
err := this.Expect(TokenKey)
if err != nil { return 0, Field { }, err }
key, err := this.parseHexNumber(this.Value(), 0xFFFF)
@@ -169,6 +218,7 @@ func (this *parser) parseField() (uint16, Field, error) {
if err != nil { return 0, Field { }, err }
return uint16(key), Field {
Name: name,
Doc: doc,
Type: typ,
}, nil
}
@@ -183,3 +233,7 @@ func (this *parser) parseHexNumber(input string, maxValue int64) (int64, error)
}
return number, nil
}
func (this *parser) parseComment(input string) string {
return strings.TrimPrefix(strings.TrimPrefix(input, "//"), " ")
}

View File

@@ -9,45 +9,63 @@ func TestParse(test *testing.T) {
correct := defaultProtocol()
correct.Messages[0x0000] = Message {
Name: "Connect",
Doc: "Connect is sent from the client to the server as the first message of an\nauthenticated transaction.",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "Name", Type: TypeNamed { Name: "String" } },
0x0001: Field { Name: "Password", Type: TypeNamed { Name: "String" } },
0x0000: Field { Name: "Name", Type: TypeString { } },
0x0001: Field { Name: "Password", Type: TypeString { } },
},
},
}
correct.Messages[0x0001] = Message {
Name: "UserList",
Doc: "UserList is sent from the server to the client in response to a Connect\nmessage.",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "Users", Type: TypeArray { Element: TypeNamed { Name: "User" } } },
},
},
}
correct.Types["User"] = TypeTableDefined {
correct.Types["User"] = Typedef {
Doc: "User holds profile information about a single user.",
Type: TypeTableDefined {
Fields: map[uint16] Field {
0x0000: Field { Name: "Name", Type: TypeNamed { Name: "String" } },
0x0001: Field { Name: "Bio", Type: TypeNamed { Name: "String" } },
0x0002: Field { Name: "Followers", Type: TypeNamed { Name: "U32" } },
0x0000: Field { Name: "Name", Type: TypeString { } },
0x0001: Field { Name: "Bio", Type: TypeString { } },
0x0002: Field { Name: "Followers", Type: TypeInt { Bits: 32 } },
0x0003: Field { Name: "Bouncy", Type: TypeBool { } },
},
},
}
correct.Types["Anything"] = Typedef {
Type: TypeAny { },
}
test.Log("CORRECT:", &correct)
got, err := ParseReader(strings.NewReader(`
got, err := ParseReader("test.pdl", strings.NewReader(`
// Connect is sent from the client to the server as the first message of an
// authenticated transaction.
M0000 Connect {
0000 Name String,
// Password is where you put your secrets, your shameful secrets
0001 Password String,
}
// UserList is sent from the server to the client in response to a Connect
// message.
M0001 UserList {
0000 Users []User,
}
// User holds profile information about a single user.
User {
0000 Name String,
0001 Bio String,
0002 Followers U32,
0003 Bouncy Bool,
}
Anything Any
`))
if err != nil { test.Fatal(parse.Format(err)) }
test.Log("GOT: ", got)

View File

@@ -1,17 +1,28 @@
package generate
import "fmt"
import "maps"
import "slices"
import "crypto/md5"
type Protocol struct {
Messages map[uint16] Message
Types map[string] Type
Types map[string] Typedef
}
type Message struct {
Name string
Doc string
Type Type
}
type Typedef struct {
Doc string
Type Type
}
type Type interface {
fmt.Stringer
}
type TypeInt struct {
@@ -19,29 +30,97 @@ type TypeInt struct {
Signed bool
}
func (typ TypeInt) String() string {
output := ""
if typ.Signed {
output += "I"
} else {
output += "U"
}
output += fmt.Sprint(typ.Bits)
return output
}
type TypeFloat struct {
Bits int
}
func (typ TypeFloat) String() string {
return fmt.Sprintf("F%d", typ.Bits)
}
type TypeBool struct { }
func (TypeBool) String() string {
return "Bool"
}
type TypeString struct { }
func (TypeString) String() string {
return "String"
}
type TypeBuffer struct { }
func (TypeBuffer) String() string {
return "Buffer"
}
type TypeArray struct {
Element Type
}
func (typ TypeArray) String() string {
return fmt.Sprintf("[]%v", typ.Element)
}
type TypeTable struct { }
func (TypeTable) String() string {
return "Table"
}
type TypeTableDefined struct {
Fields map[uint16] Field
}
func (typ TypeTableDefined) String() string {
output := "{"
for _, key := range slices.Sorted(maps.Keys(typ.Fields)) {
output += fmt.Sprintf("%04X %v", key, typ.Fields[key])
}
output += "}"
return output
}
type Field struct {
Name string
Doc string
Type Type
}
func (field Field) String() string {
return fmt.Sprintf("%s %v", field.Name, field.Type)
}
type TypeNamed struct {
Name string
}
func (typ TypeNamed) String() string {
return typ.Name
}
type TypeAny struct { }
func (typ TypeAny) String() string {
return "Any"
}
func HashType(typ Type) [16]byte {
// TODO: if we ever want to make the compiler more efficient, this would
// be a good place to start, complex string concatenation in a hot path
// (sorta)
return md5.Sum([]byte(typ.String()))
}

1
go.mod
View File

@@ -3,6 +3,7 @@ module git.tebibyte.media/sashakoshka/hopp
go 1.23.0
require (
git.tebibyte.media/sashakoshka/go-cli v0.1.3
git.tebibyte.media/sashakoshka/go-util v0.9.1
git.tebibyte.media/sashakoshka/goparse v0.2.0
)

2
go.sum
View File

@@ -1,3 +1,5 @@
git.tebibyte.media/sashakoshka/go-cli v0.1.3 h1:tSkWjyx2JrGu6KotbXWSTKSYGGS1D4O3qwCrRoZuwbs=
git.tebibyte.media/sashakoshka/go-cli v0.1.3/go.mod h1:JFA3wSdRkXxa4iQJWHfe3DokiG7Dh2XUJBzPmuVlbuY=
git.tebibyte.media/sashakoshka/go-util v0.9.1 h1:eGAbLwYhOlh4aq/0w+YnJcxT83yPhXtxnYMzz6K7xGo=
git.tebibyte.media/sashakoshka/go-util v0.9.1/go.mod h1:0Q1t+PePdx6tFYkRuJNcpM1Mru7wE6X+it1kwuOH+6Y=
git.tebibyte.media/sashakoshka/goparse v0.2.0 h1:uQmKvOCV2AOlCHEDjg9uclZCXQZzq2PxaXfZ1aIMiQI=

View File

@@ -2,7 +2,9 @@ package testutil
import "fmt"
import "slices"
import "reflect"
import "strings"
import "unicode"
// Snake lets you compare blocks of data where the ordering of certain parts may
// be swapped every which way. It is designed for comparing the encoding of
@@ -63,6 +65,18 @@ func (sn Snake) Check(data []byte) (ok bool, n int) {
return true, n
}
// Flatten returns the snake flattened to a byte array. The result of this
// function always satisfies the snake.
func (sn Snake) Flatten() []byte {
flat := []byte { }
for _, sector := range sn {
for _, variation := range sector {
flat = append(flat, variation...)
}
}
return flat
}
func (sn Snake) String() string {
if len(sn) == 0 || len(sn[0]) == 0 || len(sn[0][0]) == 0 {
return "EMPTY"
@@ -92,3 +106,85 @@ func HexBytes(data []byte) string {
}
return out.String()
}
// Describe returns a string representing the type and data of the given value.
func Describe(value any) string {
desc := describer { }
desc.describe(reflect.ValueOf(value))
return desc.String()
}
type describer struct {
strings.Builder
indent int
}
func (this *describer) describe(value reflect.Value) {
if !value.IsValid() {
this.printf("<invalid>")
return
}
value = reflect.ValueOf(value.Interface())
if !value.IsValid() {
this.printf("<invalid>")
return
}
switch value.Kind() {
case reflect.Array, reflect.Slice:
this.printf("[\n")
this.indent += 1
for index := 0; index < value.Len(); index ++ {
this.iprintf("")
this.describe(value.Index(index))
this.iprintf("\n")
}
this.indent -= 1
this.iprintf("]")
case reflect.Struct:
this.printf("struct {\n")
this.indent += 1
typ := value.Type()
for index := range typ.NumField() {
indexBuffer := [1]int { index }
field := typ.Field(index)
this.iprintf("%s: ", field.Name)
for _, char := range field.Name {
if unicode.IsUpper(char) {
this.describe(value.FieldByIndex(indexBuffer[:]))
} else {
this.printf("<private>")
}
break
}
this.iprintf("\n")
}
this.indent -= 1
this.iprintf("}")
case reflect.Map:
this.printf("map {\n")
this.indent += 1
iter := value.MapRange()
for iter.Next() {
this.iprintf("")
this.describe(iter.Key())
this.printf(": ")
this.describe(iter.Value())
this.iprintf("\n")
}
this.indent -= 1
this.iprintf("}")
case reflect.Pointer:
this.printf("& ")
this.describe(value.Elem())
default:
this.printf("<%v %v>", value.Type(), value.Interface())
}
}
func (this *describer) printf(format string, v ...any) {
fmt.Fprintf(this, format, v...)
}
func (this *describer) iprintf(format string, v ...any) {
fmt.Fprintf(this, strings.Repeat("\t", this.indent) + format, v...)
}

View File

@@ -1,52 +0,0 @@
package hopp
import "fmt"
import "encoding"
import "git.tebibyte.media/sashakoshka/hopp/tape"
// Message is any object that can be sent or received over a HOPP connection.
type Message interface {
// Method returns the method number of the message. This must be unique
// within the protocol, and should not change between calls.
Method() uint16
encoding.BinaryMarshaler
encoding.BinaryUnmarshaler
}
var _ Message = new(MessageData)
// MessageData represents a message that organizes its data into table pairs. It
// can be used to alter a protocol at runtime, transmit data with arbitrary
// keys, etc. Bear in mind that is less performant than generating code because
// it has to make extra memory allocations and such.
type MessageData struct {
// Methd holds the method number. This should only be set once.
Methd uint16
// Pairs maps tags to values.
Pairs map[uint16] []byte
}
// Method returns the message's method field.
func (this *MessageData) Method() uint16 {
return this.Methd
}
// MarshalBinary implements the [encoding.BinaryMarshaler] interface. The
// message is encoded using TAPE (Table Pair Encoding).
func (this *MessageData) MarshalBinary() ([]byte, error) {
buffer, err := tape.EncodePairs(this.Pairs)
if err != nil { return nil, fmt.Errorf("marshaling MessageData: %w", err) }
return buffer, nil
}
// UnmarshalBinary implements the [encoding.BinaryUnmarshaler] interface. The
// message is decoded using TAPE (Table Pair Encoding).
func (this *MessageData) UnmarshalBinary(buffer []byte) error {
this.Pairs = make(map[uint16] []byte)
pairs, err := tape.DecodePairs(buffer)
if err != nil { return fmt.Errorf("unmarshaling MessageData: %w", err) }
for key, value := range pairs {
this.Pairs[key] = value
}
return nil
}

View File

@@ -1,11 +1,12 @@
package hopp
import "io"
import "os"
import "fmt"
import "net"
import "sync"
import "time"
import "sync/atomic"
import "git.tebibyte.media/sashakoshka/hopp/tape"
import "git.tebibyte.media/sashakoshka/go-util/sync"
// TODO investigate why 30 never reaches the server, causing it to wait for ever
@@ -109,6 +110,10 @@ func (this *a) AcceptTrans() (Trans, error) {
}
}
func (this *a) SetDeadline(t time.Time) error {
return this.underlying.SetDeadline(t)
}
func (this *a) SetSizeLimit(limit int64) {
this.sizeLimit = limit
}
@@ -213,6 +218,10 @@ type transA struct {
currentWriter io.Closer
writeBuffer []byte
closed atomic.Bool
closeErr error
deadline *time.Timer
deadlineLock sync.Mutex
}
func (this *transA) Close() error {
@@ -222,6 +231,11 @@ func (this *transA) Close() error {
return err
}
func (this *transA) closeWithError(err error) error {
this.closeErr = err
return this.Close()
}
func (this *transA) closeDontUnlist() (err error) {
// MUST be goroutine safe
this.incoming.Close()
@@ -270,9 +284,9 @@ func (this *transA) Receive() (method uint16, data []byte, err error) {
}
func (this *transA) ReceiveReader() (uint16, io.Reader, error) {
// if the transaction has been closed, return an io.EOF
if this.closed.Load() {
return 0, nil, io.EOF
// if the transaction has been closed, return an appropriate error.
if err := this.errIfClosed(); err != nil {
return 0, nil, err
}
// drain previous reader if necessary
@@ -290,6 +304,54 @@ func (this *transA) ReceiveReader() (uint16, io.Reader, error) {
return method, reader, nil
}
func (this *transA) SetDeadline(t time.Time) error {
this.deadlineLock.Lock()
defer this.deadlineLock.Unlock()
if t == (time.Time { }) {
if this.deadline != nil {
this.deadline.Stop()
}
return nil
}
until := time.Until(t)
if this.deadline == nil {
this.deadline.Reset(until)
return nil
}
this.deadline = time.AfterFunc(until, func () {
this.closeWithError(os.ErrDeadlineExceeded)
})
return nil
}
// TODO
// func (this *transA) SetReadDeadline(t time.Time) error {
// // TODO
// }
//
// func (this *transA) SetWriteDeadline(t time.Time) error {
// // TODO
// }
func (this *transA) errIfClosed() error {
if !this.closed.Load() {
return nil
}
return this.bestErr()
}
func (this *transA) bestErr() error {
if this.parent.err != nil {
return this.parent.err
}
if this.closeErr != nil {
return this.closeErr
}
return io.EOF
}
type readerA struct {
parent *transA
leftover []byte
@@ -320,11 +382,7 @@ func (this *readerA) pull() (uint16, error) {
// close and return error on failure
this.eof = true
this.parent.Close()
if this.parent.parent.err == nil {
return 0, fmt.Errorf("could not receive message: %w", io.EOF)
} else {
return 0, this.parent.parent.err
}
return 0, fmt.Errorf("could not receive message: %w", this.parent.bestErr())
}
func (this *readerA) Read(buffer []byte) (int, error) {
@@ -406,9 +464,9 @@ func encodeMessageA(
return ErrPayloadTooLarge
}
buffer := make([]byte, 18 + len(data))
tape.EncodeI64(buffer[:8], trans)
tape.EncodeI16(buffer[8:10], method)
tape.EncodeI64(buffer[10:18], uint64(len(data)))
encodeI64(buffer[:8], trans)
encodeI16(buffer[8:10], method)
encodeI64(buffer[10:18], uint64(len(data)))
copy(buffer[18:], data)
_, err := writer.Write(buffer)
return err
@@ -427,11 +485,11 @@ func decodeMessageA(
headerBuffer := [18]byte { }
_, err = io.ReadFull(reader, headerBuffer[:])
if err != nil { return 0, 0, false, nil, err }
transID, err = tape.DecodeI64[int64](headerBuffer[:8])
transID, err = decodeI64[int64](headerBuffer[:8])
if err != nil { return 0, 0, false, nil, err }
method, err = tape.DecodeI16[uint16](headerBuffer[8:10])
method, err = decodeI16[uint16](headerBuffer[8:10])
if err != nil { return 0, 0, false, nil, err }
size, err := tape.DecodeI64[uint64](headerBuffer[10:18])
size, err := decodeI64[uint64](headerBuffer[10:18])
if err != nil { return 0, 0, false, nil, err }
chunked, size = splitCCBSize(size)
if size > uint64(sizeLimit) {

View File

@@ -2,10 +2,10 @@ package hopp
import "io"
import "net"
import "time"
import "bytes"
import "errors"
import "context"
import "git.tebibyte.media/sashakoshka/hopp/tape"
// B implements METADAPT-B over a multiplexed stream-oriented transport such as
// QUIC.
@@ -51,6 +51,10 @@ func (this *b) SetSizeLimit(limit int64) {
this.sizeLimit = limit
}
func (this *b) SetDeadline(t time.Time) error {
return this.underlying.SetDeadline(t)
}
func (this *b) newTrans(underlying Stream) *transB {
return &transB {
sizeLimit: this.sizeLimit,
@@ -125,6 +129,10 @@ func (this *transB) receiveReader() (uint16, int64, io.Reader, error) {
return method, size, data, nil
}
func (this *transB) SetDeadline(t time.Time) error {
return this.underlying.SetDeadline(t)
}
type writerB struct {
parent *transB
buffer bytes.Buffer
@@ -150,12 +158,16 @@ type MultiConn interface {
AcceptStream(context.Context) (Stream, error)
// OpenStream opens a new stream.
OpenStream() (Stream, error)
// See the documentation for [net.Conn.SetDeadline].
SetDeadline(time.Time) error
}
// Stream represents a single stream returned by a [MultiConn].
type Stream interface {
// See documentation for [net.Conn].
io.ReadWriteCloser
// See the documentation for [net.Conn.SetDeadline].
SetDeadline(time.Time) error
// ID returns the stream ID
ID() int64
}
@@ -165,8 +177,8 @@ func encodeMessageB(writer io.Writer, sizeLimit int64, method uint16, data []byt
return ErrPayloadTooLarge
}
buffer := make([]byte, 10 + len(data))
tape.EncodeI16(buffer[:2], method)
tape.EncodeI64(buffer[2:10], uint64(len(data)))
encodeI16(buffer[:2], method)
encodeI64(buffer[2:10], uint64(len(data)))
copy(buffer[10:], data)
_, err := writer.Write(buffer)
return err
@@ -187,9 +199,9 @@ func decodeMessageB(
if errors.Is(err, io.EOF) { return 0, 0, nil, io.ErrUnexpectedEOF }
return 0, 0, nil, err
}
method, err = tape.DecodeI16[uint16](headerBuffer[:2])
method, err = decodeI16[uint16](headerBuffer[:2])
if err != nil { return 0, 0, nil, err }
length, err := tape.DecodeI64[uint64](headerBuffer[2:10])
length, err := decodeI64[uint64](headerBuffer[2:10])
if err != nil { return 0, 0, nil, err }
if length > uint64(sizeLimit) {
return 0, 0, nil, ErrPayloadTooLarge

View File

@@ -112,6 +112,13 @@ func (this *Decoder) ReadUintN(bytes int) (value uint64, n int, err error) {
return value, n, nil
}
// ReadFloat16 decodes a 16-bit floating point value from the input reader.
func (this *Decoder) ReadFloat16() (value float32, n int, err error) {
bits, nn, err := this.ReadUint16()
n += nn; if err != nil { return 0, n, err }
return math.Float32frombits(f16bitsToF32bits(bits)), n, nil
}
// ReadFloat32 decldes a 32-bit floating point value from the input reader.
func (this *Decoder) ReadFloat32() (value float32, n int, err error) {
bits, nn, err := this.ReadUint32()
@@ -132,3 +139,54 @@ func (this *Decoder) ReadTag() (value Tag, n int, err error) {
n += nn; if err != nil { return 0, n, err }
return Tag(uncasted), n, nil
}
// f16bitsToF32bits returns uint32 (float32 bits) converted from specified uint16.
// Taken from https://github.com/x448/float16/blob/v0.8.4/float16
//
// MIT License
//
// Copyright (c) 2019 Montgomery Edwards⁴⁴⁸ and Faye Amacker
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
func f16bitsToF32bits(in uint16) uint32 {
// All 65536 conversions with this were confirmed to be correct
// by Montgomery Edwards⁴⁴⁸ (github.com/x448).
sign := uint32(in&0x8000) << 16 // sign for 32-bit
exp := uint32(in&0x7c00) >> 10 // exponenent for 16-bit
coef := uint32(in&0x03ff) << 13 // significand for 32-bit
if exp == 0x1f {
if coef == 0 {
// infinity
return sign | 0x7f800000 | coef
}
// NaN
return sign | 0x7fc00000 | coef
}
if exp == 0 {
if coef == 0 {
// zero
return sign
}
// normalize subnormal numbers
exp++
for coef&0x7f800000 == 0 {
coef <<= 1
exp--
}
coef &= 0x007fffff
}
return sign | ((exp + (0x7f - 0xf)) << 23) | coef
}

View File

@@ -8,10 +8,30 @@ package tape
// TODO: test all of these smaller functions individually
// For an explanation as to why this package always treats LBA/SBA as strings,
// refer to https://go.dev/blog/strings:
//
// Its important to state right up front that a string holds arbitrary
// bytes. It is not required to hold Unicode text, UTF-8 text, or any other
// predefined format. As far as the content of a string is concerned, it is
// exactly equivalent to a slice of bytes.
//
// Arbitrary byte slices and blobs won't be as common of a use case as text
// data, and if you need that anyway you can just cast it to a byte slice.
import "fmt"
import "reflect"
var dummyMap map[uint16] any
var dummyBuffer []byte
type errCantAssign string
func (err errCantAssign) Error() string {
return string(err)
}
func errCantAssignf(format string, v ...any) errCantAssign {
return errCantAssign(fmt.Sprintf(format, v...))
}
// EncodeAny encodes an "any" value. Returns an error if the underlying type is
// unsupported. Supported types are:
@@ -24,26 +44,37 @@ var dummyMap map[uint16] any
// - []<supported type>
// - map[uint16]<supported type>
func EncodeAny(encoder *Encoder, value any, tag Tag) (n int, err error) {
// TODO use reflection for all of this to ignore type names
// primitives
switch value := value.(type) {
case int: return encoder.WriteInt32(int32(value))
case uint: return encoder.WriteUint32(uint32(value))
case int8: return encoder.WriteInt8(value)
case uint8: return encoder.WriteUint8(value)
case int16: return encoder.WriteInt16(value)
case uint16: return encoder.WriteUint16(value)
case int32: return encoder.WriteInt32(value)
case uint32: return encoder.WriteUint32(value)
case int64: return encoder.WriteInt64(value)
case uint64: return encoder.WriteUint64(value)
case string: return EncodeAny(encoder, []byte(value), tag)
case []byte:
reflectValue := reflect.ValueOf(value)
switch reflectValue.Kind() {
case reflect.Int: return encoder.WriteInt32(int32(reflectValue.Int()))
case reflect.Uint: return encoder.WriteUint32(uint32(reflectValue.Uint()))
case reflect.Int8: return encoder.WriteInt8(int8(reflectValue.Int()))
case reflect.Uint8: return encoder.WriteUint8(uint8(reflectValue.Uint()))
case reflect.Int16: return encoder.WriteInt16(int16(reflectValue.Int()))
case reflect.Uint16: return encoder.WriteUint16(uint16(reflectValue.Uint()))
case reflect.Int32: return encoder.WriteInt32(int32(reflectValue.Int()))
case reflect.Uint32: return encoder.WriteUint32(uint32(reflectValue.Uint()))
case reflect.Int64: return encoder.WriteInt64(int64(reflectValue.Int()))
case reflect.Uint64: return encoder.WriteUint64(uint64(reflectValue.Uint()))
case reflect.Float32: return encoder.WriteFloat32(float32(reflectValue.Float()))
case reflect.Float64: return encoder.WriteFloat64(float64(reflectValue.Float()))
case reflect.Bool: return // SI has no payload
case reflect.String:
if reflectValue.Len() > MaxStructureLength {
return 0, ErrTooLong
}
return EncodeAny(encoder, []byte(reflectValue.String()), tag)
}
if reflectValue.CanConvert(reflect.TypeOf(dummyBuffer)) {
if reflectValue.Len() > MaxStructureLength {
return 0, ErrTooLong
}
if tag.Is(LBA) {
nn, err := encoder.WriteUintN(uint64(len(value)), tag.CN() + 1)
nn, err := encoder.WriteUintN(uint64(reflectValue.Len()), tag.CN() + 1)
n += nn; if err != nil { return n, err }
}
nn, err := encoder.Write(value)
nn, err := encoder.Write(reflectValue.Bytes())
n += nn; if err != nil { return n, err }
return n, nil
}
@@ -54,8 +85,13 @@ func EncodeAny(encoder *Encoder, value any, tag Tag) (n int, err error) {
case reflect.Slice:
return encodeAnySlice(encoder, value, tag)
// case reflect.Array:
// TODO: we can encode arrays. but can we decode into them?
// that's the fucken question. maybe we just do the first
// return encodeAnySlice(encoder, reflect.ValueOf(value).Slice(0, reflectType.Len()).Interface(), tag)
case reflect.Map:
if reflectValue.Len() > MaxStructureLength {
return 0, ErrTooLong
}
if reflectType.Key() == reflect.TypeOf(uint16(0)) {
return encodeAnyMap(encoder, value, tag)
}
@@ -64,9 +100,10 @@ func EncodeAny(encoder *Encoder, value any, tag Tag) (n int, err error) {
return n, fmt.Errorf("cannot encode type %T", value)
}
// DecodeAny decodes data and places it into destination, which must be a
// DecodeAnyInto decodes data and places it into destination, which must be a
// pointer to a supported type. See [EncodeAny] for a list of supported types.
func DecodeAny(decoder *Decoder, destination any, tag Tag) (n int, err error) {
// The head of the decoder must be at the start of the payload.
func DecodeAnyInto(decoder *Decoder, destination any, tag Tag) (n int, err error) {
reflectDestination := reflect.ValueOf(destination)
if reflectDestination.Kind() != reflect.Pointer {
return n, fmt.Errorf("expected pointer destination, not %v", destination)
@@ -74,6 +111,17 @@ func DecodeAny(decoder *Decoder, destination any, tag Tag) (n int, err error) {
return decodeAny(decoder, reflectDestination.Elem(), tag)
}
// DecodeAny is like [DecodeAnyInto], but it automatically creates the
// destination from the tag and data. The head of the decoder must be at the
// start of the payload.
func DecodeAny(decoder *Decoder, tag Tag) (value any, n int, err error) {
destination, err := skeletonPointer(decoder, tag)
if err != nil { return nil, n, err }
nn, err := decodeAny(decoder, destination.Elem(), tag)
n += nn; if err != nil { return nil, n, err }
return destination.Elem().Interface(), n, err
}
// unknownSlicePlaceholder is inserted by skeletonValue and informs the program
// that the destination for the slice needs to be generated based on the item
// tag in the OTA.
@@ -81,22 +129,41 @@ type unknownSlicePlaceholder struct { }
var unknownSlicePlaceholderType = reflect.TypeOf(unknownSlicePlaceholder { })
// decodeAny is internal to [DecodeAny]. It takes in an addressable
// [reflect.Value] as the destination.
// [reflect.Value] as the destination. If the decoded value cannot fit in the
// destination, it skims over the payload, leaves the destination empty, and
// returns without an error. The head of the decoder must be at the start of the
// payload.
func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err error) {
errWrongDestinationType := func(expected string) error {
panic(fmt.Errorf(
// return fmt.Errorf(
"expected %s destination, not %v",
expected, destination))
n, err = decodeAnyOrError(decoder, destination, tag)
if _, ok := err.(errCantAssign); ok {
if n > 0 { panic(fmt.Sprintf("decodeAnyOrError decoded more than it should: %d", n)) }
nn, err := Skim(decoder, tag)
n += nn; if err != nil { return n, err }
return n, nil
}
return n, err
}
// decodeAnyOrError is internal to [decodeAny]. It takes in an addressable
// [reflect.Value] as the destination. If the decoded value cannot fit in the
// destination, it decodes nothing and returns an error of type errCantAssign,
// except for the case of a mismatched OTA element tag, wherein it will skim
// over the rest of the payload, leave the destination empty, and return without
// an error. The head of the decoder must be at the start of the payload.
func decodeAnyOrError(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err error) {
err = canSet(destination.Type(), tag)
if err != nil { return n, err }
switch tag.WithoutCN() {
case SI:
// SI: (none)
err = setInt(destination, uint64(tag.CN()))
if err != nil { return n, err }
setUint(destination, uint64(tag.CN()), 1)
case LI:
// LI: <value: IntN>
nn, err := decodeAndSetUint(decoder, destination, tag.CN() + 1)
n += nn; if err != nil { return n, err }
case LSI:
// LSI: <value: IntN>
nn, err := decodeAndSetInt(decoder, destination, tag.CN() + 1)
n += nn; if err != nil { return n, err }
case FP:
@@ -105,56 +172,104 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err
n += nn; if err != nil { return n, err }
case SBA:
// SBA: <data: U8>*
buffer := make([]byte, tag.CN())
length := tag.CN()
if length > MaxStructureLength {
return 0, ErrTooLong
}
buffer := make([]byte, length)
nn, err := decoder.Read(buffer)
n += nn; if err != nil { return n, err }
err = setByteArray(destination, buffer)
if err != nil { return n, err }
setString(destination, string(buffer))
case LBA:
// LBA: <length: UN> <data: U8>*
length, nn, err := decoder.ReadUintN(tag.CN() + 1)
n += nn; if err != nil { return n, err }
if length > uint64(MaxStructureLength) {
return 0, ErrTooLong
}
buffer := make([]byte, length)
nn, err = decoder.Read(buffer)
n += nn; if err != nil { return n, err }
err = setByteArray(destination, buffer)
if err != nil { return n, err }
setString(destination, string(buffer))
case OTA:
// OTA: <length: UN> <elementTag: tape.Tag> <values>*
oldDestination := destination
if isTypeAny(destination.Type()) {
// need a skeleton value if we are assigning to any.
value, err := skeletonValue(decoder, tag)
if err != nil { return n, err }
destination = value
}
length, nn, err := decoder.ReadUintN(tag.CN() + 1)
n += nn; if err != nil { return n, err }
if length > uint64(MaxStructureLength) {
return 0, ErrTooLong
}
lengthCast, err := Uint64ToIntSafe(length)
if err != nil { return n, err }
oneTag, nn, err := decoder.ReadTag()
n += nn; if err != nil { return n, err }
if destination.Kind() != reflect.Slice {
return n, errWrongDestinationType("slice")
if destination.Cap() < lengthCast {
destination.Grow(lengthCast - destination.Cap())
}
if destination.Cap() < int(length) {
destination.Grow(destination.Cap() - int(length))
// skip the rest of the array if the one tag doesn't
// match up with the destination
err = canSet(destination.Type().Elem(), oneTag)
if _, ok := err.(errCantAssign); ok {
for _ = range length {
nn, err := Skim(decoder, oneTag)
n += nn; if err != nil { return n, err }
}
destination.SetLen(int(length))
break
}
if err != nil { return n, err }
destination.SetLen(lengthCast)
for index := range length {
nn, err := decodeAny(decoder, destination.Index(int(index)), oneTag)
n += nn; if err != nil { return n, err }
n += nn
if _, ok := err.(errCantAssign); ok {
continue
} else if err != nil {
return n, err
}
}
oldDestination.Set(destination)
case KTV:
// KTV: <length: UN> (<key: U16> <tag: Tag> <value>)*
table := destination
if table.Type() != reflect.TypeOf(dummyMap) {
return n, errWrongDestinationType("map[uint16] any")
}
length, nn, err := decoder.ReadUintN(tag.CN() + 1)
n += nn; if err != nil { return n, err }
table.Clear()
for _ = range length {
if length > uint64(MaxStructureLength) {
return 0, ErrTooLong
}
lengthCast, err := Uint64ToIntSafe(length)
if err != nil { return n, err }
// im fucking so done dude. im so fucking done. this was
// supposed to only run when we need it but i guess it runs all
// the time, because when we get a map destination (a valid,
// allocated one) we break apart on SetMapIndex because of a nil
// map. yeah thats right. a fucking nil map panic. on the map we
// just allocated. but running this unconditionally (whether or
// not we receive an empty any value) actually makes it fucking
// work. go figure().
//
// (the map allocation functionality in skeletonPointer was
// removed after this comment was written)
value := reflect.MakeMapWithSize(reflect.TypeOf(dummyMap), lengthCast)
destination.Set(value)
destination = value
destination.Clear()
for _ = range lengthCast {
key, nn, err := decoder.ReadUint16()
n += nn; if err != nil { return n, err }
itemTag, nn, err := decoder.ReadTag()
n += nn; if err != nil { return n, err }
value, err := skeletonValue(decoder, itemTag)
value, err := skeletonPointer(decoder, itemTag)
if err != nil { return n, err }
nn, err = decodeAny(decoder, value.Elem(), itemTag)
n += nn; if err != nil { return n, err }
table.SetMapIndex(reflect.ValueOf(key), value)
destination.SetMapIndex(reflect.ValueOf(key), value.Elem())
}
default:
return n, fmt.Errorf("unknown TN %d", tag.TN())
@@ -166,30 +281,48 @@ func decodeAny(decoder *Decoder, destination reflect.Value, tag Tag) (n int, err
// underlying type is unsupported. See [EncodeAny] for a list of supported
// types.
func TagAny(value any) (Tag, error) {
// TODO use reflection for all of this to ignore type names
return tagAny(reflect.ValueOf(value))
}
func tagAny(reflectValue reflect.Value) (Tag, error) {
// primitives
switch value := value.(type) {
case int, uint: return LI.WithCN(3), nil
case int8, uint8: return LI.WithCN(0), nil
case int16, uint16: return LI.WithCN(1), nil
case int32, uint32: return LI.WithCN(3), nil
case int64, uint64: return LI.WithCN(7), nil
case string: return bufferLenTag(len(value)), nil
case []byte: return bufferLenTag(len(value)), nil
switch reflectValue.Kind() {
case reflect.Int: return LSI.WithCN(3), nil
case reflect.Int8: return LSI.WithCN(0), nil
case reflect.Int16: return LSI.WithCN(1), nil
case reflect.Int32: return LSI.WithCN(3), nil
case reflect.Int64: return LSI.WithCN(7), nil
case reflect.Uint: return LI.WithCN(3), nil
case reflect.Uint8: return LI.WithCN(0), nil
case reflect.Uint16: return LI.WithCN(1), nil
case reflect.Uint32: return LI.WithCN(3), nil
case reflect.Uint64: return LI.WithCN(7), nil
case reflect.Float32: return FP.WithCN(3), nil
case reflect.Float64: return FP.WithCN(7), nil
case reflect.Bool:
if reflectValue.Bool() {
return SI.WithCN(1), nil
} else {
return SI.WithCN(0), nil
}
case reflect.String: return bufferLenTag(reflectValue.Len()), nil
}
if reflectValue.CanConvert(reflect.TypeOf(dummyBuffer)) {
return bufferLenTag(reflectValue.Len()), nil
}
// aggregates
reflectType := reflect.TypeOf(value)
reflectType := reflectValue.Type()
switch reflectType.Kind() {
case reflect.Slice: return OTA.WithCN(IntBytes(uint64(reflect.ValueOf(value).Len())) - 1), nil
case reflect.Slice: return OTA.WithCN(IntBytes(uint64(reflectValue.Len())) - 1), nil
case reflect.Array: return OTA.WithCN(reflectType.Len()), nil
case reflect.Map:
if reflectType.Key() == reflect.TypeOf(uint16(0)) {
return KTV.WithCN(IntBytes(uint64(reflect.ValueOf(value).Len())) - 1), nil
return KTV.WithCN(IntBytes(uint64(reflectValue.Len())) - 1), nil
}
return 0, fmt.Errorf("cannot encode map key %T, key must be uint16", value)
return 0, fmt.Errorf("cannot encode map key %v, key must be uint16", reflectType.Key())
}
return 0, fmt.Errorf("cannot get tag of type %T", value)
return 0, fmt.Errorf("cannot get tag of type %v", reflectType)
}
func encodeAnySlice(encoder *Encoder, value any, tag Tag) (n int, err error) {
@@ -198,11 +331,10 @@ func encodeAnySlice(encoder *Encoder, value any, tag Tag) (n int, err error) {
nn, err := encoder.WriteUintN(uint64(reflectValue.Len()), tag.CN() + 1)
n += nn; if err != nil { return n, err }
reflectType := reflect.TypeOf(value)
oneTag, err := TagAny(reflect.Zero(reflectType.Elem()).Interface())
oneTag, err := tagAny(reflect.Zero(reflectType.Elem()))
if err != nil { return n, err }
for index := 0; index < reflectValue.Len(); index += 1 {
item := reflectValue.Index(index).Interface()
itemTag, err := TagAny(item)
itemTag, err := tagAny(reflectValue.Index(index))
if err != nil { return n, err }
if itemTag.CN() > oneTag.CN() { oneTag = itemTag }
}
@@ -224,11 +356,12 @@ func encodeAnyMap(encoder *Encoder, value any, tag Tag) (n int, err error) {
n += nn; if err != nil { return n, err }
iter := reflectValue.MapRange()
for iter.Next() {
reflectValue := iter.Value().Elem()
key := iter.Key().Interface().(uint16)
value := iter.Value().Interface()
value := reflectValue.Interface()
nn, err = encoder.WriteUint16(key)
n += nn; if err != nil { return n, err }
itemTag, err := TagAny(value)
itemTag, err := tagAny(reflectValue)
if err != nil { return n, err }
nn, err = encoder.WriteUint8(uint8(itemTag))
n += nn; if err != nil { return n, err }
@@ -238,46 +371,122 @@ func encodeAnyMap(encoder *Encoder, value any, tag Tag) (n int, err error) {
return n, nil
}
func canSet(destination reflect.Type, tag Tag) error {
// anything can be assigned to `any`
if isTypeAny(destination) {
return nil
}
switch tag.WithoutCN() {
case SI, LI, LSI:
switch destination.Kind() {
case
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Bool:
default:
return errCantAssignf("cannot assign integer to %v", destination)
}
case FP:
switch destination.Kind() {
case reflect.Float32, reflect.Float64:
default:
return errCantAssignf("cannot assign float to %v", destination)
}
case SBA, LBA:
if destination.Kind() == reflect.String { return nil }
if destination.Kind() != reflect.Slice {
return errCantAssignf("cannot assign byte array to %v", destination)
}
if destination.Elem() != reflect.TypeOf(byte(0)) {
return errCantAssignf("cannot convert %v to *[]byte", destination)
}
case OTA:
if destination.Kind() != reflect.Slice {
return errCantAssignf("cannot assign array to %v", destination)
}
case KTV:
cantAssign :=
destination.Kind() != reflect.Map ||
destination.Key().Kind() != reflect.Uint16 ||
!isTypeAny(destination.Elem())
if cantAssign {
return errCantAssignf("cannot assign table to %v", destination)
}
default:
return fmt.Errorf("unknown TN %d", tag.TN())
}
return nil
}
// setInt expects a settable destination.
func setInt(destination reflect.Value, value uint64) error {
func setInt(destination reflect.Value, value int64, bytes int) {
switch {
case destination.CanInt():
destination.Set(reflect.ValueOf(int64(value)).Convert(destination.Type()))
case destination.CanUint():
destination.Set(reflect.ValueOf(value).Convert(destination.Type()))
default:
return fmt.Errorf("cannot assign integer to %T", destination.Interface())
case isTypeAny(destination.Type()):
switch {
case bytes > 4: destination.Set(reflect.ValueOf(int64(value)))
case bytes > 2: destination.Set(reflect.ValueOf(int32(value)))
case bytes > 1: destination.Set(reflect.ValueOf(int16(value)))
default: destination.Set(reflect.ValueOf(int8(value)))
}
default:
panic("setInt called on an unsupported type")
}
}
// setUint expects a settable destination.
func setUint(destination reflect.Value, value uint64, bytes int) {
switch {
case destination.Kind() == reflect.Bool:
destination.Set(reflect.ValueOf(value > 0))
case destination.CanInt():
destination.Set(reflect.ValueOf(int64(value)).Convert(destination.Type()))
case destination.CanUint():
destination.Set(reflect.ValueOf(value).Convert(destination.Type()))
case isTypeAny(destination.Type()):
switch {
case bytes > 4: destination.Set(reflect.ValueOf(uint64(value)))
case bytes > 2: destination.Set(reflect.ValueOf(uint32(value)))
case bytes > 1: destination.Set(reflect.ValueOf(uint16(value)))
default: destination.Set(reflect.ValueOf(uint8(value)))
}
default:
panic("setUint called on an unsupported type")
}
return nil
}
// setFloat expects a settable destination.
func setFloat(destination reflect.Value, value float64) error {
if !destination.CanFloat() {
return fmt.Errorf("cannot assign float to %T", destination.Interface())
}
func setFloat(destination reflect.Value, value float64) {
destination.Set(reflect.ValueOf(value).Convert(destination.Type()))
return nil
}
// setByteArrayexpects a settable destination.
func setByteArray(destination reflect.Value, value []byte) error {
typ := destination.Type()
if typ.Kind() != reflect.Slice {
return fmt.Errorf("cannot assign %T to ", value)
}
if typ.Elem() != reflect.TypeOf(byte(0)) {
return fmt.Errorf("cannot convert %T to *[]byte", value)
}
func setByteArray(destination reflect.Value, value []byte) {
destination.Set(reflect.ValueOf(value))
}
// setString exepctes a settable destination
func setString(destination reflect.Value, value string) {
destination.Set(reflect.ValueOf(value))
return nil
}
// decodeAndSetInt expects a settable destination.
func decodeAndSetInt(decoder *Decoder, destination reflect.Value, bytes int) (n int, err error) {
value, nn, err := decoder.ReadIntN(bytes)
n += nn; if err != nil { return n, err }
setInt(destination, value, bytes)
return n, nil
}
// decodeAndSetUint expects a settable destination.
func decodeAndSetUint(decoder *Decoder, destination reflect.Value, bytes int) (n int, err error) {
value, nn, err := decoder.ReadUintN(bytes)
n += nn; if err != nil { return n, err }
return n, setInt(destination, value)
setUint(destination, value, bytes)
return n, nil
}
// decodeAndSetInt expects a settable destination.
@@ -286,25 +495,38 @@ func decodeAndSetFloat(decoder *Decoder, destination reflect.Value, bytes int) (
case 8:
value, nn, err := decoder.ReadFloat64()
n += nn; if err != nil { return n, err }
return n, setFloat(destination, float64(value))
setFloat(destination, float64(value))
return n, nil
case 4:
value, nn, err := decoder.ReadFloat32()
n += nn; if err != nil { return n, err }
return n, setFloat(destination, float64(value))
setFloat(destination, float64(value))
return n, nil
}
return n, fmt.Errorf("cannot decode float%d", bytes * 8)
return n, errCantAssignf("unsupported bit width float%d", bytes * 8)
}
// skeletonValue returns a pointer value. In order for it to be set, it must be
// dereferenced using Elem().
// skeletonValue returns an addressable value. It can be set directly. The head
// of the decoder must be at the start of the payload when calling.
func skeletonValue(decoder *Decoder, tag Tag) (reflect.Value, error) {
ptr, err := skeletonPointer(decoder, tag)
if err != nil { return reflect.Value { }, err }
return ptr.Elem(), nil
}
// skeletonPointer returns a pointer value. In order for it to be set, it must
// be dereferenced using Elem(). The head of the decoder must be at the start of
// the payload when calling.
func skeletonPointer(decoder *Decoder, tag Tag) (reflect.Value, error) {
typ, err := typeOf(decoder, tag)
if err != nil { return reflect.Value { }, err }
return reflect.New(typ), nil
value := reflect.New(typ)
return value, nil
}
// typeOf returns the type of the current tag being decoded. It does not use up
// the decoder, it only peeks.
// the decoder, it only peeks. The head of the decoder must be at the start of
// the payload when calling.
func typeOf(decoder *Decoder, tag Tag) (reflect.Type, error) {
switch tag.WithoutCN() {
case SI:
@@ -317,14 +539,22 @@ func typeOf(decoder *Decoder, tag Tag) (reflect.Type, error) {
case 7: return reflect.TypeOf(uint64(0)), nil
}
return nil, fmt.Errorf("unknown CN %d for LI", tag.CN())
case LSI:
switch tag.CN() {
case 0: return reflect.TypeOf(int8(0)), nil
case 1: return reflect.TypeOf(int16(0)), nil
case 3: return reflect.TypeOf(int32(0)), nil
case 7: return reflect.TypeOf(int64(0)), nil
}
return nil, fmt.Errorf("unknown CN %d for LSI", tag.CN())
case FP:
switch tag.CN() {
case 3: return reflect.TypeOf(float32(0)), nil
case 7: return reflect.TypeOf(float64(0)), nil
}
return nil, fmt.Errorf("unknown CN %d for FP", tag.CN())
case SBA: return reflect.SliceOf(reflect.TypeOf(byte(0))), nil
case LBA: return reflect.SliceOf(reflect.TypeOf(byte(0))), nil
case SBA: return reflect.TypeOf(""), nil
case LBA: return reflect.TypeOf(""), nil
case OTA:
elemTag, dimension, err := peekSlice(decoder, tag)
if err != nil { return nil, err }
@@ -340,6 +570,12 @@ func typeOf(decoder *Decoder, tag Tag) (reflect.Type, error) {
return nil, fmt.Errorf("unknown TN %d", tag.TN())
}
// isTypeAny returns whether the given reflect.Type is an interface with no
// methods.
func isTypeAny(typ reflect.Type) bool {
return typ.Kind() == reflect.Interface && typ.NumMethod() == 0
}
// peekSlice returns the element tag and dimension count of the OTA currently
// being decoded. It does not use up the decoder, it only peeks.
func peekSlice(decoder *Decoder, tag Tag) (Tag, int, error) {
@@ -352,7 +588,6 @@ func peekSlice(decoder *Decoder, tag Tag) (Tag, int, error) {
currentTag = elem
offset = n
dimension += 1
fmt.Println(n)
if elem.Is(OTA) {
if !populated {
// default to a large byte array, will be

View File

@@ -1,15 +1,77 @@
package tape
import "fmt"
import "bytes"
import "testing"
import "reflect"
import tu "git.tebibyte.media/sashakoshka/hopp/internal/testutil"
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 {
/* int8 */ int8(0x45),
/* int16 */ int16(0x4567),
/* int32 */ int32(0x456789AB),
/* int64 */ int64(0x456789ABCDEF0123),
/* uint5 */ uint8(12),
/* uint8 */ uint8(0x45),
/* uint16 */ uint16(0x4567),
/* uint32 */ uint32(0x456789AB),
/* uint64 */ uint64(0x456789ABCDEF0123),
/* bool */ false,
/* bool */ true,
/* string */ "pupever",
/* []byte */ "blarg",
/* []string */ []string {
"\x45\x67\x89\xAB\xCD\xEF\x01\x23",
"\x11\x11\x11\x11\x11",
},
/* map[uint16] any */ map[uint16] any {
0x0223: int16(0x4567),
0x0224: uint32(0x456789AB),
},
/* map[uint16] any */ map[uint16] any {
0x0001: float32(489.5),
0x0002: "hi",
0x0003: uint16(0x3992),
},
}
type userDefinedInteger int16
func TestEncodeAnyInt(test *testing.T) {
err := testEncodeAny(test, uint8(0xCA), LI.WithCN(0), tu.S(0xCA))
if err != nil { test.Fatal(err) }
err = testEncodeAny(test, 400, LI.WithCN(3), tu.S(
err = testEncodeAny(test, 400, LSI.WithCN(3), tu.S(
0, 0, 0x1, 0x90,
))
if err != nil { test.Fatal(err) }
@@ -22,15 +84,21 @@ func TestEncodeAnyTable(test *testing.T) {
0x0000: "hi!",
0xFFFF: []uint16 { 0xBEE5, 0x7777 },
0x1234: [][]uint16 { []uint16 { 0x5 }, []uint16 { 0x17, 0xAAAA} },
}, KTV.WithCN(0), tu.S(5).AddVar(
0x2345: [][]int16 { []int16 { 0x5 }, []int16 { 0x17, -0xAAA } },
0x3456: userDefinedInteger(0x3921),
0x1F1F: float32(67.26),
0x0F0F: float64(5.3),
0xAAAA: false,
0xBBBB: true,
}, KTV.WithCN(0), tu.S(11).AddVar(
[]byte {
0xF3, 0xB9,
byte(LI.WithCN(3)),
byte(LSI.WithCN(3)),
0, 0, 0, 1,
},
[]byte {
0x01, 0x02,
byte(LI.WithCN(3)),
byte(LSI.WithCN(3)),
0, 0, 0, 2,
},
[]byte {
@@ -52,21 +120,157 @@ func TestEncodeAnyTable(test *testing.T) {
0, 0x17,
0xAA, 0xAA,
},
[]byte {
0x23, 0x45,
byte(OTA.WithCN(0)), 2, byte(OTA.WithCN(0)),
1, byte(LSI.WithCN(1)),
0, 0x5,
2, byte(LSI.WithCN(1)),
0, 0x17,
0xF5, 0x56,
},
[]byte {
0x34, 0x56,
byte(LSI.WithCN(1)),
0x39, 0x21,
},
[]byte {
0x1F, 0x1F,
byte(FP.WithCN(3)),
0x42, 0x86, 0x85, 0x1F,
},
[]byte {
0x0F, 0x0F,
byte(FP.WithCN(7)),
0x40, 0x15, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
},
[]byte {
0xAA, 0xAA,
byte(SI.WithCN(0)),
},
[]byte {
0xBB, 0xBB,
byte(SI.WithCN(1)),
},
))
if err != nil { test.Fatal(err) }
}
func TestEncodeDecodeAnyMap(test *testing.T) {
func TestDecodeWrongType(test *testing.T) {
for index, data := range samplePayloads {
test.Logf("data %2d %v [%s]", index, Tag(data[0]), tu.HexBytes(data[1:]))
// integers should only assign to other integers
if index > 10 {
cas := func(destination any) {
n, err := DecodeAnyInto(NewDecoder(bytes.NewBuffer(data[1:])), destination, Tag(data[0]))
if err != nil { test.Fatalf("error: %v | n: %d", err, n) }
reflectValue := reflect.ValueOf(destination).Elem()
if reflectValue.CanInt() {
if reflectValue.Int() != 0 {
test.Fatalf("destination not zero: %v", reflectValue.Elem().Interface())
}
} else if reflectValue.Kind() != reflect.Bool {
if reflectValue.Uint() != 0 {
test.Fatalf("destination not zero: %v", reflectValue.Elem().Interface())
}
}
if n != len(data) - 1 {
test.Fatalf("n not equal: %d != %d", n, len(data) - 1)
}
}
test.Log("- int8")
{ var dest int8; cas(&dest) }
test.Log("- int16")
{ var dest int16; cas(&dest) }
test.Log("- int32")
{ var dest int32; cas(&dest) }
test.Log("- int64")
{ var dest int64; cas(&dest) }
test.Log("- uint8")
{ var dest uint8; cas(&dest) }
test.Log("- uint16")
{ var dest uint16; cas(&dest) }
test.Log("- uint32")
{ var dest uint32; cas(&dest) }
test.Log("- uint64")
{ var dest uint64; cas(&dest) }
test.Log("- bool")
{ var dest bool; cas(&dest) }
}
arrayCase := func(destination any) {
n, err := DecodeAnyInto(NewDecoder(bytes.NewBuffer(data[1:])), destination, Tag(data[0]))
if err != nil { test.Fatalf("error: %v | n: %d", err, n) }
reflectDestination := reflect.ValueOf(destination)
reflectValue := reflectDestination.Elem()
if reflectValue.Len() != 0 {
test.Fatalf("len(destination) not zero: %v", reflectValue.Interface())
}
if n != len(data) - 1 {
test.Fatalf("n not equal: %d != %d", n, len(data) - 1)
}
}
// SBA/LBA types should only assign to other SBA/LBA types
if index != 11 && index != 12 {
test.Log("- string")
{ var dest string; arrayCase(&dest) }
test.Log("- []byte")
{ var dest []byte; arrayCase(&dest) }
}
// arrays should only assign to other arrays
if index != 13 {
test.Log("- []string")
{ var dest []string; arrayCase(&dest) }
}
// tables should only assign to other tables
if index != 14 && index != 15 {
test.Log("- map[uint16] any")
{ var dest = map[uint16] any { }; arrayCase(&dest) }
}
}
}
func TestEncodeDecodeAnyTable(test *testing.T) {
err := testEncodeDecodeAny(test, map[uint16] any {
0xF3B9: 1,
0x0102: 2,
0xF3B9: uint32(1),
0x0102: uint32(2),
0x0103: int64(23432),
0x0104: int64(-88777),
0x0000: "hi!",
0xFFFF: []uint16 { 0xBEE5, 0x7777 },
0x1234: [][]uint16 { []uint16 { 0x5 }, []uint16 { 0x17, 0xAAAA} },
0x1F1F: float32(67.26),
0x0F0F: float64(5.3),
}, nil)
if err != nil { test.Fatal(err) }
}
func TestEncodeDecodeAnyDestination(test *testing.T) {
var destination any
for index, data := range samplePayloads {
if _, isBool := sampleValues[index].(bool); isBool {
// test is invalid for bools because they are never
// created as a skeleton value
continue
}
tag := Tag(data[0])
payload := data[1:]
test.Logf("data %2d %v [%s]", index, tag, tu.HexBytes(payload))
n, err := DecodeAnyInto(NewDecoder(bytes.NewBuffer(payload)), &destination, tag)
if err != nil { test.Fatalf("error: %v | n: %d", err, n) }
got := destination
correct := sampleValues[index]
test.Log("got: ", tu.Describe(got))
test.Log("correct:", tu.Describe(correct))
if !reflect.DeepEqual(got, correct) {
test.Fatalf("values not equal")
}
if n != len(payload) {
test.Fatalf("n not equal: %d != %d", n, len(payload))
}
}
}
func TestPeekSlice(test *testing.T) {
buffer := bytes.NewBuffer([]byte {
2, byte(OTA.WithCN(3)),
@@ -126,70 +330,68 @@ func TestPeekSliceOnce(test *testing.T) {
}
}
func encAny(value any) ([]byte, Tag, int, error) {
tag, err := TagAny(value)
if err != nil { return nil, 0, 0, err }
buffer := bytes.Buffer { }
encoder := NewEncoder(&buffer)
n, err := EncodeAny(encoder, value, tag)
if err != nil { return nil, 0, n, err }
encoder.Flush()
return buffer.Bytes(), tag, n, nil
func TestTagAny(test *testing.T) {
cases := [][2]any {
[2]any { LSI.WithCN(3), int(9) },
[2]any { LSI.WithCN(0), int8(9) },
[2]any { LSI.WithCN(1), int16(9) },
[2]any { LSI.WithCN(3), int32(9) },
[2]any { LSI.WithCN(7), int64(9) },
[2]any { LI.WithCN(3), uint(9) },
[2]any { LI.WithCN(0), uint8(9) },
[2]any { LI.WithCN(1), uint16(9) },
[2]any { LI.WithCN(3), uint32(9) },
[2]any { LI.WithCN(7), uint64(9) },
[2]any { FP.WithCN(3), float32(9) },
[2]any { FP.WithCN(7), float64(9) },
[2]any { SBA.WithCN(12), "small string" },
[2]any { SBA.WithCN(12), []byte("small string") },
[2]any { LBA.WithCN(0), "this is a very long string that is long" },
[2]any { LBA.WithCN(0), []byte("this is a very long string that is long") },
[2]any { LBA.WithCN(1), lipsum },
[2]any { OTA.WithCN(0), []int { 1, 2, 3, 4, 5 } },
[2]any { OTA.WithCN(0), []string { "1, 2, 3, 4, 5" } },
[2]any { KTV.WithCN(0), map[uint16] any {
0: 1,
1: "wow",
2: 10.238,
45: -9,
9: map[uint16] any { },
}},
}
for _, cas := range cases {
test.Log(cas)
got, err := TagAny(cas[1])
if err != nil { test.Fatal(err) }
if correct := cas[0].(Tag); correct != got {
test.Fatalf("wrong tag: %v != %v", got, correct)
}
}
}
func decAny(data []byte) (Tag, any, int, error) {
destination := map[uint16] any { }
tag, err := TagAny(destination)
if err != nil { return 0, nil, 0, err }
n, err := DecodeAny(NewDecoder(bytes.NewBuffer(data)), &destination, tag)
if err != nil { return 0, nil, n, err }
return tag, destination, n, nil
}
func TestDecodeAny(test *testing.T) {
for index, payload := range samplePayloads {
if _, isBool := sampleValues[index].(bool); isBool {
// test is invalid for bools because they are never
// created as a skeleton value
continue
}
func testEncodeAny(test *testing.T, value any, correctTag Tag, correctBytes tu.Snake) error {
bytes, tag, n, err := encAny(value)
if err != nil { return err }
correctValue := sampleValues[index]
data := payload[1:]
decoder := NewDecoder(bytes.NewBuffer(data))
tag := Tag(payload[0])
decoded, n, err := DecodeAny(decoder, tag)
test.Log("n: ", n)
test.Log("tag: ", tag)
test.Log("got: ", tu.HexBytes(bytes))
test.Log("correct:", correctBytes)
if tag != correctTag {
return fmt.Errorf("tag not equal")
}
if ok, n := correctBytes.Check(bytes); !ok {
return fmt.Errorf("bytes not equal: %d", n)
}
if n != len(bytes) {
return fmt.Errorf("n not equal: %d != %d", n, len(bytes))
}
return nil
}
func testEncodeDecodeAny(test *testing.T, value, correctValue any) error {
if correctValue == nil {
correctValue = value
}
test.Log("encoding...")
bytes, tag, n, err := encAny(value)
if err != nil { return err }
test.Log("n: ", n)
test.Log("tag:", tag)
test.Log("got:", tu.HexBytes(bytes))
test.Log("decoding...", tag)
if n != len(bytes) {
return fmt.Errorf("n not equal: %d != %d", n, len(bytes))
}
_, decoded, n, err := decAny(bytes)
if err != nil { return err }
test.Log("got: ", decoded)
test.Log("correct:", correctValue)
test.Log("got: ", tu.Describe(decoded))
test.Log("correct:", tu.Describe(correctValue))
if err != nil { test.Fatal(err) }
if !reflect.DeepEqual(decoded, correctValue) {
return fmt.Errorf("values not equal")
test.Fatal("values not equal")
}
if n != len(data) {
test.Fatalf("n not equal: %d != %d", n, len(data))
}
if n != len(bytes) {
return fmt.Errorf("n not equal: %d != %d", n, len(bytes))
}
return nil
}

View File

@@ -102,6 +102,11 @@ func (this *Encoder) WriteUintN(value uint64, bytes int) (n int, err error) {
return n, nil
}
// WriteFloat16 encodes a 16-bit floating point value to the output writer.
func (this *Encoder) WriteFloat16(value float32) (n int, err error) {
return this.WriteUint16(f32bitsToF16bits(math.Float32bits(value)))
}
// WriteFloat32 encodes a 32-bit floating point value to the output writer.
func (this *Encoder) WriteFloat32(value float32) (n int, err error) {
return this.WriteUint32(math.Float32bits(value))
@@ -116,3 +121,69 @@ func (this *Encoder) WriteFloat64(value float64) (n int, err error) {
func (this *Encoder) WriteTag(value Tag) (n int, err error) {
return this.WriteUint8(uint8(value))
}
// f32bitsToF16bits returns uint16 (Float16 bits) converted from the specified float32.
// Conversion rounds to nearest integer with ties to even.
// Taken from https://github.com/x448/float16/blob/v0.8.4/float16
//
// MIT License
//
// Copyright (c) 2019 Montgomery Edwards⁴⁴⁸ and Faye Amacker
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
func f32bitsToF16bits(u32 uint32) uint16 {
// Translated from Rust to Go by Montgomery Edwards⁴⁴⁸ (github.com/x448).
// All 4294967296 conversions with this were confirmed to be correct by x448.
// Original Rust implementation is by Kathryn Long (github.com/starkat99) with MIT license.
sign := u32 & 0x80000000
exp := u32 & 0x7f800000
coef := u32 & 0x007fffff
if exp == 0x7f800000 {
// NaN or Infinity
nanBit := uint32(0)
if coef != 0 {
nanBit = uint32(0x0200)
}
return uint16((sign >> 16) | uint32(0x7c00) | nanBit | (coef >> 13))
}
halfSign := sign >> 16
unbiasedExp := int32(exp>>23) - 127
halfExp := unbiasedExp + 15
if halfExp >= 0x1f {
return uint16(halfSign | uint32(0x7c00))
}
if halfExp <= 0 {
if 14-halfExp > 24 {
return uint16(halfSign)
}
coef := coef | uint32(0x00800000)
halfCoef := coef >> uint32(14-halfExp)
roundBit := uint32(1) << uint32(13-halfExp)
if (coef&roundBit) != 0 && (coef&(3*roundBit-1)) != 0 {
halfCoef++
}
return uint16(halfSign | halfCoef)
}
uHalfExp := uint32(halfExp) << 10
halfCoef := coef >> 13
roundBit := uint32(0x00001000)
if (coef&roundBit) != 0 && (coef&(3*roundBit-1)) != 0 {
return uint16((halfSign | uHalfExp | halfCoef) + 1)
}
return uint16(halfSign | uHalfExp | halfCoef)
}

12
tape/error.go Normal file
View File

@@ -0,0 +1,12 @@
package tape
// Error enumerates common errors in this package.
type Error string; const (
ErrTooLong Error = "data structure too long"
ErrTooLarge Error = "number too large"
)
// Error implements the error interface.
func (err Error) Error() string {
return string(err)
}

26
tape/limits.go Normal file
View File

@@ -0,0 +1,26 @@
package tape
// MaxStructureLength determines how long a TAPE data structure can be. This
// applies to:
//
// - OTA
// - SBA/LBA
// - KTV
//
// By default it is set at 2^20 (about a million).
// You shouldn't need to change this. If you do, it should only be set once at
// the start of the program.
var MaxStructureLength = 1024 * 1024
// MaxInt is the maximum value an int can hold. This varies depending on the
// system.
const MaxInt int = int(^uint(0) >> 1)
// Uint64ToIntSafe casts the input to an int if it can be done without overflow,
// or returns an error otherwise.
func Uint64ToIntSafe(input uint64) (int, error) {
if input > uint64(MaxInt) {
return 0, ErrTooLarge
}
return int(input), nil
}

75
tape/misc_test.go Normal file
View File

@@ -0,0 +1,75 @@
package tape
import "fmt"
import "bytes"
import "testing"
import "reflect"
import tu "git.tebibyte.media/sashakoshka/hopp/internal/testutil"
func encAny(value any) ([]byte, Tag, int, error) {
tag, err := TagAny(value)
if err != nil { return nil, 0, 0, err }
buffer := bytes.Buffer { }
encoder := NewEncoder(&buffer)
n, err := EncodeAny(encoder, value, tag)
if err != nil { return nil, 0, n, err }
encoder.Flush()
return buffer.Bytes(), tag, n, nil
}
func decAny(data []byte) (Tag, any, int, error) {
destination := map[uint16] any { }
tag, err := TagAny(destination)
if err != nil { return 0, nil, 0, err }
n, err := DecodeAnyInto(NewDecoder(bytes.NewBuffer(data)), &destination, tag)
if err != nil { return 0, nil, n, err }
return tag, destination, n, nil
}
func testEncodeAny(test *testing.T, value any, correctTag Tag, correctBytes tu.Snake) error {
bytes, tag, n, err := encAny(value)
if err != nil { return err }
test.Log("n: ", n)
test.Log("tag: ", tag)
test.Log("got: ", tu.HexBytes(bytes))
test.Log("correct:", correctBytes)
if tag != correctTag {
return fmt.Errorf("tag not equal: %v != %v", tag, correctTag)
}
if ok, n := correctBytes.Check(bytes); !ok {
return fmt.Errorf("bytes not equal at index %d", n)
}
if n != len(bytes) {
return fmt.Errorf("n not equal: %d != %d", n, len(bytes))
}
return nil
}
func testEncodeDecodeAny(test *testing.T, value, correctValue any) error {
if correctValue == nil {
correctValue = value
}
test.Log("encoding...")
bytes, tag, n, err := encAny(value)
if err != nil { return err }
test.Log("n: ", n)
test.Log("tag:", tag)
test.Log("got:", tu.HexBytes(bytes))
test.Log("decoding...", tag)
if n != len(bytes) {
return fmt.Errorf("n not equal: %d != %d", n, len(bytes))
}
_, decoded, n, err := decAny(bytes)
if err != nil { return err }
test.Log("got: ", tu.Describe(decoded))
test.Log("correct:", tu.Describe(correctValue))
if !reflect.DeepEqual(decoded, correctValue) {
return fmt.Errorf("values not equal")
}
if n != len(bytes) {
return fmt.Errorf("n not equal: %d != %d", n, len(bytes))
}
return nil
}

54
tape/skim.go Normal file
View File

@@ -0,0 +1,54 @@
package tape
import "fmt"
// Skim uses up data from a decoder to "skim" over one value (and all else
// contained within it) without actually putting the data anywhere.
func Skim(decoder *Decoder, tag Tag) (n int, err error) {
switch tag.WithoutCN() {
case SI:
// SI: (none)
return n, nil
case LI, LSI, FP:
// LI: <value: IntN>
// LSI: <value: IntN>
// FP: <value: FloatN>
nn, err := decoder.Discard(tag.CN() + 1)
n += nn; if err != nil { return n, err }
case SBA:
// SBA: <data: U8>*
nn, err := decoder.Discard(tag.CN())
n += nn; if err != nil { return n, err }
case LBA:
// LBA: <length: UN> <data: U8>*
length, nn, err := decoder.ReadUintN(tag.CN() + 1)
n += nn; if err != nil { return n, err }
nn, err = decoder.Discard(int(length))
n += nn; if err != nil { return n, err }
case OTA:
// OTA: <length: UN> <elementTag: tape.Tag> <values>*
length, nn, err := decoder.ReadUintN(tag.CN() + 1)
n += nn; if err != nil { return n, err }
oneTag, nn, err := decoder.ReadTag()
n += nn; if err != nil { return n, err }
for _ = range length {
nn, err := Skim(decoder, oneTag)
n += nn; if err != nil { return n, err }
}
case KTV:
// KTV: <length: UN> (<key: U16> <tag: Tag> <value>)*
length, nn, err := decoder.ReadUintN(tag.CN() + 1)
n += nn; if err != nil { return n, err }
for _ = range length {
nn, err := decoder.Discard(2)
n += nn; if err != nil { return n, err }
itemTag, nn, err := decoder.ReadTag()
n += nn; if err != nil { return n, err }
nn, err = Skim(decoder, itemTag)
n += nn; if err != nil { return n, err }
}
default:
return n, fmt.Errorf("unknown TN %d", tag.TN())
}
return n, nil
}

137
tape/skim_test.go Normal file
View File

@@ -0,0 +1,137 @@
package tape
import "bytes"
import "testing"
func TestSkimInteger(test *testing.T) {
data := []byte {
0x12, 0x45, 0x23, 0xF9,
}
mainDataLen := len(data)
// extra junk
data = append(data, 0x00, 0x01, 0x02, 0x03,)
n, err := Skim(NewDecoder(bytes.NewBuffer(data)), LI.WithCN(3))
if err != nil {
test.Fatal(err)
}
if got, correct := n, mainDataLen; got != correct {
test.Fatalf("n not equal: %d != %d", got, correct)
}
}
func TestSkimArray(test *testing.T) {
data := []byte {
2, byte(LI.WithCN(1)),
0xBE, 0xE5, 0x77, 0x77,
}
mainDataLen := len(data)
// extra junk
data = append(data, 0x00, 0x01, 0x02, 0x03,)
n, err := Skim(NewDecoder(bytes.NewBuffer(data)), OTA.WithCN(0))
if err != nil {
test.Fatal(err)
}
if got, correct := n, mainDataLen; got != correct {
test.Fatalf("n not equal: %d != %d", got, correct)
}
}
func TestSkimNestedArray(test *testing.T) {
data := []byte {
2, byte(OTA.WithCN(0)),
1, byte(LSI.WithCN(1)),
0, 0x5,
2, byte(LSI.WithCN(1)),
0, 0x17,
0xF5, 0x56,
}
mainDataLen := len(data)
// extra junk
data = append(data, 0x00, 0x01, 0x02, 0x03,)
n, err := Skim(NewDecoder(bytes.NewBuffer(data)), OTA.WithCN(0))
if err != nil {
test.Fatal(err)
}
if got, correct := n, mainDataLen; got != correct {
test.Fatalf("n not equal: %d != %d", got, correct)
}
}
func TestSkimTable(test *testing.T) {
data := []byte {
2,
0xF3, 0xB9,
byte(LSI.WithCN(3)),
0, 0, 0, 1,
0x01, 0x02,
byte(LSI.WithCN(3)),
0, 0, 0, 2,
}
mainDataLen := len(data)
// extra junk
data = append(data, 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03)
n, err := Skim(NewDecoder(bytes.NewBuffer(data)), KTV.WithCN(0))
if got, correct := n, mainDataLen; got != correct {
test.Fatalf("n not equal: %d != %d ... (%d)", got, correct, len(data))
}
if err != nil {
test.Fatal(err)
}
}
func TestSkimTableComplex(test *testing.T) {
data := []byte {
7,
0xF3, 0xB9,
byte(LSI.WithCN(3)),
0, 0, 0, 1,
0x01, 0x02,
byte(LSI.WithCN(3)),
0, 0, 0, 2,
0, 0,
byte(SBA.WithCN(3)),
'h', 'i', '!',
0xFF, 0xFF,
byte(OTA.WithCN(0)), 2, byte(LI.WithCN(1)),
0xBE, 0xE5, 0x77, 0x77,
0x12, 0x34,
byte(OTA.WithCN(0)), 2, byte(OTA.WithCN(0)),
1, byte(LI.WithCN(1)),
0, 0x5,
2, byte(LI.WithCN(1)),
0, 0x17,
0xAA, 0xAA,
0x23, 0x45,
byte(OTA.WithCN(0)), 2, byte(OTA.WithCN(0)),
1, byte(LSI.WithCN(1)),
0, 0x5,
2, byte(LSI.WithCN(1)),
0, 0x17,
0xF5, 0x56,
0x34, 0x56,
byte(LSI.WithCN(1)),
0x39, 0x21,
}
mainDataLen := len(data)
// extra junk
data = append(data, 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03)
n, err := Skim(NewDecoder(bytes.NewBuffer(data)), KTV.WithCN(0))
if got, correct := n, mainDataLen; got != correct {
test.Fatalf("n not equal: %d != %d ... (%d)", got, correct, len(data))
}
if err != nil {
test.Fatal(err)
}
}

11
tape/strings.go Normal file
View File

@@ -0,0 +1,11 @@
package tape
const lipsum = `Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.
Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.
Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.
Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.
Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.`

View File

@@ -2,19 +2,33 @@ package tape
import "fmt"
// TODO: fix #7
type Tag byte; const (
SI Tag = 0 << 5 // Small integer
LI Tag = 1 << 5 // Large integer
FP Tag = 2 << 5 // Floating point
SBA Tag = 3 << 5 // Small byte array
LBA Tag = 4 << 5 // Large byte array
OTA Tag = 5 << 5 // One-tag array
KTV Tag = 6 << 5 // Key-tag-value table
LI Tag = 1 << 5 // Large unsigned integer
LSI Tag = 2 << 5 // Large signed integer
FP Tag = 3 << 5 // Floating point
SBA Tag = 4 << 5 // Small byte array
LBA Tag = 5 << 5 // Large byte array
OTA Tag = 6 << 5 // One-tag array
KTV Tag = 7 << 5 // Key-tag-value table
TNMask Tag = 0xE0 // The entire TN bitfield
CNMask Tag = 0x1F // The entire CN bitfield
CNLimit Tag = 32 // All valid CNs are < CNLimit
)
// what the first nybble of a tag means:
//
// 0-1 : SI
// 2-3 : LI
// 4-5 : LSI
// 6-7 : FP
// 8-9 : SBA
// A-B : LBA
// C-D : OTA
// E-F : KTV
func (tag Tag) TN() int {
return int(tag >> 5)
}
@@ -40,6 +54,7 @@ func (tag Tag) String() string {
switch tag.WithoutCN() {
case SI: tn = "SI"
case LI: tn = "LI"
case LSI: tn = "LSI"
case FP: tn = "FP"
case SBA: tn = "SBA"
case LBA: tn = "LBA"
@@ -54,10 +69,15 @@ func BufferTag(value []byte) Tag {
return bufferLenTag(len(value))
}
// StringTag returns the appropriate tag for a string.
func StringTag(value string) Tag {
return bufferLenTag(len(value))
}
func bufferLenTag(length int) Tag {
if length < int(CNLimit) {
return SBA.WithCN(length)
} else {
return LBA.WithCN(IntBytes(uint64(length)))
return LBA.WithCN(IntBytes(uint64(length)) - 1)
}
}