// Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package xeddata import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "path" "reflect" "strings" "testing" ) // Small database to generate state/xtype/width input files and validate parse results. // // Tests should use only those symbols that are defined inside test maps. // For example, if {"foo"=>"bar"} element is not in statesMap, tests // can't expect that "foo" get's replaced by "bar". var ( statesMap = map[string]string{ "not64": "MODE!=2", "mode64": "MODE=2", "mode32": "MODE=1", "mode16": "MODE=0", "rexw_prefix": "REXW=1 SKIP_OSZ=1", "norexw_prefix": "REXW=0 SKIP_OSZ=1", "W1": "REXW=1 SKIP_OSZ=1", "W0": "REXW=0 SKIP_OSZ=1", "VV1": "VEXVALID=1", "V66": "VEX_PREFIX=1", "VF2": "VEX_PREFIX=2", "VF3": "VEX_PREFIX=3", "V0F": "MAP=1", "V0F38": "MAP=2", "V0F3A": "MAP=3", "VL128": "VL=0", "VL256": "VL=1", } xtypesMap = map[string]*xtype{ "int": {name: "int", baseType: "INT", size: "0"}, "i8": {name: "i8", baseType: "INT", size: "8"}, "i64": {name: "i64", baseType: "INT", size: "64"}, "i32": {name: "i32", baseType: "INT", size: "32"}, "u8": {name: "u8", baseType: "UINT", size: "8"}, "f32": {name: "f32", baseType: "SIGNLE", size: "32"}, "f64": {name: "f64", baseType: "DOUBLE", size: "64"}, "var": {name: "var", baseType: "VARIABLE", size: "0"}, } widthsMap = map[string]*width{ "q": {xtype: "i64", sizes: [3]string{"8", "8", "8"}}, "z": {xtype: "int", sizes: [3]string{"2", "4", "4"}}, "b": {xtype: "u8", sizes: [3]string{"1", "1", "1"}}, "d": {xtype: "i32", sizes: [3]string{"4", "4", "4"}}, "ps": {xtype: "f32", sizes: [3]string{"16", "16", "16"}}, "dq": {xtype: "i32", sizes: [3]string{"16", "16", "16"}}, "i32": {xtype: "i32", sizes: [3]string{"4", "4", "4"}}, "i64": {xtype: "i64", sizes: [3]string{"8", "8", "8"}}, "vv": {xtype: "var", sizes: [3]string{"0", "0", "0"}}, "mskw": {xtype: "i1", sizes: [3]string{"64bits", "64bits", "64bits"}}, "zf32": {xtype: "f32", sizes: [3]string{"512bits", "512bits", "512bits"}}, "zf64": {xtype: "f64", sizes: [3]string{"512bits", "512bits", "512bits"}}, "mem80real": {xtype: "f80", sizes: [3]string{"10", "10", "10"}}, "mfpxenv": {xtype: "struct", sizes: [3]string{"512", "512", "512"}}, } ) // newStatesSource returns a reader that mocks "all-state.txt" file. // Input content is generated based on statesMap. func newStatesSource() io.Reader { var buf bytes.Buffer i := 0 for k, v := range statesMap { buf.WriteString("# Line comment\n") buf.WriteString("#\n\n\n") fmt.Fprintf(&buf, "\t%-20s%s", k, v) if i%3 == 0 { buf.WriteString("\t# Trailing comment") } buf.WriteByte('\n') i++ } return &buf } // newWidthsSource returns a reader that mocks "all-widths.txt" file. // Input content is generated based on widthsMap. func newWidthsSource() io.Reader { var buf bytes.Buffer i := 0 for name, width := range widthsMap { buf.WriteString("# Line comment\n") buf.WriteString("#\n\n\n") eqSizes := width.sizes[0] == width.sizes[1] && width.sizes[0] == width.sizes[2] if i%2 == 0 && eqSizes { fmt.Fprintf(&buf, "\t%-16s%-12s%-8s", name, width.xtype, width.sizes[0]) } else { fmt.Fprintf(&buf, "\t%-16s%-12s%-8s%-8s%-8s", name, width.xtype, width.sizes[0], width.sizes[1], width.sizes[2]) } if i%3 == 0 { buf.WriteString("\t# Trailing comment") } buf.WriteByte('\n') i++ } return &buf } // newXtypesSource returns a reader that mocks "all-element-types.txt" file. // Input content is generated based on xtypesMap. func newXtypesSource() io.Reader { var buf bytes.Buffer i := 0 for _, v := range xtypesMap { buf.WriteString("# Line comment\n") buf.WriteString("#\n\n\n") fmt.Fprintf(&buf, "\t%s %s %s", v.name, v.baseType, v.size) if i%3 == 0 { buf.WriteString("\t# Trailing comment") } buf.WriteByte('\n') i++ } return &buf } func newTestDatabase(t *testing.T) *Database { var db Database err := db.LoadStates(newStatesSource()) if err != nil { t.Fatal(err) } err = db.LoadWidths(newWidthsSource()) if err != nil { t.Fatal(err) } err = db.LoadXtypes(newXtypesSource()) if err != nil { t.Fatal(err) } return &db } func TestContainsWord(t *testing.T) { tests := []struct { attrs string attrName string output bool }{ {"ATT1", "ATT1", true}, {" ATT1", "ATT1", true}, {"ATT1 ", "ATT1", true}, {" ATT1 ", "ATT1", true}, {"ATT1 ATT2 ATT3", "ATT1", true}, {"ATT1 ATT2 ATT3", "ATT2", true}, {"ATT1 ATT2 ATT3", "ATT2", true}, {"ATT1 ATT2 ATT3", "ATT4", false}, {"ATT1ATT1", "ATT1", false}, {".ATT1", "ATT1", false}, {".ATT1.", "ATT1", false}, {"ATT1.", "ATT1", false}, {"", "ATT1", false}, {"AT", "ATT1", false}, {"ATT 1", "ATT1", false}, {" ATT1 ", "TT", false}, {" ATT1 ", "T1", false}, {" ATT1 ", "AT", false}, } for _, test := range tests { output := containsWord(test.attrs, test.attrName) if output != test.output { t.Errorf("containsWord(%q, %q)):\nhave: %v\nwant: %v", test.attrs, test.attrName, output, test.output) } } } func TestParseWidths(t *testing.T) { have, err := parseWidths(newWidthsSource()) if err != nil { t.Fatal(err) } for k := range widthsMap { if have[k] == nil { t.Fatalf("missing key %s", k) } if *have[k] != *widthsMap[k] { t.Fatalf("key %s:\nhave: %#v\nwant: %#v", k, have[k], widthsMap[k]) } } if !reflect.DeepEqual(have, widthsMap) { t.Errorf("widths output mismatch:\nhave: %#v\nwant: %#v", have, widthsMap) } } func TestParseStates(t *testing.T) { have, err := parseStates(newStatesSource()) if err != nil { t.Fatal(err) } want := statesMap if !reflect.DeepEqual(have, want) { t.Errorf("states output mismatch:\nhave: %v\nwant: %v", have, want) } } func TestParseXtypes(t *testing.T) { have, err := parseXtypes(newXtypesSource()) if err != nil { t.Fatal(err) } for k := range xtypesMap { if have[k] == nil { t.Fatalf("missing key %s", k) } if *have[k] != *xtypesMap[k] { t.Fatalf("key %s:\nhave: %#v\nwant: %#v", k, have[k], xtypesMap[k]) } } if !reflect.DeepEqual(have, xtypesMap) { t.Fatalf("xtype maps are not equal") } } func TestNewOperand(t *testing.T) { tests := []struct { input string op Operand }{ // Simple cases. { "REG0=XMM_R():r", Operand{Name: "REG0=XMM_R()", Action: "r"}, }, { "REG0=XMM_R:w", Operand{Name: "REG0=XMM_R", Action: "w"}, }, { "MEM0:rw:q", Operand{Name: "MEM0", Action: "rw", Width: "q"}, }, { "REG0=XMM_R():rcw:ps:f32", Operand{Name: "REG0=XMM_R()", Action: "rcw", Width: "ps", Xtype: "f32"}, }, { "IMM0:r:z", Operand{Name: "IMM0", Action: "r", Width: "z"}, }, { "IMM1:cw:b:i8", Operand{Name: "IMM1", Action: "cw", Width: "b", Xtype: "i8"}, }, // Optional fields and visibility. { "REG2:r:EXPL", Operand{Name: "REG2", Action: "r", Visibility: VisExplicit}, }, { "MEM1:w:d:IMPL", Operand{Name: "MEM1", Action: "w", Width: "d", Visibility: VisImplicit}, }, { "MEM1:w:IMPL:d", Operand{Name: "MEM1", Action: "w", Width: "d", Visibility: VisImplicit}, }, { "MEM1:w:d:SUPP:i32", Operand{Name: "MEM1", Action: "w", Width: "d", Visibility: VisSuppressed, Xtype: "i32"}, }, { "MEM1:w:SUPP:d:i32", Operand{Name: "MEM1", Action: "w", Width: "d", Visibility: VisSuppressed, Xtype: "i32"}, }, // Ambiguity: xtypes that look like widths. { "REG0=XMM_R():w:dq:i64", Operand{Name: "REG0=XMM_R()", Action: "w", Width: "dq", Xtype: "i64"}, }, // TXT=X field. { "REG1=MASK1():r:mskw:TXT=ZEROSTR", Operand{Name: "REG1=MASK1()", Action: "r", Width: "mskw", Attributes: map[string]bool{"TXT=ZEROSTR": true}}, }, { "MEM0:r:vv:f64:TXT=BCASTSTR", Operand{Name: "MEM0", Action: "r", Width: "vv", Xtype: "f64", Attributes: map[string]bool{"TXT=BCASTSTR": true}}, }, { "REG0=ZMM_R3():w:zf32:TXT=SAESTR", Operand{Name: "REG0=ZMM_R3()", Action: "w", Width: "zf32", Attributes: map[string]bool{"TXT=SAESTR": true}}, }, { "REG0=ZMM_R3():w:zf64:TXT=ROUNDC", Operand{Name: "REG0=ZMM_R3()", Action: "w", Width: "zf64", Attributes: map[string]bool{"TXT=ROUNDC": true}}, }, // Multi-source. { "REG2=ZMM_N3():r:zf32:MULTISOURCE4", Operand{Name: "REG2=ZMM_N3()", Action: "r", Width: "zf32", Attributes: map[string]bool{"MULTISOURCE4": true}}, }, // Multi-source + EVEX.b context. { "REG2=ZMM_N3():r:zf32:MULTISOURCE4:TXT=SAESTR", Operand{Name: "REG2=ZMM_N3()", Action: "r", Width: "zf32", Attributes: map[string]bool{"MULTISOURCE4": true, "TXT=SAESTR": true}}, }, } db := newTestDatabase(t) for _, test := range tests { op, err := NewOperand(db, test.input) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(*op, test.op) { t.Errorf("parse(`%s`): output mismatch\nhave: %#v\nwant: %#v", test.input, op, test.op, ) } } } func TestReader(t *testing.T) { type test struct { name string input string output string } var tests []test { b, err := ioutil.ReadFile(path.Join("testdata", "xed_objects.txt")) if err != nil { t.Fatal(err) } cases := strings.Split(string(b), "------")[1:] for _, c := range cases { name := c[:strings.Index(c, "\n")] parts := strings.Split(c[len(name):], "====") tests = append(tests, test{ name: strings.TrimSpace(name), input: strings.TrimSpace(parts[0]), output: strings.TrimSpace(parts[1]), }) } } for _, test := range tests { r := NewReader(strings.NewReader(test.input)) objects, err := r.ReadAll() if strings.Contains(test.name, "INVALID") { if err == nil { t.Errorf("%s: expected non-nil error", test.name) continue } if err.Error() != test.output { t.Errorf("%s: error mismatch\nhave: `%s`\nwant: `%s`\n", test.name, err.Error(), test.output) } t.Logf("PASS: %s", test.name) continue } if err != nil { t.Fatal(err) } var have []map[string]string for _, o := range objects { for _, inst := range o.Insts { var result map[string]string err := json.Unmarshal([]byte(inst.String()), &result) if err != nil { t.Fatal(err) } have = append(have, result) } } var want []map[string]string err = json.Unmarshal([]byte(test.output), &want) if err != nil { t.Fatal(err) } for i := range want { for k := range want[i] { if want[i][k] == have[i][k] { continue } // i - index inside array of JSON objects. // k - i'th object key (example: "Iclass"). t.Errorf("%s: insts[%d].%s mismatch\nhave: `%s`\nwant: `%s`", test.name, i, k, have[i][k], want[i][k]) } } if !t.Failed() { t.Logf("PASS: %s", test.name) } } } func TestMacroExpand(t *testing.T) { tests := [...]struct { input string output string }{ 0: { "a not64 b c", "a MODE!=2 b c", }, 1: { "mode16 W0", "MODE=0 REXW=0 SKIP_OSZ=1", }, 2: { "W1 mode32", "REXW=1 SKIP_OSZ=1 MODE=1", }, 3: { "W1 W1", "REXW=1 SKIP_OSZ=1 REXW=1 SKIP_OSZ=1", }, 4: { "W1W1", "W1W1", }, 5: { "mode64 1 2 3 rexw_prefix", "MODE=2 1 2 3 REXW=1 SKIP_OSZ=1", }, 6: { "a b c", "a b c", }, 7: { "mode16 mode32 mode16 mode16", "MODE=0 MODE=1 MODE=0 MODE=0", }, 8: { "V0F38 V0FV0F V0FV0F38", "MAP=2 V0FV0F V0FV0F38", }, 9: { "VV1 0x2E V66 V0F38 VL128 norexw_prefix MOD[mm] MOD!=3 REG[rrr] RM[nnn] MODRM()", "VEXVALID=1 0x2E VEX_PREFIX=1 MAP=2 VL=0 REXW=0 SKIP_OSZ=1 MOD[mm] MOD!=3 REG[rrr] RM[nnn] MODRM()", }, } db := newTestDatabase(t) for id, test := range tests { have := ExpandStates(db, test.input) if test.output != have { t.Errorf("test %d: output mismatch:\nhave: `%s`\nwant: `%s`", id, have, test.output) } } }