├── .gitattributes ├── .editorconfig ├── go.mod ├── README.md ├── u64 ├── u64.go └── u64_test.go ├── binary ├── errors.go ├── header.go ├── custom.go ├── custom_test.go ├── memory.go ├── export.go ├── global_test.go ├── table.go ├── limits_test.go ├── limits.go ├── global.go ├── memory_test.go ├── import.go ├── encoder.go ├── value.go ├── code_test.go ├── table_test.go ├── export_test.go ├── value_test.go ├── data.go ├── function.go ├── const_expr.go ├── import_test.go ├── code.go ├── decoder.go ├── data_test.go ├── const_expr_test.go ├── function_test.go ├── decoder_test.go ├── section_test.go ├── encoder_test.go ├── names.go ├── element.go ├── names_test.go └── section.go ├── RATIONALE.md ├── .gitignore ├── ieee754 └── ieee754.go ├── wasm ├── module_test.go ├── memory_test.go ├── memory.go ├── counts.go ├── features_test.go ├── types_test.go ├── types.go ├── counts_test.go └── features.go ├── go.sum ├── .github └── workflows │ └── commit.yaml ├── Makefile ├── example_test.go ├── leb128 ├── leb128.go └── leb128_test.go └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | # Improves experience of commands like `make format` on Windows 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tetratelabs/wabin 2 | 3 | go 1.18 4 | 5 | require github.com/stretchr/testify v1.8.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wabin: WebAssembly Binary Format in Go 2 | 3 | wabin includes WebAssembly an WebAssembly data model and binary encoder. Most 4 | won't use this library. It mainly supports advanced manipulation of WebAssembly 5 | binaries prior to instantiation with wazero. Notably, this has no dependencies, 6 | so is cleaner to use in Go projects. 7 | -------------------------------------------------------------------------------- /u64/u64.go: -------------------------------------------------------------------------------- 1 | package u64 2 | 3 | // LeBytes returns a byte slice corresponding to the 8 bytes in the uint64 in little-endian byte order. 4 | func LeBytes(v uint64) []byte { 5 | return []byte{ 6 | byte(v), 7 | byte(v >> 8), 8 | byte(v >> 16), 9 | byte(v >> 24), 10 | byte(v >> 32), 11 | byte(v >> 40), 12 | byte(v >> 48), 13 | byte(v >> 56), 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /binary/errors.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrInvalidByte = errors.New("invalid byte") 7 | ErrInvalidMagicNumber = errors.New("invalid magic number") 8 | ErrInvalidVersion = errors.New("invalid version header") 9 | ErrInvalidSectionID = errors.New("invalid section id") 10 | ErrCustomSectionNotFound = errors.New("custom section not found") 11 | ) 12 | -------------------------------------------------------------------------------- /binary/header.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | // Magic is the 4 byte preamble (literally "\0asm") of the binary format 4 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-magic 5 | var Magic = []byte{0x00, 0x61, 0x73, 0x6D} 6 | 7 | // version is format version and doesn't change between known specification versions 8 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-version 9 | var version = []byte{0x01, 0x00, 0x00, 0x00} 10 | -------------------------------------------------------------------------------- /RATIONALE.md: -------------------------------------------------------------------------------- 1 | # wabin rationale 2 | 3 | ## Why wabin? 4 | wabin is "wa" as it WebAssembly and "bin" as in Binary format. 5 | 6 | This is intentionally similarly named to [wabt][1], although there will be a 7 | small amount of overlap. In both cases, the names are puns, how [Elmer Fudd][2] 8 | would say the word "rabbit" and "robin" respectively. 9 | 10 | [1]: https://github.com/WebAssembly/wabt 11 | [2]: https://en.wikipedia.org/wiki/Elmer_Fudd#Elmer-speak 12 | -------------------------------------------------------------------------------- /binary/custom.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/tetratelabs/wabin/wasm" 7 | ) 8 | 9 | // decodeCustomSection deserializes the data **not** associated with the "name" key in SectionIDCustom. 10 | // 11 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#custom-section%E2%91%A0 12 | func decodeCustomSection(r *bytes.Reader, name string, limit uint64) (result *wasm.CustomSection, err error) { 13 | buf := make([]byte, limit) 14 | _, err = r.Read(buf) 15 | 16 | result = &wasm.CustomSection{ 17 | Name: name, 18 | Data: buf, 19 | } 20 | 21 | return 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | # Goland 24 | .idea 25 | 26 | # codecov.io 27 | /coverage.txt 28 | 29 | .DS_Store 30 | -------------------------------------------------------------------------------- /u64/u64_test.go: -------------------------------------------------------------------------------- 1 | package u64 2 | 3 | import ( 4 | "encoding/binary" 5 | "math" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestBytes(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | input uint64 15 | }{ 16 | { 17 | name: "zero", 18 | input: 0, 19 | }, 20 | { 21 | name: "half", 22 | input: math.MaxUint32, 23 | }, 24 | { 25 | name: "max", 26 | input: math.MaxUint64, 27 | }, 28 | } 29 | 30 | for _, tt := range tests { 31 | tc := tt 32 | 33 | t.Run(tc.name, func(t *testing.T) { 34 | expected := make([]byte, 8) 35 | binary.LittleEndian.PutUint64(expected, tc.input) 36 | require.Equal(t, expected, LeBytes(tc.input)) 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /binary/custom_test.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/tetratelabs/wabin/wasm" 9 | ) 10 | 11 | func TestEncodeCustom(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | custom *wasm.CustomSection 15 | expected []byte 16 | }{ 17 | { 18 | name: "custom section", 19 | custom: &wasm.CustomSection{ 20 | Name: "test", 21 | Data: []byte("12345"), 22 | }, 23 | expected: []byte{ 24 | 4, 't', 'e', 's', 't', 25 | '1', '2', '3', '4', '5', 26 | }, 27 | }, 28 | } 29 | 30 | for _, tt := range tests { 31 | tc := tt 32 | 33 | t.Run(tc.name, func(t *testing.T) { 34 | bytes := encodeCustomSection(tc.custom) 35 | require.Equal(t, tc.expected, bytes) 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ieee754/ieee754.go: -------------------------------------------------------------------------------- 1 | package ieee754 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | "math" 7 | ) 8 | 9 | // DecodeFloat32 decodes a float32 in IEEE 754 binary representation. 10 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#floating-point%E2%91%A2 11 | func DecodeFloat32(r io.Reader) (float32, error) { 12 | buf := make([]byte, 4) 13 | _, err := io.ReadFull(r, buf) 14 | if err != nil { 15 | return 0, err 16 | } 17 | raw := binary.LittleEndian.Uint32(buf) 18 | return math.Float32frombits(raw), nil 19 | } 20 | 21 | // DecodeFloat64 decodes a float64 in IEEE 754 binary representation. 22 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#floating-point%E2%91%A2 23 | func DecodeFloat64(r io.Reader) (float64, error) { 24 | buf := make([]byte, 8) 25 | _, err := io.ReadFull(r, buf) 26 | if err != nil { 27 | return 0, err 28 | } 29 | raw := binary.LittleEndian.Uint64(buf) 30 | return math.Float64frombits(raw), nil 31 | } 32 | -------------------------------------------------------------------------------- /wasm/module_test.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestSectionIDName(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | input SectionID 13 | expected string 14 | }{ 15 | {"custom", SectionIDCustom, "custom"}, 16 | {"type", SectionIDType, "type"}, 17 | {"import", SectionIDImport, "import"}, 18 | {"function", SectionIDFunction, "function"}, 19 | {"table", SectionIDTable, "table"}, 20 | {"memory", SectionIDMemory, "memory"}, 21 | {"global", SectionIDGlobal, "global"}, 22 | {"export", SectionIDExport, "export"}, 23 | {"start", SectionIDStart, "start"}, 24 | {"element", SectionIDElement, "element"}, 25 | {"code", SectionIDCode, "code"}, 26 | {"data", SectionIDData, "data"}, 27 | {"unknown", 100, "unknown"}, 28 | } 29 | 30 | for _, tt := range tests { 31 | tc := tt 32 | 33 | t.Run(tc.name, func(t *testing.T) { 34 | require.Equal(t, tc.expected, SectionIDName(tc.input)) 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /binary/memory.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/tetratelabs/wabin/wasm" 8 | ) 9 | 10 | // decodeMemory returns the wasm.Memory decoded with the WebAssembly 11 | // Binary Format. 12 | // 13 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-memory 14 | func decodeMemory(r *bytes.Reader) (*wasm.Memory, error) { 15 | min, maxP, err := decodeLimitsType(r) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | mem := &wasm.Memory{Min: min} 21 | if maxP != nil { 22 | mem.Max = *maxP 23 | mem.IsMaxEncoded = true 24 | 25 | if min > mem.Max { 26 | return nil, fmt.Errorf("min %d pages (%s) > max %d pages (%s)", 27 | min, wasm.PagesToUnitOfBytes(min), mem.Max, wasm.PagesToUnitOfBytes(mem.Max)) 28 | } 29 | } 30 | 31 | return mem, nil 32 | } 33 | 34 | // encodeMemory returns the wasm.Memory encoded in WebAssembly Binary Format. 35 | // 36 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-memory 37 | func encodeMemory(i *wasm.Memory) []byte { 38 | maxPtr := &i.Max 39 | if !i.IsMaxEncoded { 40 | maxPtr = nil 41 | } 42 | return encodeLimitsType(i.Min, maxPtr) 43 | } 44 | -------------------------------------------------------------------------------- /binary/export.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/tetratelabs/wabin/leb128" 8 | "github.com/tetratelabs/wabin/wasm" 9 | ) 10 | 11 | func decodeExport(r *bytes.Reader) (i *wasm.Export, err error) { 12 | i = &wasm.Export{} 13 | 14 | if i.Name, _, err = decodeUTF8(r, "export name"); err != nil { 15 | return nil, err 16 | } 17 | 18 | b, err := r.ReadByte() 19 | if err != nil { 20 | return nil, fmt.Errorf("error decoding export kind: %w", err) 21 | } 22 | 23 | i.Type = b 24 | switch i.Type { 25 | case wasm.ExternTypeFunc, wasm.ExternTypeTable, wasm.ExternTypeMemory, wasm.ExternTypeGlobal: 26 | if i.Index, _, err = leb128.DecodeUint32(r); err != nil { 27 | return nil, fmt.Errorf("error decoding export index: %w", err) 28 | } 29 | default: 30 | return nil, fmt.Errorf("%w: invalid byte for exportdesc: %#x", ErrInvalidByte, b) 31 | } 32 | return 33 | } 34 | 35 | // encodeExport returns the wasm.Export encoded in WebAssembly Binary Format. 36 | // 37 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#export-section%E2%91%A0 38 | func encodeExport(i *wasm.Export) []byte { 39 | data := encodeSizePrefixed([]byte(i.Name)) 40 | data = append(data, i.Type) 41 | data = append(data, leb128.EncodeUint32(i.Index)...) 42 | return data 43 | } 44 | -------------------------------------------------------------------------------- /binary/global_test.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/tetratelabs/wabin/leb128" 9 | "github.com/tetratelabs/wabin/wasm" 10 | ) 11 | 12 | func TestEncodeGlobal(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | input *wasm.Global 16 | expected []byte 17 | }{ 18 | { 19 | name: "const", 20 | input: &wasm.Global{ 21 | Type: &wasm.GlobalType{ValType: wasm.ValueTypeI32}, 22 | Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(1)}, 23 | }, 24 | expected: []byte{ 25 | wasm.ValueTypeI32, 0x00, // 0 == const 26 | wasm.OpcodeI32Const, 0x01, wasm.OpcodeEnd, 27 | }, 28 | }, 29 | { 30 | name: "var", 31 | input: &wasm.Global{ 32 | Type: &wasm.GlobalType{ValType: wasm.ValueTypeI32, Mutable: true}, 33 | Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(1)}, 34 | }, 35 | expected: []byte{ 36 | wasm.ValueTypeI32, 0x01, // 1 == var 37 | wasm.OpcodeI32Const, 0x01, wasm.OpcodeEnd, 38 | }, 39 | }, 40 | } 41 | 42 | for _, tt := range tests { 43 | tc := tt 44 | 45 | t.Run(tc.name, func(t *testing.T) { 46 | bytes := encodeGlobal(tc.input) 47 | require.Equal(t, tc.expected, bytes) 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 9 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 10 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 13 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 14 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 15 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | -------------------------------------------------------------------------------- /wasm/memory_test.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestMemoryPageConsts(t *testing.T) { 11 | require.Equal(t, MemoryPageSize, uint32(1)< wasm.MaximumFunctionIndex { 30 | return nil, fmt.Errorf("table min must be at most %d", wasm.MaximumFunctionIndex) 31 | } 32 | if max != nil { 33 | if *max < min { 34 | return nil, fmt.Errorf("table size minimum must not be greater than maximum") 35 | } 36 | } 37 | return &wasm.Table{Min: min, Max: max, Type: tableType}, nil 38 | } 39 | 40 | // encodeTable returns the wasm.Table encoded in WebAssembly Binary Format. 41 | // 42 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-table 43 | func encodeTable(i *wasm.Table) []byte { 44 | return append([]byte{i.Type}, encodeLimitsType(i.Min, i.Max)...) 45 | } 46 | -------------------------------------------------------------------------------- /wasm/memory.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | // MemoryPageSize is the unit of memory length in WebAssembly, 9 | // and is defined as 2^16 = 65536. 10 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0 11 | MemoryPageSize = uint32(65536) 12 | // MemoryPageSizeInBits satisfies the relation: "1 << MemoryPageSizeInBits == MemoryPageSize". 13 | MemoryPageSizeInBits = 16 14 | ) 15 | 16 | // MemoryPagesToBytesNum converts the given pages into the number of bytes contained in these pages. 17 | func MemoryPagesToBytesNum(pages uint32) (bytesNum uint64) { 18 | return uint64(pages) << MemoryPageSizeInBits 19 | } 20 | 21 | // PagesToUnitOfBytes converts the pages to a human-readable form similar to what's specified. Ex. 1 -> "64Ki" 22 | // 23 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instances%E2%91%A0 24 | func PagesToUnitOfBytes(pages uint32) string { 25 | k := pages * 64 26 | if k < 1024 { 27 | return fmt.Sprintf("%d Ki", k) 28 | } 29 | m := k / 1024 30 | if m < 1024 { 31 | return fmt.Sprintf("%d Mi", m) 32 | } 33 | g := m / 1024 34 | if g < 1024 { 35 | return fmt.Sprintf("%d Gi", g) 36 | } 37 | return fmt.Sprintf("%d Ti", g/1024) 38 | } 39 | 40 | // Below are raw functions used to implement the api.Memory API: 41 | 42 | // memoryBytesNumToPages converts the given number of bytes into the number of pages. 43 | func memoryBytesNumToPages(bytesNum uint64) (pages uint32) { 44 | return uint32(bytesNum >> MemoryPageSizeInBits) 45 | } 46 | -------------------------------------------------------------------------------- /binary/limits_test.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestLimitsType(t *testing.T) { 13 | zero := uint32(0) 14 | largest := uint32(math.MaxUint32) 15 | 16 | tests := []struct { 17 | name string 18 | min uint32 19 | max *uint32 20 | expected []byte 21 | }{ 22 | { 23 | name: "min 0", 24 | expected: []byte{0x0, 0}, 25 | }, 26 | { 27 | name: "min 0, max 0", 28 | max: &zero, 29 | expected: []byte{0x1, 0, 0}, 30 | }, 31 | { 32 | name: "min largest", 33 | min: largest, 34 | expected: []byte{0x0, 0xff, 0xff, 0xff, 0xff, 0xf}, 35 | }, 36 | { 37 | name: "min 0, max largest", 38 | max: &largest, 39 | expected: []byte{0x1, 0, 0xff, 0xff, 0xff, 0xff, 0xf}, 40 | }, 41 | { 42 | name: "min largest max largest", 43 | min: largest, 44 | max: &largest, 45 | expected: []byte{0x1, 0xff, 0xff, 0xff, 0xff, 0xf, 0xff, 0xff, 0xff, 0xff, 0xf}, 46 | }, 47 | } 48 | 49 | for _, tt := range tests { 50 | tc := tt 51 | 52 | b := encodeLimitsType(tc.min, tc.max) 53 | t.Run(fmt.Sprintf("encode - %s", tc.name), func(t *testing.T) { 54 | require.Equal(t, tc.expected, b) 55 | }) 56 | 57 | t.Run(fmt.Sprintf("decode - %s", tc.name), func(t *testing.T) { 58 | min, max, err := decodeLimitsType(bytes.NewReader(b)) 59 | require.NoError(t, err) 60 | require.Equal(t, min, tc.min) 61 | require.Equal(t, max, tc.max) 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /binary/limits.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/tetratelabs/wabin/leb128" 8 | ) 9 | 10 | // decodeLimitsType returns the `limitsType` (min, max) decoded with the WebAssembly Binary Format. 11 | // 12 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#limits%E2%91%A6 13 | func decodeLimitsType(r *bytes.Reader) (min uint32, max *uint32, err error) { 14 | var flag byte 15 | if flag, err = r.ReadByte(); err != nil { 16 | err = fmt.Errorf("read leading byte: %v", err) 17 | return 18 | } 19 | 20 | switch flag { 21 | case 0x00: 22 | min, _, err = leb128.DecodeUint32(r) 23 | if err != nil { 24 | err = fmt.Errorf("read min of limit: %v", err) 25 | } 26 | case 0x01: 27 | min, _, err = leb128.DecodeUint32(r) 28 | if err != nil { 29 | err = fmt.Errorf("read min of limit: %v", err) 30 | return 31 | } 32 | var m uint32 33 | if m, _, err = leb128.DecodeUint32(r); err != nil { 34 | err = fmt.Errorf("read max of limit: %v", err) 35 | } else { 36 | max = &m 37 | } 38 | default: 39 | err = fmt.Errorf("%v for limits: %#x != 0x00 or 0x01", ErrInvalidByte, flag) 40 | } 41 | return 42 | } 43 | 44 | // encodeLimitsType returns the `limitsType` (min, max) encoded in WebAssembly Binary Format. 45 | // 46 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#limits%E2%91%A6 47 | func encodeLimitsType(min uint32, max *uint32) []byte { 48 | if max == nil { 49 | return append(leb128.EncodeUint32(0x00), leb128.EncodeUint32(min)...) 50 | } 51 | return append(leb128.EncodeUint32(0x01), append(leb128.EncodeUint32(min), leb128.EncodeUint32(*max)...)...) 52 | } 53 | -------------------------------------------------------------------------------- /binary/global.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/tetratelabs/wabin/wasm" 8 | ) 9 | 10 | // decodeGlobal returns the wasm.Global decoded with the WebAssembly Binary Format. 11 | // 12 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-global 13 | func decodeGlobal(r *bytes.Reader, features wasm.CoreFeatures) (*wasm.Global, error) { 14 | gt, err := decodeGlobalType(r) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | init, err := decodeConstantExpression(r, features) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return &wasm.Global{Type: gt, Init: init}, nil 25 | } 26 | 27 | // decodeGlobalType returns the wasm.GlobalType decoded with the WebAssembly Binary Format. 28 | // 29 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-globaltype 30 | func decodeGlobalType(r *bytes.Reader) (*wasm.GlobalType, error) { 31 | vt, err := decodeValueTypes(r, 1) 32 | if err != nil { 33 | return nil, fmt.Errorf("read value type: %w", err) 34 | } 35 | 36 | ret := &wasm.GlobalType{ 37 | ValType: vt[0], 38 | } 39 | 40 | b, err := r.ReadByte() 41 | if err != nil { 42 | return nil, fmt.Errorf("read mutablity: %w", err) 43 | } 44 | 45 | switch mut := b; mut { 46 | case 0x00: // not mutable 47 | case 0x01: // mutable 48 | ret.Mutable = true 49 | default: 50 | return nil, fmt.Errorf("%w for mutability: %#x != 0x00 or 0x01", ErrInvalidByte, mut) 51 | } 52 | return ret, nil 53 | } 54 | 55 | // encodeGlobal returns the wasm.Global encoded in WebAssembly Binary Format. 56 | // 57 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#global-section%E2%91%A0 58 | func encodeGlobal(g *wasm.Global) (data []byte) { 59 | var mutable byte 60 | if g.Type.Mutable { 61 | mutable = 1 62 | } 63 | data = []byte{g.Type.ValType, mutable} 64 | data = append(data, encodeConstantExpression(g.Init)...) 65 | return 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/commit.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | branches: [main] 5 | paths-ignore: 6 | - '**/*.md' 7 | push: 8 | branches: [main] 9 | paths-ignore: 10 | - '**/*.md' 11 | 12 | env: # Update this prior to requiring a higher minor version in go.mod 13 | GO_VERSION: "1.18" # 1.xx == latest patch of 1.xx 14 | 15 | defaults: 16 | run: # use bash for all operating systems unless overridden 17 | shell: bash 18 | 19 | jobs: 20 | check: 21 | name: Pre-commit check, Go-${{ matrix.go-version }} 22 | runs-on: ubuntu-20.04 23 | strategy: 24 | matrix: # use latest available versions and be consistent on all workflows! 25 | go-version: 26 | - "1.18" # == ${{ env.GO_VERSION }} because matrix cannot expand env variables 27 | - "1.19" 28 | 29 | steps: 30 | - uses: actions/checkout@v3 31 | 32 | - uses: actions/setup-go@v3 33 | with: 34 | go-version: ${{ matrix.go-version }} 35 | cache: true 36 | 37 | - run: make check 38 | 39 | test_amd64: 40 | name: amd64, ${{ matrix.os }}, Go-${{ matrix.go-version }} 41 | runs-on: ${{ matrix.os }} 42 | strategy: 43 | fail-fast: false # don't fail fast as sometimes failures are arch/OS specific 44 | matrix: # use latest available versions and be consistent on all workflows! 45 | os: [ubuntu-20.04, macos-12, windows-2022] 46 | go-version: 47 | - "1.18" # == ${{ env.GO_VERSION }} because matrix cannot expand env variables 48 | - "1.19" 49 | 50 | steps: 51 | - uses: actions/checkout@v3 52 | 53 | - uses: actions/setup-go@v3 54 | with: 55 | go-version: ${{ matrix.go-version }} 56 | cache: true 57 | 58 | - run: make test 59 | 60 | - name: "Generate coverage report" # only once (not per OS) 61 | if: runner.os == 'Linux' 62 | run: make coverage 63 | 64 | - name: "Upload coverage report" # only on main push and only once (not per OS) 65 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' && runner.os == 'Linux' 66 | env: 67 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 68 | run: bash <(curl -s https://codecov.io/bash) 69 | -------------------------------------------------------------------------------- /binary/memory_test.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/tetratelabs/wabin/wasm" 11 | ) 12 | 13 | func TestMemoryType(t *testing.T) { 14 | zero := uint32(0) 15 | 16 | tests := []struct { 17 | name string 18 | input *wasm.Memory 19 | expected []byte 20 | }{ 21 | { 22 | name: "min 0", 23 | input: &wasm.Memory{}, 24 | expected: []byte{0, 0}, 25 | }, 26 | { 27 | name: "min 0, max 0", 28 | input: &wasm.Memory{Max: zero, IsMaxEncoded: true}, 29 | expected: []byte{0x1, 0, 0}, 30 | }, 31 | { 32 | name: "min=max", 33 | input: &wasm.Memory{Min: 1, Max: 1, IsMaxEncoded: true}, 34 | expected: []byte{0x1, 1, 1}, 35 | }, 36 | { 37 | name: "min 0, max largest", 38 | input: &wasm.Memory{Max: uint32(65536), IsMaxEncoded: true}, 39 | expected: []byte{0x1, 0, 0x80, 0x80, 0x4}, 40 | }, 41 | { 42 | name: "min largest max largest", 43 | input: &wasm.Memory{Min: uint32(65536), Max: uint32(65536), IsMaxEncoded: true}, 44 | expected: []byte{0x1, 0x80, 0x80, 0x4, 0x80, 0x80, 0x4}, 45 | }, 46 | } 47 | 48 | for _, tt := range tests { 49 | tc := tt 50 | 51 | b := encodeMemory(tc.input) 52 | t.Run(fmt.Sprintf("encode %s", tc.name), func(t *testing.T) { 53 | require.Equal(t, tc.expected, b) 54 | }) 55 | 56 | t.Run(fmt.Sprintf("decode %s", tc.name), func(t *testing.T) { 57 | binary, err := decodeMemory(bytes.NewReader(b)) 58 | require.NoError(t, err) 59 | require.Equal(t, binary, tc.input) 60 | }) 61 | } 62 | } 63 | 64 | func TestDecodeMemoryType_Errors(t *testing.T) { 65 | tests := []struct { 66 | name string 67 | input []byte 68 | expectedErr string 69 | }{ 70 | { 71 | name: "max < min && max == 0", 72 | input: []byte{0x1, 0x80, 0x80, 0x4, 0}, 73 | expectedErr: "min 65536 pages (4 Gi) > max 0 pages (0 Ki)", 74 | }, 75 | { 76 | name: "max < min && max != 0", 77 | input: []byte{0x1, 0x80, 0x80, 0x4, 1}, 78 | expectedErr: "min 65536 pages (4 Gi) > max 1 pages (64 Ki)", 79 | }, 80 | } 81 | 82 | for _, tt := range tests { 83 | tc := tt 84 | 85 | t.Run(tc.name, func(t *testing.T) { 86 | _, err := decodeMemory(bytes.NewReader(tc.input)) 87 | require.EqualError(t, err, tc.expectedErr) 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /binary/import.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/tetratelabs/wabin/leb128" 8 | "github.com/tetratelabs/wabin/wasm" 9 | ) 10 | 11 | func decodeImport( 12 | r *bytes.Reader, 13 | idx uint32, 14 | features wasm.CoreFeatures, 15 | ) (i *wasm.Import, err error) { 16 | i = &wasm.Import{} 17 | if i.Module, _, err = decodeUTF8(r, "import module"); err != nil { 18 | return nil, fmt.Errorf("import[%d] error decoding module: %w", idx, err) 19 | } 20 | 21 | if i.Name, _, err = decodeUTF8(r, "import name"); err != nil { 22 | return nil, fmt.Errorf("import[%d] error decoding name: %w", idx, err) 23 | } 24 | 25 | b, err := r.ReadByte() 26 | if err != nil { 27 | return nil, fmt.Errorf("import[%d] error decoding type: %w", idx, err) 28 | } 29 | i.Type = b 30 | switch i.Type { 31 | case wasm.ExternTypeFunc: 32 | i.DescFunc, _, err = leb128.DecodeUint32(r) 33 | case wasm.ExternTypeTable: 34 | i.DescTable, err = decodeTable(r, features) 35 | case wasm.ExternTypeMemory: 36 | i.DescMem, err = decodeMemory(r) 37 | case wasm.ExternTypeGlobal: 38 | i.DescGlobal, err = decodeGlobalType(r) 39 | default: 40 | err = fmt.Errorf("%w: invalid byte for importdesc: %#x", ErrInvalidByte, b) 41 | } 42 | if err != nil { 43 | return nil, fmt.Errorf("import[%d] %s[%s.%s]: %w", idx, wasm.ExternTypeName(i.Type), i.Module, i.Name, err) 44 | } 45 | return 46 | } 47 | 48 | // encodeImport returns the wasm.Import encoded in WebAssembly Binary Format. 49 | // 50 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-import 51 | func encodeImport(i *wasm.Import) []byte { 52 | data := encodeSizePrefixed([]byte(i.Module)) 53 | data = append(data, encodeSizePrefixed([]byte(i.Name))...) 54 | data = append(data, i.Type) 55 | switch i.Type { 56 | case wasm.ExternTypeFunc: 57 | data = append(data, leb128.EncodeUint32(i.DescFunc)...) 58 | case wasm.ExternTypeTable: 59 | data = append(data, wasm.RefTypeFuncref) 60 | data = append(data, encodeLimitsType(i.DescTable.Min, i.DescTable.Max)...) 61 | case wasm.ExternTypeMemory: 62 | maxPtr := &i.DescMem.Max 63 | if !i.DescMem.IsMaxEncoded { 64 | maxPtr = nil 65 | } 66 | data = append(data, encodeLimitsType(i.DescMem.Min, maxPtr)...) 67 | case wasm.ExternTypeGlobal: 68 | g := i.DescGlobal 69 | var mutable byte 70 | if g.Mutable { 71 | mutable = 1 72 | } 73 | data = append(data, g.ValType, mutable) 74 | default: 75 | panic(fmt.Errorf("invalid externtype: %s", wasm.ExternTypeName(i.Type))) 76 | } 77 | return data 78 | } 79 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Make functions strip spaces and use commas to separate parameters. The below variables escape these characters. 2 | comma := , 3 | space := 4 | space += 5 | 6 | goimports := golang.org/x/tools/cmd/goimports@v0.1.12 7 | golangci_lint := github.com/golangci/golangci-lint/cmd/golangci-lint@v1.49.0 8 | 9 | # Make 3.81 doesn't support '**' globbing: Set explicitly instead of recursion. 10 | all_sources := $(wildcard *.go */*.go */*/*.go */*/*/*.go */*/*/*.go */*/*/*/*.go) 11 | all_testdata := $(wildcard testdata/* */testdata/* */*/testdata/* */*/testdata/*/* */*/*/testdata/*) 12 | all_testing := $(wildcard internal/testing/* internal/testing/*/* internal/testing/*/*/*) 13 | # main_sources exclude any test or example related code 14 | main_sources := $(wildcard $(filter-out %_test.go $(all_testdata) $(all_testing), $(all_sources))) 15 | # main_packages collect the unique main source directories (sort will dedupe). 16 | # Paths need to all start with ./, so we do that manually vs foreach which strips it. 17 | main_packages := $(sort $(foreach f,$(dir $(main_sources)),$(if $(findstring ./,$(f)),./,./$(f)))) 18 | 19 | .PHONY: test 20 | test: 21 | @go test ./... -timeout 120s 22 | 23 | .PHONY: coverage 24 | coverpkg = $(subst $(space),$(comma),$(main_packages)) 25 | coverage: ## Generate test coverage 26 | @go test -coverprofile=coverage.txt -covermode=atomic --coverpkg=$(coverpkg) $(main_packages) 27 | @go tool cover -func coverage.txt 28 | 29 | golangci_lint_path := $(shell go env GOPATH)/bin/golangci-lint 30 | 31 | $(golangci_lint_path): 32 | @go install $(golangci_lint) 33 | 34 | golangci_lint_goarch ?= $(shell go env GOARCH) 35 | 36 | .PHONY: lint 37 | lint: $(golangci_lint_path) 38 | @GOARCH=$(golangci_lint_goarch) CGO_ENABLED=0 $(golangci_lint_path) run --timeout 5m 39 | 40 | .PHONY: format 41 | format: 42 | @find . -type f -name '*.go' | xargs gofmt -s -w 43 | @for f in `find . -name '*.go'`; do \ 44 | awk '/^import \($$/,/^\)$$/{if($$0=="")next}{print}' $$f > /tmp/fmt; \ 45 | mv /tmp/fmt $$f; \ 46 | done 47 | @go run $(goimports) -w -local github.com/tetratelabs/wabin `find . -name '*.go'` 48 | 49 | .PHONY: check 50 | check: 51 | @$(MAKE) lint golangci_lint_goarch=arm64 52 | @$(MAKE) lint golangci_lint_goarch=amd64 53 | @$(MAKE) format 54 | @go mod tidy 55 | @if [ ! -z "`git status -s`" ]; then \ 56 | echo "The following differences will fail CI until committed:"; \ 57 | git diff --exit-code; \ 58 | fi 59 | 60 | .PHONY: clean 61 | clean: ## Ensure a clean build 62 | @rm -rf dist build coverage.txt 63 | @go clean -testcache 64 | -------------------------------------------------------------------------------- /binary/encoder.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "github.com/tetratelabs/wabin/wasm" 5 | ) 6 | 7 | var sizePrefixedName = []byte{4, 'n', 'a', 'm', 'e'} 8 | 9 | // EncodeModule implements wasm.EncodeModule for the WebAssembly Binary Format. 10 | // Note: If saving to a file, the conventional extension is wasm 11 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-format%E2%91%A0 12 | func EncodeModule(m *wasm.Module) (bytes []byte) { 13 | bytes = append(Magic, version...) 14 | if m.SectionElementCount(wasm.SectionIDType) > 0 { 15 | bytes = append(bytes, encodeTypeSection(m.TypeSection)...) 16 | } 17 | if m.SectionElementCount(wasm.SectionIDImport) > 0 { 18 | bytes = append(bytes, encodeImportSection(m.ImportSection)...) 19 | } 20 | if m.SectionElementCount(wasm.SectionIDFunction) > 0 { 21 | bytes = append(bytes, encodeFunctionSection(m.FunctionSection)...) 22 | } 23 | if m.SectionElementCount(wasm.SectionIDTable) > 0 { 24 | bytes = append(bytes, encodeTableSection(m.TableSection)...) 25 | } 26 | if m.SectionElementCount(wasm.SectionIDMemory) > 0 { 27 | bytes = append(bytes, encodeMemorySection(m.MemorySection)...) 28 | } 29 | if m.SectionElementCount(wasm.SectionIDGlobal) > 0 { 30 | bytes = append(bytes, encodeGlobalSection(m.GlobalSection)...) 31 | } 32 | if m.SectionElementCount(wasm.SectionIDExport) > 0 { 33 | bytes = append(bytes, encodeExportSection(m.ExportSection)...) 34 | } 35 | if m.SectionElementCount(wasm.SectionIDStart) > 0 { 36 | bytes = append(bytes, encodeStartSection(*m.StartSection)...) 37 | } 38 | if m.SectionElementCount(wasm.SectionIDElement) > 0 { 39 | bytes = append(bytes, encodeElementSection(m.ElementSection)...) 40 | } 41 | if m.SectionElementCount(wasm.SectionIDCode) > 0 { 42 | bytes = append(bytes, encodeCodeSection(m.CodeSection)...) 43 | } 44 | if m.SectionElementCount(wasm.SectionIDData) > 0 { 45 | bytes = append(bytes, encodeDataSection(m.DataSection)...) 46 | } 47 | if m.SectionElementCount(wasm.SectionIDCustom) > 0 { 48 | // >> The name section should appear only once in a module, and only after the data section. 49 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-namesec 50 | if m.NameSection != nil { 51 | nameSection := append(sizePrefixedName, encodeNameSectionData(m.NameSection)...) 52 | bytes = append(bytes, encodeSection(wasm.SectionIDCustom, nameSection)...) 53 | } 54 | for _, custom := range m.CustomSections { 55 | bytes = append(bytes, encodeSection(wasm.SectionIDCustom, encodeCustomSection(custom))...) 56 | } 57 | } 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /wasm/counts.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import "fmt" 4 | 5 | // ImportFuncCount returns the possibly empty count of imported functions. This plus SectionElementCount of 6 | // SectionIDFunction is the size of the function index namespace. 7 | func (m *Module) ImportFuncCount() uint32 { 8 | return m.importCount(ExternTypeFunc) 9 | } 10 | 11 | // ImportTableCount returns the possibly empty count of imported tables. This plus SectionElementCount of SectionIDTable 12 | // is the size of the table index namespace. 13 | func (m *Module) ImportTableCount() uint32 { 14 | return m.importCount(ExternTypeTable) 15 | } 16 | 17 | // ImportMemoryCount returns the possibly empty count of imported memories. This plus SectionElementCount of 18 | // SectionIDMemory is the size of the memory index namespace. 19 | func (m *Module) ImportMemoryCount() uint32 { 20 | return m.importCount(ExternTypeMemory) // TODO: once validation happens on decode, this is zero or one. 21 | } 22 | 23 | // ImportGlobalCount returns the possibly empty count of imported globals. This plus SectionElementCount of 24 | // SectionIDGlobal is the size of the global index namespace. 25 | func (m *Module) ImportGlobalCount() uint32 { 26 | return m.importCount(ExternTypeGlobal) 27 | } 28 | 29 | // importCount returns the count of a specific type of import. This is important because it is easy to mistake the 30 | // length of the import section with the count of a specific kind of import. 31 | func (m *Module) importCount(et ExternType) (res uint32) { 32 | for _, im := range m.ImportSection { 33 | if im.Type == et { 34 | res++ 35 | } 36 | } 37 | return 38 | } 39 | 40 | // SectionElementCount returns the count of elements in a given section ID 41 | // 42 | // For example... 43 | // * SectionIDType returns the count of FunctionType 44 | // * SectionIDCustom returns one if the NameSection is present 45 | // * SectionIDHostFunction returns the count of HostFunctionSection 46 | // * SectionIDExport returns the count of unique export names 47 | func (m *Module) SectionElementCount(sectionID SectionID) uint32 { // element as in vector elements! 48 | switch sectionID { 49 | case SectionIDCustom: 50 | numCustomSections := uint32(len(m.CustomSections)) 51 | if m.NameSection != nil { 52 | numCustomSections++ 53 | } 54 | return numCustomSections 55 | case SectionIDType: 56 | return uint32(len(m.TypeSection)) 57 | case SectionIDImport: 58 | return uint32(len(m.ImportSection)) 59 | case SectionIDFunction: 60 | return uint32(len(m.FunctionSection)) 61 | case SectionIDTable: 62 | return uint32(len(m.TableSection)) 63 | case SectionIDMemory: 64 | if m.MemorySection != nil { 65 | return 1 66 | } 67 | return 0 68 | case SectionIDGlobal: 69 | return uint32(len(m.GlobalSection)) 70 | case SectionIDExport: 71 | return uint32(len(m.ExportSection)) 72 | case SectionIDStart: 73 | if m.StartSection != nil { 74 | return 1 75 | } 76 | return 0 77 | case SectionIDElement: 78 | return uint32(len(m.ElementSection)) 79 | case SectionIDCode: 80 | return uint32(len(m.CodeSection)) 81 | case SectionIDData: 82 | return uint32(len(m.DataSection)) 83 | default: 84 | panic(fmt.Errorf("BUG: unknown section: %d", sectionID)) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /binary/value.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "unicode/utf8" 8 | 9 | "github.com/tetratelabs/wabin/leb128" 10 | "github.com/tetratelabs/wabin/wasm" 11 | ) 12 | 13 | var noValType = []byte{0} 14 | 15 | // encodedValTypes is a cache of size prefixed binary encoding of known val types. 16 | var encodedValTypes = map[wasm.ValueType][]byte{ 17 | wasm.ValueTypeI32: {1, wasm.ValueTypeI32}, 18 | wasm.ValueTypeI64: {1, wasm.ValueTypeI64}, 19 | wasm.ValueTypeF32: {1, wasm.ValueTypeF32}, 20 | wasm.ValueTypeF64: {1, wasm.ValueTypeF64}, 21 | wasm.ValueTypeExternref: {1, wasm.ValueTypeExternref}, 22 | wasm.ValueTypeFuncref: {1, wasm.ValueTypeFuncref}, 23 | wasm.ValueTypeV128: {1, wasm.ValueTypeV128}, 24 | } 25 | 26 | // encodeValTypes fast paths binary encoding of common value type lengths 27 | func encodeValTypes(vt []wasm.ValueType) []byte { 28 | // Special case nullary and parameter lengths of wasi_snapshot_preview1 to avoid excess allocations 29 | switch uint32(len(vt)) { 30 | case 0: // nullary 31 | return noValType 32 | case 1: // ex $wasi.fd_close or any result 33 | if encoded, ok := encodedValTypes[vt[0]]; ok { 34 | return encoded 35 | } 36 | case 2: // ex $wasi.environ_sizes_get 37 | return []byte{2, vt[0], vt[1]} 38 | case 4: // ex $wasi.fd_write 39 | return []byte{4, vt[0], vt[1], vt[2], vt[3]} 40 | case 9: // ex $wasi.fd_write 41 | return []byte{9, vt[0], vt[1], vt[2], vt[3], vt[4], vt[5], vt[6], vt[7], vt[8]} 42 | } 43 | // Slow path others until someone complains with a valid signature 44 | count := leb128.EncodeUint32(uint32(len(vt))) 45 | return append(count, vt...) 46 | } 47 | 48 | func decodeValueTypes(r *bytes.Reader, num uint32) ([]wasm.ValueType, error) { 49 | if num == 0 { 50 | return nil, nil 51 | } 52 | ret := make([]wasm.ValueType, num) 53 | buf := make([]wasm.ValueType, num) 54 | _, err := io.ReadFull(r, buf) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | for i, v := range buf { 60 | switch v { 61 | case wasm.ValueTypeI32, wasm.ValueTypeF32, wasm.ValueTypeI64, wasm.ValueTypeF64, 62 | wasm.ValueTypeExternref, wasm.ValueTypeFuncref, wasm.ValueTypeV128: 63 | ret[i] = v 64 | default: 65 | return nil, fmt.Errorf("invalid value type: %d", v) 66 | } 67 | } 68 | return ret, nil 69 | } 70 | 71 | // decodeUTF8 decodes a size prefixed string from the reader, returning it and the count of bytes read. 72 | // contextFormat and contextArgs apply an error format when present 73 | func decodeUTF8(r *bytes.Reader, contextFormat string, contextArgs ...interface{}) (string, uint32, error) { 74 | size, sizeOfSize, err := leb128.DecodeUint32(r) 75 | if err != nil { 76 | return "", 0, fmt.Errorf("failed to read %s size: %w", fmt.Sprintf(contextFormat, contextArgs...), err) 77 | } 78 | 79 | buf := make([]byte, size) 80 | if _, err = io.ReadFull(r, buf); err != nil { 81 | return "", 0, fmt.Errorf("failed to read %s: %w", fmt.Sprintf(contextFormat, contextArgs...), err) 82 | } 83 | 84 | if !utf8.Valid(buf) { 85 | return "", 0, fmt.Errorf("%s is not valid UTF-8", fmt.Sprintf(contextFormat, contextArgs...)) 86 | } 87 | 88 | return string(buf), size + uint32(sizeOfSize), nil 89 | } 90 | -------------------------------------------------------------------------------- /binary/code_test.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/tetratelabs/wabin/wasm" 9 | ) 10 | 11 | var addLocalZeroLocalTwo = []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeLocalGet, 2, wasm.OpcodeI32Add, wasm.OpcodeEnd} 12 | 13 | func TestEncodeCode(t *testing.T) { 14 | addLocalZeroLocalOne := []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeLocalGet, 1, wasm.OpcodeI32Add, wasm.OpcodeEnd} 15 | tests := []struct { 16 | name string 17 | input *wasm.Code 18 | expected []byte 19 | }{ 20 | { 21 | name: "smallest function body", 22 | input: &wasm.Code{ // Ex. (func) 23 | Body: []byte{wasm.OpcodeEnd}, 24 | }, 25 | expected: []byte{ 26 | 0x02, // 2 bytes to encode locals and the body 27 | 0x00, // no local blocks 28 | wasm.OpcodeEnd, // Body 29 | }, 30 | }, 31 | { 32 | name: "params and instructions", // local.get index space is params, then locals 33 | input: &wasm.Code{ // Ex. (func (type 3) local.get 0 local.get 1 i32.add) 34 | Body: addLocalZeroLocalOne, 35 | }, 36 | expected: append([]byte{ 37 | 0x07, // 7 bytes to encode locals and the body 38 | 0x00, // no local blocks 39 | }, 40 | addLocalZeroLocalOne..., // Body 41 | ), 42 | }, 43 | { 44 | name: "locals and instructions", 45 | input: &wasm.Code{ // Ex. (func (result i32) (local i32, i32) local.get 0 local.get 1 i32.add) 46 | LocalTypes: []wasm.ValueType{wasm.ValueTypeI32, wasm.ValueTypeI32}, 47 | Body: addLocalZeroLocalOne, 48 | }, 49 | expected: append([]byte{ 50 | 0x09, // 9 bytes to encode locals and the body 51 | 0x01, // 1 local block 52 | 0x02, wasm.ValueTypeI32, // local block 1 53 | }, 54 | addLocalZeroLocalOne..., // Body 55 | ), 56 | }, 57 | { 58 | name: "mixed locals and instructions", 59 | input: &wasm.Code{ // Ex. (func (result i32) (local i32) (local i64) (local i32) local.get 0 local.get 2 i32.add) 60 | LocalTypes: []wasm.ValueType{wasm.ValueTypeI32, wasm.ValueTypeI64, wasm.ValueTypeI32}, 61 | Body: addLocalZeroLocalTwo, 62 | }, 63 | expected: append([]byte{ 64 | 0x0d, // 13 bytes to encode locals and the body 65 | 0x03, // 3 local blocks 66 | 0x01, wasm.ValueTypeI32, // local block 1 67 | 0x01, wasm.ValueTypeI64, // local block 2 68 | 0x01, wasm.ValueTypeI32, // local block 3 69 | }, 70 | addLocalZeroLocalTwo..., // Body 71 | ), 72 | }, 73 | } 74 | 75 | for _, tt := range tests { 76 | tc := tt 77 | 78 | t.Run(tc.name, func(t *testing.T) { 79 | bytes := encodeCode(tc.input) 80 | require.Equal(t, tc.expected, bytes) 81 | }) 82 | } 83 | } 84 | 85 | func BenchmarkEncodeCode(b *testing.B) { 86 | input := &wasm.Code{ // Ex. (func (result i32) (local i32) (local i64) (local i32) local.get 0 local.get 2 i32.add) 87 | LocalTypes: []wasm.ValueType{wasm.ValueTypeI32, wasm.ValueTypeI64, wasm.ValueTypeI32}, 88 | Body: addLocalZeroLocalTwo, 89 | } 90 | 91 | b.ReportAllocs() 92 | for i := 0; i < b.N; i++ { 93 | if bytes := encodeCode(input); len(bytes) == 0 { 94 | b.Fatal("didn't encode anything") 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /binary/table_test.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/tetratelabs/wabin/wasm" 11 | ) 12 | 13 | func TestTableType(t *testing.T) { 14 | zero := uint32(0) 15 | max := wasm.MaximumFunctionIndex 16 | 17 | tests := []struct { 18 | name string 19 | input *wasm.Table 20 | expected []byte 21 | }{ 22 | { 23 | name: "min 0 - funcref", 24 | input: &wasm.Table{Type: wasm.RefTypeFuncref}, 25 | expected: []byte{wasm.RefTypeFuncref, 0x0, 0}, 26 | }, 27 | { 28 | name: "min 0 - externref", 29 | input: &wasm.Table{Type: wasm.RefTypeExternref}, 30 | expected: []byte{wasm.RefTypeExternref, 0x0, 0}, 31 | }, 32 | { 33 | name: "min 0, max 0", 34 | input: &wasm.Table{Max: &zero, Type: wasm.RefTypeFuncref}, 35 | expected: []byte{wasm.RefTypeFuncref, 0x1, 0, 0}, 36 | }, 37 | { 38 | name: "min largest", 39 | input: &wasm.Table{Min: max, Type: wasm.RefTypeFuncref}, 40 | expected: []byte{wasm.RefTypeFuncref, 0x0, 0x80, 0x80, 0x80, 0x40}, 41 | }, 42 | { 43 | name: "min 0, max largest", 44 | input: &wasm.Table{Max: &max, Type: wasm.RefTypeFuncref}, 45 | expected: []byte{wasm.RefTypeFuncref, 0x1, 0, 0x80, 0x80, 0x80, 0x40}, 46 | }, 47 | { 48 | name: "min largest max largest", 49 | input: &wasm.Table{Min: max, Max: &max, Type: wasm.RefTypeFuncref}, 50 | expected: []byte{wasm.RefTypeFuncref, 0x1, 0x80, 0x80, 0x80, 0x40, 0x80, 0x80, 0x80, 0x40}, 51 | }, 52 | } 53 | 54 | for _, tt := range tests { 55 | tc := tt 56 | 57 | b := encodeTable(tc.input) 58 | t.Run(fmt.Sprintf("encode - %s", tc.name), func(t *testing.T) { 59 | require.Equal(t, tc.expected, b) 60 | }) 61 | 62 | t.Run(fmt.Sprintf("decode - %s", tc.name), func(t *testing.T) { 63 | decoded, err := decodeTable(bytes.NewReader(b), wasm.CoreFeatureReferenceTypes) 64 | require.NoError(t, err) 65 | require.Equal(t, decoded, tc.input) 66 | }) 67 | } 68 | } 69 | 70 | func TestDecodeTableType_Errors(t *testing.T) { 71 | tests := []struct { 72 | name string 73 | input []byte 74 | expectedErr string 75 | features wasm.CoreFeatures 76 | }{ 77 | { 78 | name: "not func ref", 79 | input: []byte{0x50, 0x1, 0x80, 0x80, 0x4, 0}, 80 | expectedErr: "table type funcref is invalid: feature \"reference-types\" is disabled", 81 | }, 82 | { 83 | name: "max < min", 84 | input: []byte{wasm.RefTypeFuncref, 0x1, 0x80, 0x80, 0x4, 0}, 85 | expectedErr: "table size minimum must not be greater than maximum", 86 | features: wasm.CoreFeatureReferenceTypes, 87 | }, 88 | { 89 | name: "min > limit", 90 | input: []byte{wasm.RefTypeFuncref, 0x0, 0xff, 0xff, 0xff, 0xff, 0xf}, 91 | expectedErr: "table min must be at most 134217728", 92 | features: wasm.CoreFeatureReferenceTypes, 93 | }, 94 | } 95 | 96 | for _, tt := range tests { 97 | tc := tt 98 | t.Run(tc.name, func(t *testing.T) { 99 | _, err := decodeTable(bytes.NewReader(tc.input), tc.features) 100 | require.EqualError(t, err, tc.expectedErr) 101 | }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /binary/export_test.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/tetratelabs/wabin/wasm" 9 | ) 10 | 11 | func TestEncodeExport(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | input *wasm.Export 15 | expected []byte 16 | }{ 17 | { 18 | name: "func no name, index 0", 19 | input: &wasm.Export{ // Ex. (export "" (func 0))) 20 | Type: wasm.ExternTypeFunc, 21 | Name: "", 22 | Index: 0, 23 | }, 24 | expected: []byte{wasm.ExternTypeFunc, 0x00, 0x00}, 25 | }, 26 | { 27 | name: "func name, func index 0", 28 | input: &wasm.Export{ // Ex. (export "pi" (func 0)) 29 | Type: wasm.ExternTypeFunc, 30 | Name: "pi", 31 | Index: 0, 32 | }, 33 | expected: []byte{ 34 | 0x02, 'p', 'i', 35 | wasm.ExternTypeFunc, 36 | 0x00, 37 | }, 38 | }, 39 | { 40 | name: "func name, index 10", 41 | input: &wasm.Export{ // Ex. (export "pi" (func 10)) 42 | Type: wasm.ExternTypeFunc, 43 | Name: "pi", 44 | Index: 10, 45 | }, 46 | expected: []byte{ 47 | 0x02, 'p', 'i', 48 | wasm.ExternTypeFunc, 49 | 0x0a, 50 | }, 51 | }, 52 | { 53 | name: "global no name, index 0", 54 | input: &wasm.Export{ // Ex. (export "" (global 0))) 55 | Type: wasm.ExternTypeGlobal, 56 | Name: "", 57 | Index: 0, 58 | }, 59 | expected: []byte{0x00, wasm.ExternTypeGlobal, 0x00}, 60 | }, 61 | { 62 | name: "global name, global index 0", 63 | input: &wasm.Export{ // Ex. (export "pi" (global 0)) 64 | Type: wasm.ExternTypeGlobal, 65 | Name: "pi", 66 | Index: 0, 67 | }, 68 | expected: []byte{ 69 | 0x02, 'p', 'i', 70 | wasm.ExternTypeGlobal, 71 | 0x00, 72 | }, 73 | }, 74 | { 75 | name: "global name, index 10", 76 | input: &wasm.Export{ // Ex. (export "pi" (global 10)) 77 | Type: wasm.ExternTypeGlobal, 78 | Name: "pi", 79 | Index: 10, 80 | }, 81 | expected: []byte{ 82 | 0x02, 'p', 'i', 83 | wasm.ExternTypeGlobal, 84 | 0x0a, 85 | }, 86 | }, 87 | { 88 | name: "memory no name, index 0", 89 | input: &wasm.Export{ // Ex. (export "" (memory 0))) 90 | Type: wasm.ExternTypeMemory, 91 | Name: "", 92 | Index: 0, 93 | }, 94 | expected: []byte{0x00, wasm.ExternTypeMemory, 0x00}, 95 | }, 96 | { 97 | name: "memory name, memory index 0", 98 | input: &wasm.Export{ // Ex. (export "mem" (memory 0)) 99 | Type: wasm.ExternTypeMemory, 100 | Name: "mem", 101 | Index: 0, 102 | }, 103 | expected: []byte{ 104 | 0x03, 'm', 'e', 'm', 105 | wasm.ExternTypeMemory, 106 | 0x00, 107 | }, 108 | }, 109 | { 110 | name: "memory name, index 10", 111 | input: &wasm.Export{ // Ex. (export "mem" (memory 10)) 112 | Type: wasm.ExternTypeMemory, 113 | Name: "mem", 114 | Index: 10, 115 | }, 116 | expected: []byte{ 117 | 0x03, 'm', 'e', 'm', 118 | wasm.ExternTypeMemory, 119 | 0x0a, 120 | }, 121 | }, 122 | } 123 | 124 | for _, tt := range tests { 125 | tc := tt 126 | 127 | t.Run(tc.name, func(t *testing.T) { 128 | bytes := encodeExport(tc.input) 129 | require.Equal(t, tc.expected, bytes) 130 | }) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /binary/value_test.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/tetratelabs/wabin/wasm" 9 | ) 10 | 11 | func TestEncodeValTypes(t *testing.T) { 12 | i32, i64, f32, f64, ext, fref := wasm.ValueTypeI32, wasm.ValueTypeI64, wasm.ValueTypeF32, wasm.ValueTypeF64, wasm.ValueTypeExternref, wasm.ValueTypeFuncref 13 | tests := []struct { 14 | name string 15 | input []wasm.ValueType 16 | expected []byte 17 | }{ 18 | { 19 | name: "empty", 20 | input: []wasm.ValueType{}, 21 | expected: []byte{0}, 22 | }, 23 | { 24 | name: "undefined", // ensure future spec changes don't panic 25 | input: []wasm.ValueType{0x6f}, 26 | expected: []byte{1, 0x6f}, 27 | }, 28 | { 29 | name: "funcref", 30 | input: []wasm.ValueType{fref}, 31 | expected: []byte{1, fref}, 32 | }, 33 | { 34 | name: "externref", 35 | input: []wasm.ValueType{ext}, 36 | expected: []byte{1, ext}, 37 | }, 38 | { 39 | name: "i32", 40 | input: []wasm.ValueType{i32}, 41 | expected: []byte{1, i32}, 42 | }, 43 | { 44 | name: "i64", 45 | input: []wasm.ValueType{i64}, 46 | expected: []byte{1, i64}, 47 | }, 48 | { 49 | name: "f32", 50 | input: []wasm.ValueType{f32}, 51 | expected: []byte{1, f32}, 52 | }, 53 | { 54 | name: "f64", 55 | input: []wasm.ValueType{f64}, 56 | expected: []byte{1, f64}, 57 | }, 58 | { 59 | name: "i32i64", 60 | input: []wasm.ValueType{i32, i64}, 61 | expected: []byte{2, i32, i64}, 62 | }, 63 | { 64 | name: "i32i64f32", 65 | input: []wasm.ValueType{i32, i64, f32}, 66 | expected: []byte{3, i32, i64, f32}, 67 | }, 68 | { 69 | name: "i32i64f32f64", 70 | input: []wasm.ValueType{i32, i64, f32, f64}, 71 | expected: []byte{4, i32, i64, f32, f64}, 72 | }, 73 | { 74 | name: "i32i64f32f64i32", 75 | input: []wasm.ValueType{i32, i64, f32, f64, i32}, 76 | expected: []byte{5, i32, i64, f32, f64, i32}, 77 | }, 78 | { 79 | name: "i32i64f32f64i32i64", 80 | input: []wasm.ValueType{i32, i64, f32, f64, i32, i64}, 81 | expected: []byte{6, i32, i64, f32, f64, i32, i64}, 82 | }, 83 | { 84 | name: "i32i64f32f64i32i64f32", 85 | input: []wasm.ValueType{i32, i64, f32, f64, i32, i64, f32}, 86 | expected: []byte{7, i32, i64, f32, f64, i32, i64, f32}, 87 | }, 88 | { 89 | name: "i32i64f32f64i32i64f32f64", 90 | input: []wasm.ValueType{i32, i64, f32, f64, i32, i64, f32, f64}, 91 | expected: []byte{8, i32, i64, f32, f64, i32, i64, f32, f64}, 92 | }, 93 | { 94 | name: "i32i64f32f64i32i64f32f64i32", 95 | input: []wasm.ValueType{i32, i64, f32, f64, i32, i64, f32, f64, i32}, 96 | expected: []byte{9, i32, i64, f32, f64, i32, i64, f32, f64, i32}, 97 | }, 98 | { 99 | name: "i32i64f32f64i32i64f32f64i32i64", 100 | input: []wasm.ValueType{i32, i64, f32, f64, i32, i64, f32, f64, i32, i64}, 101 | expected: []byte{10, i32, i64, f32, f64, i32, i64, f32, f64, i32, i64}, 102 | }, 103 | } 104 | 105 | for _, tt := range tests { 106 | tc := tt 107 | 108 | t.Run(tc.name, func(t *testing.T) { 109 | bytes := encodeValTypes(tc.input) 110 | require.Equal(t, tc.expected, bytes) 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /binary/data.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/tetratelabs/wabin/leb128" 9 | "github.com/tetratelabs/wabin/wasm" 10 | ) 11 | 12 | // dataSegmentPrefix represents three types of data segments. 13 | // 14 | // https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/binary/modules.html#data-section 15 | type dataSegmentPrefix = uint32 16 | 17 | const ( 18 | // dataSegmentPrefixActive is the prefix for the version 1.0 compatible 19 | // data segment, which is classified as "active" in 2.0. 20 | dataSegmentPrefixActive dataSegmentPrefix = 0x0 21 | // dataSegmentPrefixPassive prefixes the "passive" data segment as in 22 | // version 2.0 specification. 23 | dataSegmentPrefixPassive dataSegmentPrefix = 0x1 24 | // dataSegmentPrefixActiveWithMemoryIndex is the active prefix with memory 25 | //index encoded which is defined for future use as of 2.0. 26 | dataSegmentPrefixActiveWithMemoryIndex dataSegmentPrefix = 0x2 27 | ) 28 | 29 | func decodeDataSegment(r *bytes.Reader, features wasm.CoreFeatures) (*wasm.DataSegment, error) { 30 | dataSegmentPrefix, _, err := leb128.DecodeUint32(r) 31 | if err != nil { 32 | return nil, fmt.Errorf("read data segment prefix: %w", err) 33 | } 34 | 35 | if dataSegmentPrefix != dataSegmentPrefixActive { 36 | if err := features.RequireEnabled(wasm.CoreFeatureBulkMemoryOperations); err != nil { 37 | return nil, fmt.Errorf("non-zero prefix for data segment is invalid as %w", err) 38 | } 39 | } 40 | 41 | var expr *wasm.ConstantExpression 42 | switch dataSegmentPrefix { 43 | case dataSegmentPrefixActive, 44 | dataSegmentPrefixActiveWithMemoryIndex: 45 | // Active data segment as in 46 | // https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/binary/modules.html#data-section 47 | if dataSegmentPrefix == 0x2 { 48 | d, _, err := leb128.DecodeUint32(r) 49 | if err != nil { 50 | return nil, fmt.Errorf("read memory index: %v", err) 51 | } else if d != 0 { 52 | return nil, fmt.Errorf("memory index must be zero but was %d", d) 53 | } 54 | } 55 | 56 | expr, err = decodeConstantExpression(r, features) 57 | if err != nil { 58 | return nil, fmt.Errorf("read offset expression: %v", err) 59 | } 60 | case dataSegmentPrefixPassive: 61 | // Passive data segment doesn't need const expr nor memory index encoded. 62 | // https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/binary/modules.html#data-section 63 | default: 64 | return nil, fmt.Errorf("invalid data segment prefix: 0x%x", dataSegmentPrefix) 65 | } 66 | 67 | vs, _, err := leb128.DecodeUint32(r) 68 | if err != nil { 69 | return nil, fmt.Errorf("get the size of vector: %v", err) 70 | } 71 | 72 | b := make([]byte, vs) 73 | if _, err := io.ReadFull(r, b); err != nil { 74 | return nil, fmt.Errorf("read bytes for init: %v", err) 75 | } 76 | 77 | return &wasm.DataSegment{ 78 | OffsetExpression: expr, 79 | Init: b, 80 | }, nil 81 | } 82 | 83 | func encodeDataSegment(d *wasm.DataSegment) (ret []byte) { 84 | if d.OffsetExpression == nil { 85 | ret = append(ret, leb128.EncodeInt32(int32(dataSegmentPrefixPassive))...) 86 | } else { 87 | // Currently multiple memories are not supported. 88 | ret = append(ret, leb128.EncodeInt32(int32(dataSegmentPrefixActive))...) 89 | ret = append(ret, encodeConstantExpression(d.OffsetExpression)...) 90 | } 91 | ret = append(ret, leb128.EncodeUint32(uint32(len(d.Init)))...) 92 | ret = append(ret, d.Init...) 93 | return 94 | } 95 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package wabin 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/tetratelabs/wabin/binary" 9 | "github.com/tetratelabs/wabin/wasm" 10 | ) 11 | 12 | func newExample() *wasm.Module { 13 | three := wasm.Index(3) 14 | f32, i32, i64 := wasm.ValueTypeF32, wasm.ValueTypeI32, wasm.ValueTypeI64 15 | return &wasm.Module{ 16 | TypeSection: []*wasm.FunctionType{ 17 | {Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}, 18 | {}, 19 | {Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}}, 20 | {Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i64}}, 21 | {Params: []wasm.ValueType{f32}, Results: []wasm.ValueType{i32}}, 22 | {Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32, i32}}, 23 | }, 24 | ImportSection: []*wasm.Import{ 25 | { 26 | Module: "wasi_snapshot_preview1", Name: "args_sizes_get", 27 | Type: wasm.ExternTypeFunc, 28 | DescFunc: 0, 29 | }, { 30 | Module: "wasi_snapshot_preview1", Name: "fd_write", 31 | Type: wasm.ExternTypeFunc, 32 | DescFunc: 2, 33 | }, 34 | }, 35 | FunctionSection: []wasm.Index{wasm.Index(1), wasm.Index(1), wasm.Index(0), wasm.Index(3), wasm.Index(4), wasm.Index(5)}, 36 | CodeSection: []*wasm.Code{ 37 | {Body: []byte{wasm.OpcodeCall, 3, wasm.OpcodeEnd}}, 38 | {Body: []byte{wasm.OpcodeEnd}}, 39 | {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeLocalGet, 1, wasm.OpcodeI32Add, wasm.OpcodeEnd}}, 40 | {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeI64Extend16S, wasm.OpcodeEnd}}, 41 | {Body: []byte{ 42 | wasm.OpcodeLocalGet, 0x00, 43 | wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI32TruncSatF32S, 44 | wasm.OpcodeEnd, 45 | }}, 46 | {Body: []byte{wasm.OpcodeLocalGet, 1, wasm.OpcodeLocalGet, 0, wasm.OpcodeEnd}}, 47 | }, 48 | MemorySection: &wasm.Memory{Min: 1, Max: three, IsMaxEncoded: true}, 49 | ExportSection: []*wasm.Export{ 50 | {Name: "AddInt", Type: wasm.ExternTypeFunc, Index: wasm.Index(4)}, 51 | {Name: "", Type: wasm.ExternTypeFunc, Index: wasm.Index(3)}, 52 | {Name: "mem", Type: wasm.ExternTypeMemory, Index: wasm.Index(0)}, 53 | {Name: "swap", Type: wasm.ExternTypeFunc, Index: wasm.Index(7)}, 54 | }, 55 | StartSection: &three, 56 | NameSection: &wasm.NameSection{ 57 | ModuleName: "example", 58 | FunctionNames: wasm.NameMap{ 59 | {Index: wasm.Index(0), Name: "wasi.args_sizes_get"}, 60 | {Index: wasm.Index(1), Name: "wasi.fd_write"}, 61 | {Index: wasm.Index(2), Name: "call_hello"}, 62 | {Index: wasm.Index(3), Name: "hello"}, 63 | {Index: wasm.Index(4), Name: "addInt"}, 64 | {Index: wasm.Index(7), Name: "swap"}, 65 | }, 66 | LocalNames: wasm.IndirectNameMap{ 67 | {Index: wasm.Index(1), NameMap: wasm.NameMap{ 68 | {Index: wasm.Index(0), Name: "fd"}, 69 | {Index: wasm.Index(1), Name: "iovs_ptr"}, 70 | {Index: wasm.Index(2), Name: "iovs_len"}, 71 | {Index: wasm.Index(3), Name: "nwritten_ptr"}, 72 | }}, 73 | {Index: wasm.Index(4), NameMap: wasm.NameMap{ 74 | {Index: wasm.Index(0), Name: "value_1"}, 75 | {Index: wasm.Index(1), Name: "value_2"}, 76 | }}, 77 | }, 78 | }, 79 | } 80 | } 81 | 82 | func Example_binary() { 83 | bin := binary.EncodeModule(newExample()) 84 | 85 | if mod, err := binary.DecodeModule(bin, wasm.CoreFeaturesV2); err != nil { 86 | log.Panicln(err) 87 | } else { 88 | fmt.Println(mod.NameSection.ModuleName) 89 | } 90 | 91 | // Output: 92 | // example 93 | } 94 | -------------------------------------------------------------------------------- /wasm/features_test.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | // TestCoreFeatures_ZeroIsInvalid reminds maintainers that a bitset cannot use zero as a flag! 10 | // This is why we start iota with 1. 11 | func TestCoreFeatures_ZeroIsInvalid(t *testing.T) { 12 | f := CoreFeatures(0) 13 | f = f.SetEnabled(0, true) 14 | require.False(t, f.IsEnabled(0)) 15 | } 16 | 17 | // TestCoreFeatures tests the bitset works as expected 18 | func TestCoreFeatures(t *testing.T) { 19 | tests := []struct { 20 | name string 21 | feature CoreFeatures 22 | }{ 23 | { 24 | name: "one is the smallest flag", 25 | feature: 1, 26 | }, 27 | { 28 | name: "63 is the largest feature flag", // because uint64 29 | feature: 1 << 63, 30 | }, 31 | } 32 | 33 | for _, tt := range tests { 34 | tc := tt 35 | 36 | t.Run(tc.name, func(t *testing.T) { 37 | f := CoreFeatures(0) 38 | 39 | // Defaults to false 40 | require.False(t, f.IsEnabled(tc.feature)) 41 | 42 | // Set true makes it true 43 | f = f.SetEnabled(tc.feature, true) 44 | require.True(t, f.IsEnabled(tc.feature)) 45 | 46 | // Set false makes it false again 47 | f = f.SetEnabled(tc.feature, false) 48 | require.False(t, f.IsEnabled(tc.feature)) 49 | }) 50 | } 51 | } 52 | 53 | func TestCoreFeatures_String(t *testing.T) { 54 | tests := []struct { 55 | name string 56 | feature CoreFeatures 57 | expected string 58 | }{ 59 | {name: "none", feature: 0, expected: ""}, 60 | {name: "mutable-global", feature: CoreFeatureMutableGlobal, expected: "mutable-global"}, 61 | {name: "sign-extension-ops", feature: CoreFeatureSignExtensionOps, expected: "sign-extension-ops"}, 62 | {name: "multi-value", feature: CoreFeatureMultiValue, expected: "multi-value"}, 63 | {name: "simd", feature: CoreFeatureSIMD, expected: "simd"}, 64 | {name: "features", feature: CoreFeatureMutableGlobal | CoreFeatureMultiValue, expected: "multi-value|mutable-global"}, 65 | {name: "undefined", feature: 1 << 63, expected: ""}, 66 | {name: "2.0", feature: CoreFeaturesV2, 67 | expected: "bulk-memory-operations|multi-value|mutable-global|" + 68 | "nontrapping-float-to-int-conversion|reference-types|sign-extension-ops|simd"}, 69 | } 70 | 71 | for _, tt := range tests { 72 | tc := tt 73 | t.Run(tc.name, func(t *testing.T) { 74 | require.Equal(t, tc.expected, tc.feature.String()) 75 | }) 76 | } 77 | } 78 | 79 | func TestCoreFeatures_Require(t *testing.T) { 80 | tests := []struct { 81 | name string 82 | feature CoreFeatures 83 | expectedErr string 84 | }{ 85 | {name: "none", feature: 0, expectedErr: "feature \"mutable-global\" is disabled"}, 86 | {name: "mutable-global", feature: CoreFeatureMutableGlobal}, 87 | {name: "sign-extension-ops", feature: CoreFeatureSignExtensionOps, expectedErr: "feature \"mutable-global\" is disabled"}, 88 | {name: "multi-value", feature: CoreFeatureMultiValue, expectedErr: "feature \"mutable-global\" is disabled"}, 89 | {name: "undefined", feature: 1 << 63, expectedErr: "feature \"mutable-global\" is disabled"}, 90 | } 91 | 92 | for _, tt := range tests { 93 | tc := tt 94 | t.Run(tc.name, func(t *testing.T) { 95 | err := tc.feature.RequireEnabled(CoreFeatureMutableGlobal) 96 | if tc.expectedErr != "" { 97 | require.EqualError(t, err, tc.expectedErr) 98 | } else { 99 | require.NoError(t, err) 100 | } 101 | }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /binary/function.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/tetratelabs/wabin/leb128" 8 | "github.com/tetratelabs/wabin/wasm" 9 | ) 10 | 11 | var nullary = []byte{0x60, 0, 0} 12 | 13 | // encodedOneParam is a cache of wasm.FunctionType values for param length 1 and result length 0 14 | var encodedOneParam = map[wasm.ValueType][]byte{ 15 | wasm.ValueTypeI32: {0x60, 1, wasm.ValueTypeI32, 0}, 16 | wasm.ValueTypeI64: {0x60, 1, wasm.ValueTypeI64, 0}, 17 | wasm.ValueTypeF32: {0x60, 1, wasm.ValueTypeF32, 0}, 18 | wasm.ValueTypeF64: {0x60, 1, wasm.ValueTypeF64, 0}, 19 | } 20 | 21 | // encodedOneResult is a cache of wasm.FunctionType values for param length 0 and result length 1 22 | var encodedOneResult = map[wasm.ValueType][]byte{ 23 | wasm.ValueTypeI32: {0x60, 0, 1, wasm.ValueTypeI32}, 24 | wasm.ValueTypeI64: {0x60, 0, 1, wasm.ValueTypeI64}, 25 | wasm.ValueTypeF32: {0x60, 0, 1, wasm.ValueTypeF32}, 26 | wasm.ValueTypeF64: {0x60, 0, 1, wasm.ValueTypeF64}, 27 | } 28 | 29 | // encodeFunctionType returns the wasm.FunctionType encoded in WebAssembly Binary Format. 30 | // 31 | // Note: Function types are encoded by the byte 0x60 followed by the respective vectors of parameter and result types. 32 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#function-types%E2%91%A4 33 | func encodeFunctionType(t *wasm.FunctionType) []byte { 34 | paramCount, resultCount := len(t.Params), len(t.Results) 35 | if paramCount == 0 && resultCount == 0 { 36 | return nullary 37 | } 38 | if resultCount == 0 { 39 | if paramCount == 1 { 40 | if encoded, ok := encodedOneParam[t.Params[0]]; ok { 41 | return encoded 42 | } 43 | } 44 | return append(append([]byte{0x60}, encodeValTypes(t.Params)...), 0) 45 | } else if resultCount == 1 { 46 | if paramCount == 0 { 47 | if encoded, ok := encodedOneResult[t.Results[0]]; ok { 48 | return encoded 49 | } 50 | } 51 | return append(append([]byte{0x60}, encodeValTypes(t.Params)...), 1, t.Results[0]) 52 | } 53 | // Only reached when "multi-value" is enabled because WebAssembly supports at most 1 result. 54 | data := append([]byte{0x60}, encodeValTypes(t.Params)...) 55 | return append(data, encodeValTypes(t.Results)...) 56 | } 57 | 58 | func decodeFunctionType(features wasm.CoreFeatures, r *bytes.Reader) (*wasm.FunctionType, error) { 59 | b, err := r.ReadByte() 60 | if err != nil { 61 | return nil, fmt.Errorf("read leading byte: %w", err) 62 | } 63 | 64 | if b != 0x60 { 65 | return nil, fmt.Errorf("%w: %#x != 0x60", ErrInvalidByte, b) 66 | } 67 | 68 | paramCount, _, err := leb128.DecodeUint32(r) 69 | if err != nil { 70 | return nil, fmt.Errorf("could not read parameter count: %w", err) 71 | } 72 | 73 | paramTypes, err := decodeValueTypes(r, paramCount) 74 | if err != nil { 75 | return nil, fmt.Errorf("could not read parameter types: %w", err) 76 | } 77 | 78 | resultCount, _, err := leb128.DecodeUint32(r) 79 | if err != nil { 80 | return nil, fmt.Errorf("could not read result count: %w", err) 81 | } 82 | 83 | // Guard >1.0 feature multi-value 84 | if resultCount > 1 { 85 | if err = features.RequireEnabled(wasm.CoreFeatureMultiValue); err != nil { 86 | return nil, fmt.Errorf("multiple result types invalid as %v", err) 87 | } 88 | } 89 | 90 | resultTypes, err := decodeValueTypes(r, resultCount) 91 | if err != nil { 92 | return nil, fmt.Errorf("could not read result types: %w", err) 93 | } 94 | 95 | ret := &wasm.FunctionType{ 96 | Params: paramTypes, 97 | Results: resultTypes, 98 | } 99 | return ret, nil 100 | } 101 | -------------------------------------------------------------------------------- /binary/const_expr.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/tetratelabs/wabin/ieee754" 8 | "github.com/tetratelabs/wabin/leb128" 9 | "github.com/tetratelabs/wabin/wasm" 10 | ) 11 | 12 | func decodeConstantExpression(r *bytes.Reader, features wasm.CoreFeatures) (*wasm.ConstantExpression, error) { 13 | b, err := r.ReadByte() 14 | if err != nil { 15 | return nil, fmt.Errorf("read opcode: %v", err) 16 | } 17 | 18 | remainingBeforeData := int64(r.Len()) 19 | offsetAtData := r.Size() - remainingBeforeData 20 | 21 | opcode := b 22 | switch opcode { 23 | case wasm.OpcodeI32Const: 24 | // Treat constants as signed as their interpretation is not yet known per /RATIONALE.md 25 | _, _, err = leb128.DecodeInt32(r) 26 | case wasm.OpcodeI64Const: 27 | // Treat constants as signed as their interpretation is not yet known per /RATIONALE.md 28 | _, _, err = leb128.DecodeInt64(r) 29 | case wasm.OpcodeF32Const: 30 | _, err = ieee754.DecodeFloat32(r) 31 | case wasm.OpcodeF64Const: 32 | _, err = ieee754.DecodeFloat64(r) 33 | case wasm.OpcodeGlobalGet: 34 | _, _, err = leb128.DecodeUint32(r) 35 | case wasm.OpcodeRefNull: 36 | if err := features.RequireEnabled(wasm.CoreFeatureBulkMemoryOperations); err != nil { 37 | return nil, fmt.Errorf("ref.null is not supported as %w", err) 38 | } 39 | reftype, err := r.ReadByte() 40 | if err != nil { 41 | return nil, fmt.Errorf("read reference type for ref.null: %w", err) 42 | } else if reftype != wasm.RefTypeFuncref && reftype != wasm.RefTypeExternref { 43 | return nil, fmt.Errorf("invalid type for ref.null: 0x%x", reftype) 44 | } 45 | case wasm.OpcodeRefFunc: 46 | if err := features.RequireEnabled(wasm.CoreFeatureBulkMemoryOperations); err != nil { 47 | return nil, fmt.Errorf("ref.func is not supported as %w", err) 48 | } 49 | // Parsing index. 50 | _, _, err = leb128.DecodeUint32(r) 51 | case wasm.OpcodeVecPrefix: 52 | if err := features.RequireEnabled(wasm.CoreFeatureSIMD); err != nil { 53 | return nil, fmt.Errorf("vector instructions are not supported as %w", err) 54 | } 55 | opcode, err = r.ReadByte() 56 | if err != nil { 57 | return nil, fmt.Errorf("read vector instruction opcode suffix: %w", err) 58 | } 59 | 60 | if opcode != wasm.OpcodeVecV128Const { 61 | return nil, fmt.Errorf("invalid vector opcode for const expression: %#x", opcode) 62 | } 63 | 64 | remainingBeforeData = int64(r.Len()) 65 | offsetAtData = r.Size() - remainingBeforeData 66 | 67 | n, err := r.Read(make([]byte, 16)) 68 | if err != nil { 69 | return nil, fmt.Errorf("read vector const instruction immediates: %w", err) 70 | } else if n != 16 { 71 | return nil, fmt.Errorf("read vector const instruction immediates: needs 16 bytes but was %d bytes", n) 72 | } 73 | default: 74 | return nil, fmt.Errorf("%v for const expression opt code: %#x", ErrInvalidByte, b) 75 | } 76 | 77 | if err != nil { 78 | return nil, fmt.Errorf("read value: %v", err) 79 | } 80 | 81 | if b, err = r.ReadByte(); err != nil { 82 | return nil, fmt.Errorf("look for end opcode: %v", err) 83 | } 84 | 85 | if b != wasm.OpcodeEnd { 86 | return nil, fmt.Errorf("constant expression has been not terminated") 87 | } 88 | 89 | data := make([]byte, remainingBeforeData-int64(r.Len())-1) 90 | if _, err := r.ReadAt(data, offsetAtData); err != nil { 91 | return nil, fmt.Errorf("error re-buffering ConstantExpression.Data") 92 | } 93 | 94 | return &wasm.ConstantExpression{Opcode: opcode, Data: data}, nil 95 | } 96 | 97 | func encodeConstantExpression(expr *wasm.ConstantExpression) (ret []byte) { 98 | ret = append(ret, expr.Opcode) 99 | ret = append(ret, expr.Data...) 100 | ret = append(ret, wasm.OpcodeEnd) 101 | return 102 | } 103 | -------------------------------------------------------------------------------- /binary/import_test.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/tetratelabs/wabin/wasm" 9 | ) 10 | 11 | func TestEncodeImport(t *testing.T) { 12 | ptrOfUint32 := func(v uint32) *uint32 { 13 | return &v 14 | } 15 | 16 | tests := []struct { 17 | name string 18 | input *wasm.Import 19 | expected []byte 20 | }{ 21 | { 22 | name: "func no module, no name, type index 0", 23 | input: &wasm.Import{ // Ex. (import "" "" (func (type 0))) 24 | Type: wasm.ExternTypeFunc, 25 | Module: "", 26 | Name: "", 27 | DescFunc: 0, 28 | }, 29 | expected: []byte{wasm.ExternTypeFunc, 0x00, 0x00, 0x00}, 30 | }, 31 | { 32 | name: "func module, no name, type index 0", 33 | input: &wasm.Import{ // Ex. (import "$test" "" (func (type 0))) 34 | Type: wasm.ExternTypeFunc, 35 | Module: "test", 36 | Name: "", 37 | DescFunc: 0, 38 | }, 39 | expected: []byte{ 40 | 0x04, 't', 'e', 's', 't', 41 | 0x00, 42 | wasm.ExternTypeFunc, 43 | 0x00, 44 | }, 45 | }, 46 | { 47 | name: "func module, name, type index 0", 48 | input: &wasm.Import{ // Ex. (import "$math" "$pi" (func (type 0))) 49 | Type: wasm.ExternTypeFunc, 50 | Module: "math", 51 | Name: "pi", 52 | DescFunc: 0, 53 | }, 54 | expected: []byte{ 55 | 0x04, 'm', 'a', 't', 'h', 56 | 0x02, 'p', 'i', 57 | wasm.ExternTypeFunc, 58 | 0x00, 59 | }, 60 | }, 61 | { 62 | name: "func module, name, type index 10", 63 | input: &wasm.Import{ // Ex. (import "$math" "$pi" (func (type 10))) 64 | Type: wasm.ExternTypeFunc, 65 | Module: "math", 66 | Name: "pi", 67 | DescFunc: 10, 68 | }, 69 | expected: []byte{ 70 | 0x04, 'm', 'a', 't', 'h', 71 | 0x02, 'p', 'i', 72 | wasm.ExternTypeFunc, 73 | 0x0a, 74 | }, 75 | }, 76 | { 77 | name: "global const", 78 | input: &wasm.Import{ 79 | Type: wasm.ExternTypeGlobal, 80 | Module: "math", 81 | Name: "pi", 82 | DescGlobal: &wasm.GlobalType{ValType: wasm.ValueTypeF64}, 83 | }, 84 | expected: []byte{ 85 | 0x04, 'm', 'a', 't', 'h', 86 | 0x02, 'p', 'i', 87 | wasm.ExternTypeGlobal, 88 | wasm.ValueTypeF64, 0x00, // 0 == const 89 | }, 90 | }, 91 | { 92 | name: "global var", 93 | input: &wasm.Import{ 94 | Type: wasm.ExternTypeGlobal, 95 | Module: "math", 96 | Name: "pi", 97 | DescGlobal: &wasm.GlobalType{ValType: wasm.ValueTypeF64, Mutable: true}, 98 | }, 99 | expected: []byte{ 100 | 0x04, 'm', 'a', 't', 'h', 101 | 0x02, 'p', 'i', 102 | wasm.ExternTypeGlobal, 103 | wasm.ValueTypeF64, 0x01, // 1 == var 104 | }, 105 | }, 106 | { 107 | name: "table", 108 | input: &wasm.Import{ 109 | Type: wasm.ExternTypeTable, 110 | Module: "my", 111 | Name: "table", 112 | DescTable: &wasm.Table{Min: 1, Max: ptrOfUint32(2)}, 113 | }, 114 | expected: []byte{ 115 | 0x02, 'm', 'y', 116 | 0x05, 't', 'a', 'b', 'l', 'e', 117 | wasm.ExternTypeTable, 118 | wasm.RefTypeFuncref, 119 | 0x1, 0x1, 0x2, // Limit with max. 120 | }, 121 | }, 122 | { 123 | name: "memory", 124 | input: &wasm.Import{ 125 | Type: wasm.ExternTypeMemory, 126 | Module: "my", 127 | Name: "memory", 128 | DescMem: &wasm.Memory{Min: 1, Max: 2, IsMaxEncoded: true}, 129 | }, 130 | expected: []byte{ 131 | 0x02, 'm', 'y', 132 | 0x06, 'm', 'e', 'm', 'o', 'r', 'y', 133 | wasm.ExternTypeMemory, 134 | 0x1, 0x1, 0x2, // Limit with max. 135 | }, 136 | }, 137 | } 138 | 139 | for _, tt := range tests { 140 | tc := tt 141 | 142 | t.Run(tc.name, func(t *testing.T) { 143 | bytes := encodeImport(tc.input) 144 | require.Equal(t, tc.expected, bytes) 145 | }) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /wasm/types_test.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | "unsafe" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestExternTypeName(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | input ExternType 16 | expected string 17 | }{ 18 | {"func", ExternTypeFunc, "func"}, 19 | {"table", ExternTypeTable, "table"}, 20 | {"mem", ExternTypeMemory, "memory"}, 21 | {"global", ExternTypeGlobal, "global"}, 22 | {"unknown", 100, "0x64"}, 23 | } 24 | 25 | for _, tt := range tests { 26 | tc := tt 27 | 28 | t.Run(tc.name, func(t *testing.T) { 29 | require.Equal(t, tc.expected, ExternTypeName(tc.input)) 30 | }) 31 | } 32 | } 33 | 34 | func TestValueTypeName(t *testing.T) { 35 | tests := []struct { 36 | name string 37 | input ValueType 38 | expected string 39 | }{ 40 | {"i32", ValueTypeI32, "i32"}, 41 | {"i64", ValueTypeI64, "i64"}, 42 | {"f32", ValueTypeF32, "f32"}, 43 | {"f64", ValueTypeF64, "f64"}, 44 | {"externref", ValueTypeExternref, "externref"}, 45 | {"unknown", 100, "unknown"}, 46 | } 47 | 48 | for _, tt := range tests { 49 | tc := tt 50 | 51 | t.Run(tc.name, func(t *testing.T) { 52 | require.Equal(t, tc.expected, ValueTypeName(tc.input)) 53 | }) 54 | } 55 | } 56 | 57 | func TestEncodeDecodeExternRef(t *testing.T) { 58 | for _, v := range []uintptr{ 59 | 0, uintptr(unsafe.Pointer(t)), 60 | } { 61 | t.Run(fmt.Sprintf("%x", v), func(t *testing.T) { 62 | encoded := EncodeExternref(v) 63 | binary := DecodeExternref(encoded) 64 | require.Equal(t, v, binary) 65 | }) 66 | } 67 | } 68 | 69 | func TestEncodeDecodeF32(t *testing.T) { 70 | for _, v := range []float32{ 71 | 0, 100, -100, 1, -1, 72 | 100.01234124, -100.01234124, 200.12315, 73 | math.MaxFloat32, 74 | math.SmallestNonzeroFloat32, 75 | float32(math.Inf(1)), float32(math.Inf(-1)), float32(math.NaN()), 76 | } { 77 | t.Run(fmt.Sprintf("%f", v), func(t *testing.T) { 78 | encoded := EncodeF32(v) 79 | binary := DecodeF32(encoded) 80 | require.Zero(t, encoded>>32) // Ensures high bits aren't set 81 | if math.IsNaN(float64(binary)) { // NaN cannot be compared with themselves, so we have to use IsNaN 82 | require.True(t, math.IsNaN(float64(binary))) 83 | } else { 84 | require.Equal(t, v, binary) 85 | } 86 | }) 87 | } 88 | } 89 | 90 | func TestEncodeDecodeF64(t *testing.T) { 91 | for _, v := range []float64{ 92 | 0, 100, -100, 1, -1, 93 | 100.01234124, -100.01234124, 200.12315, 94 | math.MaxFloat32, 95 | math.SmallestNonzeroFloat32, 96 | math.MaxFloat64, 97 | math.SmallestNonzeroFloat64, 98 | 6.8719476736e+10, /* = 1 << 36 */ 99 | 1.37438953472e+11, /* = 1 << 37 */ 100 | math.Inf(1), math.Inf(-1), math.NaN(), 101 | } { 102 | t.Run(fmt.Sprintf("%f", v), func(t *testing.T) { 103 | encoded := EncodeF64(v) 104 | val := DecodeF64(encoded) 105 | if math.IsNaN(val) { // cannot use require.Equal as NaN by definition doesn't equal itself 106 | require.True(t, math.IsNaN(val)) 107 | } else { 108 | require.Equal(t, v, val) 109 | } 110 | }) 111 | } 112 | } 113 | 114 | func TestEncodeCastI32(t *testing.T) { 115 | for _, v := range []int32{ 116 | 0, 100, -100, 1, -1, 117 | math.MaxInt32, 118 | math.MinInt32, 119 | } { 120 | t.Run(fmt.Sprintf("%d", v), func(t *testing.T) { 121 | encoded := EncodeI32(v) 122 | require.Zero(t, encoded>>32) // Ensures high bits aren't set 123 | binary := int32(encoded) 124 | require.Equal(t, v, binary) 125 | }) 126 | } 127 | } 128 | 129 | func TestEncodeCastI64(t *testing.T) { 130 | for _, v := range []int64{ 131 | 0, 100, -100, 1, -1, 132 | math.MaxInt64, 133 | math.MinInt64, 134 | } { 135 | t.Run(fmt.Sprintf("%d", v), func(t *testing.T) { 136 | encoded := EncodeI64(v) 137 | binary := int64(encoded) 138 | require.Equal(t, v, binary) 139 | }) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /binary/code.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "math" 8 | 9 | "github.com/tetratelabs/wabin/leb128" 10 | "github.com/tetratelabs/wabin/wasm" 11 | ) 12 | 13 | func decodeCode(r *bytes.Reader) (*wasm.Code, error) { 14 | ss, _, err := leb128.DecodeUint32(r) 15 | if err != nil { 16 | return nil, fmt.Errorf("get the size of code: %w", err) 17 | } 18 | remaining := int64(ss) 19 | 20 | // parse locals 21 | ls, bytesRead, err := leb128.DecodeUint32(r) 22 | remaining -= int64(bytesRead) 23 | if err != nil { 24 | return nil, fmt.Errorf("get the size locals: %v", err) 25 | } else if remaining < 0 { 26 | return nil, io.EOF 27 | } 28 | 29 | var nums []uint64 30 | var types []wasm.ValueType 31 | var sum uint64 32 | var n uint32 33 | for i := uint32(0); i < ls; i++ { 34 | n, bytesRead, err = leb128.DecodeUint32(r) 35 | remaining -= int64(bytesRead) + 1 // +1 for the subsequent ReadByte 36 | if err != nil { 37 | return nil, fmt.Errorf("read n of locals: %v", err) 38 | } else if remaining < 0 { 39 | return nil, io.EOF 40 | } 41 | 42 | sum += uint64(n) 43 | nums = append(nums, uint64(n)) 44 | 45 | b, err := r.ReadByte() 46 | if err != nil { 47 | return nil, fmt.Errorf("read type of local: %v", err) 48 | } 49 | switch vt := b; vt { 50 | case wasm.ValueTypeI32, wasm.ValueTypeF32, wasm.ValueTypeI64, wasm.ValueTypeF64, 51 | wasm.ValueTypeFuncref, wasm.ValueTypeExternref, wasm.ValueTypeV128: 52 | types = append(types, vt) 53 | default: 54 | return nil, fmt.Errorf("invalid local type: 0x%x", vt) 55 | } 56 | } 57 | 58 | if sum > math.MaxUint32 { 59 | return nil, fmt.Errorf("too many locals: %d", sum) 60 | } 61 | 62 | var localTypes []wasm.ValueType 63 | for i, num := range nums { 64 | t := types[i] 65 | for j := uint64(0); j < num; j++ { 66 | localTypes = append(localTypes, t) 67 | } 68 | } 69 | 70 | body := make([]byte, remaining) 71 | if _, err = io.ReadFull(r, body); err != nil { 72 | return nil, fmt.Errorf("read body: %w", err) 73 | } 74 | 75 | if endIndex := len(body) - 1; endIndex < 0 || body[endIndex] != wasm.OpcodeEnd { 76 | return nil, fmt.Errorf("expr not end with OpcodeEnd") 77 | } 78 | 79 | return &wasm.Code{Body: body, LocalTypes: localTypes}, nil 80 | } 81 | 82 | // encodeCode returns the wasm.Code encoded in WebAssembly Binary Format. 83 | // 84 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-code 85 | func encodeCode(c *wasm.Code) []byte { 86 | // local blocks compress locals while preserving index order by grouping locals of the same type. 87 | // https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#code-section%E2%91%A0 88 | localBlockCount := uint32(0) // how many blocks of locals with the same type (types can repeat!) 89 | var localBlocks []byte 90 | localTypeLen := len(c.LocalTypes) 91 | if localTypeLen > 0 { 92 | i := localTypeLen - 1 93 | var runCount uint32 // count of the same type 94 | var lastValueType wasm.ValueType // initialize to an invalid type 0 95 | 96 | // iterate backwards so it is easier to size prefix 97 | for ; i >= 0; i-- { 98 | vt := c.LocalTypes[i] 99 | if lastValueType != vt { 100 | if runCount != 0 { // Only on the first iteration, this is zero when vt is compared against invalid 101 | localBlocks = append(leb128.EncodeUint32(runCount), localBlocks...) 102 | } 103 | lastValueType = vt 104 | localBlocks = append(leb128.EncodeUint32(uint32(vt)), localBlocks...) // reuse the EncodeUint32 cache 105 | localBlockCount++ 106 | runCount = 1 107 | } else { 108 | runCount++ 109 | } 110 | } 111 | localBlocks = append(leb128.EncodeUint32(runCount), localBlocks...) 112 | localBlocks = append(leb128.EncodeUint32(localBlockCount), localBlocks...) 113 | } else { 114 | localBlocks = leb128.EncodeUint32(0) 115 | } 116 | code := append(localBlocks, c.Body...) 117 | return append(leb128.EncodeUint32(uint32(len(code))), code...) 118 | } 119 | -------------------------------------------------------------------------------- /binary/decoder.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/tetratelabs/wabin/leb128" 10 | "github.com/tetratelabs/wabin/wasm" 11 | ) 12 | 13 | // DecodeModule implements wasm.DecodeModule for the WebAssembly Binary Format 14 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-format%E2%91%A0 15 | func DecodeModule(binary []byte, features wasm.CoreFeatures) (*wasm.Module, error) { 16 | r := bytes.NewReader(binary) 17 | 18 | // Magic number. 19 | buf := make([]byte, 4) 20 | if _, err := io.ReadFull(r, buf); err != nil || !bytes.Equal(buf, Magic) { 21 | return nil, ErrInvalidMagicNumber 22 | } 23 | 24 | // Version. 25 | if _, err := io.ReadFull(r, buf); err != nil || !bytes.Equal(buf, version) { 26 | return nil, ErrInvalidVersion 27 | } 28 | 29 | m := &wasm.Module{} 30 | for { 31 | // TODO: except custom sections, all others are required to be in order, but we aren't checking yet. 32 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#modules%E2%91%A0%E2%93%AA 33 | sectionID, err := r.ReadByte() 34 | if err == io.EOF { 35 | break 36 | } else if err != nil { 37 | return nil, fmt.Errorf("read section id: %w", err) 38 | } 39 | 40 | sectionSize, _, err := leb128.DecodeUint32(r) 41 | if err != nil { 42 | return nil, fmt.Errorf("get size of section %s: %v", wasm.SectionIDName(sectionID), err) 43 | } 44 | 45 | sectionContentStart := r.Len() 46 | switch sectionID { 47 | case wasm.SectionIDCustom: 48 | // First, validate the section and determine if the section for this name has already been set 49 | name, nameSize, decodeErr := decodeUTF8(r, "custom section name") 50 | if decodeErr != nil { 51 | err = decodeErr 52 | break 53 | } else if sectionSize < nameSize { 54 | err = fmt.Errorf("malformed custom section %s", name) 55 | break 56 | } else if name == "name" && m.NameSection != nil { 57 | err = fmt.Errorf("redundant custom section %s", name) 58 | break 59 | } 60 | 61 | // Now, either decode the NameSection or CustomSection 62 | limit := sectionSize - nameSize 63 | if name == "name" { 64 | m.NameSection, err = decodeNameSection(r, uint64(limit)) 65 | } else { 66 | custom, err := decodeCustomSection(r, name, uint64(limit)) 67 | if err != nil { 68 | return nil, fmt.Errorf("failed to read custom section name[%s]: %w", name, err) 69 | } 70 | m.CustomSections = append(m.CustomSections, custom) 71 | } 72 | 73 | case wasm.SectionIDType: 74 | m.TypeSection, err = decodeTypeSection(features, r) 75 | case wasm.SectionIDImport: 76 | if m.ImportSection, err = decodeImportSection(r, features); err != nil { 77 | return nil, err // avoid re-wrapping the error. 78 | } 79 | case wasm.SectionIDFunction: 80 | m.FunctionSection, err = decodeFunctionSection(r) 81 | case wasm.SectionIDTable: 82 | m.TableSection, err = decodeTableSection(r, features) 83 | case wasm.SectionIDMemory: 84 | m.MemorySection, err = decodeMemorySection(r) 85 | case wasm.SectionIDGlobal: 86 | if m.GlobalSection, err = decodeGlobalSection(r, features); err != nil { 87 | return nil, err // avoid re-wrapping the error. 88 | } 89 | case wasm.SectionIDExport: 90 | m.ExportSection, err = decodeExportSection(r) 91 | case wasm.SectionIDStart: 92 | if m.StartSection != nil { 93 | return nil, errors.New("multiple start sections are invalid") 94 | } 95 | m.StartSection, err = decodeStartSection(r) 96 | case wasm.SectionIDElement: 97 | m.ElementSection, err = decodeElementSection(r, features) 98 | case wasm.SectionIDCode: 99 | m.CodeSection, err = decodeCodeSection(r) 100 | case wasm.SectionIDData: 101 | m.DataSection, err = decodeDataSection(r, features) 102 | case wasm.SectionIDDataCount: 103 | if err := features.RequireEnabled(wasm.CoreFeatureBulkMemoryOperations); err != nil { 104 | return nil, fmt.Errorf("data count section not supported as %v", err) 105 | } 106 | m.DataCountSection, err = decodeDataCountSection(r) 107 | default: 108 | err = ErrInvalidSectionID 109 | } 110 | 111 | readBytes := sectionContentStart - r.Len() 112 | if err == nil && int(sectionSize) != readBytes { 113 | err = fmt.Errorf("invalid section length: expected to be %d but got %d", sectionSize, readBytes) 114 | } 115 | 116 | if err != nil { 117 | return nil, fmt.Errorf("section %s: %v", wasm.SectionIDName(sectionID), err) 118 | } 119 | } 120 | 121 | functionCount, codeCount := m.SectionElementCount(wasm.SectionIDFunction), m.SectionElementCount(wasm.SectionIDCode) 122 | if functionCount != codeCount { 123 | return nil, fmt.Errorf("function and code section have inconsistent lengths: %d != %d", functionCount, codeCount) 124 | } 125 | return m, nil 126 | } 127 | -------------------------------------------------------------------------------- /binary/data_test.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/tetratelabs/wabin/wasm" 11 | ) 12 | 13 | func Test_decodeDataSegment(t *testing.T) { 14 | tests := []struct { 15 | in []byte 16 | exp *wasm.DataSegment 17 | features wasm.CoreFeatures 18 | expErr string 19 | }{ 20 | { 21 | in: []byte{0xf, 22 | // Const expression. 23 | wasm.OpcodeI32Const, 0x1, wasm.OpcodeEnd, 24 | // Two initial data. 25 | 0x2, 0xf, 0xf, 26 | }, 27 | features: wasm.CoreFeatureBulkMemoryOperations, 28 | expErr: "invalid data segment prefix: 0xf", 29 | }, 30 | { 31 | in: []byte{0x0, 32 | // Const expression. 33 | wasm.OpcodeI32Const, 0x1, wasm.OpcodeEnd, 34 | // Two initial data. 35 | 0x2, 0xf, 0xf, 36 | }, 37 | exp: &wasm.DataSegment{ 38 | OffsetExpression: &wasm.ConstantExpression{ 39 | Opcode: wasm.OpcodeI32Const, 40 | Data: []byte{0x1}, 41 | }, 42 | Init: []byte{0xf, 0xf}, 43 | }, 44 | features: wasm.CoreFeatureBulkMemoryOperations, 45 | }, 46 | { 47 | in: []byte{0x0, 48 | // Const expression. 49 | wasm.OpcodeI32Const, 0x1, 50 | 0x2, 0xf, 0xf, 51 | }, 52 | expErr: "read offset expression: constant expression has been not terminated", 53 | features: wasm.CoreFeatureBulkMemoryOperations, 54 | }, 55 | { 56 | in: []byte{0x1, // Passive data segment without memory index and const expr. 57 | // Two initial data. 58 | 0x2, 0xf, 0xf, 59 | }, 60 | exp: &wasm.DataSegment{ 61 | OffsetExpression: nil, 62 | Init: []byte{0xf, 0xf}, 63 | }, 64 | features: wasm.CoreFeatureBulkMemoryOperations, 65 | }, 66 | { 67 | in: []byte{0x2, 68 | 0x0, // Memory index. 69 | // Const expression. 70 | wasm.OpcodeI32Const, 0x1, wasm.OpcodeEnd, 71 | // Two initial data. 72 | 0x2, 0xf, 0xf, 73 | }, 74 | exp: &wasm.DataSegment{ 75 | OffsetExpression: &wasm.ConstantExpression{ 76 | Opcode: wasm.OpcodeI32Const, 77 | Data: []byte{0x1}, 78 | }, 79 | Init: []byte{0xf, 0xf}, 80 | }, 81 | features: wasm.CoreFeatureBulkMemoryOperations, 82 | }, 83 | { 84 | in: []byte{0x2, 85 | 0x1, // Memory index. 86 | // Const expression. 87 | wasm.OpcodeI32Const, 0x1, wasm.OpcodeEnd, 88 | // Two initial data. 89 | 0x2, 0xf, 0xf, 90 | }, 91 | expErr: "memory index must be zero but was 1", 92 | features: wasm.CoreFeatureBulkMemoryOperations, 93 | }, 94 | { 95 | in: []byte{0x2, 96 | 0x0, // Memory index. 97 | // Const expression. 98 | wasm.OpcodeI32Const, 0x1, 99 | // Two initial data. 100 | 0x2, 0xf, 0xf, 101 | }, 102 | expErr: "read offset expression: constant expression has been not terminated", 103 | features: wasm.CoreFeatureBulkMemoryOperations, 104 | }, 105 | { 106 | in: []byte{0x2, 107 | 0x0, // Memory index. 108 | // Const expression. 109 | wasm.OpcodeI32Const, 0x1, wasm.OpcodeEnd, 110 | // Two initial data. 111 | 0x2, 0xf, 0xf, 112 | }, 113 | features: wasm.CoreFeatureMutableGlobal, 114 | expErr: "non-zero prefix for data segment is invalid as feature \"bulk-memory-operations\" is disabled", 115 | }, 116 | } 117 | 118 | for i, tt := range tests { 119 | tc := tt 120 | t.Run(strconv.Itoa(i), func(t *testing.T) { 121 | actual, err := decodeDataSegment(bytes.NewReader(tc.in), tc.features) 122 | if tc.expErr == "" { 123 | require.NoError(t, err) 124 | require.Equal(t, tc.exp, actual) 125 | } else { 126 | require.EqualError(t, err, tc.expErr) 127 | } 128 | }) 129 | } 130 | } 131 | 132 | func Test_encodeDataSegment(t *testing.T) { 133 | tests := []struct { 134 | in *wasm.DataSegment 135 | exp []byte 136 | features wasm.CoreFeatures 137 | expErr string 138 | }{ 139 | { 140 | in: &wasm.DataSegment{ 141 | OffsetExpression: &wasm.ConstantExpression{ 142 | Opcode: wasm.OpcodeI32Const, 143 | Data: []byte{0x1}, 144 | }, 145 | Init: []byte{0xf, 0xf}, 146 | }, 147 | exp: []byte{0x0, 148 | // Const expression. 149 | wasm.OpcodeI32Const, 0x1, wasm.OpcodeEnd, 150 | // Two initial data. 151 | 0x2, 0xf, 0xf, 152 | }, 153 | features: wasm.CoreFeatureBulkMemoryOperations, 154 | }, 155 | { 156 | exp: []byte{0x1, // Passive data segment without memory index and const expr. 157 | // Two initial data. 158 | 0x2, 0xf, 0xf, 159 | }, 160 | in: &wasm.DataSegment{ 161 | OffsetExpression: nil, 162 | Init: []byte{0xf, 0xf}, 163 | }, 164 | features: wasm.CoreFeatureBulkMemoryOperations, 165 | }, 166 | } 167 | 168 | for i, tt := range tests { 169 | tc := tt 170 | t.Run(strconv.Itoa(i), func(t *testing.T) { 171 | actual := encodeDataSegment(tc.in) 172 | require.Equal(t, tc.exp, actual) 173 | }) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /binary/const_expr_test.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/tetratelabs/wabin/wasm" 11 | ) 12 | 13 | func TestDecodeConstantExpression(t *testing.T) { 14 | tests := []struct { 15 | in []byte 16 | exp *wasm.ConstantExpression 17 | }{ 18 | { 19 | in: []byte{ 20 | wasm.OpcodeRefFunc, 21 | 0x80, 0, // Multi byte zero. 22 | wasm.OpcodeEnd, 23 | }, 24 | exp: &wasm.ConstantExpression{ 25 | Opcode: wasm.OpcodeRefFunc, 26 | Data: []byte{0x80, 0}, 27 | }, 28 | }, 29 | { 30 | in: []byte{ 31 | wasm.OpcodeRefFunc, 32 | 0x80, 0x80, 0x80, 0x4f, // 165675008 in varint encoding. 33 | wasm.OpcodeEnd, 34 | }, 35 | exp: &wasm.ConstantExpression{ 36 | Opcode: wasm.OpcodeRefFunc, 37 | Data: []byte{0x80, 0x80, 0x80, 0x4f}, 38 | }, 39 | }, 40 | { 41 | in: []byte{ 42 | wasm.OpcodeRefNull, 43 | wasm.RefTypeFuncref, 44 | wasm.OpcodeEnd, 45 | }, 46 | exp: &wasm.ConstantExpression{ 47 | Opcode: wasm.OpcodeRefNull, 48 | Data: []byte{ 49 | wasm.RefTypeFuncref, 50 | }, 51 | }, 52 | }, 53 | { 54 | in: []byte{ 55 | wasm.OpcodeRefNull, 56 | wasm.RefTypeExternref, 57 | wasm.OpcodeEnd, 58 | }, 59 | exp: &wasm.ConstantExpression{ 60 | Opcode: wasm.OpcodeRefNull, 61 | Data: []byte{ 62 | wasm.RefTypeExternref, 63 | }, 64 | }, 65 | }, 66 | { 67 | in: []byte{ 68 | wasm.OpcodeVecPrefix, 69 | wasm.OpcodeVecV128Const, 70 | 1, 1, 1, 1, 1, 1, 1, 1, 71 | 1, 1, 1, 1, 1, 1, 1, 1, 72 | wasm.OpcodeEnd, 73 | }, 74 | exp: &wasm.ConstantExpression{ 75 | Opcode: wasm.OpcodeVecV128Const, 76 | Data: []byte{ 77 | 1, 1, 1, 1, 1, 1, 1, 1, 78 | 1, 1, 1, 1, 1, 1, 1, 1, 79 | }, 80 | }, 81 | }, 82 | } 83 | 84 | for i, tt := range tests { 85 | tc := tt 86 | t.Run(strconv.Itoa(i), func(t *testing.T) { 87 | actual, err := decodeConstantExpression(bytes.NewReader(tc.in), 88 | wasm.CoreFeatureBulkMemoryOperations|wasm.CoreFeatureSIMD) 89 | require.NoError(t, err) 90 | require.Equal(t, tc.exp, actual) 91 | }) 92 | } 93 | } 94 | 95 | func TestDecodeConstantExpression_errors(t *testing.T) { 96 | tests := []struct { 97 | in []byte 98 | expectedErr string 99 | features wasm.CoreFeatures 100 | }{ 101 | { 102 | in: []byte{ 103 | wasm.OpcodeRefFunc, 104 | 0, 105 | }, 106 | expectedErr: "look for end opcode: EOF", 107 | features: wasm.CoreFeatureBulkMemoryOperations, 108 | }, 109 | { 110 | in: []byte{ 111 | wasm.OpcodeRefNull, 112 | }, 113 | expectedErr: "read reference type for ref.null: EOF", 114 | features: wasm.CoreFeatureBulkMemoryOperations, 115 | }, 116 | { 117 | in: []byte{ 118 | wasm.OpcodeRefNull, 119 | 0xff, 120 | wasm.OpcodeEnd, 121 | }, 122 | expectedErr: "invalid type for ref.null: 0xff", 123 | features: wasm.CoreFeatureBulkMemoryOperations, 124 | }, 125 | { 126 | in: []byte{ 127 | wasm.OpcodeRefNull, 128 | wasm.RefTypeExternref, 129 | wasm.OpcodeEnd, 130 | }, 131 | expectedErr: "ref.null is not supported as feature \"bulk-memory-operations\" is disabled", 132 | features: wasm.CoreFeaturesV1, 133 | }, 134 | { 135 | in: []byte{ 136 | wasm.OpcodeRefFunc, 137 | 0x80, 0, 138 | wasm.OpcodeEnd, 139 | }, 140 | expectedErr: "ref.func is not supported as feature \"bulk-memory-operations\" is disabled", 141 | features: wasm.CoreFeaturesV1, 142 | }, 143 | { 144 | in: []byte{ 145 | wasm.OpcodeVecPrefix, 146 | wasm.OpcodeVecV128Const, 147 | 1, 1, 1, 1, 1, 1, 1, 1, 148 | 1, 1, 1, 1, 1, 1, 1, 1, 149 | wasm.OpcodeEnd, 150 | }, 151 | expectedErr: "vector instructions are not supported as feature \"simd\" is disabled", 152 | features: wasm.CoreFeaturesV1, 153 | }, 154 | { 155 | in: []byte{ 156 | wasm.OpcodeVecPrefix, 157 | }, 158 | expectedErr: "read vector instruction opcode suffix: EOF", 159 | features: wasm.CoreFeatureSIMD, 160 | }, 161 | { 162 | in: []byte{ 163 | wasm.OpcodeVecPrefix, 164 | 1, 1, 1, 1, 1, 1, 1, 1, 165 | 1, 1, 1, 1, 1, 1, 1, 1, 166 | wasm.OpcodeEnd, 167 | }, 168 | expectedErr: "invalid vector opcode for const expression: 0x1", 169 | features: wasm.CoreFeatureSIMD, 170 | }, 171 | { 172 | in: []byte{ 173 | wasm.OpcodeVecPrefix, 174 | wasm.OpcodeVecV128Const, 175 | 1, 1, 1, 1, 1, 1, 1, 1, 176 | }, 177 | expectedErr: "read vector const instruction immediates: needs 16 bytes but was 8 bytes", 178 | features: wasm.CoreFeatureSIMD, 179 | }, 180 | } 181 | 182 | for _, tt := range tests { 183 | tc := tt 184 | t.Run(tc.expectedErr, func(t *testing.T) { 185 | _, err := decodeConstantExpression(bytes.NewReader(tc.in), tc.features) 186 | require.EqualError(t, err, tc.expectedErr) 187 | }) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /binary/function_test.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/tetratelabs/wabin/wasm" 11 | ) 12 | 13 | func TestFunctionType(t *testing.T) { 14 | i32, i64, funcRef, externRef := wasm.ValueTypeI32, wasm.ValueTypeI64, wasm.ValueTypeFuncref, wasm.ValueTypeExternref 15 | tests := []struct { 16 | name string 17 | input *wasm.FunctionType 18 | expected []byte 19 | }{ 20 | { 21 | name: "empty", 22 | input: &wasm.FunctionType{}, 23 | expected: []byte{0x60, 0, 0}, 24 | }, 25 | { 26 | name: "one param no result", 27 | input: &wasm.FunctionType{Params: []wasm.ValueType{i32}}, 28 | expected: []byte{0x60, 1, i32, 0}, 29 | }, 30 | { 31 | name: "no param one result", 32 | input: &wasm.FunctionType{Results: []wasm.ValueType{i32}}, 33 | expected: []byte{0x60, 0, 1, i32}, 34 | }, 35 | { 36 | name: "one param one result", 37 | input: &wasm.FunctionType{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i32}}, 38 | expected: []byte{0x60, 1, i64, 1, i32}, 39 | }, 40 | { 41 | name: "two params no result", 42 | input: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}}, 43 | expected: []byte{0x60, 2, i32, i64, 0}, 44 | }, 45 | { 46 | name: "two param one result", 47 | input: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{i32}}, 48 | expected: []byte{0x60, 2, i32, i64, 1, i32}, 49 | }, 50 | { 51 | name: "no param two results", 52 | input: &wasm.FunctionType{Results: []wasm.ValueType{i32, i64}}, 53 | expected: []byte{0x60, 0, 2, i32, i64}, 54 | }, 55 | { 56 | name: "one param two results", 57 | input: &wasm.FunctionType{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i32, i64}}, 58 | expected: []byte{0x60, 1, i64, 2, i32, i64}, 59 | }, 60 | { 61 | name: "two param two results", 62 | input: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{i32, i64}}, 63 | expected: []byte{0x60, 2, i32, i64, 2, i32, i64}, 64 | }, 65 | { 66 | name: "two param two results with funcrefs", 67 | input: &wasm.FunctionType{Params: []wasm.ValueType{i32, funcRef}, Results: []wasm.ValueType{funcRef, i64}}, 68 | expected: []byte{0x60, 2, i32, funcRef, 2, funcRef, i64}, 69 | }, 70 | { 71 | name: "two param two results with externrefs", 72 | input: &wasm.FunctionType{Params: []wasm.ValueType{i32, externRef}, Results: []wasm.ValueType{externRef, i64}}, 73 | expected: []byte{0x60, 2, i32, externRef, 2, externRef, i64}, 74 | }, 75 | } 76 | 77 | for _, tt := range tests { 78 | tc := tt 79 | 80 | b := encodeFunctionType(tc.input) 81 | t.Run(fmt.Sprintf("encode - %s", tc.name), func(t *testing.T) { 82 | require.Equal(t, tc.expected, b) 83 | }) 84 | 85 | t.Run(fmt.Sprintf("decode - %s", tc.name), func(t *testing.T) { 86 | binary, err := decodeFunctionType(wasm.CoreFeaturesV2, bytes.NewReader(b)) 87 | require.NoError(t, err) 88 | require.Equal(t, binary, tc.input) 89 | }) 90 | } 91 | } 92 | 93 | func TestDecodeFunctionType_Errors(t *testing.T) { 94 | i32, i64 := wasm.ValueTypeI32, wasm.ValueTypeI64 95 | tests := []struct { 96 | name string 97 | input []byte 98 | features wasm.CoreFeatures 99 | expectedErr string 100 | }{ 101 | { 102 | name: "undefined param no result", 103 | input: []byte{0x60, 1, 0x6e, 0}, 104 | expectedErr: "could not read parameter types: invalid value type: 110", 105 | }, 106 | { 107 | name: "no param undefined result", 108 | input: []byte{0x60, 0, 1, 0x6e}, 109 | expectedErr: "could not read result types: invalid value type: 110", 110 | }, 111 | { 112 | name: "undefined param undefined result", 113 | input: []byte{0x60, 1, 0x6e, 1, 0x6e}, 114 | expectedErr: "could not read parameter types: invalid value type: 110", 115 | }, 116 | { 117 | name: "no param two results - multi-value not enabled", 118 | input: []byte{0x60, 0, 2, i32, i64}, 119 | expectedErr: "multiple result types invalid as feature \"multi-value\" is disabled", 120 | }, 121 | { 122 | name: "one param two results - multi-value not enabled", 123 | input: []byte{0x60, 1, i64, 2, i32, i64}, 124 | expectedErr: "multiple result types invalid as feature \"multi-value\" is disabled", 125 | }, 126 | { 127 | name: "two param two results - multi-value not enabled", 128 | input: []byte{0x60, 2, i32, i64, 2, i32, i64}, 129 | expectedErr: "multiple result types invalid as feature \"multi-value\" is disabled", 130 | }, 131 | } 132 | 133 | for _, tt := range tests { 134 | tc := tt 135 | 136 | t.Run(tc.name, func(t *testing.T) { 137 | _, err := decodeFunctionType(wasm.CoreFeaturesV1, bytes.NewReader(tc.input)) 138 | require.EqualError(t, err, tc.expectedErr) 139 | }) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /binary/decoder_test.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/tetratelabs/wabin/wasm" 9 | ) 10 | 11 | // TestDecodeModule relies on unit tests for Module.Encode, specifically that the encoding is both known and correct. 12 | // This avoids having to copy/paste or share variables to assert against byte arrays. 13 | func TestDecodeModule(t *testing.T) { 14 | i32, f32 := wasm.ValueTypeI32, wasm.ValueTypeF32 15 | zero := uint32(0) 16 | 17 | tests := []struct { 18 | name string 19 | input *wasm.Module // round trip test! 20 | }{ 21 | { 22 | name: "empty", 23 | input: &wasm.Module{}, 24 | }, 25 | { 26 | name: "only name section", 27 | input: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "simple"}}, 28 | }, 29 | { 30 | name: "type section", 31 | input: &wasm.Module{ 32 | TypeSection: []*wasm.FunctionType{ 33 | {}, 34 | {Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}, 35 | {Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}}, 36 | }, 37 | }, 38 | }, 39 | { 40 | name: "type and import section", 41 | input: &wasm.Module{ 42 | TypeSection: []*wasm.FunctionType{ 43 | {Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}, 44 | {Params: []wasm.ValueType{f32, f32}, Results: []wasm.ValueType{f32}}, 45 | }, 46 | ImportSection: []*wasm.Import{ 47 | { 48 | Module: "Math", Name: "Mul", 49 | Type: wasm.ExternTypeFunc, 50 | DescFunc: 1, 51 | }, { 52 | Module: "Math", Name: "Add", 53 | Type: wasm.ExternTypeFunc, 54 | DescFunc: 0, 55 | }, 56 | }, 57 | }, 58 | }, 59 | { 60 | name: "table and memory section", 61 | input: &wasm.Module{ 62 | TableSection: []*wasm.Table{{Min: 3, Type: wasm.RefTypeFuncref}}, 63 | MemorySection: &wasm.Memory{Min: 1, Max: 1, IsMaxEncoded: true}, 64 | }, 65 | }, 66 | { 67 | name: "type function and start section", 68 | input: &wasm.Module{ 69 | TypeSection: []*wasm.FunctionType{{}}, 70 | ImportSection: []*wasm.Import{{ 71 | Module: "", Name: "hello", 72 | Type: wasm.ExternTypeFunc, 73 | DescFunc: 0, 74 | }}, 75 | StartSection: &zero, 76 | }, 77 | }, 78 | } 79 | 80 | for _, tt := range tests { 81 | tc := tt 82 | 83 | t.Run(tc.name, func(t *testing.T) { 84 | m, e := DecodeModule(EncodeModule(tc.input), wasm.CoreFeaturesV1) 85 | require.NoError(t, e) 86 | require.Equal(t, tc.input, m) 87 | }) 88 | } 89 | 90 | t.Run("reads custom sections", func(t *testing.T) { 91 | input := append(append(Magic, version...), 92 | wasm.SectionIDCustom, 0xf, // 15 bytes in this section 93 | 0x04, 'm', 'e', 'm', 'e', 94 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 0) 95 | m, e := DecodeModule(input, wasm.CoreFeaturesV1) 96 | require.NoError(t, e) 97 | require.Equal(t, &wasm.Module{ 98 | CustomSections: []*wasm.CustomSection{ 99 | { 100 | Name: "meme", 101 | Data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, 102 | }, 103 | }, 104 | }, m) 105 | }) 106 | 107 | t.Run("read custom sections and name separately", func(t *testing.T) { 108 | input := append(append(Magic, version...), 109 | wasm.SectionIDCustom, 0xf, // 15 bytes in this section 110 | 0x04, 'm', 'e', 'm', 'e', 111 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 112 | wasm.SectionIDCustom, 0x0e, // 14 bytes in this section 113 | 0x04, 'n', 'a', 'm', 'e', 114 | subsectionIDModuleName, 0x07, // 7 bytes in this subsection 115 | 0x06, // the Module name simple is 6 bytes long 116 | 's', 'i', 'm', 'p', 'l', 'e') 117 | m, e := DecodeModule(input, wasm.CoreFeaturesV1) 118 | require.NoError(t, e) 119 | require.Equal(t, &wasm.Module{ 120 | NameSection: &wasm.NameSection{ModuleName: "simple"}, 121 | CustomSections: []*wasm.CustomSection{ 122 | { 123 | Name: "meme", 124 | Data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, 125 | }, 126 | }, 127 | }, m) 128 | }) 129 | t.Run("data count section disabled", func(t *testing.T) { 130 | input := append(append(Magic, version...), 131 | wasm.SectionIDDataCount, 1, 0) 132 | _, e := DecodeModule(input, wasm.CoreFeaturesV1) 133 | require.EqualError(t, e, `data count section not supported as feature "bulk-memory-operations" is disabled`) 134 | }) 135 | } 136 | 137 | func TestDecodeModule_Errors(t *testing.T) { 138 | tests := []struct { 139 | name string 140 | input []byte 141 | expectedErr string 142 | }{ 143 | { 144 | name: "wrong magic", 145 | input: []byte("wasm\x01\x00\x00\x00"), 146 | expectedErr: "invalid magic number", 147 | }, 148 | { 149 | name: "wrong version", 150 | input: []byte("\x00asm\x01\x00\x00\x01"), 151 | expectedErr: "invalid version header", 152 | }, 153 | { 154 | name: "multiple start sections", 155 | input: append(append(Magic, version...), 156 | wasm.SectionIDType, 4, 1, 0x60, 0, 0, 157 | wasm.SectionIDFunction, 2, 1, 0, 158 | wasm.SectionIDCode, 4, 1, 159 | 2, 0, wasm.OpcodeEnd, 160 | wasm.SectionIDStart, 1, 0, 161 | wasm.SectionIDStart, 1, 0, 162 | ), 163 | expectedErr: `multiple start sections are invalid`, 164 | }, 165 | { 166 | name: "redundant name section", 167 | input: append(append(Magic, version...), 168 | wasm.SectionIDCustom, 0x09, // 9 bytes in this section 169 | 0x04, 'n', 'a', 'm', 'e', 170 | subsectionIDModuleName, 0x02, 0x01, 'x', 171 | wasm.SectionIDCustom, 0x09, // 9 bytes in this section 172 | 0x04, 'n', 'a', 'm', 'e', 173 | subsectionIDModuleName, 0x02, 0x01, 'x'), 174 | expectedErr: "section custom: redundant custom section name", 175 | }, 176 | } 177 | 178 | for _, tt := range tests { 179 | tc := tt 180 | 181 | t.Run(tc.name, func(t *testing.T) { 182 | _, e := DecodeModule(tc.input, wasm.CoreFeaturesV1) 183 | require.EqualError(t, e, tc.expectedErr) 184 | }) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /wasm/types.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // ValueType describes a numeric type used in Web Assembly 1.0 (20191205). For example, Function parameters and results are 9 | // only definable as a value type. 10 | // 11 | // The following describes how to convert between Wasm and Golang types: 12 | // 13 | // - ValueTypeI32 - uint64(uint32,int32) 14 | // - ValueTypeI64 - uint64(int64) 15 | // - ValueTypeF32 - EncodeF32 DecodeF32 from float32 16 | // - ValueTypeF64 - EncodeF64 DecodeF64 from float64 17 | // - ValueTypeExternref - uintptr(unsafe.Pointer(p)) where p is any pointer type in Go (e.g. *string) 18 | // 19 | // Ex. Given a Text Format type use (param i64) (result i64), no conversion is necessary. 20 | // 21 | // results, _ := fn(ctx, input) 22 | // result := result[0] 23 | // 24 | // Ex. Given a Text Format type use (param f64) (result f64), conversion is necessary. 25 | // 26 | // results, _ := fn(ctx, api.EncodeF64(input)) 27 | // result := api.DecodeF64(result[0]) 28 | // 29 | // Note: This is a type alias as it is easier to encode and decode in the binary format. 30 | // 31 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-valtype 32 | type ValueType = byte 33 | 34 | const ( 35 | // ValueTypeI32 is a 32-bit integer. 36 | ValueTypeI32 ValueType = 0x7f 37 | // ValueTypeI64 is a 64-bit integer. 38 | ValueTypeI64 ValueType = 0x7e 39 | // ValueTypeF32 is a 32-bit floating point number. 40 | ValueTypeF32 ValueType = 0x7d 41 | // ValueTypeF64 is a 64-bit floating point number. 42 | ValueTypeF64 ValueType = 0x7c 43 | 44 | // ValueTypeExternref is an externref type. 45 | // 46 | // Note: in wazero, externref type value are opaque raw 64-bit pointers, 47 | // and the ValueTypeExternref type in the signature will be translated as 48 | // uintptr in wazero's API level. 49 | // 50 | // For example, given the import function: 51 | // (func (import "env" "f") (param externref) (result externref)) 52 | // 53 | // This can be defined in Go as: 54 | // r.NewModuleBuilder("env").ExportFunctions(map[string]interface{}{ 55 | // "f": func(externref uintptr) (resultExternRef uintptr) { return }, 56 | // }) 57 | // 58 | // Note: The usage of this type is toggled with WithFeatureBulkMemoryOperations. 59 | ValueTypeExternref ValueType = 0x6f 60 | 61 | ValueTypeV128 ValueType = 0x7b 62 | ValueTypeFuncref ValueType = 0x70 63 | ) 64 | 65 | // ValueTypeName returns the type name of the given ValueType as a string. 66 | // These type names match the names used in the WebAssembly text format. 67 | // 68 | // Note: This returns "unknown", if an undefined ValueType value is passed. 69 | func ValueTypeName(t ValueType) string { 70 | switch t { 71 | case ValueTypeI32: 72 | return "i32" 73 | case ValueTypeI64: 74 | return "i64" 75 | case ValueTypeF32: 76 | return "f32" 77 | case ValueTypeF64: 78 | return "f64" 79 | case ValueTypeExternref: 80 | return "externref" 81 | case ValueTypeFuncref: 82 | return "funcref" 83 | case ValueTypeV128: 84 | return "v128" 85 | } 86 | return "unknown" 87 | } 88 | 89 | // ExternType classifies imports and exports with their respective types. 90 | // 91 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#external-types%E2%91%A0 92 | type ExternType = byte 93 | 94 | const ( 95 | ExternTypeFunc ExternType = 0x00 96 | ExternTypeTable ExternType = 0x01 97 | ExternTypeMemory ExternType = 0x02 98 | ExternTypeGlobal ExternType = 0x03 99 | ) 100 | 101 | // The below are exported to consolidate parsing behavior for external types. 102 | const ( 103 | // ExternTypeFuncName is the name of the WebAssembly Text Format field for ExternTypeFunc. 104 | ExternTypeFuncName = "func" 105 | // ExternTypeTableName is the name of the WebAssembly Text Format field for ExternTypeTable. 106 | ExternTypeTableName = "table" 107 | // ExternTypeMemoryName is the name of the WebAssembly Text Format field for ExternTypeMemory. 108 | ExternTypeMemoryName = "memory" 109 | // ExternTypeGlobalName is the name of the WebAssembly Text Format field for ExternTypeGlobal. 110 | ExternTypeGlobalName = "global" 111 | ) 112 | 113 | // ExternTypeName returns the name of the WebAssembly Text Format field of the given type. 114 | // 115 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#exports%E2%91%A4 116 | func ExternTypeName(et ExternType) string { 117 | switch et { 118 | case ExternTypeFunc: 119 | return ExternTypeFuncName 120 | case ExternTypeTable: 121 | return ExternTypeTableName 122 | case ExternTypeMemory: 123 | return ExternTypeMemoryName 124 | case ExternTypeGlobal: 125 | return ExternTypeGlobalName 126 | } 127 | return fmt.Sprintf("%#x", et) 128 | } 129 | 130 | // EncodeI32 encodes the input as a ValueTypeI32. 131 | func EncodeI32(input int32) uint64 { 132 | return uint64(uint32(input)) 133 | } 134 | 135 | // EncodeI64 encodes the input as a ValueTypeI64. 136 | func EncodeI64(input int64) uint64 { 137 | return uint64(input) 138 | } 139 | 140 | // EncodeF32 encodes the input as a ValueTypeF32. 141 | // 142 | // See DecodeF32 143 | func EncodeF32(input float32) uint64 { 144 | return uint64(math.Float32bits(input)) 145 | } 146 | 147 | // DecodeF32 decodes the input as a ValueTypeF32. 148 | // 149 | // See EncodeF32 150 | func DecodeF32(input uint64) float32 { 151 | return math.Float32frombits(uint32(input)) 152 | } 153 | 154 | // EncodeF64 encodes the input as a ValueTypeF64. 155 | // 156 | // See EncodeF32 157 | func EncodeF64(input float64) uint64 { 158 | return math.Float64bits(input) 159 | } 160 | 161 | // DecodeF64 decodes the input as a ValueTypeF64. 162 | // 163 | // See EncodeF64 164 | func DecodeF64(input uint64) float64 { 165 | return math.Float64frombits(input) 166 | } 167 | 168 | // EncodeExternref encodes the input as a ValueTypeExternref. 169 | // 170 | // See DecodeExternref 171 | func EncodeExternref(input uintptr) uint64 { 172 | return uint64(input) 173 | } 174 | 175 | // DecodeExternref decodes the input as a ValueTypeExternref. 176 | // 177 | // See EncodeExternref 178 | func DecodeExternref(input uint64) uintptr { 179 | return uintptr(input) 180 | } 181 | -------------------------------------------------------------------------------- /binary/section_test.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/tetratelabs/wabin/wasm" 10 | ) 11 | 12 | func TestTableSection(t *testing.T) { 13 | three := uint32(3) 14 | tests := []struct { 15 | name string 16 | input []byte 17 | expected []*wasm.Table 18 | }{ 19 | { 20 | name: "min and min with max", 21 | input: []byte{ 22 | 0x01, // 1 table 23 | wasm.RefTypeFuncref, 0x01, 2, 3, // (table 2 3) 24 | }, 25 | expected: []*wasm.Table{{Min: 2, Max: &three, Type: wasm.RefTypeFuncref}}, 26 | }, 27 | { 28 | name: "min and min with max - three tables", 29 | input: []byte{ 30 | 0x03, // 3 table 31 | wasm.RefTypeFuncref, 0x01, 2, 3, // (table 2 3) 32 | wasm.RefTypeExternref, 0x01, 2, 3, // (table 2 3) 33 | wasm.RefTypeFuncref, 0x01, 2, 3, // (table 2 3) 34 | }, 35 | expected: []*wasm.Table{ 36 | {Min: 2, Max: &three, Type: wasm.RefTypeFuncref}, 37 | {Min: 2, Max: &three, Type: wasm.RefTypeExternref}, 38 | {Min: 2, Max: &three, Type: wasm.RefTypeFuncref}, 39 | }, 40 | }, 41 | } 42 | 43 | for _, tt := range tests { 44 | tc := tt 45 | 46 | t.Run(tc.name, func(t *testing.T) { 47 | tables, err := decodeTableSection(bytes.NewReader(tc.input), wasm.CoreFeatureReferenceTypes) 48 | require.NoError(t, err) 49 | require.Equal(t, tc.expected, tables) 50 | }) 51 | } 52 | } 53 | 54 | func TestTableSection_Errors(t *testing.T) { 55 | tests := []struct { 56 | name string 57 | input []byte 58 | expectedErr string 59 | features wasm.CoreFeatures 60 | }{ 61 | { 62 | name: "min and min with max", 63 | input: []byte{ 64 | 0x02, // 2 tables 65 | wasm.RefTypeFuncref, 0x00, 0x01, // (table 1) 66 | wasm.RefTypeFuncref, 0x01, 0x02, 0x03, // (table 2 3) 67 | }, 68 | expectedErr: "at most one table allowed in module as feature \"reference-types\" is disabled", 69 | features: wasm.CoreFeaturesV1, 70 | }, 71 | } 72 | 73 | for _, tt := range tests { 74 | tc := tt 75 | 76 | t.Run(tc.name, func(t *testing.T) { 77 | _, err := decodeTableSection(bytes.NewReader(tc.input), tc.features) 78 | require.EqualError(t, err, tc.expectedErr) 79 | }) 80 | } 81 | } 82 | 83 | func TestMemorySection(t *testing.T) { 84 | three := uint32(3) 85 | tests := []struct { 86 | name string 87 | input []byte 88 | expected *wasm.Memory 89 | }{ 90 | { 91 | name: "min and min with max", 92 | input: []byte{ 93 | 0x01, // 1 memory 94 | 0x01, 0x02, 0x03, // (memory 2 3) 95 | }, 96 | expected: &wasm.Memory{Min: 2, Max: three, IsMaxEncoded: true}, 97 | }, 98 | } 99 | 100 | for _, tt := range tests { 101 | tc := tt 102 | 103 | t.Run(tc.name, func(t *testing.T) { 104 | memories, err := decodeMemorySection(bytes.NewReader(tc.input)) 105 | require.NoError(t, err) 106 | require.Equal(t, tc.expected, memories) 107 | }) 108 | } 109 | } 110 | 111 | func TestMemorySection_Errors(t *testing.T) { 112 | tests := []struct { 113 | name string 114 | input []byte 115 | expectedErr string 116 | }{ 117 | { 118 | name: "min and min with max", 119 | input: []byte{ 120 | 0x02, // 2 memories 121 | 0x01, // (memory 1) 122 | 0x02, 0x03, // (memory 2 3) 123 | }, 124 | expectedErr: "at most one memory allowed in module, but read 2", 125 | }, 126 | } 127 | 128 | for _, tt := range tests { 129 | tc := tt 130 | 131 | t.Run(tc.name, func(t *testing.T) { 132 | _, err := decodeMemorySection(bytes.NewReader(tc.input)) 133 | require.EqualError(t, err, tc.expectedErr) 134 | }) 135 | } 136 | } 137 | 138 | func TestDecodeExportSection(t *testing.T) { 139 | tests := []struct { 140 | name string 141 | input []byte 142 | expected []*wasm.Export 143 | }{ 144 | { 145 | name: "empty and non-empty name", 146 | input: []byte{ 147 | 0x02, // 2 exports 148 | 0x00, // Size of empty name 149 | wasm.ExternTypeFunc, 0x02, // func[2] 150 | 0x01, 'a', // Size of name, name 151 | wasm.ExternTypeFunc, 0x01, // func[1] 152 | }, 153 | expected: []*wasm.Export{ 154 | {Name: "", Type: wasm.ExternTypeFunc, Index: wasm.Index(2)}, 155 | {Name: "a", Type: wasm.ExternTypeFunc, Index: wasm.Index(1)}, 156 | }, 157 | }, 158 | } 159 | 160 | for _, tt := range tests { 161 | tc := tt 162 | 163 | t.Run(tc.name, func(t *testing.T) { 164 | exports, err := decodeExportSection(bytes.NewReader(tc.input)) 165 | require.NoError(t, err) 166 | require.Equal(t, tc.expected, exports) 167 | }) 168 | } 169 | } 170 | 171 | func TestDecodeExportSection_Errors(t *testing.T) { 172 | tests := []struct { 173 | name string 174 | input []byte 175 | expectedErr string 176 | }{ 177 | { 178 | name: "duplicates empty name", 179 | input: []byte{ 180 | 0x02, // 2 exports 181 | 0x00, // Size of empty name 182 | wasm.ExternTypeFunc, 0x00, // func[0] 183 | 0x00, // Size of empty name 184 | wasm.ExternTypeFunc, 0x00, // func[0] 185 | }, 186 | expectedErr: "export[1] duplicates name \"\"", 187 | }, 188 | { 189 | name: "duplicates name", 190 | input: []byte{ 191 | 0x02, // 2 exports 192 | 0x01, 'a', // Size of name, name 193 | wasm.ExternTypeFunc, 0x00, // func[0] 194 | 0x01, 'a', // Size of name, name 195 | wasm.ExternTypeFunc, 0x00, // func[0] 196 | }, 197 | expectedErr: "export[1] duplicates name \"a\"", 198 | }, 199 | } 200 | 201 | for _, tt := range tests { 202 | tc := tt 203 | 204 | t.Run(tc.name, func(t *testing.T) { 205 | _, err := decodeExportSection(bytes.NewReader(tc.input)) 206 | require.EqualError(t, err, tc.expectedErr) 207 | }) 208 | } 209 | } 210 | 211 | func TestEncodeFunctionSection(t *testing.T) { 212 | require.Equal(t, []byte{wasm.SectionIDFunction, 0x2, 0x01, 0x05}, encodeFunctionSection([]wasm.Index{5})) 213 | } 214 | 215 | // TestEncodeStartSection uses the same index as TestEncodeFunctionSection to highlight the encoding is different. 216 | func TestEncodeStartSection(t *testing.T) { 217 | require.Equal(t, []byte{wasm.SectionIDStart, 0x01, 0x05}, encodeStartSection(5)) 218 | } 219 | 220 | func TestDecodeDataCountSection(t *testing.T) { 221 | t.Run("ok", func(t *testing.T) { 222 | v, err := decodeDataCountSection(bytes.NewReader([]byte{0x1})) 223 | require.NoError(t, err) 224 | require.Equal(t, uint32(1), *v) 225 | }) 226 | t.Run("eof", func(t *testing.T) { 227 | // EOF is fine as the data count is optional. 228 | _, err := decodeDataCountSection(bytes.NewReader([]byte{})) 229 | require.NoError(t, err) 230 | }) 231 | } 232 | -------------------------------------------------------------------------------- /leb128/leb128.go: -------------------------------------------------------------------------------- 1 | package leb128 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | const ( 10 | maxVarintLen32 = 5 11 | maxVarintLen64 = 10 12 | ) 13 | 14 | var ( 15 | errOverflow32 = errors.New("overflows a 32-bit integer") 16 | errOverflow33 = errors.New("overflows a 33-bit integer") 17 | errOverflow64 = errors.New("overflows a 64-bit integer") 18 | ) 19 | 20 | // EncodeInt32 encodes the signed value into a buffer in LEB128 format 21 | // 22 | // See https://en.wikipedia.org/wiki/LEB128#Encode_signed_integer 23 | func EncodeInt32(value int32) []byte { 24 | return EncodeInt64(int64(value)) 25 | } 26 | 27 | // EncodeInt64 encodes the signed value into a buffer in LEB128 format 28 | // 29 | // See https://en.wikipedia.org/wiki/LEB128#Encode_signed_integer 30 | func EncodeInt64(value int64) (buf []byte) { 31 | for { 32 | // Take 7 remaining low-order bits from the value into b. 33 | b := uint8(value & 0x7f) 34 | // Extract the sign bit. 35 | s := uint8(value & 0x40) 36 | value >>= 7 37 | 38 | // The encoding unsigned numbers is simpler as it only needs to check if the value is non-zero to tell if there 39 | // are more bits to encode. Signed is a little more complicated as you have to double-check the sign bit. 40 | // If either case, set the high-order bit to tell the reader there are more bytes in this int. 41 | if (value != -1 || s == 0) && (value != 0 || s != 0) { 42 | b |= 0x80 43 | } 44 | 45 | // Append b into the buffer 46 | buf = append(buf, b) 47 | if b&0x80 == 0 { 48 | break 49 | } 50 | } 51 | return buf 52 | } 53 | 54 | // EncodeUint32 encodes the value into a buffer in LEB128 format 55 | // 56 | // See https://en.wikipedia.org/wiki/LEB128#Encode_unsigned_integer 57 | func EncodeUint32(value uint32) []byte { 58 | return EncodeUint64(uint64(value)) 59 | } 60 | 61 | // EncodeUint64 encodes the value into a buffer in LEB128 format 62 | // 63 | // See https://en.wikipedia.org/wiki/LEB128#Encode_unsigned_integer 64 | func EncodeUint64(value uint64) (buf []byte) { 65 | // This is effectively a do/while loop where we take 7 bits of the value and encode them until it is zero. 66 | for { 67 | // Take 7 remaining low-order bits from the value into b. 68 | b := uint8(value & 0x7f) 69 | value = value >> 7 70 | 71 | // If there are remaining bits, the value won't be zero: Set the high 72 | // order bit to tell the reader there are more bytes in this uint. 73 | if value != 0 { 74 | b |= 0x80 75 | } 76 | 77 | // Append b into the buffer 78 | buf = append(buf, b) 79 | if b&0x80 == 0 { 80 | return buf 81 | } 82 | } 83 | } 84 | 85 | func DecodeUint32(r *bytes.Reader) (ret uint32, bytesRead uint64, err error) { 86 | // Derived from https://github.com/golang/go/blob/aafad20b617ee63d58fcd4f6e0d98fe27760678c/src/encoding/binary/varint.go 87 | // with the modification on the overflow handling tailored for 32-bits. 88 | var s uint32 89 | for i := 0; i < maxVarintLen32; i++ { 90 | b, err := r.ReadByte() 91 | if err != nil { 92 | return 0, 0, err 93 | } 94 | if b < 0x80 { 95 | // Unused bits must be all zero. 96 | if i == maxVarintLen32-1 && (b&0xf0) > 0 { 97 | return 0, 0, errOverflow32 98 | } 99 | return ret | uint32(b)< 1 { 118 | return 0, 0, errOverflow64 119 | } 120 | return ret | uint64(b)< 5 { 146 | return 0, 0, errOverflow32 147 | } else if unused := b & 0b00110000; bytesRead == 5 && ret < 0 && unused != 0b00110000 { 148 | return 0, 0, errOverflow32 149 | } else if bytesRead == 5 && ret >= 0 && unused != 0x00 { 150 | return 0, 0, errOverflow32 151 | } 152 | return 153 | } 154 | } 155 | } 156 | 157 | // DecodeInt33AsInt64 is a special cased decoder for wasm.BlockType which is encoded as a positive signed integer, yet 158 | // still needs to fit the 32-bit range of allowed indices. Hence, this is 33, not 32-bit! 159 | // 160 | // See https://webassembly.github.io/spec/core/binary/instructions.html#control-instructions 161 | func DecodeInt33AsInt64(r *bytes.Reader) (ret int64, bytesRead uint64, err error) { 162 | const ( 163 | int33Mask int64 = 1 << 7 164 | int33Mask2 = ^int33Mask 165 | int33Mask3 = 1 << 6 166 | int33Mask4 = 8589934591 // 2^33-1 167 | int33Mask5 = 1 << 32 168 | int33Mask6 = int33Mask4 + 1 // 2^33 169 | ) 170 | var shift int 171 | var b int64 172 | var rb byte 173 | for shift < 35 { 174 | rb, err = r.ReadByte() 175 | if err != nil { 176 | return 0, 0, fmt.Errorf("readByte failed: %w", err) 177 | } 178 | b = int64(rb) 179 | ret |= (b & int33Mask2) << shift 180 | shift += 7 181 | bytesRead++ 182 | if b&int33Mask == 0 { 183 | break 184 | } 185 | } 186 | 187 | // fixme: can be optimized 188 | if shift < 33 && (b&int33Mask3) == int33Mask3 { 189 | ret |= int33Mask4 << shift 190 | } 191 | ret = ret & int33Mask4 192 | 193 | // if 33rd bit == 1, we translate it as a corresponding signed-33bit minus value 194 | if ret&int33Mask5 > 0 { 195 | ret = ret - int33Mask6 196 | } 197 | // Over flow checks. 198 | // fixme: can be optimized. 199 | if bytesRead > 5 { 200 | return 0, 0, errOverflow33 201 | } else if unused := b & 0b00100000; bytesRead == 5 && ret < 0 && unused != 0b00100000 { 202 | return 0, 0, errOverflow33 203 | } else if bytesRead == 5 && ret >= 0 && unused != 0x00 { 204 | return 0, 0, errOverflow33 205 | } 206 | return ret, bytesRead, nil 207 | } 208 | 209 | func DecodeInt64(r *bytes.Reader) (ret int64, bytesRead uint64, err error) { 210 | const ( 211 | int64Mask3 = 1 << 6 212 | int64Mask4 = ^0 213 | ) 214 | var shift int 215 | var b byte 216 | for { 217 | b, err = r.ReadByte() 218 | if err != nil { 219 | return 0, 0, fmt.Errorf("readByte failed: %w", err) 220 | } 221 | ret |= (int64(b) & 0x7f) << shift 222 | shift += 7 223 | bytesRead++ 224 | if b&0x80 == 0 { 225 | if shift < 64 && (b&int64Mask3) == int64Mask3 { 226 | ret |= int64Mask4 << shift 227 | } 228 | // Over flow checks. 229 | // fixme: can be optimized. 230 | if bytesRead > 10 { 231 | return 0, 0, errOverflow64 232 | } else if unused := b & 0b00111110; bytesRead == 10 && ret < 0 && unused != 0b00111110 { 233 | return 0, 0, errOverflow64 234 | } else if bytesRead == 10 && ret >= 0 && unused != 0x00 { 235 | return 0, 0, errOverflow64 236 | } 237 | return 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /binary/encoder_test.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/tetratelabs/wabin/leb128" 9 | "github.com/tetratelabs/wabin/wasm" 10 | ) 11 | 12 | func TestModule_Encode(t *testing.T) { 13 | i32, f32 := wasm.ValueTypeI32, wasm.ValueTypeF32 14 | zero := uint32(0) 15 | 16 | tests := []struct { 17 | name string 18 | input *wasm.Module 19 | expected []byte 20 | }{ 21 | { 22 | name: "empty", 23 | input: &wasm.Module{}, 24 | expected: append(Magic, version...), 25 | }, 26 | { 27 | name: "only name section", 28 | input: &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "simple"}}, 29 | expected: append(append(Magic, version...), 30 | wasm.SectionIDCustom, 0x0e, // 14 bytes in this section 31 | 0x04, 'n', 'a', 'm', 'e', 32 | subsectionIDModuleName, 0x07, // 7 bytes in this subsection 33 | 0x06, // the Module name simple is 6 bytes long 34 | 's', 'i', 'm', 'p', 'l', 'e'), 35 | }, 36 | { 37 | name: "type section", 38 | input: &wasm.Module{ 39 | TypeSection: []*wasm.FunctionType{ 40 | {}, 41 | {Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}, 42 | {Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}}, 43 | }, 44 | }, 45 | expected: append(append(Magic, version...), 46 | wasm.SectionIDType, 0x12, // 18 bytes in this section 47 | 0x03, // 3 types 48 | 0x60, 0x00, 0x00, // func=0x60 no param no result 49 | 0x60, 0x02, i32, i32, 0x01, i32, // func=0x60 2 params and 1 result 50 | 0x60, 0x04, i32, i32, i32, i32, 0x01, i32, // func=0x60 4 params and 1 result 51 | ), 52 | }, 53 | { 54 | name: "type and import section", 55 | input: &wasm.Module{ 56 | TypeSection: []*wasm.FunctionType{ 57 | {Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}, 58 | {Params: []wasm.ValueType{f32, f32}, Results: []wasm.ValueType{f32}}, 59 | }, 60 | ImportSection: []*wasm.Import{ 61 | { 62 | Module: "Math", Name: "Mul", 63 | Type: wasm.ExternTypeFunc, 64 | DescFunc: 1, 65 | }, { 66 | Module: "Math", Name: "Add", 67 | Type: wasm.ExternTypeFunc, 68 | DescFunc: 0, 69 | }, 70 | }, 71 | }, 72 | expected: append(append(Magic, version...), 73 | wasm.SectionIDType, 0x0d, // 13 bytes in this section 74 | 0x02, // 2 types 75 | 0x60, 0x02, i32, i32, 0x01, i32, // func=0x60 2 params and 1 result 76 | 0x60, 0x02, f32, f32, 0x01, f32, // func=0x60 2 params and 1 result 77 | wasm.SectionIDImport, 0x17, // 23 bytes in this section 78 | 0x02, // 2 imports 79 | 0x04, 'M', 'a', 't', 'h', 0x03, 'M', 'u', 'l', wasm.ExternTypeFunc, 80 | 0x01, // type index 81 | 0x04, 'M', 'a', 't', 'h', 0x03, 'A', 'd', 'd', wasm.ExternTypeFunc, 82 | 0x00, // type index 83 | ), 84 | }, 85 | { 86 | name: "type function and start section", 87 | input: &wasm.Module{ 88 | TypeSection: []*wasm.FunctionType{{}}, 89 | ImportSection: []*wasm.Import{{ 90 | Module: "", Name: "hello", 91 | Type: wasm.ExternTypeFunc, 92 | DescFunc: 0, 93 | }}, 94 | StartSection: &zero, 95 | }, 96 | expected: append(append(Magic, version...), 97 | wasm.SectionIDType, 0x04, // 4 bytes in this section 98 | 0x01, // 1 type 99 | 0x60, 0x0, 0x0, // func=0x60 0 params and 0 result 100 | wasm.SectionIDImport, 0x0a, // 10 bytes in this section 101 | 0x01, // 1 import 102 | 0x00, 0x05, 'h', 'e', 'l', 'l', 'o', wasm.ExternTypeFunc, 103 | 0x00, // type index 104 | wasm.SectionIDStart, 0x01, 105 | 0x00, // start function index 106 | ), 107 | }, 108 | { 109 | name: "table and memory section", 110 | input: &wasm.Module{ 111 | TableSection: []*wasm.Table{{Min: 3, Type: wasm.RefTypeFuncref}}, 112 | MemorySection: &wasm.Memory{Min: 1, Max: 1, IsMaxEncoded: true}, 113 | }, 114 | expected: append(append(Magic, version...), 115 | wasm.SectionIDTable, 0x04, // 4 bytes in this section 116 | 0x01, // 1 table 117 | wasm.RefTypeFuncref, 0x0, 0x03, // func, only min: 3 118 | wasm.SectionIDMemory, 0x04, // 4 bytes in this section 119 | 0x01, // 1 memory 120 | 0x01, 0x01, 0x01, // min and max = 1 121 | ), 122 | }, 123 | { 124 | name: "exported func with instructions", 125 | input: &wasm.Module{ 126 | TypeSection: []*wasm.FunctionType{ 127 | {Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}, 128 | }, 129 | FunctionSection: []wasm.Index{0}, 130 | CodeSection: []*wasm.Code{ 131 | {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeLocalGet, 1, wasm.OpcodeI32Add, wasm.OpcodeEnd}}, 132 | }, 133 | ExportSection: []*wasm.Export{ 134 | {Name: "AddInt", Type: wasm.ExternTypeFunc, Index: wasm.Index(0)}, 135 | }, 136 | NameSection: &wasm.NameSection{ 137 | FunctionNames: wasm.NameMap{{Index: wasm.Index(0), Name: "addInt"}}, 138 | LocalNames: wasm.IndirectNameMap{ 139 | {Index: wasm.Index(0), NameMap: wasm.NameMap{ 140 | {Index: wasm.Index(0), Name: "value_1"}, 141 | {Index: wasm.Index(1), Name: "value_2"}, 142 | }}, 143 | }, 144 | }, 145 | }, 146 | expected: append(append(Magic, version...), 147 | wasm.SectionIDType, 0x07, // 7 bytes in this section 148 | 0x01, // 1 type 149 | 0x60, 0x02, i32, i32, 0x01, i32, // func=0x60 2 params and 1 result 150 | wasm.SectionIDFunction, 0x02, // 2 bytes in this section 151 | 0x01, // 1 function 152 | 0x00, // func[0] type index 0 153 | wasm.SectionIDExport, 0x0a, // 10 bytes in this section 154 | 0x01, // 1 export 155 | 0x06, 'A', 'd', 'd', 'I', 'n', 't', // size of "AddInt", "AddInt" 156 | wasm.ExternTypeFunc, 0x00, // func[0] 157 | wasm.SectionIDCode, 0x09, // 9 bytes in this section 158 | 01, // one code section 159 | 07, // length of the body + locals 160 | 00, // count of local blocks 161 | wasm.OpcodeLocalGet, 0, // local.get 0 162 | wasm.OpcodeLocalGet, 1, // local.get 1 163 | wasm.OpcodeI32Add, // i32.add 164 | wasm.OpcodeEnd, // end of instructions/code 165 | wasm.SectionIDCustom, 0x27, // 39 bytes in this section 166 | 0x04, 'n', 'a', 'm', 'e', 167 | subsectionIDFunctionNames, 0x09, // 9 bytes 168 | 0x01, // two function names 169 | 0x00, 0x06, 'a', 'd', 'd', 'I', 'n', 't', // index 0, size of "addInt", "addInt" 170 | subsectionIDLocalNames, 0x15, // 21 bytes 171 | 0x01, // one function 172 | 0x00, 0x02, // index 0 has 2 locals 173 | 0x00, 0x07, 'v', 'a', 'l', 'u', 'e', '_', '1', // index 0, size of "value_1", "value_1" 174 | 0x01, 0x07, 'v', 'a', 'l', 'u', 'e', '_', '2', // index 1, size of "value_2", "value_2" 175 | ), 176 | }, 177 | { 178 | name: "exported global var", 179 | input: &wasm.Module{ 180 | GlobalSection: []*wasm.Global{ 181 | { 182 | Type: &wasm.GlobalType{ValType: i32, Mutable: true}, 183 | Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(0)}, 184 | }, 185 | }, 186 | ExportSection: []*wasm.Export{ 187 | {Name: "sp", Type: wasm.ExternTypeGlobal, Index: wasm.Index(0)}, 188 | }, 189 | }, 190 | expected: append(append(Magic, version...), 191 | wasm.SectionIDGlobal, 0x06, // 6 bytes in this section 192 | 0x01, wasm.ValueTypeI32, 0x01, // 1 global i32 mutable 193 | wasm.OpcodeI32Const, 0x00, wasm.OpcodeEnd, // arbitrary init to zero 194 | wasm.SectionIDExport, 0x06, // 6 bytes in this section 195 | 0x01, // 1 export 196 | 0x02, 's', 'p', // size of "sp", "sp" 197 | wasm.ExternTypeGlobal, 0x00, // global[0] 198 | ), 199 | }, 200 | } 201 | 202 | for _, tt := range tests { 203 | tc := tt 204 | 205 | t.Run(tc.name, func(t *testing.T) { 206 | bytes := EncodeModule(tc.input) 207 | require.Equal(t, tc.expected, bytes) 208 | }) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /leb128/leb128_test.go: -------------------------------------------------------------------------------- 1 | package leb128 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestEncode_DecodeInt32(t *testing.T) { 13 | for _, c := range []struct { 14 | input int32 15 | expected []byte 16 | }{ 17 | {input: -165675008, expected: []byte{0x80, 0x80, 0x80, 0xb1, 0x7f}}, 18 | {input: -624485, expected: []byte{0x9b, 0xf1, 0x59}}, 19 | {input: -16256, expected: []byte{0x80, 0x81, 0x7f}}, 20 | {input: -4, expected: []byte{0x7c}}, 21 | {input: -1, expected: []byte{0x7f}}, 22 | {input: 0, expected: []byte{0x00}}, 23 | {input: 1, expected: []byte{0x01}}, 24 | {input: 4, expected: []byte{0x04}}, 25 | {input: 16256, expected: []byte{0x80, 0xff, 0x0}}, 26 | {input: 624485, expected: []byte{0xe5, 0x8e, 0x26}}, 27 | {input: 165675008, expected: []byte{0x80, 0x80, 0x80, 0xcf, 0x0}}, 28 | {input: int32(math.MaxInt32), expected: []byte{0xff, 0xff, 0xff, 0xff, 0x7}}, 29 | } { 30 | require.Equal(t, c.expected, EncodeInt32(c.input)) 31 | decoded, _, err := DecodeInt32(bytes.NewReader(c.expected)) 32 | require.NoError(t, err) 33 | require.Equal(t, c.input, decoded) 34 | } 35 | } 36 | 37 | func TestEncode_DecodeInt64(t *testing.T) { 38 | for _, c := range []struct { 39 | input int64 40 | expected []byte 41 | }{ 42 | {input: -math.MaxInt32, expected: []byte{0x81, 0x80, 0x80, 0x80, 0x78}}, 43 | {input: -165675008, expected: []byte{0x80, 0x80, 0x80, 0xb1, 0x7f}}, 44 | {input: -624485, expected: []byte{0x9b, 0xf1, 0x59}}, 45 | {input: -16256, expected: []byte{0x80, 0x81, 0x7f}}, 46 | {input: -4, expected: []byte{0x7c}}, 47 | {input: -1, expected: []byte{0x7f}}, 48 | {input: 0, expected: []byte{0x00}}, 49 | {input: 1, expected: []byte{0x01}}, 50 | {input: 4, expected: []byte{0x04}}, 51 | {input: 16256, expected: []byte{0x80, 0xff, 0x0}}, 52 | {input: 624485, expected: []byte{0xe5, 0x8e, 0x26}}, 53 | {input: 165675008, expected: []byte{0x80, 0x80, 0x80, 0xcf, 0x0}}, 54 | {input: math.MaxInt32, expected: []byte{0xff, 0xff, 0xff, 0xff, 0x7}}, 55 | {input: math.MaxInt64, expected: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0}}, 56 | } { 57 | require.Equal(t, c.expected, EncodeInt64(c.input)) 58 | decoded, _, err := DecodeInt64(bytes.NewReader(c.expected)) 59 | require.NoError(t, err) 60 | require.Equal(t, c.input, decoded) 61 | } 62 | } 63 | 64 | func TestEncodeUint32(t *testing.T) { 65 | for _, c := range []struct { 66 | input uint32 67 | expected []byte 68 | }{ 69 | {input: 0, expected: []byte{0x00}}, 70 | {input: 1, expected: []byte{0x01}}, 71 | {input: 4, expected: []byte{0x04}}, 72 | {input: 16256, expected: []byte{0x80, 0x7f}}, 73 | {input: 624485, expected: []byte{0xe5, 0x8e, 0x26}}, 74 | {input: 165675008, expected: []byte{0x80, 0x80, 0x80, 0x4f}}, 75 | {input: uint32(math.MaxUint32), expected: []byte{0xff, 0xff, 0xff, 0xff, 0xf}}, 76 | } { 77 | require.Equal(t, c.expected, EncodeUint32(c.input)) 78 | } 79 | } 80 | 81 | func TestEncodeUint64(t *testing.T) { 82 | for _, c := range []struct { 83 | input uint64 84 | expected []byte 85 | }{ 86 | {input: 0, expected: []byte{0x00}}, 87 | {input: 1, expected: []byte{0x01}}, 88 | {input: 4, expected: []byte{0x04}}, 89 | {input: 16256, expected: []byte{0x80, 0x7f}}, 90 | {input: 624485, expected: []byte{0xe5, 0x8e, 0x26}}, 91 | {input: 165675008, expected: []byte{0x80, 0x80, 0x80, 0x4f}}, 92 | {input: math.MaxUint32, expected: []byte{0xff, 0xff, 0xff, 0xff, 0xf}}, 93 | {input: math.MaxUint64, expected: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1}}, 94 | } { 95 | require.Equal(t, c.expected, EncodeUint64(c.input)) 96 | } 97 | } 98 | 99 | func TestDecodeUint32(t *testing.T) { 100 | for _, c := range []struct { 101 | bytes []byte 102 | exp uint32 103 | expErr bool 104 | }{ 105 | {bytes: []byte{0xff, 0xff, 0xff, 0xff, 0xf}, exp: 0xffffffff}, 106 | {bytes: []byte{0x00}, exp: 0}, 107 | {bytes: []byte{0x04}, exp: 4}, 108 | {bytes: []byte{0x01}, exp: 1}, 109 | {bytes: []byte{0x80, 0}, exp: 0}, 110 | {bytes: []byte{0x80, 0x7f}, exp: 16256}, 111 | {bytes: []byte{0xe5, 0x8e, 0x26}, exp: 624485}, 112 | {bytes: []byte{0x80, 0x80, 0x80, 0x4f}, exp: 165675008}, 113 | {bytes: []byte{0xff, 0xff, 0xff, 0xff, 0xf}, exp: math.MaxUint32}, 114 | {bytes: []byte{0x83, 0x80, 0x80, 0x80, 0x80, 0x00}, expErr: true}, 115 | {bytes: []byte{0x82, 0x80, 0x80, 0x80, 0x70}, expErr: true}, 116 | {bytes: []byte{0x80, 0x80, 0x80, 0x80, 0x80, 0x00}, expErr: true}, 117 | } { 118 | actual, num, err := DecodeUint32(bytes.NewReader(c.bytes)) 119 | if c.expErr { 120 | require.Error(t, err) 121 | } else { 122 | require.NoError(t, err) 123 | require.Equal(t, c.exp, actual) 124 | require.Equal(t, uint64(len(c.bytes)), num) 125 | } 126 | } 127 | } 128 | 129 | func TestDecodeUint64(t *testing.T) { 130 | for _, c := range []struct { 131 | bytes []byte 132 | exp uint64 133 | expErr bool 134 | }{ 135 | {bytes: []byte{0x04}, exp: 4}, 136 | {bytes: []byte{0x80, 0x7f}, exp: 16256}, 137 | {bytes: []byte{0xe5, 0x8e, 0x26}, exp: 624485}, 138 | {bytes: []byte{0x80, 0x80, 0x80, 0x4f}, exp: 165675008}, 139 | {bytes: []byte{0xff, 0xff, 0xff, 0xff, 0xf}, exp: math.MaxUint32}, 140 | {bytes: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1}, exp: math.MaxUint64}, 141 | {bytes: []byte{0x89, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x71}, expErr: true}, 142 | } { 143 | actual, num, err := DecodeUint64(bytes.NewReader(c.bytes)) 144 | if c.expErr { 145 | require.Error(t, err) 146 | } else { 147 | require.NoError(t, err) 148 | require.Equal(t, c.exp, actual) 149 | require.Equal(t, uint64(len(c.bytes)), num) 150 | } 151 | } 152 | } 153 | 154 | func TestDecodeInt32(t *testing.T) { 155 | for i, c := range []struct { 156 | bytes []byte 157 | exp int32 158 | expErr bool 159 | }{ 160 | {bytes: []byte{0x13}, exp: 19}, 161 | {bytes: []byte{0x00}, exp: 0}, 162 | {bytes: []byte{0x04}, exp: 4}, 163 | {bytes: []byte{0xFF, 0x00}, exp: 127}, 164 | {bytes: []byte{0x81, 0x01}, exp: 129}, 165 | {bytes: []byte{0x7f}, exp: -1}, 166 | {bytes: []byte{0x81, 0x7f}, exp: -127}, 167 | {bytes: []byte{0xFF, 0x7e}, exp: -129}, 168 | {bytes: []byte{0xff, 0xff, 0xff, 0xff, 0x0f}, expErr: true}, 169 | {bytes: []byte{0xff, 0xff, 0xff, 0xff, 0x4f}, expErr: true}, 170 | {bytes: []byte{0x80, 0x80, 0x80, 0x80, 0x70}, expErr: true}, 171 | } { 172 | actual, num, err := DecodeInt32(bytes.NewReader(c.bytes)) 173 | if c.expErr { 174 | require.Error(t, err, fmt.Sprintf("%d-th got value %d", i, actual)) 175 | } else { 176 | require.NoError(t, err, i) 177 | require.Equal(t, c.exp, actual, i) 178 | require.Equal(t, uint64(len(c.bytes)), num, i) 179 | } 180 | } 181 | } 182 | 183 | func TestDecodeInt33AsInt64(t *testing.T) { 184 | for _, c := range []struct { 185 | bytes []byte 186 | exp int64 187 | }{ 188 | {bytes: []byte{0x00}, exp: 0}, 189 | {bytes: []byte{0x04}, exp: 4}, 190 | {bytes: []byte{0x40}, exp: -64}, 191 | {bytes: []byte{0x7f}, exp: -1}, 192 | {bytes: []byte{0x7e}, exp: -2}, 193 | {bytes: []byte{0x7d}, exp: -3}, 194 | {bytes: []byte{0x7c}, exp: -4}, 195 | {bytes: []byte{0xFF, 0x00}, exp: 127}, 196 | {bytes: []byte{0x81, 0x01}, exp: 129}, 197 | {bytes: []byte{0x7f}, exp: -1}, 198 | {bytes: []byte{0x81, 0x7f}, exp: -127}, 199 | {bytes: []byte{0xFF, 0x7e}, exp: -129}, 200 | } { 201 | actual, num, err := DecodeInt33AsInt64(bytes.NewReader(c.bytes)) 202 | require.NoError(t, err) 203 | require.Equal(t, c.exp, actual) 204 | require.Equal(t, uint64(len(c.bytes)), num) 205 | } 206 | } 207 | 208 | func TestDecodeInt64(t *testing.T) { 209 | for _, c := range []struct { 210 | bytes []byte 211 | exp int64 212 | }{ 213 | {bytes: []byte{0x00}, exp: 0}, 214 | {bytes: []byte{0x04}, exp: 4}, 215 | {bytes: []byte{0xFF, 0x00}, exp: 127}, 216 | {bytes: []byte{0x81, 0x01}, exp: 129}, 217 | {bytes: []byte{0x7f}, exp: -1}, 218 | {bytes: []byte{0x81, 0x7f}, exp: -127}, 219 | {bytes: []byte{0xFF, 0x7e}, exp: -129}, 220 | {bytes: []byte{0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x7f}, 221 | exp: -9223372036854775808}, 222 | } { 223 | actual, num, err := DecodeInt64(bytes.NewReader(c.bytes)) 224 | require.NoError(t, err) 225 | require.Equal(t, c.exp, actual) 226 | require.Equal(t, uint64(len(c.bytes)), num) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /binary/names.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/tetratelabs/wabin/leb128" 9 | "github.com/tetratelabs/wabin/wasm" 10 | ) 11 | 12 | const ( 13 | // subsectionIDModuleName contains only the module name. 14 | subsectionIDModuleName = uint8(0) 15 | // subsectionIDFunctionNames is a map of indices to function names, in ascending order by function index 16 | subsectionIDFunctionNames = uint8(1) 17 | // subsectionIDLocalNames contain a map of function indices to a map of local indices to their names, in ascending 18 | // order by function and local index 19 | subsectionIDLocalNames = uint8(2) 20 | ) 21 | 22 | // decodeNameSection deserializes the data associated with the "name" key in SectionIDCustom according to the 23 | // standard: 24 | // 25 | // * ModuleName decode from subsection 0 26 | // * FunctionNames decode from subsection 1 27 | // * LocalNames decode from subsection 2 28 | // 29 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-namesec 30 | func decodeNameSection(r *bytes.Reader, limit uint64) (result *wasm.NameSection, err error) { 31 | // TODO: add leb128 functions that work on []byte and offset. While using a reader allows us to reuse reader-based 32 | // leb128 functions, it is less efficient, causes untestable code and in some cases more complex vs plain []byte. 33 | result = &wasm.NameSection{} 34 | 35 | // subsectionID is decoded if known, and skipped if not 36 | var subsectionID uint8 37 | // subsectionSize is the length to skip when the subsectionID is unknown 38 | var subsectionSize uint32 39 | var bytesRead uint64 40 | for limit > 0 { 41 | if subsectionID, err = r.ReadByte(); err != nil { 42 | if err == io.EOF { 43 | return result, nil 44 | } 45 | // TODO: untestable as this can't fail for a reason beside EOF reading a byte from a buffer 46 | return nil, fmt.Errorf("failed to read a subsection ID: %w", err) 47 | } 48 | limit-- 49 | 50 | if subsectionSize, bytesRead, err = leb128.DecodeUint32(r); err != nil { 51 | return nil, fmt.Errorf("failed to read the size of subsection[%d]: %w", subsectionID, err) 52 | } 53 | limit -= bytesRead 54 | 55 | switch subsectionID { 56 | case subsectionIDModuleName: 57 | if result.ModuleName, _, err = decodeUTF8(r, "module name"); err != nil { 58 | return nil, err 59 | } 60 | case subsectionIDFunctionNames: 61 | if result.FunctionNames, err = decodeFunctionNames(r); err != nil { 62 | return nil, err 63 | } 64 | case subsectionIDLocalNames: 65 | if result.LocalNames, err = decodeLocalNames(r); err != nil { 66 | return nil, err 67 | } 68 | default: // Skip other subsections. 69 | // Note: Not Seek because it doesn't err when given an offset past EOF. Rather, it leads to undefined state. 70 | if _, err = io.CopyN(io.Discard, r, int64(subsectionSize)); err != nil { 71 | return nil, fmt.Errorf("failed to skip subsection[%d]: %w", subsectionID, err) 72 | } 73 | } 74 | limit -= uint64(subsectionSize) 75 | } 76 | return 77 | } 78 | 79 | func decodeFunctionNames(r *bytes.Reader) (wasm.NameMap, error) { 80 | functionCount, err := decodeFunctionCount(r, subsectionIDFunctionNames) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | result := make(wasm.NameMap, functionCount) 86 | for i := uint32(0); i < functionCount; i++ { 87 | functionIndex, err := decodeFunctionIndex(r, subsectionIDFunctionNames) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | name, _, err := decodeUTF8(r, "function[%d] name", functionIndex) 93 | if err != nil { 94 | return nil, err 95 | } 96 | result[i] = &wasm.NameAssoc{Index: functionIndex, Name: name} 97 | } 98 | return result, nil 99 | } 100 | 101 | func decodeLocalNames(r *bytes.Reader) (wasm.IndirectNameMap, error) { 102 | functionCount, err := decodeFunctionCount(r, subsectionIDLocalNames) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | result := make(wasm.IndirectNameMap, functionCount) 108 | for i := uint32(0); i < functionCount; i++ { 109 | functionIndex, err := decodeFunctionIndex(r, subsectionIDLocalNames) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | localCount, _, err := leb128.DecodeUint32(r) 115 | if err != nil { 116 | return nil, fmt.Errorf("failed to read the local count for function[%d]: %w", functionIndex, err) 117 | } 118 | 119 | locals := make(wasm.NameMap, localCount) 120 | for j := uint32(0); j < localCount; j++ { 121 | localIndex, _, err := leb128.DecodeUint32(r) 122 | if err != nil { 123 | return nil, fmt.Errorf("failed to read a local index of function[%d]: %w", functionIndex, err) 124 | } 125 | 126 | name, _, err := decodeUTF8(r, "function[%d] local[%d] name", functionIndex, localIndex) 127 | if err != nil { 128 | return nil, err 129 | } 130 | locals[j] = &wasm.NameAssoc{Index: localIndex, Name: name} 131 | } 132 | result[i] = &wasm.NameMapAssoc{Index: functionIndex, NameMap: locals} 133 | } 134 | return result, nil 135 | } 136 | 137 | func decodeFunctionIndex(r *bytes.Reader, subsectionID uint8) (uint32, error) { 138 | functionIndex, _, err := leb128.DecodeUint32(r) 139 | if err != nil { 140 | return 0, fmt.Errorf("failed to read a function index in subsection[%d]: %w", subsectionID, err) 141 | } 142 | return functionIndex, nil 143 | } 144 | 145 | func decodeFunctionCount(r *bytes.Reader, subsectionID uint8) (uint32, error) { 146 | functionCount, _, err := leb128.DecodeUint32(r) 147 | if err != nil { 148 | return 0, fmt.Errorf("failed to read the function count of subsection[%d]: %w", subsectionID, err) 149 | } 150 | return functionCount, nil 151 | } 152 | 153 | // encodeNameSectionData serializes the data for the "name" key in wasm.SectionIDCustom according to the 154 | // standard: 155 | // 156 | // Note: The result can be nil because this does not encode empty subsections 157 | // 158 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-namesec 159 | func encodeNameSectionData(n *wasm.NameSection) (data []byte) { 160 | if n.ModuleName != "" { 161 | data = append(data, encodeNameSubsection(subsectionIDModuleName, encodeSizePrefixed([]byte(n.ModuleName)))...) 162 | } 163 | if fd := encodeFunctionNameData(n); len(fd) > 0 { 164 | data = append(data, encodeNameSubsection(subsectionIDFunctionNames, fd)...) 165 | } 166 | if ld := encodeLocalNameData(n); len(ld) > 0 { 167 | data = append(data, encodeNameSubsection(subsectionIDLocalNames, ld)...) 168 | } 169 | return 170 | } 171 | 172 | // encodeFunctionNameData encodes the data for the function name subsection. 173 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-funcnamesec 174 | func encodeFunctionNameData(n *wasm.NameSection) []byte { 175 | if len(n.FunctionNames) == 0 { 176 | return nil 177 | } 178 | 179 | return encodeNameMap(n.FunctionNames) 180 | } 181 | 182 | func encodeNameMap(m wasm.NameMap) []byte { 183 | count := uint32(len(m)) 184 | data := leb128.EncodeUint32(count) 185 | for _, na := range m { 186 | data = append(data, encodeNameAssoc(na)...) 187 | } 188 | return data 189 | } 190 | 191 | // encodeLocalNameData encodes the data for the local name subsection. 192 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-localnamesec 193 | func encodeLocalNameData(n *wasm.NameSection) []byte { 194 | if len(n.LocalNames) == 0 { 195 | return nil 196 | } 197 | 198 | funcNameCount := uint32(len(n.LocalNames)) 199 | subsection := leb128.EncodeUint32(funcNameCount) 200 | 201 | for _, na := range n.LocalNames { 202 | locals := encodeNameMap(na.NameMap) 203 | subsection = append(subsection, append(leb128.EncodeUint32(na.Index), locals...)...) 204 | } 205 | return subsection 206 | } 207 | 208 | // encodeNameSubsection returns a buffer encoding the given subsection 209 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#subsections%E2%91%A0 210 | func encodeNameSubsection(subsectionID uint8, content []byte) []byte { 211 | contentSizeInBytes := leb128.EncodeUint32(uint32(len(content))) 212 | result := []byte{subsectionID} 213 | result = append(result, contentSizeInBytes...) 214 | result = append(result, content...) 215 | return result 216 | } 217 | 218 | // encodeNameAssoc encodes the index and data prefixed by their size. 219 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-namemap 220 | func encodeNameAssoc(na *wasm.NameAssoc) []byte { 221 | return append(leb128.EncodeUint32(na.Index), encodeSizePrefixed([]byte(na.Name))...) 222 | } 223 | 224 | // encodeSizePrefixed encodes the data prefixed by their size. 225 | func encodeSizePrefixed(data []byte) []byte { 226 | size := leb128.EncodeUint32(uint32(len(data))) 227 | return append(size, data...) 228 | } 229 | -------------------------------------------------------------------------------- /wasm/counts_test.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/tetratelabs/wabin/leb128" 9 | ) 10 | 11 | func TestModule_ImportFuncCount(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | input *Module 15 | expected uint32 16 | }{ 17 | { 18 | name: "none", 19 | input: &Module{}, 20 | }, 21 | { 22 | name: "none with function section", 23 | input: &Module{FunctionSection: []Index{0}}, 24 | }, 25 | { 26 | name: "one", 27 | input: &Module{ImportSection: []*Import{{Type: ExternTypeFunc}}}, 28 | expected: 1, 29 | }, 30 | { 31 | name: "one with function section", 32 | input: &Module{ImportSection: []*Import{{Type: ExternTypeFunc}}, FunctionSection: []Index{0}}, 33 | expected: 1, 34 | }, 35 | { 36 | name: "one with other imports", 37 | input: &Module{ImportSection: []*Import{{Type: ExternTypeFunc}, {Type: ExternTypeMemory}}}, 38 | expected: 1, 39 | }, 40 | { 41 | name: "two", 42 | input: &Module{ImportSection: []*Import{{Type: ExternTypeFunc}, {Type: ExternTypeFunc}}}, 43 | expected: 2, 44 | }, 45 | } 46 | 47 | for _, tt := range tests { 48 | tc := tt 49 | 50 | t.Run(tc.name, func(t *testing.T) { 51 | require.Equal(t, tc.expected, tc.input.ImportFuncCount()) 52 | }) 53 | } 54 | } 55 | 56 | func TestModule_ImportTableCount(t *testing.T) { 57 | tests := []struct { 58 | name string 59 | input *Module 60 | expected uint32 61 | }{ 62 | { 63 | name: "none", 64 | input: &Module{}, 65 | }, 66 | { 67 | name: "none with table section", 68 | input: &Module{TableSection: []*Table{{Min: 1, Max: nil}}}, 69 | }, 70 | { 71 | name: "one", 72 | input: &Module{ImportSection: []*Import{{Type: ExternTypeTable}}}, 73 | expected: 1, 74 | }, 75 | { 76 | name: "one with table section", 77 | input: &Module{ 78 | ImportSection: []*Import{{Type: ExternTypeTable}}, 79 | TableSection: []*Table{{Min: 1, Max: nil}}, 80 | }, 81 | expected: 1, 82 | }, 83 | { 84 | name: "one with other imports", 85 | input: &Module{ImportSection: []*Import{{Type: ExternTypeTable}, {Type: ExternTypeMemory}}}, 86 | expected: 1, 87 | }, 88 | { 89 | name: "two", 90 | input: &Module{ImportSection: []*Import{{Type: ExternTypeTable}, {Type: ExternTypeTable}}}, 91 | expected: 2, 92 | }, 93 | } 94 | 95 | for _, tt := range tests { 96 | tc := tt 97 | 98 | t.Run(tc.name, func(t *testing.T) { 99 | require.Equal(t, tc.expected, tc.input.ImportTableCount()) 100 | }) 101 | } 102 | } 103 | 104 | // TODO: once we fix up-front validation, this only needs to check zero or one 105 | func TestModule_ImportMemoryCount(t *testing.T) { 106 | tests := []struct { 107 | name string 108 | input *Module 109 | expected uint32 110 | }{ 111 | { 112 | name: "none", 113 | input: &Module{}, 114 | }, 115 | { 116 | name: "none with memory section", 117 | input: &Module{MemorySection: &Memory{Min: 1}}, 118 | }, 119 | { 120 | name: "one", 121 | input: &Module{ImportSection: []*Import{{Type: ExternTypeMemory}}}, 122 | expected: 1, 123 | }, 124 | { 125 | name: "one with memory section", 126 | input: &Module{ 127 | ImportSection: []*Import{{Type: ExternTypeMemory}}, 128 | MemorySection: &Memory{Min: 1}, 129 | }, 130 | expected: 1, 131 | }, 132 | { 133 | name: "one with other imports", 134 | input: &Module{ImportSection: []*Import{{Type: ExternTypeMemory}, {Type: ExternTypeTable}}}, 135 | expected: 1, 136 | }, 137 | { 138 | name: "two", 139 | input: &Module{ImportSection: []*Import{{Type: ExternTypeMemory}, {Type: ExternTypeMemory}}}, 140 | expected: 2, 141 | }, 142 | } 143 | 144 | for _, tt := range tests { 145 | tc := tt 146 | 147 | t.Run(tc.name, func(t *testing.T) { 148 | require.Equal(t, tc.expected, tc.input.ImportMemoryCount()) 149 | }) 150 | } 151 | } 152 | 153 | func TestModule_ImportGlobalCount(t *testing.T) { 154 | tests := []struct { 155 | name string 156 | input *Module 157 | expected uint32 158 | }{ 159 | { 160 | name: "none", 161 | input: &Module{}, 162 | }, 163 | { 164 | name: "none with global section", 165 | input: &Module{GlobalSection: []*Global{{Type: &GlobalType{ValType: ValueTypeI64}}}}, 166 | }, 167 | { 168 | name: "one", 169 | input: &Module{ImportSection: []*Import{{Type: ExternTypeGlobal}}}, 170 | expected: 1, 171 | }, 172 | { 173 | name: "one with global section", 174 | input: &Module{ 175 | ImportSection: []*Import{{Type: ExternTypeGlobal}}, 176 | GlobalSection: []*Global{{Type: &GlobalType{ValType: ValueTypeI64}}}, 177 | }, 178 | expected: 1, 179 | }, 180 | { 181 | name: "one with other imports", 182 | input: &Module{ImportSection: []*Import{{Type: ExternTypeGlobal}, {Type: ExternTypeMemory}}}, 183 | expected: 1, 184 | }, 185 | { 186 | name: "two", 187 | input: &Module{ImportSection: []*Import{{Type: ExternTypeGlobal}, {Type: ExternTypeGlobal}}}, 188 | expected: 2, 189 | }, 190 | } 191 | 192 | for _, tt := range tests { 193 | tc := tt 194 | 195 | t.Run(tc.name, func(t *testing.T) { 196 | require.Equal(t, tc.expected, tc.input.ImportGlobalCount()) 197 | }) 198 | } 199 | } 200 | 201 | var const0 = leb128.EncodeInt32(0) 202 | 203 | func TestModule_SectionElementCount(t *testing.T) { 204 | i32, f32 := ValueTypeI32, ValueTypeF32 205 | zero := uint32(0) 206 | empty := &ConstantExpression{Opcode: OpcodeI32Const, Data: const0} 207 | 208 | tests := []struct { 209 | name string 210 | input *Module 211 | expected map[string]uint32 212 | }{ 213 | { 214 | name: "empty", 215 | input: &Module{}, 216 | expected: map[string]uint32{}, 217 | }, 218 | { 219 | name: "NameSection", 220 | input: &Module{NameSection: &NameSection{ModuleName: "simple"}}, 221 | expected: map[string]uint32{"custom": 1}, 222 | }, 223 | { 224 | name: "TypeSection", 225 | input: &Module{ 226 | TypeSection: []*FunctionType{ 227 | {}, 228 | {Params: []ValueType{i32, i32}, Results: []ValueType{i32}}, 229 | {Params: []ValueType{i32, i32, i32, i32}, Results: []ValueType{i32}}, 230 | }, 231 | }, 232 | expected: map[string]uint32{"type": 3}, 233 | }, 234 | { 235 | name: "TypeSection and ImportSection", 236 | input: &Module{ 237 | TypeSection: []*FunctionType{ 238 | {Params: []ValueType{i32, i32}, Results: []ValueType{i32}}, 239 | {Params: []ValueType{f32, f32}, Results: []ValueType{f32}}, 240 | }, 241 | ImportSection: []*Import{ 242 | { 243 | Module: "Math", Name: "Mul", 244 | Type: ExternTypeFunc, 245 | DescFunc: 1, 246 | }, { 247 | Module: "Math", Name: "Add", 248 | Type: ExternTypeFunc, 249 | DescFunc: 0, 250 | }, 251 | }, 252 | }, 253 | expected: map[string]uint32{"import": 2, "type": 2}, 254 | }, 255 | { 256 | name: "TypeSection, FunctionSection, CodeSection, ExportSection and StartSection", 257 | input: &Module{ 258 | TypeSection: []*FunctionType{{}}, 259 | FunctionSection: []Index{0}, 260 | CodeSection: []*Code{ 261 | {Body: []byte{OpcodeLocalGet, 0, OpcodeLocalGet, 1, OpcodeI32Add, OpcodeEnd}}, 262 | }, 263 | ExportSection: []*Export{ 264 | {Name: "AddInt", Type: ExternTypeFunc, Index: Index(0)}, 265 | }, 266 | StartSection: &zero, 267 | }, 268 | expected: map[string]uint32{"code": 1, "export": 1, "function": 1, "start": 1, "type": 1}, 269 | }, 270 | { 271 | name: "MemorySection and DataSection", 272 | input: &Module{ 273 | MemorySection: &Memory{Min: 1}, 274 | DataSection: []*DataSegment{{OffsetExpression: empty}}, 275 | }, 276 | expected: map[string]uint32{"data": 1, "memory": 1}, 277 | }, 278 | { 279 | name: "TableSection and ElementSection", 280 | input: &Module{ 281 | TableSection: []*Table{{Min: 1}}, 282 | ElementSection: []*ElementSegment{{OffsetExpr: empty}}, 283 | }, 284 | expected: map[string]uint32{"element": 1, "table": 1}, 285 | }, 286 | { 287 | name: "TableSection (multiple tables) and ElementSection", 288 | input: &Module{ 289 | TableSection: []*Table{{Min: 1}, {Min: 2}}, 290 | ElementSection: []*ElementSegment{{OffsetExpr: empty}}, 291 | }, 292 | expected: map[string]uint32{"element": 1, "table": 2}, 293 | }, 294 | } 295 | 296 | for _, tt := range tests { 297 | tc := tt 298 | 299 | t.Run(tc.name, func(t *testing.T) { 300 | actual := map[string]uint32{} 301 | for i := SectionID(0); i <= SectionIDData; i++ { 302 | if size := tc.input.SectionElementCount(i); size > 0 { 303 | actual[SectionIDName(i)] = size 304 | } 305 | } 306 | require.Equal(t, tc.expected, actual) 307 | }) 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /wasm/features.go: -------------------------------------------------------------------------------- 1 | package wasm 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // CoreFeatures is a bit flag of WebAssembly Core specification features. See 9 | // https://github.com/WebAssembly/proposals for proposals and their status. 10 | // 11 | // Constants define individual features, such as CoreFeatureMultiValue, or 12 | // groups of "finished" features, assigned to a WebAssembly Core Specification 13 | // version, ex. CoreFeaturesV1 or CoreFeaturesV2. 14 | // 15 | // Note: Numeric values are not intended to be interpreted except as bit flags. 16 | type CoreFeatures uint64 17 | 18 | // CoreFeaturesV1 are features included in the WebAssembly Core Specification 19 | // 1.0. As of late 2022, this is the only version that is a Web Standard (W3C 20 | // Recommendation). 21 | // 22 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/ 23 | const CoreFeaturesV1 = CoreFeatureMutableGlobal 24 | 25 | // CoreFeaturesV2 are features included in the WebAssembly Core Specification 26 | // 2.0 (20220419). As of late 2022, version 2.0 is a W3C working draft, not yet 27 | // a Web Standard (W3C Recommendation). 28 | // 29 | // See https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/appendix/changes.html#release-1-1 30 | const CoreFeaturesV2 = CoreFeaturesV1 | 31 | CoreFeatureBulkMemoryOperations | 32 | CoreFeatureMultiValue | 33 | CoreFeatureNonTrappingFloatToIntConversion | 34 | CoreFeatureReferenceTypes | 35 | CoreFeatureSignExtensionOps | 36 | CoreFeatureSIMD 37 | 38 | const ( 39 | // CoreFeatureBulkMemoryOperations adds instructions modify ranges of 40 | // memory or table entries ("bulk-memory-operations"). This is included in 41 | // CoreFeaturesV2, but not CoreFeaturesV1. 42 | // 43 | // Here are the notable effects: 44 | // - Adds `memory.fill`, `memory.init`, `memory.copy` and `data.drop` 45 | // instructions. 46 | // - Adds `table.init`, `table.copy` and `elem.drop` instructions. 47 | // - Introduces a "passive" form of element and data segments. 48 | // - Stops checking "active" element and data segment boundaries at 49 | // compile-time, meaning they can error at runtime. 50 | // 51 | // Note: "bulk-memory-operations" is mixed with the "reference-types" 52 | // proposal due to the WebAssembly Working Group merging them 53 | // "mutually dependent". Therefore, enabling this feature requires enabling 54 | // CoreFeatureReferenceTypes, and vice-versa. 55 | // 56 | // See https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/bulk-memory-operations/Overview.md 57 | // https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/reference-types/Overview.md and 58 | // https://github.com/WebAssembly/spec/pull/1287 59 | CoreFeatureBulkMemoryOperations CoreFeatures = 1 << iota 60 | 61 | // CoreFeatureMultiValue enables multiple values ("multi-value"). This is 62 | // included in CoreFeaturesV2, but not CoreFeaturesV1. 63 | // 64 | // Here are the notable effects: 65 | // - Function (`func`) types allow more than one result. 66 | // - Block types (`block`, `loop` and `if`) can be arbitrary function 67 | // types. 68 | // 69 | // See https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/multi-value/Overview.md 70 | CoreFeatureMultiValue 71 | 72 | // CoreFeatureMutableGlobal allows globals to be mutable. This is included 73 | // in both CoreFeaturesV1 and CoreFeaturesV2. 74 | // 75 | // When false, an api.Global can never be cast to an api.MutableGlobal, and 76 | // any wasm that includes global vars will fail to parse. 77 | CoreFeatureMutableGlobal 78 | 79 | // CoreFeatureNonTrappingFloatToIntConversion enables non-trapping 80 | // float-to-int conversions ("nontrapping-float-to-int-conversion"). This 81 | // is included in CoreFeaturesV2, but not CoreFeaturesV1. 82 | // 83 | // The only effect of enabling is allowing the following instructions, 84 | // which return 0 on NaN instead of panicking. 85 | // - `i32.trunc_sat_f32_s` 86 | // - `i32.trunc_sat_f32_u` 87 | // - `i32.trunc_sat_f64_s` 88 | // - `i32.trunc_sat_f64_u` 89 | // - `i64.trunc_sat_f32_s` 90 | // - `i64.trunc_sat_f32_u` 91 | // - `i64.trunc_sat_f64_s` 92 | // - `i64.trunc_sat_f64_u` 93 | // 94 | // See https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/nontrapping-float-to-int-conversion/Overview.md 95 | CoreFeatureNonTrappingFloatToIntConversion 96 | 97 | // CoreFeatureReferenceTypes enables various instructions and features 98 | // related to table and new reference types. This is included in 99 | // CoreFeaturesV2, but not CoreFeaturesV1. 100 | // 101 | // - Introduction of new value types: `funcref` and `externref`. 102 | // - Support for the following new instructions: 103 | // - `ref.null` 104 | // - `ref.func` 105 | // - `ref.is_null` 106 | // - `table.fill` 107 | // - `table.get` 108 | // - `table.grow` 109 | // - `table.set` 110 | // - `table.size` 111 | // - Support for multiple tables per module: 112 | // - `call_indirect`, `table.init`, `table.copy` and `elem.drop` 113 | // - Support for instructions can take non-zero table index. 114 | // - Element segments can take non-zero table index. 115 | // 116 | // Note: "reference-types" is mixed with the "bulk-memory-operations" 117 | // proposal due to the WebAssembly Working Group merging them 118 | // "mutually dependent". Therefore, enabling this feature requires enabling 119 | // CoreFeatureBulkMemoryOperations, and vice-versa. 120 | // 121 | // See https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/bulk-memory-operations/Overview.md 122 | // https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/reference-types/Overview.md and 123 | // https://github.com/WebAssembly/spec/pull/1287 124 | CoreFeatureReferenceTypes 125 | 126 | // CoreFeatureSignExtensionOps enables sign extension instructions 127 | // ("sign-extension-ops"). This is included in CoreFeaturesV2, but not 128 | // CoreFeaturesV1. 129 | // 130 | // Adds instructions: 131 | // - `i32.extend8_s` 132 | // - `i32.extend16_s` 133 | // - `i64.extend8_s` 134 | // - `i64.extend16_s` 135 | // - `i64.extend32_s` 136 | // 137 | // See https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/sign-extension-ops/Overview.md 138 | CoreFeatureSignExtensionOps 139 | 140 | // CoreFeatureSIMD enables the vector value type and vector instructions 141 | // (aka SIMD). This is included in CoreFeaturesV2, but not CoreFeaturesV1. 142 | // 143 | // Note: The instruction list is too long to enumerate in godoc. 144 | // See https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/simd/SIMD.md 145 | CoreFeatureSIMD 146 | ) 147 | 148 | // SetEnabled enables or disables the feature or group of features. 149 | func (f CoreFeatures) SetEnabled(feature CoreFeatures, val bool) CoreFeatures { 150 | if val { 151 | return f | feature 152 | } 153 | return f &^ feature 154 | } 155 | 156 | // IsEnabled returns true if the feature (or group of features) is enabled. 157 | func (f CoreFeatures) IsEnabled(feature CoreFeatures) bool { 158 | return f&feature != 0 159 | } 160 | 161 | // RequireEnabled returns an error if the feature (or group of features) is not 162 | // enabled. 163 | func (f CoreFeatures) RequireEnabled(feature CoreFeatures) error { 164 | if f&feature == 0 { 165 | return fmt.Errorf("feature %q is disabled", feature) 166 | } 167 | return nil 168 | } 169 | 170 | // String implements fmt.Stringer by returning each enabled feature. 171 | func (f CoreFeatures) String() string { 172 | var builder strings.Builder 173 | for i := 0; i <= 63; i++ { // cycle through all bits to reduce code and maintenance 174 | target := CoreFeatures(1 << i) 175 | if f.IsEnabled(target) { 176 | if name := featureName(target); name != "" { 177 | if builder.Len() > 0 { 178 | builder.WriteByte('|') 179 | } 180 | builder.WriteString(name) 181 | } 182 | } 183 | } 184 | return builder.String() 185 | } 186 | 187 | func featureName(f CoreFeatures) string { 188 | switch f { 189 | case CoreFeatureMutableGlobal: 190 | // match https://github.com/WebAssembly/mutable-global 191 | return "mutable-global" 192 | case CoreFeatureSignExtensionOps: 193 | // match https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/sign-extension-ops/Overview.md 194 | return "sign-extension-ops" 195 | case CoreFeatureMultiValue: 196 | // match https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/multi-value/Overview.md 197 | return "multi-value" 198 | case CoreFeatureNonTrappingFloatToIntConversion: 199 | // match https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/nontrapping-float-to-int-conversion/Overview.md 200 | return "nontrapping-float-to-int-conversion" 201 | case CoreFeatureBulkMemoryOperations: 202 | // match https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/bulk-memory-operations/Overview.md 203 | return "bulk-memory-operations" 204 | case CoreFeatureReferenceTypes: 205 | // match https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/reference-types/Overview.md 206 | return "reference-types" 207 | case CoreFeatureSIMD: 208 | // match https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/simd/SIMD.md 209 | return "simd" 210 | } 211 | return "" 212 | } 213 | -------------------------------------------------------------------------------- /binary/element.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/tetratelabs/wabin/leb128" 9 | "github.com/tetratelabs/wabin/wasm" 10 | ) 11 | 12 | func ensureElementKindFuncRef(r *bytes.Reader) error { 13 | elemKind, err := r.ReadByte() 14 | if err != nil { 15 | return fmt.Errorf("read element prefix: %w", err) 16 | } 17 | if elemKind != 0x0 { // ElemKind is fixed to 0x0 now: https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/binary/modules.html#element-section 18 | return fmt.Errorf("element kind must be zero but was 0x%x", elemKind) 19 | } 20 | return nil 21 | } 22 | 23 | func decodeElementInitValueVector(r *bytes.Reader) ([]*wasm.Index, error) { 24 | vs, _, err := leb128.DecodeUint32(r) 25 | if err != nil { 26 | return nil, fmt.Errorf("get size of vector: %w", err) 27 | } 28 | 29 | vec := make([]*wasm.Index, vs) 30 | for i := range vec { 31 | u32, _, err := leb128.DecodeUint32(r) 32 | if err != nil { 33 | return nil, fmt.Errorf("read function index: %w", err) 34 | } 35 | vec[i] = &u32 36 | } 37 | return vec, nil 38 | } 39 | 40 | func decodeElementConstExprVector(r *bytes.Reader, elemType wasm.RefType, features wasm.CoreFeatures) ([]*wasm.Index, error) { 41 | vs, _, err := leb128.DecodeUint32(r) 42 | if err != nil { 43 | return nil, fmt.Errorf("failed to get the size of constexpr vector: %w", err) 44 | } 45 | vec := make([]*wasm.Index, vs) 46 | for i := range vec { 47 | expr, err := decodeConstantExpression(r, features) 48 | if err != nil { 49 | return nil, err 50 | } 51 | switch expr.Opcode { 52 | case wasm.OpcodeRefFunc: 53 | if elemType != wasm.RefTypeFuncref { 54 | return nil, fmt.Errorf("element type mismatch: want %s, but constexpr has funcref", wasm.RefTypeName(elemType)) 55 | } 56 | v, _, _ := leb128.DecodeUint32(bytes.NewReader(expr.Data)) 57 | vec[i] = &v 58 | case wasm.OpcodeRefNull: 59 | if elemType != expr.Data[0] { 60 | return nil, fmt.Errorf("element type mismatch: want %s, but constexpr has %s", 61 | wasm.RefTypeName(elemType), wasm.RefTypeName(expr.Data[0])) 62 | } 63 | // vec[i] is already nil, so nothing to do. 64 | default: 65 | return nil, fmt.Errorf("const expr must be either ref.null or ref.func but was %s", wasm.InstructionName(expr.Opcode)) 66 | } 67 | } 68 | return vec, nil 69 | } 70 | 71 | func decodeElementRefType(r *bytes.Reader) (ret wasm.RefType, err error) { 72 | ret, err = r.ReadByte() 73 | if err != nil { 74 | err = fmt.Errorf("read element ref type: %w", err) 75 | return 76 | } 77 | if ret != wasm.RefTypeFuncref && ret != wasm.RefTypeExternref { 78 | return 0, errors.New("ref type must be funcref or externref for element as of WebAssembly 2.0") 79 | } 80 | return 81 | } 82 | 83 | const ( 84 | // The prefix is explained at https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/binary/modules.html#element-section 85 | 86 | // elementSegmentPrefixLegacy is the legacy prefix and is only valid one 87 | // before FeatureBulkMemoryOperations. 88 | elementSegmentPrefixLegacy = iota 89 | // elementSegmentPrefixPassiveFuncrefValueVector is the passive element 90 | // whose indexes are encoded as vec(varint), and reftype is fixed to funcref. 91 | elementSegmentPrefixPassiveFuncrefValueVector 92 | // elementSegmentPrefixActiveFuncrefValueVectorWithTableIndex is the same 93 | // as elementSegmentPrefixPassiveFuncrefValueVector but active and table 94 | // index is encoded. 95 | elementSegmentPrefixActiveFuncrefValueVectorWithTableIndex 96 | // elementSegmentPrefixDeclarativeFuncrefValueVector is the same as 97 | // elementSegmentPrefixPassiveFuncrefValueVector but declarative. 98 | elementSegmentPrefixDeclarativeFuncrefValueVector 99 | // elementSegmentPrefixActiveFuncrefConstExprVector is active and reftype 100 | // is fixed to funcref and indexes are encoded as vec(const_expr). 101 | elementSegmentPrefixActiveFuncrefConstExprVector 102 | // elementSegmentPrefixPassiveConstExprVector is passive where indexes 103 | // are encoded as vec(const_expr), and reftype is encoded. 104 | elementSegmentPrefixPassiveConstExprVector 105 | // elementSegmentPrefixPassiveConstExprVector is active where indexes are 106 | // encoded as vec(const_expr), and reftype and table index are encoded. 107 | elementSegmentPrefixActiveConstExprVector 108 | // elementSegmentPrefixDeclarativeConstExprVector is declarative where 109 | // indexes are encoded as vec(const_expr), and reftype is encoded. 110 | elementSegmentPrefixDeclarativeConstExprVector 111 | ) 112 | 113 | func decodeElementSegment(r *bytes.Reader, features wasm.CoreFeatures) (*wasm.ElementSegment, error) { 114 | prefix, _, err := leb128.DecodeUint32(r) 115 | if err != nil { 116 | return nil, fmt.Errorf("read element prefix: %w", err) 117 | } 118 | 119 | if prefix != elementSegmentPrefixLegacy { 120 | if err := features.RequireEnabled(wasm.CoreFeatureBulkMemoryOperations); err != nil { 121 | return nil, fmt.Errorf("non-zero prefix for element segment is invalid as %w", err) 122 | } 123 | } 124 | 125 | // Encoding depends on the prefix and described at https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/binary/modules.html#element-section 126 | switch prefix { 127 | case elementSegmentPrefixLegacy: 128 | // Legacy prefix which is WebAssembly 1.0 compatible. 129 | expr, err := decodeConstantExpression(r, features) 130 | if err != nil { 131 | return nil, fmt.Errorf("read expr for offset: %w", err) 132 | } 133 | 134 | init, err := decodeElementInitValueVector(r) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | return &wasm.ElementSegment{ 140 | OffsetExpr: expr, 141 | Init: init, 142 | Type: wasm.RefTypeFuncref, 143 | Mode: wasm.ElementModeActive, 144 | // Legacy prefix has the fixed table index zero. 145 | TableIndex: 0, 146 | }, nil 147 | case elementSegmentPrefixPassiveFuncrefValueVector: 148 | // Prefix 1 requires funcref. 149 | if err = ensureElementKindFuncRef(r); err != nil { 150 | return nil, err 151 | } 152 | 153 | init, err := decodeElementInitValueVector(r) 154 | if err != nil { 155 | return nil, err 156 | } 157 | return &wasm.ElementSegment{ 158 | Init: init, 159 | Type: wasm.RefTypeFuncref, 160 | Mode: wasm.ElementModePassive, 161 | }, nil 162 | case elementSegmentPrefixActiveFuncrefValueVectorWithTableIndex: 163 | tableIndex, _, err := leb128.DecodeUint32(r) 164 | if err != nil { 165 | return nil, fmt.Errorf("get size of vector: %w", err) 166 | } 167 | 168 | if tableIndex != 0 { 169 | if err := features.RequireEnabled(wasm.CoreFeatureReferenceTypes); err != nil { 170 | return nil, fmt.Errorf("table index must be zero but was %d: %w", tableIndex, err) 171 | } 172 | } 173 | 174 | expr, err := decodeConstantExpression(r, features) 175 | if err != nil { 176 | return nil, fmt.Errorf("read expr for offset: %w", err) 177 | } 178 | 179 | // Prefix 2 requires funcref. 180 | if err = ensureElementKindFuncRef(r); err != nil { 181 | return nil, err 182 | } 183 | 184 | init, err := decodeElementInitValueVector(r) 185 | if err != nil { 186 | return nil, err 187 | } 188 | return &wasm.ElementSegment{ 189 | OffsetExpr: expr, 190 | Init: init, 191 | Type: wasm.RefTypeFuncref, 192 | Mode: wasm.ElementModeActive, 193 | TableIndex: tableIndex, 194 | }, nil 195 | case elementSegmentPrefixDeclarativeFuncrefValueVector: 196 | // Prefix 3 requires funcref. 197 | if err = ensureElementKindFuncRef(r); err != nil { 198 | return nil, err 199 | } 200 | init, err := decodeElementInitValueVector(r) 201 | if err != nil { 202 | return nil, err 203 | } 204 | return &wasm.ElementSegment{ 205 | Init: init, 206 | Type: wasm.RefTypeFuncref, 207 | Mode: wasm.ElementModeDeclarative, 208 | }, nil 209 | case elementSegmentPrefixActiveFuncrefConstExprVector: 210 | expr, err := decodeConstantExpression(r, features) 211 | if err != nil { 212 | return nil, fmt.Errorf("read expr for offset: %w", err) 213 | } 214 | 215 | init, err := decodeElementConstExprVector(r, wasm.RefTypeFuncref, features) 216 | if err != nil { 217 | return nil, err 218 | } 219 | 220 | return &wasm.ElementSegment{ 221 | OffsetExpr: expr, 222 | Init: init, 223 | Type: wasm.RefTypeFuncref, 224 | Mode: wasm.ElementModeActive, 225 | TableIndex: 0, 226 | }, nil 227 | case elementSegmentPrefixPassiveConstExprVector: 228 | refType, err := decodeElementRefType(r) 229 | if err != nil { 230 | return nil, err 231 | } 232 | init, err := decodeElementConstExprVector(r, refType, features) 233 | if err != nil { 234 | return nil, err 235 | } 236 | return &wasm.ElementSegment{ 237 | Init: init, 238 | Type: refType, 239 | Mode: wasm.ElementModePassive, 240 | }, nil 241 | case elementSegmentPrefixActiveConstExprVector: 242 | tableIndex, _, err := leb128.DecodeUint32(r) 243 | if err != nil { 244 | return nil, fmt.Errorf("get size of vector: %w", err) 245 | } 246 | 247 | if tableIndex != 0 { 248 | if err := features.RequireEnabled(wasm.CoreFeatureReferenceTypes); err != nil { 249 | return nil, fmt.Errorf("table index must be zero but was %d: %w", tableIndex, err) 250 | } 251 | } 252 | expr, err := decodeConstantExpression(r, features) 253 | if err != nil { 254 | return nil, fmt.Errorf("read expr for offset: %w", err) 255 | } 256 | 257 | refType, err := decodeElementRefType(r) 258 | if err != nil { 259 | return nil, err 260 | } 261 | 262 | init, err := decodeElementConstExprVector(r, refType, features) 263 | if err != nil { 264 | return nil, err 265 | } 266 | 267 | return &wasm.ElementSegment{ 268 | OffsetExpr: expr, 269 | Init: init, 270 | Type: refType, 271 | Mode: wasm.ElementModeActive, 272 | TableIndex: tableIndex, 273 | }, nil 274 | case elementSegmentPrefixDeclarativeConstExprVector: 275 | refType, err := decodeElementRefType(r) 276 | if err != nil { 277 | return nil, err 278 | } 279 | init, err := decodeElementConstExprVector(r, refType, features) 280 | if err != nil { 281 | return nil, err 282 | } 283 | return &wasm.ElementSegment{ 284 | Init: init, 285 | Type: refType, 286 | Mode: wasm.ElementModeDeclarative, 287 | }, nil 288 | default: 289 | return nil, fmt.Errorf("invalid element segment prefix: 0x%x", prefix) 290 | } 291 | } 292 | 293 | // encodeCode returns the wasm.ElementSegment encoded in WebAssembly Binary Format. 294 | // 295 | // https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#element-section%E2%91%A0 296 | func encodeElement(e *wasm.ElementSegment) (ret []byte) { 297 | if e.Mode == wasm.ElementModeActive { 298 | ret = append(ret, leb128.EncodeInt32(int32(e.TableIndex))...) 299 | ret = append(ret, encodeConstantExpression(e.OffsetExpr)...) 300 | ret = append(ret, leb128.EncodeUint32(uint32(len(e.Init)))...) 301 | for _, idx := range e.Init { 302 | ret = append(ret, leb128.EncodeInt32(int32(*idx))...) 303 | } 304 | } else { 305 | panic("TODO: support encoding for non-active elements in bulk-memory-operations proposal") 306 | } 307 | return 308 | } 309 | -------------------------------------------------------------------------------- /binary/names_test.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/tetratelabs/wabin/wasm" 10 | ) 11 | 12 | func TestEncodeNameSectionData(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | input *wasm.NameSection 16 | expected []byte 17 | }{ 18 | { 19 | name: "empty", 20 | input: &wasm.NameSection{}, 21 | }, 22 | { 23 | name: "only module", 24 | // Ex. (module $simple ) 25 | input: &wasm.NameSection{ModuleName: "simple"}, 26 | expected: []byte{ 27 | subsectionIDModuleName, 0x07, // 7 bytes 28 | 0x06, // the Module name simple is 6 bytes long 29 | 's', 'i', 'm', 'p', 'l', 'e', 30 | }, 31 | }, 32 | { 33 | name: "module and function name", 34 | // (module $simple 35 | // (import "" "Hello" (func $hello)) 36 | // (start $hello) 37 | // ) 38 | input: &wasm.NameSection{ 39 | ModuleName: "simple", 40 | FunctionNames: wasm.NameMap{{Index: wasm.Index(0), Name: "hello"}}, 41 | }, 42 | expected: []byte{ 43 | subsectionIDModuleName, 0x07, // 7 bytes 44 | 0x06, // the Module name simple is 6 bytes long 45 | 's', 'i', 'm', 'p', 'l', 'e', 46 | subsectionIDFunctionNames, 0x08, // 8 bytes 47 | 0x01, // one function name 48 | 0x00, // the function index is zero 49 | 0x05, // the function name hello is 5 bytes long 50 | 'h', 'e', 'l', 'l', 'o', 51 | }, 52 | }, 53 | { 54 | name: "two function names", // Ex. TinyGo which at one point didn't set a module name 55 | // (module 56 | // (import "wasi_snapshot_preview1" "args_sizes_get" (func $wasi.args_sizes_get (param i32, i32) (result i32))) 57 | // (import "wasi_snapshot_preview1" "fd_write" (func $wasi.fd_write (param i32, i32, i32, i32) (result i32))) 58 | // ) 59 | input: &wasm.NameSection{ 60 | FunctionNames: wasm.NameMap{ 61 | {Index: wasm.Index(0), Name: "wasi.args_sizes_get"}, 62 | {Index: wasm.Index(1), Name: "wasi.fd_write"}, 63 | }, 64 | }, 65 | expected: []byte{ 66 | subsectionIDFunctionNames, 0x25, // 37 bytes 67 | 0x02, // two function names 68 | 0x00, // the function index is zero 69 | 0x13, // the function name wasi.args_sizes_get is 19 bytes long 70 | 'w', 'a', 's', 'i', '.', 'a', 'r', 'g', 's', '_', 's', 'i', 'z', 'e', 's', '_', 'g', 'e', 't', 71 | 0x01, // the function index is one 72 | 0x0d, // the function name wasi.fd_write is 13 bytes long 73 | 'w', 'a', 's', 'i', '.', 'f', 'd', '_', 'w', 'r', 'i', 't', 'e', 74 | }, 75 | }, 76 | { 77 | name: "function with local names", 78 | // (module 79 | // (import "Math" "Mul" (func $mul (param $x f32) (param $y f32) (result f32))) 80 | // (import "Math" "Add" (func $add (param $l f32) (param $r f32) (result f32))) 81 | // ) 82 | input: &wasm.NameSection{ 83 | FunctionNames: wasm.NameMap{ 84 | {Index: wasm.Index(0), Name: "mul"}, 85 | {Index: wasm.Index(1), Name: "add"}, 86 | }, 87 | LocalNames: wasm.IndirectNameMap{ 88 | {Index: wasm.Index(0), NameMap: wasm.NameMap{ 89 | {Index: wasm.Index(0), Name: "x"}, 90 | {Index: wasm.Index(1), Name: "y"}, 91 | }}, 92 | {Index: wasm.Index(1), NameMap: wasm.NameMap{ 93 | {Index: wasm.Index(0), Name: "l"}, 94 | {Index: wasm.Index(1), Name: "r"}, 95 | }}, 96 | }, 97 | }, 98 | expected: []byte{ 99 | subsectionIDFunctionNames, 0x0b, // 7 bytes 100 | 0x02, // two function names 101 | 0x00, 0x03, 'm', 'u', 'l', // index 0, size of "mul", "mul" 102 | 0x01, 0x03, 'a', 'd', 'd', // index 1, size of "add", "add" 103 | subsectionIDLocalNames, 0x11, // 17 bytes 104 | 0x02, // two functions 105 | 0x00, 0x02, // index 0 has 2 locals 106 | 0x00, 0x01, 'x', // index 0, size of "x", "x" 107 | 0x01, 0x01, 'y', // index 1, size of "y", "y" 108 | 0x01, 0x02, // index 1 has 2 locals 109 | 0x00, 0x01, 'l', // index 0, size of "l", "l" 110 | 0x01, 0x01, 'r', // index 1, size of "r", "r" 111 | }, 112 | }, 113 | } 114 | 115 | for _, tt := range tests { 116 | tc := tt 117 | 118 | t.Run(tc.name, func(t *testing.T) { 119 | bytes := encodeNameSectionData(tc.input) 120 | require.Equal(t, tc.expected, bytes) 121 | }) 122 | } 123 | } 124 | 125 | func TestEncodeNameSubsection(t *testing.T) { 126 | subsectionID := uint8(1) 127 | name := []byte("simple") 128 | require.Equal(t, []byte{ 129 | subsectionID, 130 | byte(1 + 6), // 1 is the size of 6 in LEB128 encoding 131 | 6, 's', 'i', 'm', 'p', 'l', 'e'}, encodeNameSubsection(subsectionID, encodeSizePrefixed(name))) 132 | } 133 | 134 | func TestEncodeNameAssoc(t *testing.T) { 135 | na := &wasm.NameAssoc{Index: 1, Name: "hello"} 136 | require.Equal(t, []byte{byte(na.Index), 5, 'h', 'e', 'l', 'l', 'o'}, encodeNameAssoc(na)) 137 | } 138 | 139 | func TestEncodeNameMap(t *testing.T) { 140 | na := &wasm.NameAssoc{Index: 1, Name: "hello"} 141 | m := wasm.NameMap{na} 142 | require.Equal(t, []byte{byte(1), byte(na.Index), 5, 'h', 'e', 'l', 'l', 'o'}, encodeNameMap(m)) 143 | } 144 | 145 | func TestEncodeSizePrefixed(t *testing.T) { 146 | // We expect size in bytes (LEB128 encoded) then the bytes 147 | require.Equal(t, []byte{5, 'h', 'e', 'l', 'l', 'o'}, encodeSizePrefixed([]byte("hello"))) 148 | } 149 | 150 | // TestDecodeNameSection relies on unit tests for NameSection.EncodeData, specifically that the encoding is 151 | // both known and correct. This avoids having to copy/paste or share variables to assert against byte arrays. 152 | func TestDecodeNameSection(t *testing.T) { 153 | tests := []struct { 154 | name string 155 | input *wasm.NameSection // round trip test! 156 | }{{ 157 | name: "empty", 158 | input: &wasm.NameSection{}, 159 | }, 160 | { 161 | name: "only module", 162 | input: &wasm.NameSection{ModuleName: "simple"}, 163 | }, 164 | { 165 | name: "module and function name", 166 | input: &wasm.NameSection{ 167 | ModuleName: "simple", 168 | FunctionNames: wasm.NameMap{{Index: wasm.Index(0), Name: "wasi.hello"}}, 169 | }, 170 | }, 171 | { 172 | name: "two function names", 173 | input: &wasm.NameSection{ 174 | FunctionNames: wasm.NameMap{ 175 | {Index: wasm.Index(0), Name: "wasi.args_sizes_get"}, 176 | {Index: wasm.Index(1), Name: "wasi.fd_write"}, 177 | }, 178 | }, 179 | }, 180 | { 181 | name: "function with local names", 182 | input: &wasm.NameSection{ 183 | FunctionNames: wasm.NameMap{ 184 | {Index: wasm.Index(0), Name: "mul"}, 185 | {Index: wasm.Index(1), Name: "add"}, 186 | }, 187 | LocalNames: wasm.IndirectNameMap{ 188 | {Index: wasm.Index(0), NameMap: wasm.NameMap{ 189 | {Index: wasm.Index(0), Name: "x"}, 190 | {Index: wasm.Index(1), Name: "y"}, 191 | }}, 192 | {Index: wasm.Index(1), NameMap: wasm.NameMap{ 193 | {Index: wasm.Index(0), Name: "l"}, 194 | {Index: wasm.Index(1), Name: "r"}, 195 | }}, 196 | }, 197 | }, 198 | }, 199 | } 200 | 201 | for _, tt := range tests { 202 | tc := tt 203 | 204 | t.Run(tc.name, func(t *testing.T) { 205 | data := encodeNameSectionData(tc.input) 206 | ns, err := decodeNameSection(bytes.NewReader(data), uint64(len(data))) 207 | require.NoError(t, err) 208 | require.Equal(t, tc.input, ns) 209 | }) 210 | } 211 | } 212 | 213 | func TestDecodeNameSection_Errors(t *testing.T) { 214 | // currently, we ignore the size of known subsections 215 | ignoredSubsectionSize := byte(50) 216 | tests := []struct { 217 | name string 218 | input []byte 219 | expectedErr string 220 | }{ 221 | { 222 | name: "EOF after module name subsection ID", 223 | input: []byte{subsectionIDModuleName}, 224 | expectedErr: "failed to read the size of subsection[0]: EOF", 225 | }, 226 | { 227 | name: "EOF after function names subsection ID", 228 | input: []byte{subsectionIDFunctionNames}, 229 | expectedErr: "failed to read the size of subsection[1]: EOF", 230 | }, 231 | { 232 | name: "EOF after local names subsection ID", 233 | input: []byte{subsectionIDLocalNames}, 234 | expectedErr: "failed to read the size of subsection[2]: EOF", 235 | }, 236 | { 237 | name: "EOF after unknown subsection ID", 238 | input: []byte{4}, 239 | expectedErr: "failed to read the size of subsection[4]: EOF", 240 | }, 241 | { 242 | name: "EOF after module name subsection size", 243 | input: []byte{subsectionIDModuleName, ignoredSubsectionSize}, 244 | expectedErr: "failed to read module name size: EOF", 245 | }, 246 | { 247 | name: "EOF after function names subsection size", 248 | input: []byte{subsectionIDFunctionNames, ignoredSubsectionSize}, 249 | expectedErr: "failed to read the function count of subsection[1]: EOF", 250 | }, 251 | { 252 | name: "EOF after local names subsection size", 253 | input: []byte{subsectionIDLocalNames, ignoredSubsectionSize}, 254 | expectedErr: "failed to read the function count of subsection[2]: EOF", 255 | }, 256 | { 257 | name: "EOF skipping unknown subsection size", 258 | input: []byte{4, 100}, 259 | expectedErr: "failed to skip subsection[4]: EOF", 260 | }, 261 | { 262 | name: "EOF after module name size", 263 | input: []byte{subsectionIDModuleName, ignoredSubsectionSize, 5}, 264 | expectedErr: "failed to read module name: EOF", 265 | }, 266 | { 267 | name: "EOF after function name count", 268 | input: []byte{subsectionIDFunctionNames, ignoredSubsectionSize, 2}, 269 | expectedErr: "failed to read a function index in subsection[1]: EOF", 270 | }, 271 | { 272 | name: "EOF after local names function count", 273 | input: []byte{subsectionIDLocalNames, ignoredSubsectionSize, 2}, 274 | expectedErr: "failed to read a function index in subsection[2]: EOF", 275 | }, 276 | { 277 | name: "EOF after function name index", 278 | input: []byte{subsectionIDFunctionNames, ignoredSubsectionSize, 2, 0}, 279 | expectedErr: "failed to read function[0] name size: EOF", 280 | }, 281 | { 282 | name: "EOF after local names function index", 283 | input: []byte{subsectionIDLocalNames, ignoredSubsectionSize, 2, 0}, 284 | expectedErr: "failed to read the local count for function[0]: EOF", 285 | }, 286 | { 287 | name: "EOF after function name size", 288 | input: []byte{subsectionIDFunctionNames, ignoredSubsectionSize, 2, 0, 5}, 289 | expectedErr: "failed to read function[0] name: EOF", 290 | }, 291 | { 292 | name: "EOF after local names count for a function index", 293 | input: []byte{subsectionIDLocalNames, ignoredSubsectionSize, 2, 0, 2}, 294 | expectedErr: "failed to read a local index of function[0]: EOF", 295 | }, 296 | { 297 | name: "EOF after local name size", 298 | input: []byte{subsectionIDLocalNames, ignoredSubsectionSize, 2, 0, 2, 1}, 299 | expectedErr: "failed to read function[0] local[1] name size: EOF", 300 | }, 301 | } 302 | 303 | for _, tt := range tests { 304 | tc := tt 305 | 306 | t.Run(tc.name, func(t *testing.T) { 307 | _, err := decodeNameSection(bytes.NewReader(tc.input), uint64(len(tc.input))) 308 | require.EqualError(t, err, tc.expectedErr) 309 | }) 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020-2022 wazero authors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /binary/section.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/tetratelabs/wabin/leb128" 9 | "github.com/tetratelabs/wabin/wasm" 10 | ) 11 | 12 | func decodeTypeSection(features wasm.CoreFeatures, r *bytes.Reader) ([]*wasm.FunctionType, error) { 13 | vs, _, err := leb128.DecodeUint32(r) 14 | if err != nil { 15 | return nil, fmt.Errorf("get size of vector: %w", err) 16 | } 17 | 18 | result := make([]*wasm.FunctionType, vs) 19 | for i := uint32(0); i < vs; i++ { 20 | if result[i], err = decodeFunctionType(features, r); err != nil { 21 | return nil, fmt.Errorf("read %d-th type: %v", i, err) 22 | } 23 | } 24 | return result, nil 25 | } 26 | 27 | func decodeImportSection(r *bytes.Reader, features wasm.CoreFeatures) ([]*wasm.Import, error) { 28 | vs, _, err := leb128.DecodeUint32(r) 29 | if err != nil { 30 | return nil, fmt.Errorf("get size of vector: %w", err) 31 | } 32 | 33 | result := make([]*wasm.Import, vs) 34 | for i := uint32(0); i < vs; i++ { 35 | if result[i], err = decodeImport(r, i, features); err != nil { 36 | return nil, err 37 | } 38 | } 39 | return result, nil 40 | } 41 | 42 | func decodeFunctionSection(r *bytes.Reader) ([]uint32, error) { 43 | vs, _, err := leb128.DecodeUint32(r) 44 | if err != nil { 45 | return nil, fmt.Errorf("get size of vector: %w", err) 46 | } 47 | 48 | result := make([]uint32, vs) 49 | for i := uint32(0); i < vs; i++ { 50 | if result[i], _, err = leb128.DecodeUint32(r); err != nil { 51 | return nil, fmt.Errorf("get type index: %w", err) 52 | } 53 | } 54 | return result, err 55 | } 56 | 57 | func decodeTableSection(r *bytes.Reader, features wasm.CoreFeatures) ([]*wasm.Table, error) { 58 | vs, _, err := leb128.DecodeUint32(r) 59 | if err != nil { 60 | return nil, fmt.Errorf("error reading size") 61 | } 62 | if vs > 1 { 63 | if err := features.RequireEnabled(wasm.CoreFeatureReferenceTypes); err != nil { 64 | return nil, fmt.Errorf("at most one table allowed in module as %w", err) 65 | } 66 | } 67 | 68 | ret := make([]*wasm.Table, vs) 69 | for i := range ret { 70 | table, err := decodeTable(r, features) 71 | if err != nil { 72 | return nil, err 73 | } 74 | ret[i] = table 75 | } 76 | return ret, nil 77 | } 78 | 79 | func decodeMemorySection(r *bytes.Reader) (*wasm.Memory, error) { 80 | vs, _, err := leb128.DecodeUint32(r) 81 | if err != nil { 82 | return nil, fmt.Errorf("error reading size") 83 | } 84 | if vs > 1 { 85 | return nil, fmt.Errorf("at most one memory allowed in module, but read %d", vs) 86 | } 87 | 88 | return decodeMemory(r) 89 | } 90 | 91 | func decodeGlobalSection(r *bytes.Reader, features wasm.CoreFeatures) ([]*wasm.Global, error) { 92 | vs, _, err := leb128.DecodeUint32(r) 93 | if err != nil { 94 | return nil, fmt.Errorf("get size of vector: %w", err) 95 | } 96 | 97 | result := make([]*wasm.Global, vs) 98 | for i := uint32(0); i < vs; i++ { 99 | if result[i], err = decodeGlobal(r, features); err != nil { 100 | return nil, fmt.Errorf("global[%d]: %w", i, err) 101 | } 102 | } 103 | return result, nil 104 | } 105 | 106 | func decodeExportSection(r *bytes.Reader) ([]*wasm.Export, error) { 107 | vs, _, sizeErr := leb128.DecodeUint32(r) 108 | if sizeErr != nil { 109 | return nil, fmt.Errorf("get size of vector: %v", sizeErr) 110 | } 111 | 112 | usedName := make(map[string]struct{}, vs) 113 | exportSection := make([]*wasm.Export, 0, vs) 114 | for i := wasm.Index(0); i < vs; i++ { 115 | export, err := decodeExport(r) 116 | if err != nil { 117 | return nil, fmt.Errorf("read export: %w", err) 118 | } 119 | if _, ok := usedName[export.Name]; ok { 120 | return nil, fmt.Errorf("export[%d] duplicates name %q", i, export.Name) 121 | } else { 122 | usedName[export.Name] = struct{}{} 123 | } 124 | exportSection = append(exportSection, export) 125 | } 126 | return exportSection, nil 127 | } 128 | 129 | func decodeStartSection(r *bytes.Reader) (*wasm.Index, error) { 130 | vs, _, err := leb128.DecodeUint32(r) 131 | if err != nil { 132 | return nil, fmt.Errorf("get function index: %w", err) 133 | } 134 | return &vs, nil 135 | } 136 | 137 | func decodeElementSection(r *bytes.Reader, features wasm.CoreFeatures) ([]*wasm.ElementSegment, error) { 138 | vs, _, err := leb128.DecodeUint32(r) 139 | if err != nil { 140 | return nil, fmt.Errorf("get size of vector: %w", err) 141 | } 142 | 143 | result := make([]*wasm.ElementSegment, vs) 144 | for i := uint32(0); i < vs; i++ { 145 | if result[i], err = decodeElementSegment(r, features); err != nil { 146 | return nil, fmt.Errorf("read element: %w", err) 147 | } 148 | } 149 | return result, nil 150 | } 151 | 152 | func decodeCodeSection(r *bytes.Reader) ([]*wasm.Code, error) { 153 | vs, _, err := leb128.DecodeUint32(r) 154 | if err != nil { 155 | return nil, fmt.Errorf("get size of vector: %w", err) 156 | } 157 | 158 | result := make([]*wasm.Code, vs) 159 | for i := uint32(0); i < vs; i++ { 160 | if result[i], err = decodeCode(r); err != nil { 161 | return nil, fmt.Errorf("read %d-th code segment: %v", i, err) 162 | } 163 | } 164 | return result, nil 165 | } 166 | 167 | func decodeDataSection(r *bytes.Reader, features wasm.CoreFeatures) ([]*wasm.DataSegment, error) { 168 | vs, _, err := leb128.DecodeUint32(r) 169 | if err != nil { 170 | return nil, fmt.Errorf("get size of vector: %w", err) 171 | } 172 | 173 | result := make([]*wasm.DataSegment, vs) 174 | for i := uint32(0); i < vs; i++ { 175 | if result[i], err = decodeDataSegment(r, features); err != nil { 176 | return nil, fmt.Errorf("read data segment: %w", err) 177 | } 178 | } 179 | return result, nil 180 | } 181 | 182 | func decodeDataCountSection(r *bytes.Reader) (count *uint32, err error) { 183 | v, _, err := leb128.DecodeUint32(r) 184 | if err != nil && err != io.EOF { 185 | // data count is optional, so EOF is fine. 186 | return nil, err 187 | } 188 | return &v, nil 189 | } 190 | 191 | // encodeSection encodes the sectionID, the size of its contents in bytes, 192 | // followed by the contents. 193 | // 194 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#sections%E2%91%A0 195 | func encodeSection(sectionID wasm.SectionID, contents []byte) []byte { 196 | return append([]byte{sectionID}, encodeSizePrefixed(contents)...) 197 | } 198 | 199 | // encodeTypeSection encodes a wasm.SectionIDType for the given imports in 200 | // WebAssembly Binary Format. 201 | // 202 | // See encodeFunctionType 203 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#type-section%E2%91%A0 204 | func encodeTypeSection(types []*wasm.FunctionType) []byte { 205 | contents := leb128.EncodeUint32(uint32(len(types))) 206 | for _, t := range types { 207 | contents = append(contents, encodeFunctionType(t)...) 208 | } 209 | return encodeSection(wasm.SectionIDType, contents) 210 | } 211 | 212 | // encodeImportSection encodes a wasm.SectionIDImport for the given imports in 213 | // WebAssembly Binary Format. 214 | // 215 | // See encodeImport 216 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#import-section%E2%91%A0 217 | func encodeImportSection(imports []*wasm.Import) []byte { 218 | contents := leb128.EncodeUint32(uint32(len(imports))) 219 | for _, i := range imports { 220 | contents = append(contents, encodeImport(i)...) 221 | } 222 | return encodeSection(wasm.SectionIDImport, contents) 223 | } 224 | 225 | // encodeFunctionSection encodes a wasm.SectionIDFunction for the type indices 226 | // associated with module-defined functions in WebAssembly Binary Format. 227 | // 228 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#function-section%E2%91%A0 229 | func encodeFunctionSection(typeIndices []wasm.Index) []byte { 230 | contents := leb128.EncodeUint32(uint32(len(typeIndices))) 231 | for _, index := range typeIndices { 232 | contents = append(contents, leb128.EncodeUint32(index)...) 233 | } 234 | return encodeSection(wasm.SectionIDFunction, contents) 235 | } 236 | 237 | // encodeCodeSection encodes a wasm.SectionIDCode for the module-defined 238 | // function in WebAssembly Binary Format. 239 | // 240 | // See encodeCode 241 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#code-section%E2%91%A0 242 | func encodeCodeSection(code []*wasm.Code) []byte { 243 | contents := leb128.EncodeUint32(uint32(len(code))) 244 | for _, i := range code { 245 | contents = append(contents, encodeCode(i)...) 246 | } 247 | return encodeSection(wasm.SectionIDCode, contents) 248 | } 249 | 250 | // encodeTableSection encodes a wasm.SectionIDTable for the module-defined 251 | // function in WebAssembly Binary Format. 252 | // 253 | // See encodeTable 254 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#table-section%E2%91%A0 255 | func encodeTableSection(tables []*wasm.Table) []byte { 256 | var contents = leb128.EncodeUint32(uint32(len(tables))) 257 | for _, table := range tables { 258 | contents = append(contents, encodeTable(table)...) 259 | } 260 | return encodeSection(wasm.SectionIDTable, contents) 261 | } 262 | 263 | // encodeMemorySection encodes a wasm.SectionIDMemory for the module-defined 264 | // function in WebAssembly Binary Format. 265 | // 266 | // See encodeMemory 267 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-section%E2%91%A0 268 | func encodeMemorySection(memory *wasm.Memory) []byte { 269 | contents := append([]byte{1}, encodeMemory(memory)...) 270 | return encodeSection(wasm.SectionIDMemory, contents) 271 | } 272 | 273 | // encodeGlobalSection encodes a wasm.SectionIDGlobal for the given globals in 274 | // WebAssembly Binary Format. 275 | // 276 | // See encodeGlobal 277 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#global-section%E2%91%A0 278 | func encodeGlobalSection(globals []*wasm.Global) []byte { 279 | contents := leb128.EncodeUint32(uint32(len(globals))) 280 | for _, g := range globals { 281 | contents = append(contents, encodeGlobal(g)...) 282 | } 283 | return encodeSection(wasm.SectionIDGlobal, contents) 284 | } 285 | 286 | // encodeExportSection encodes a wasm.SectionIDExport for the given exports in 287 | // WebAssembly Binary Format. 288 | // 289 | // See encodeExport 290 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#export-section%E2%91%A0 291 | func encodeExportSection(exports []*wasm.Export) []byte { 292 | contents := leb128.EncodeUint32(uint32(len(exports))) 293 | for _, e := range exports { 294 | contents = append(contents, encodeExport(e)...) 295 | } 296 | return encodeSection(wasm.SectionIDExport, contents) 297 | } 298 | 299 | // encodeStartSection encodes a wasm.SectionIDStart for the given function 300 | // index in WebAssembly Binary Format. 301 | // 302 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#start-section%E2%91%A0 303 | func encodeStartSection(funcidx wasm.Index) []byte { 304 | return encodeSection(wasm.SectionIDStart, leb128.EncodeUint32(funcidx)) 305 | } 306 | 307 | // encodeElementSection encodes a wasm.SectionIDElement for the elements in 308 | // WebAssembly Binary Format. 309 | // 310 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#element-section%E2%91%A0 311 | func encodeElementSection(elements []*wasm.ElementSegment) []byte { 312 | contents := leb128.EncodeUint32(uint32(len(elements))) 313 | for _, e := range elements { 314 | contents = append(contents, encodeElement(e)...) 315 | } 316 | return encodeSection(wasm.SectionIDElement, contents) 317 | } 318 | 319 | // encodeDataSection encodes a wasm.SectionIDData for the data in WebAssembly 1.0 (20191205) 320 | // Binary Format. 321 | // 322 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#data-section%E2%91%A0 323 | func encodeDataSection(datum []*wasm.DataSegment) []byte { 324 | contents := leb128.EncodeUint32(uint32(len(datum))) 325 | for _, d := range datum { 326 | contents = append(contents, encodeDataSegment(d)...) 327 | } 328 | return encodeSection(wasm.SectionIDData, contents) 329 | } 330 | 331 | // encodeCustomSection encodes a wasm.SectionIDCustom for the data in WebAssembly 1.0 (20191205) 332 | // Binary Format. This is used for custom sections that are **not** associated with the "name" key. 333 | // 334 | // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#custom-section%E2%91%A0 335 | func encodeCustomSection(c *wasm.CustomSection) (data []byte) { 336 | data = make([]byte, 0, 1+len(c.Name)+len(c.Data)) 337 | l := byte(len(c.Name)) 338 | data = append(data, l) 339 | data = append(data, []byte(c.Name)...) 340 | data = append(data, c.Data...) 341 | return 342 | } 343 | --------------------------------------------------------------------------------