Merge 2c9c8ea83388495f96efe4a0831d582e03e03e6f into ba4ad521151d3500de111a037937f8e1dd3922d2
3 files · +397 -1
M cmd/xtgen/main.go +31 -1
| @@ -1,4 +1,34 @@ | |||
| 1 | 1 | // Command xtgen generates bindings from the pinned xcb-proto XML. | |
| 2 | 2 | package main | |
| 3 | 3 | ||
| 4 | - | func main() {} | |
| 4 | + | import ( | |
| 5 | + | "flag" | |
| 6 | + | "fmt" | |
| 7 | + | "os" | |
| 8 | + | "path/filepath" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func main() { | |
| 12 | + | all := flag.Bool("all", false, "process all vendored modules") | |
| 13 | + | flag.Parse() | |
| 14 | + | paths := flag.Args() | |
| 15 | + | if *all { | |
| 16 | + | var err error | |
| 17 | + | paths, err = filepath.Glob("spec/xml/*.xml") | |
| 18 | + | if err != nil { | |
| 19 | + | fmt.Fprintln(os.Stderr, err) | |
| 20 | + | os.Exit(1) | |
| 21 | + | } | |
| 22 | + | } | |
| 23 | + | if len(paths) == 0 { | |
| 24 | + | fmt.Fprintln(os.Stderr, "xtgen: supply XML paths or -all") | |
| 25 | + | os.Exit(1) | |
| 26 | + | } | |
| 27 | + | // T2.1 validates and builds the AST. Resolution and emission follow in T2.2. | |
| 28 | + | for _, path := range paths { | |
| 29 | + | if _, err := parseFile(path); err != nil { | |
| 30 | + | fmt.Fprintln(os.Stderr, err) | |
| 31 | + | os.Exit(1) | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | } | |
| @@ -1,4 +1,34 @@ | |||||
| 1 | // Command xtgen generates bindings from the pinned xcb-proto XML. | 1 | // Command xtgen generates bindings from the pinned xcb-proto XML. | ||
| 2 | package main | 2 | package main | ||
| 3 | 3 | ||||
| 4 | - | func main() {} | 4 | + | import ( |
| 5 | + | "flag" | |||
| 6 | + | "fmt" | |||
| 7 | + | "os" | |||
| 8 | + | "path/filepath" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func main() { | |||
| 12 | + | all := flag.Bool("all", false, "process all vendored modules") | |||
| 13 | + | flag.Parse() | |||
| 14 | + | paths := flag.Args() | |||
| 15 | + | if *all { | |||
| 16 | + | var err error | |||
| 17 | + | paths, err = filepath.Glob("spec/xml/*.xml") | |||
| 18 | + | if err != nil { | |||
| 19 | + | fmt.Fprintln(os.Stderr, err) | |||
| 20 | + | os.Exit(1) | |||
| 21 | + | } | |||
| 22 | + | } | |||
| 23 | + | if len(paths) == 0 { | |||
| 24 | + | fmt.Fprintln(os.Stderr, "xtgen: supply XML paths or -all") | |||
| 25 | + | os.Exit(1) | |||
| 26 | + | } | |||
| 27 | + | // T2.1 validates and builds the AST. Resolution and emission follow in T2.2. | |||
| 28 | + | for _, path := range paths { | |||
| 29 | + | if _, err := parseFile(path); err != nil { | |||
| 30 | + | fmt.Fprintln(os.Stderr, err) | |||
| 31 | + | os.Exit(1) | |||
| 32 | + | } | |||
| 33 | + | } | |||
| 34 | + | } | |||
A cmd/xtgen/parse.go +292 -0
| @@ -0,0 +1,292 @@ | |||
| 1 | + | package main | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/xml" | |
| 5 | + | "fmt" | |
| 6 | + | "io" | |
| 7 | + | "os" | |
| 8 | + | "strconv" | |
| 9 | + | "strings" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // Node retains XML order and source locations, including expressions. Resolution | |
| 13 | + | // turns this syntax tree into the flat, typed emission IR; parsing makes no Go | |
| 14 | + | // naming or layout choices. | |
| 15 | + | type Node struct { | |
| 16 | + | Kind string | |
| 17 | + | Attr map[string]string | |
| 18 | + | Text string | |
| 19 | + | Children []*Node | |
| 20 | + | File string | |
| 21 | + | Line int | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | func (n *Node) fail(format string, args ...any) error { | |
| 25 | + | return fmt.Errorf("%s:%d: %s", n.File, n.Line, fmt.Sprintf(format, args...)) | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // Attribute and child whitelists are per element, not global: a known attribute | |
| 29 | + | // or element used in the wrong place is also an error. Documentation is ignored. | |
| 30 | + | var attributes = map[string]string{ | |
| 31 | + | "xcb": "header extension-name extension-xname extension-multiword major-version minor-version", | |
| 32 | + | "import": "", "typedef": "oldname newname", "xidtype": "name", "xidunion": "name", "type": "", | |
| 33 | + | "enum": "name", "item": "name", "value": "", "bit": "", "struct": "name", "union": "name", | |
| 34 | + | "request": "name opcode combine-adjacent", "reply": "", "event": "name number no-sequence-number xge", | |
| 35 | + | "eventcopy": "name number ref", "error": "name number type", "errorcopy": "name number ref", | |
| 36 | + | "field": "name type enum altenum mask", "pad": "bytes align", "list": "name type enum", | |
| 37 | + | "exprfield": "name type", "switch": "name", "bitcase": "", "fieldref": "", "enumref": "ref", | |
| 38 | + | "op": "op", "unop": "op", "popcount": "", "doc": "", | |
| 39 | + | } | |
| 40 | + | var children = map[string]string{ | |
| 41 | + | "xcb": "import typedef xidtype xidunion enum struct union request event eventcopy error errorcopy doc", | |
| 42 | + | "xidunion": "type", "enum": "item doc", "item": "value bit doc", | |
| 43 | + | "struct": "field pad list doc", "union": "field pad list doc", | |
| 44 | + | "request": "field pad list exprfield switch reply doc", "reply": "field pad list exprfield doc", | |
| 45 | + | "event": "field pad list exprfield doc", "error": "field pad doc", | |
| 46 | + | "list": "value fieldref op unop popcount enumref", "exprfield": "value fieldref op unop popcount enumref", | |
| 47 | + | "switch": "fieldref op unop popcount enumref bitcase", "bitcase": "enumref value field pad doc", | |
| 48 | + | "op": "value fieldref op unop popcount enumref", "unop": "value fieldref op unop popcount enumref", | |
| 49 | + | "popcount": "value fieldref op unop popcount enumref", | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | func contains(words, word string) bool { | |
| 53 | + | for _, s := range strings.Fields(words) { | |
| 54 | + | if s == word { | |
| 55 | + | return true | |
| 56 | + | } | |
| 57 | + | } | |
| 58 | + | return false | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | func parseFile(path string) (*Node, error) { | |
| 62 | + | f, err := os.Open(path) | |
| 63 | + | if err != nil { | |
| 64 | + | return nil, err | |
| 65 | + | } | |
| 66 | + | defer f.Close() | |
| 67 | + | return parseXML(path, f) | |
| 68 | + | } | |
| 69 | + | func parseXML(file string, r io.Reader) (*Node, error) { | |
| 70 | + | d := xml.NewDecoder(r) | |
| 71 | + | var root *Node | |
| 72 | + | var stack []*Node | |
| 73 | + | for { | |
| 74 | + | token, err := d.Token() | |
| 75 | + | if err == io.EOF { | |
| 76 | + | break | |
| 77 | + | } | |
| 78 | + | if err != nil { | |
| 79 | + | line, _ := d.InputPos() | |
| 80 | + | return nil, fmt.Errorf("%s:%d: %w", file, line, err) | |
| 81 | + | } | |
| 82 | + | switch t := token.(type) { | |
| 83 | + | case xml.StartElement: | |
| 84 | + | line, _ := d.InputPos() | |
| 85 | + | n := &Node{Kind: t.Name.Local, Attr: make(map[string]string), File: file, Line: line} | |
| 86 | + | allowed, ok := attributes[n.Kind] | |
| 87 | + | if !ok || t.Name.Space != "" { | |
| 88 | + | return nil, n.fail("unsupported element <%s>", t.Name.Local) | |
| 89 | + | } | |
| 90 | + | if len(stack) == 0 { | |
| 91 | + | if root != nil || n.Kind != "xcb" { | |
| 92 | + | return nil, n.fail("expected a single <xcb> root") | |
| 93 | + | } | |
| 94 | + | root = n | |
| 95 | + | } else { | |
| 96 | + | parent := stack[len(stack)-1] | |
| 97 | + | if !contains(children[parent.Kind], n.Kind) { | |
| 98 | + | return nil, n.fail("unsupported <%s> in <%s>", n.Kind, parent.Kind) | |
| 99 | + | } | |
| 100 | + | parent.Children = append(parent.Children, n) | |
| 101 | + | } | |
| 102 | + | if n.Kind == "doc" { | |
| 103 | + | if err := d.Skip(); err != nil { | |
| 104 | + | return nil, n.fail("invalid doc: %v", err) | |
| 105 | + | } | |
| 106 | + | continue | |
| 107 | + | } | |
| 108 | + | for _, a := range t.Attr { | |
| 109 | + | if a.Name.Space != "" || !contains(allowed, a.Name.Local) { | |
| 110 | + | return nil, n.fail("unknown attribute %q on <%s>", a.Name.Local, n.Kind) | |
| 111 | + | } | |
| 112 | + | if _, ok := n.Attr[a.Name.Local]; ok { | |
| 113 | + | return nil, n.fail("duplicate attribute %q", a.Name.Local) | |
| 114 | + | } | |
| 115 | + | n.Attr[a.Name.Local] = a.Value | |
| 116 | + | } | |
| 117 | + | stack = append(stack, n) | |
| 118 | + | case xml.EndElement: | |
| 119 | + | if len(stack) == 0 { | |
| 120 | + | return nil, fmt.Errorf("%s: unexpected closing element", file) | |
| 121 | + | } | |
| 122 | + | n := stack[len(stack)-1] | |
| 123 | + | n.Text = strings.TrimSpace(n.Text) | |
| 124 | + | if err := validateNode(n); err != nil { | |
| 125 | + | return nil, err | |
| 126 | + | } | |
| 127 | + | stack = stack[:len(stack)-1] | |
| 128 | + | case xml.CharData: | |
| 129 | + | if len(stack) > 0 { | |
| 130 | + | stack[len(stack)-1].Text += string(t) | |
| 131 | + | } else if strings.TrimSpace(string(t)) != "" { | |
| 132 | + | return nil, fmt.Errorf("%s: text outside root", file) | |
| 133 | + | } | |
| 134 | + | case xml.Directive: | |
| 135 | + | return nil, fmt.Errorf("%s: XML directives are unsupported", file) | |
| 136 | + | } | |
| 137 | + | } | |
| 138 | + | if root == nil { | |
| 139 | + | return nil, fmt.Errorf("%s:1: missing <xcb> root", file) | |
| 140 | + | } | |
| 141 | + | return root, nil | |
| 142 | + | } | |
| 143 | + | func require(n *Node, names string) error { | |
| 144 | + | for _, name := range strings.Fields(names) { | |
| 145 | + | if n.Attr[name] == "" { | |
| 146 | + | return n.fail("missing %s on <%s>", name, n.Kind) | |
| 147 | + | } | |
| 148 | + | } | |
| 149 | + | return nil | |
| 150 | + | } | |
| 151 | + | func number(n *Node, attr string, bits int) error { | |
| 152 | + | _, err := strconv.ParseUint(n.Attr[attr], 0, bits) | |
| 153 | + | if err != nil { | |
| 154 | + | return n.fail("invalid %s %q", attr, n.Attr[attr]) | |
| 155 | + | } | |
| 156 | + | return nil | |
| 157 | + | } | |
| 158 | + | func exprCount(n *Node) int { | |
| 159 | + | count := 0 | |
| 160 | + | for _, c := range n.Children { | |
| 161 | + | if c.Kind != "doc" { | |
| 162 | + | count++ | |
| 163 | + | } | |
| 164 | + | } | |
| 165 | + | return count | |
| 166 | + | } | |
| 167 | + | func validateNode(n *Node) error { | |
| 168 | + | required := map[string]string{ | |
| 169 | + | "xcb": "header", "typedef": "oldname newname", "xidtype": "name", "xidunion": "name", | |
| 170 | + | "enum": "name", "item": "name", "struct": "name", "union": "name", "request": "name opcode", | |
| 171 | + | "event": "name number", "eventcopy": "name number ref", "error": "name number", "errorcopy": "name number ref", | |
| 172 | + | "field": "name type", "list": "name type", "exprfield": "name type", "switch": "name", "enumref": "ref", "op": "op", "unop": "op", | |
| 173 | + | } | |
| 174 | + | if err := require(n, required[n.Kind]); err != nil { | |
| 175 | + | return err | |
| 176 | + | } | |
| 177 | + | switch n.Kind { | |
| 178 | + | case "import", "type", "fieldref", "enumref": | |
| 179 | + | if n.Text == "" { | |
| 180 | + | return n.fail("empty <%s>", n.Kind) | |
| 181 | + | } | |
| 182 | + | case "value", "bit": | |
| 183 | + | if _, err := strconv.ParseInt(n.Text, 0, 64); err != nil { | |
| 184 | + | return n.fail("invalid <%s> integer %q", n.Kind, n.Text) | |
| 185 | + | } | |
| 186 | + | case "pad": | |
| 187 | + | _, b := n.Attr["bytes"] | |
| 188 | + | _, a := n.Attr["align"] | |
| 189 | + | if b == a { | |
| 190 | + | return n.fail("pad requires exactly one of bytes or align") | |
| 191 | + | } | |
| 192 | + | key := "bytes" | |
| 193 | + | if a { | |
| 194 | + | key = "align" | |
| 195 | + | } | |
| 196 | + | if err := number(n, key, 31); err != nil { | |
| 197 | + | return err | |
| 198 | + | } | |
| 199 | + | if a { | |
| 200 | + | v, _ := strconv.Atoi(n.Attr[key]) | |
| 201 | + | if v == 0 || v&(v-1) != 0 { | |
| 202 | + | return n.fail("pad alignment must be a positive power of two") | |
| 203 | + | } | |
| 204 | + | } | |
| 205 | + | case "request": | |
| 206 | + | if err := number(n, "opcode", 8); err != nil { | |
| 207 | + | return err | |
| 208 | + | } | |
| 209 | + | case "event", "eventcopy", "error", "errorcopy": | |
| 210 | + | if err := number(n, "number", 8); err != nil { | |
| 211 | + | return err | |
| 212 | + | } | |
| 213 | + | if n.Attr["xge"] == "true" { | |
| 214 | + | v, _ := strconv.Atoi(n.Attr["number"]) | |
| 215 | + | if v >= 128 { | |
| 216 | + | return n.fail("xge evtype must be < 128") | |
| 217 | + | } | |
| 218 | + | } | |
| 219 | + | case "op": | |
| 220 | + | if !contains("+ - * / & | << >>", n.Attr["op"]) { | |
| 221 | + | return n.fail("unsupported operator %q", n.Attr["op"]) | |
| 222 | + | } | |
| 223 | + | if exprCount(n) != 2 { | |
| 224 | + | return n.fail("op requires two operands") | |
| 225 | + | } | |
| 226 | + | case "unop": | |
| 227 | + | if n.Attr["op"] != "~" { | |
| 228 | + | return n.fail("unsupported unary operator %q", n.Attr["op"]) | |
| 229 | + | } | |
| 230 | + | if exprCount(n) != 1 { | |
| 231 | + | return n.fail("unop requires one operand") | |
| 232 | + | } | |
| 233 | + | case "popcount", "exprfield", "item": | |
| 234 | + | if exprCount(n) != 1 { | |
| 235 | + | return n.fail("<%s> requires one expression", n.Kind) | |
| 236 | + | } | |
| 237 | + | case "list": | |
| 238 | + | if exprCount(n) > 1 { | |
| 239 | + | return n.fail("list has multiple length expressions") | |
| 240 | + | } | |
| 241 | + | case "bitcase": | |
| 242 | + | fields := 0 | |
| 243 | + | for _, c := range n.Children { | |
| 244 | + | if c.Kind == "field" { | |
| 245 | + | fields++ | |
| 246 | + | if contains("CARD8 CARD16 INT8 INT16 BOOL char BYTE void", c.Attr["type"]) { | |
| 247 | + | return c.fail("value-list bitcase requires a 4-byte field") | |
| 248 | + | } | |
| 249 | + | } | |
| 250 | + | } | |
| 251 | + | if fields != 1 { | |
| 252 | + | return n.fail("value-list bitcase requires exactly one field") | |
| 253 | + | } | |
| 254 | + | } | |
| 255 | + | for _, key := range []string{"xge", "no-sequence-number", "combine-adjacent", "extension-multiword"} { | |
| 256 | + | if v, ok := n.Attr[key]; ok && v != "true" && v != "false" { | |
| 257 | + | return n.fail("invalid boolean %s=%q", key, v) | |
| 258 | + | } | |
| 259 | + | } | |
| 260 | + | // Forward references cannot be laid out in one pass. Reject at the length | |
| 261 | + | // expression's location rather than failing later while emitting Go. | |
| 262 | + | declared := map[string]bool{} | |
| 263 | + | for _, c := range n.Children { | |
| 264 | + | if c.Kind == "field" || c.Kind == "exprfield" { | |
| 265 | + | declared[c.Attr["name"]] = true | |
| 266 | + | } | |
| 267 | + | } | |
| 268 | + | seen := map[string]bool{} | |
| 269 | + | for _, c := range n.Children { | |
| 270 | + | if c.Kind == "list" { | |
| 271 | + | var visit func(*Node) error | |
| 272 | + | visit = func(e *Node) error { | |
| 273 | + | if e.Kind == "fieldref" && declared[e.Text] && !seen[e.Text] { | |
| 274 | + | return e.fail("list length references later field %q", e.Text) | |
| 275 | + | } | |
| 276 | + | for _, child := range e.Children { | |
| 277 | + | if err := visit(child); err != nil { | |
| 278 | + | return err | |
| 279 | + | } | |
| 280 | + | } | |
| 281 | + | return nil | |
| 282 | + | } | |
| 283 | + | if err := visit(c); err != nil { | |
| 284 | + | return err | |
| 285 | + | } | |
| 286 | + | } | |
| 287 | + | if c.Kind == "field" || c.Kind == "exprfield" { | |
| 288 | + | seen[c.Attr["name"]] = true | |
| 289 | + | } | |
| 290 | + | } | |
| 291 | + | return nil | |
| 292 | + | } | |
| @@ -0,0 +1,292 @@ | |||||
| 1 | + | package main | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/xml" | |||
| 5 | + | "fmt" | |||
| 6 | + | "io" | |||
| 7 | + | "os" | |||
| 8 | + | "strconv" | |||
| 9 | + | "strings" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // Node retains XML order and source locations, including expressions. Resolution | |||
| 13 | + | // turns this syntax tree into the flat, typed emission IR; parsing makes no Go | |||
| 14 | + | // naming or layout choices. | |||
| 15 | + | type Node struct { | |||
| 16 | + | Kind string | |||
| 17 | + | Attr map[string]string | |||
| 18 | + | Text string | |||
| 19 | + | Children []*Node | |||
| 20 | + | File string | |||
| 21 | + | Line int | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | func (n *Node) fail(format string, args ...any) error { | |||
| 25 | + | return fmt.Errorf("%s:%d: %s", n.File, n.Line, fmt.Sprintf(format, args...)) | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // Attribute and child whitelists are per element, not global: a known attribute | |||
| 29 | + | // or element used in the wrong place is also an error. Documentation is ignored. | |||
| 30 | + | var attributes = map[string]string{ | |||
| 31 | + | "xcb": "header extension-name extension-xname extension-multiword major-version minor-version", | |||
| 32 | + | "import": "", "typedef": "oldname newname", "xidtype": "name", "xidunion": "name", "type": "", | |||
| 33 | + | "enum": "name", "item": "name", "value": "", "bit": "", "struct": "name", "union": "name", | |||
| 34 | + | "request": "name opcode combine-adjacent", "reply": "", "event": "name number no-sequence-number xge", | |||
| 35 | + | "eventcopy": "name number ref", "error": "name number type", "errorcopy": "name number ref", | |||
| 36 | + | "field": "name type enum altenum mask", "pad": "bytes align", "list": "name type enum", | |||
| 37 | + | "exprfield": "name type", "switch": "name", "bitcase": "", "fieldref": "", "enumref": "ref", | |||
| 38 | + | "op": "op", "unop": "op", "popcount": "", "doc": "", | |||
| 39 | + | } | |||
| 40 | + | var children = map[string]string{ | |||
| 41 | + | "xcb": "import typedef xidtype xidunion enum struct union request event eventcopy error errorcopy doc", | |||
| 42 | + | "xidunion": "type", "enum": "item doc", "item": "value bit doc", | |||
| 43 | + | "struct": "field pad list doc", "union": "field pad list doc", | |||
| 44 | + | "request": "field pad list exprfield switch reply doc", "reply": "field pad list exprfield doc", | |||
| 45 | + | "event": "field pad list exprfield doc", "error": "field pad doc", | |||
| 46 | + | "list": "value fieldref op unop popcount enumref", "exprfield": "value fieldref op unop popcount enumref", | |||
| 47 | + | "switch": "fieldref op unop popcount enumref bitcase", "bitcase": "enumref value field pad doc", | |||
| 48 | + | "op": "value fieldref op unop popcount enumref", "unop": "value fieldref op unop popcount enumref", | |||
| 49 | + | "popcount": "value fieldref op unop popcount enumref", | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | func contains(words, word string) bool { | |||
| 53 | + | for _, s := range strings.Fields(words) { | |||
| 54 | + | if s == word { | |||
| 55 | + | return true | |||
| 56 | + | } | |||
| 57 | + | } | |||
| 58 | + | return false | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | func parseFile(path string) (*Node, error) { | |||
| 62 | + | f, err := os.Open(path) | |||
| 63 | + | if err != nil { | |||
| 64 | + | return nil, err | |||
| 65 | + | } | |||
| 66 | + | defer f.Close() | |||
| 67 | + | return parseXML(path, f) | |||
| 68 | + | } | |||
| 69 | + | func parseXML(file string, r io.Reader) (*Node, error) { | |||
| 70 | + | d := xml.NewDecoder(r) | |||
| 71 | + | var root *Node | |||
| 72 | + | var stack []*Node | |||
| 73 | + | for { | |||
| 74 | + | token, err := d.Token() | |||
| 75 | + | if err == io.EOF { | |||
| 76 | + | break | |||
| 77 | + | } | |||
| 78 | + | if err != nil { | |||
| 79 | + | line, _ := d.InputPos() | |||
| 80 | + | return nil, fmt.Errorf("%s:%d: %w", file, line, err) | |||
| 81 | + | } | |||
| 82 | + | switch t := token.(type) { | |||
| 83 | + | case xml.StartElement: | |||
| 84 | + | line, _ := d.InputPos() | |||
| 85 | + | n := &Node{Kind: t.Name.Local, Attr: make(map[string]string), File: file, Line: line} | |||
| 86 | + | allowed, ok := attributes[n.Kind] | |||
| 87 | + | if !ok || t.Name.Space != "" { | |||
| 88 | + | return nil, n.fail("unsupported element <%s>", t.Name.Local) | |||
| 89 | + | } | |||
| 90 | + | if len(stack) == 0 { | |||
| 91 | + | if root != nil || n.Kind != "xcb" { | |||
| 92 | + | return nil, n.fail("expected a single <xcb> root") | |||
| 93 | + | } | |||
| 94 | + | root = n | |||
| 95 | + | } else { | |||
| 96 | + | parent := stack[len(stack)-1] | |||
| 97 | + | if !contains(children[parent.Kind], n.Kind) { | |||
| 98 | + | return nil, n.fail("unsupported <%s> in <%s>", n.Kind, parent.Kind) | |||
| 99 | + | } | |||
| 100 | + | parent.Children = append(parent.Children, n) | |||
| 101 | + | } | |||
| 102 | + | if n.Kind == "doc" { | |||
| 103 | + | if err := d.Skip(); err != nil { | |||
| 104 | + | return nil, n.fail("invalid doc: %v", err) | |||
| 105 | + | } | |||
| 106 | + | continue | |||
| 107 | + | } | |||
| 108 | + | for _, a := range t.Attr { | |||
| 109 | + | if a.Name.Space != "" || !contains(allowed, a.Name.Local) { | |||
| 110 | + | return nil, n.fail("unknown attribute %q on <%s>", a.Name.Local, n.Kind) | |||
| 111 | + | } | |||
| 112 | + | if _, ok := n.Attr[a.Name.Local]; ok { | |||
| 113 | + | return nil, n.fail("duplicate attribute %q", a.Name.Local) | |||
| 114 | + | } | |||
| 115 | + | n.Attr[a.Name.Local] = a.Value | |||
| 116 | + | } | |||
| 117 | + | stack = append(stack, n) | |||
| 118 | + | case xml.EndElement: | |||
| 119 | + | if len(stack) == 0 { | |||
| 120 | + | return nil, fmt.Errorf("%s: unexpected closing element", file) | |||
| 121 | + | } | |||
| 122 | + | n := stack[len(stack)-1] | |||
| 123 | + | n.Text = strings.TrimSpace(n.Text) | |||
| 124 | + | if err := validateNode(n); err != nil { | |||
| 125 | + | return nil, err | |||
| 126 | + | } | |||
| 127 | + | stack = stack[:len(stack)-1] | |||
| 128 | + | case xml.CharData: | |||
| 129 | + | if len(stack) > 0 { | |||
| 130 | + | stack[len(stack)-1].Text += string(t) | |||
| 131 | + | } else if strings.TrimSpace(string(t)) != "" { | |||
| 132 | + | return nil, fmt.Errorf("%s: text outside root", file) | |||
| 133 | + | } | |||
| 134 | + | case xml.Directive: | |||
| 135 | + | return nil, fmt.Errorf("%s: XML directives are unsupported", file) | |||
| 136 | + | } | |||
| 137 | + | } | |||
| 138 | + | if root == nil { | |||
| 139 | + | return nil, fmt.Errorf("%s:1: missing <xcb> root", file) | |||
| 140 | + | } | |||
| 141 | + | return root, nil | |||
| 142 | + | } | |||
| 143 | + | func require(n *Node, names string) error { | |||
| 144 | + | for _, name := range strings.Fields(names) { | |||
| 145 | + | if n.Attr[name] == "" { | |||
| 146 | + | return n.fail("missing %s on <%s>", name, n.Kind) | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | return nil | |||
| 150 | + | } | |||
| 151 | + | func number(n *Node, attr string, bits int) error { | |||
| 152 | + | _, err := strconv.ParseUint(n.Attr[attr], 0, bits) | |||
| 153 | + | if err != nil { | |||
| 154 | + | return n.fail("invalid %s %q", attr, n.Attr[attr]) | |||
| 155 | + | } | |||
| 156 | + | return nil | |||
| 157 | + | } | |||
| 158 | + | func exprCount(n *Node) int { | |||
| 159 | + | count := 0 | |||
| 160 | + | for _, c := range n.Children { | |||
| 161 | + | if c.Kind != "doc" { | |||
| 162 | + | count++ | |||
| 163 | + | } | |||
| 164 | + | } | |||
| 165 | + | return count | |||
| 166 | + | } | |||
| 167 | + | func validateNode(n *Node) error { | |||
| 168 | + | required := map[string]string{ | |||
| 169 | + | "xcb": "header", "typedef": "oldname newname", "xidtype": "name", "xidunion": "name", | |||
| 170 | + | "enum": "name", "item": "name", "struct": "name", "union": "name", "request": "name opcode", | |||
| 171 | + | "event": "name number", "eventcopy": "name number ref", "error": "name number", "errorcopy": "name number ref", | |||
| 172 | + | "field": "name type", "list": "name type", "exprfield": "name type", "switch": "name", "enumref": "ref", "op": "op", "unop": "op", | |||
| 173 | + | } | |||
| 174 | + | if err := require(n, required[n.Kind]); err != nil { | |||
| 175 | + | return err | |||
| 176 | + | } | |||
| 177 | + | switch n.Kind { | |||
| 178 | + | case "import", "type", "fieldref", "enumref": | |||
| 179 | + | if n.Text == "" { | |||
| 180 | + | return n.fail("empty <%s>", n.Kind) | |||
| 181 | + | } | |||
| 182 | + | case "value", "bit": | |||
| 183 | + | if _, err := strconv.ParseInt(n.Text, 0, 64); err != nil { | |||
| 184 | + | return n.fail("invalid <%s> integer %q", n.Kind, n.Text) | |||
| 185 | + | } | |||
| 186 | + | case "pad": | |||
| 187 | + | _, b := n.Attr["bytes"] | |||
| 188 | + | _, a := n.Attr["align"] | |||
| 189 | + | if b == a { | |||
| 190 | + | return n.fail("pad requires exactly one of bytes or align") | |||
| 191 | + | } | |||
| 192 | + | key := "bytes" | |||
| 193 | + | if a { | |||
| 194 | + | key = "align" | |||
| 195 | + | } | |||
| 196 | + | if err := number(n, key, 31); err != nil { | |||
| 197 | + | return err | |||
| 198 | + | } | |||
| 199 | + | if a { | |||
| 200 | + | v, _ := strconv.Atoi(n.Attr[key]) | |||
| 201 | + | if v == 0 || v&(v-1) != 0 { | |||
| 202 | + | return n.fail("pad alignment must be a positive power of two") | |||
| 203 | + | } | |||
| 204 | + | } | |||
| 205 | + | case "request": | |||
| 206 | + | if err := number(n, "opcode", 8); err != nil { | |||
| 207 | + | return err | |||
| 208 | + | } | |||
| 209 | + | case "event", "eventcopy", "error", "errorcopy": | |||
| 210 | + | if err := number(n, "number", 8); err != nil { | |||
| 211 | + | return err | |||
| 212 | + | } | |||
| 213 | + | if n.Attr["xge"] == "true" { | |||
| 214 | + | v, _ := strconv.Atoi(n.Attr["number"]) | |||
| 215 | + | if v >= 128 { | |||
| 216 | + | return n.fail("xge evtype must be < 128") | |||
| 217 | + | } | |||
| 218 | + | } | |||
| 219 | + | case "op": | |||
| 220 | + | if !contains("+ - * / & | << >>", n.Attr["op"]) { | |||
| 221 | + | return n.fail("unsupported operator %q", n.Attr["op"]) | |||
| 222 | + | } | |||
| 223 | + | if exprCount(n) != 2 { | |||
| 224 | + | return n.fail("op requires two operands") | |||
| 225 | + | } | |||
| 226 | + | case "unop": | |||
| 227 | + | if n.Attr["op"] != "~" { | |||
| 228 | + | return n.fail("unsupported unary operator %q", n.Attr["op"]) | |||
| 229 | + | } | |||
| 230 | + | if exprCount(n) != 1 { | |||
| 231 | + | return n.fail("unop requires one operand") | |||
| 232 | + | } | |||
| 233 | + | case "popcount", "exprfield", "item": | |||
| 234 | + | if exprCount(n) != 1 { | |||
| 235 | + | return n.fail("<%s> requires one expression", n.Kind) | |||
| 236 | + | } | |||
| 237 | + | case "list": | |||
| 238 | + | if exprCount(n) > 1 { | |||
| 239 | + | return n.fail("list has multiple length expressions") | |||
| 240 | + | } | |||
| 241 | + | case "bitcase": | |||
| 242 | + | fields := 0 | |||
| 243 | + | for _, c := range n.Children { | |||
| 244 | + | if c.Kind == "field" { | |||
| 245 | + | fields++ | |||
| 246 | + | if contains("CARD8 CARD16 INT8 INT16 BOOL char BYTE void", c.Attr["type"]) { | |||
| 247 | + | return c.fail("value-list bitcase requires a 4-byte field") | |||
| 248 | + | } | |||
| 249 | + | } | |||
| 250 | + | } | |||
| 251 | + | if fields != 1 { | |||
| 252 | + | return n.fail("value-list bitcase requires exactly one field") | |||
| 253 | + | } | |||
| 254 | + | } | |||
| 255 | + | for _, key := range []string{"xge", "no-sequence-number", "combine-adjacent", "extension-multiword"} { | |||
| 256 | + | if v, ok := n.Attr[key]; ok && v != "true" && v != "false" { | |||
| 257 | + | return n.fail("invalid boolean %s=%q", key, v) | |||
| 258 | + | } | |||
| 259 | + | } | |||
| 260 | + | // Forward references cannot be laid out in one pass. Reject at the length | |||
| 261 | + | // expression's location rather than failing later while emitting Go. | |||
| 262 | + | declared := map[string]bool{} | |||
| 263 | + | for _, c := range n.Children { | |||
| 264 | + | if c.Kind == "field" || c.Kind == "exprfield" { | |||
| 265 | + | declared[c.Attr["name"]] = true | |||
| 266 | + | } | |||
| 267 | + | } | |||
| 268 | + | seen := map[string]bool{} | |||
| 269 | + | for _, c := range n.Children { | |||
| 270 | + | if c.Kind == "list" { | |||
| 271 | + | var visit func(*Node) error | |||
| 272 | + | visit = func(e *Node) error { | |||
| 273 | + | if e.Kind == "fieldref" && declared[e.Text] && !seen[e.Text] { | |||
| 274 | + | return e.fail("list length references later field %q", e.Text) | |||
| 275 | + | } | |||
| 276 | + | for _, child := range e.Children { | |||
| 277 | + | if err := visit(child); err != nil { | |||
| 278 | + | return err | |||
| 279 | + | } | |||
| 280 | + | } | |||
| 281 | + | return nil | |||
| 282 | + | } | |||
| 283 | + | if err := visit(c); err != nil { | |||
| 284 | + | return err | |||
| 285 | + | } | |||
| 286 | + | } | |||
| 287 | + | if c.Kind == "field" || c.Kind == "exprfield" { | |||
| 288 | + | seen[c.Attr["name"]] = true | |||
| 289 | + | } | |||
| 290 | + | } | |||
| 291 | + | return nil | |||
| 292 | + | } | |||
A cmd/xtgen/parse_test.go +74 -0
| @@ -0,0 +1,74 @@ | |||
| 1 | + | package main | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "path/filepath" | |
| 5 | + | "strings" | |
| 6 | + | "testing" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestParseVendored(t *testing.T) { | |
| 10 | + | files, err := filepath.Glob("../../spec/xml/*.xml") | |
| 11 | + | if err != nil { | |
| 12 | + | t.Fatal(err) | |
| 13 | + | } | |
| 14 | + | if len(files) != 9 { | |
| 15 | + | t.Fatalf("got %d XML files, want 9", len(files)) | |
| 16 | + | } | |
| 17 | + | for _, file := range files { | |
| 18 | + | t.Run(filepath.Base(file), func(t *testing.T) { | |
| 19 | + | n, err := parseFile(file) | |
| 20 | + | if err != nil { | |
| 21 | + | t.Fatal(err) | |
| 22 | + | } | |
| 23 | + | if n.Kind != "xcb" || len(n.Children) == 0 { | |
| 24 | + | t.Fatal("missing module AST") | |
| 25 | + | } | |
| 26 | + | }) | |
| 27 | + | } | |
| 28 | + | } | |
| 29 | + | func TestParseRejects(t *testing.T) { | |
| 30 | + | cases := []struct{ name, body, want string }{ | |
| 31 | + | {"unknown element", "<surprise/>", "unsupported element"}, | |
| 32 | + | {"unknown attribute", `<xidtype name="WINDOW" surprise="yes"/>`, "unknown attribute"}, | |
| 33 | + | {"misplaced known element", `<struct name="S"><reply/></struct>`, "unsupported <reply>"}, | |
| 34 | + | {"sumof", `<request name="R" opcode="1"><list name="a" type="CARD8"><sumof ref="a"/></list></request>`, "sumof"}, | |
| 35 | + | {"paramref", `<request name="R" opcode="1"><list name="a" type="CARD8"><paramref type="CARD8">a</paramref></list></request>`, "paramref"}, | |
| 36 | + | {"case", `<request name="R" opcode="1"><switch name="s"><case/></switch></request>`, "case"}, | |
| 37 | + | {"struct switch", `<struct name="S"><switch name="s"/></struct>`, "unsupported <switch>"}, | |
| 38 | + | {"reply switch", `<request name="R" opcode="1"><reply><switch name="s"/></reply></request>`, "unsupported <switch>"}, | |
| 39 | + | {"nested switch", `<request name="R" opcode="1"><switch name="s"><bitcase><switch name="s"/></bitcase></switch></request>`, "unsupported <switch>"}, | |
| 40 | + | {"fd", `<request name="R" opcode="1"><fd name="fd"/></request>`, "fd"}, | |
| 41 | + | {"eventstruct", `<eventstruct name="E"/>`, "eventstruct"}, | |
| 42 | + | {"required alignment", `<struct name="S"><required_start_align align="4"/></struct>`, "required_start_align"}, | |
| 43 | + | {"forward length", `<struct name="S"><list name="a" type="CARD8"><fieldref>n</fieldref></list><field name="n" type="CARD8"/></struct>`, "later field"}, | |
| 44 | + | {"xge code", `<event name="E" number="128" xge="true"/>`, "evtype"}, | |
| 45 | + | {"bad opcode", `<request name="R" opcode="256"/>`, "invalid opcode"}, | |
| 46 | + | {"bad operator", `<request name="R" opcode="1"><exprfield name="x" type="CARD32"><op op="%"><value>1</value><value>2</value></op></exprfield></request>`, "unsupported operator"}, | |
| 47 | + | {"wrong operands", `<request name="R" opcode="1"><exprfield name="x" type="CARD32"><op op="+"><value>1</value></op></exprfield></request>`, "two operands"}, | |
| 48 | + | {"bad pad", `<struct name="S"><pad align="3"/></struct>`, "power of two"}, | |
| 49 | + | {"namespace", `<xidtype xmlns="other" name="X"/>`, "unsupported element"}, | |
| 50 | + | {"duplicate attr", `<xidtype name="X" name="Y"/>`, "duplicate attribute"}, | |
| 51 | + | } | |
| 52 | + | for _, tc := range cases { | |
| 53 | + | t.Run(tc.name, func(t *testing.T) { | |
| 54 | + | _, err := parseXML("bad.xml", strings.NewReader("<xcb header=\"test\">\n"+tc.body+"\n</xcb>")) | |
| 55 | + | if err == nil || !strings.Contains(err.Error(), tc.want) || !strings.Contains(err.Error(), "bad.xml:2:") { | |
| 56 | + | t.Fatalf("got %v, want file:line and %q", err, tc.want) | |
| 57 | + | } | |
| 58 | + | }) | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | func TestParseAST(t *testing.T) { | |
| 62 | + | n, err := parseXML("ok.xml", strings.NewReader(`<xcb header="a"><doc><arbitrary/></doc><request name="R" opcode="1"><field type="CARD32" name="n"/><list type="CARD8" name="data"><op op="*"><fieldref>n</fieldref><value>4</value></op></list></request></xcb>`)) | |
| 63 | + | if err != nil { | |
| 64 | + | t.Fatal(err) | |
| 65 | + | } | |
| 66 | + | r := n.Children[1] | |
| 67 | + | if r.Attr["name"] != "R" || r.Children[1].Children[0].Children[0].Text != "n" { | |
| 68 | + | t.Fatal("AST lost order or expression") | |
| 69 | + | } | |
| 70 | + | } | |
| 71 | + | func FuzzParseXML(f *testing.F) { | |
| 72 | + | f.Add(`<xcb header="test"/>`) | |
| 73 | + | f.Fuzz(func(t *testing.T, raw string) { _, _ = parseXML("fuzz.xml", strings.NewReader(raw)) }) | |
| 74 | + | } | |
| @@ -0,0 +1,74 @@ | |||||
| 1 | + | package main | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "path/filepath" | |||
| 5 | + | "strings" | |||
| 6 | + | "testing" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestParseVendored(t *testing.T) { | |||
| 10 | + | files, err := filepath.Glob("../../spec/xml/*.xml") | |||
| 11 | + | if err != nil { | |||
| 12 | + | t.Fatal(err) | |||
| 13 | + | } | |||
| 14 | + | if len(files) != 9 { | |||
| 15 | + | t.Fatalf("got %d XML files, want 9", len(files)) | |||
| 16 | + | } | |||
| 17 | + | for _, file := range files { | |||
| 18 | + | t.Run(filepath.Base(file), func(t *testing.T) { | |||
| 19 | + | n, err := parseFile(file) | |||
| 20 | + | if err != nil { | |||
| 21 | + | t.Fatal(err) | |||
| 22 | + | } | |||
| 23 | + | if n.Kind != "xcb" || len(n.Children) == 0 { | |||
| 24 | + | t.Fatal("missing module AST") | |||
| 25 | + | } | |||
| 26 | + | }) | |||
| 27 | + | } | |||
| 28 | + | } | |||
| 29 | + | func TestParseRejects(t *testing.T) { | |||
| 30 | + | cases := []struct{ name, body, want string }{ | |||
| 31 | + | {"unknown element", "<surprise/>", "unsupported element"}, | |||
| 32 | + | {"unknown attribute", `<xidtype name="WINDOW" surprise="yes"/>`, "unknown attribute"}, | |||
| 33 | + | {"misplaced known element", `<struct name="S"><reply/></struct>`, "unsupported <reply>"}, | |||
| 34 | + | {"sumof", `<request name="R" opcode="1"><list name="a" type="CARD8"><sumof ref="a"/></list></request>`, "sumof"}, | |||
| 35 | + | {"paramref", `<request name="R" opcode="1"><list name="a" type="CARD8"><paramref type="CARD8">a</paramref></list></request>`, "paramref"}, | |||
| 36 | + | {"case", `<request name="R" opcode="1"><switch name="s"><case/></switch></request>`, "case"}, | |||
| 37 | + | {"struct switch", `<struct name="S"><switch name="s"/></struct>`, "unsupported <switch>"}, | |||
| 38 | + | {"reply switch", `<request name="R" opcode="1"><reply><switch name="s"/></reply></request>`, "unsupported <switch>"}, | |||
| 39 | + | {"nested switch", `<request name="R" opcode="1"><switch name="s"><bitcase><switch name="s"/></bitcase></switch></request>`, "unsupported <switch>"}, | |||
| 40 | + | {"fd", `<request name="R" opcode="1"><fd name="fd"/></request>`, "fd"}, | |||
| 41 | + | {"eventstruct", `<eventstruct name="E"/>`, "eventstruct"}, | |||
| 42 | + | {"required alignment", `<struct name="S"><required_start_align align="4"/></struct>`, "required_start_align"}, | |||
| 43 | + | {"forward length", `<struct name="S"><list name="a" type="CARD8"><fieldref>n</fieldref></list><field name="n" type="CARD8"/></struct>`, "later field"}, | |||
| 44 | + | {"xge code", `<event name="E" number="128" xge="true"/>`, "evtype"}, | |||
| 45 | + | {"bad opcode", `<request name="R" opcode="256"/>`, "invalid opcode"}, | |||
| 46 | + | {"bad operator", `<request name="R" opcode="1"><exprfield name="x" type="CARD32"><op op="%"><value>1</value><value>2</value></op></exprfield></request>`, "unsupported operator"}, | |||
| 47 | + | {"wrong operands", `<request name="R" opcode="1"><exprfield name="x" type="CARD32"><op op="+"><value>1</value></op></exprfield></request>`, "two operands"}, | |||
| 48 | + | {"bad pad", `<struct name="S"><pad align="3"/></struct>`, "power of two"}, | |||
| 49 | + | {"namespace", `<xidtype xmlns="other" name="X"/>`, "unsupported element"}, | |||
| 50 | + | {"duplicate attr", `<xidtype name="X" name="Y"/>`, "duplicate attribute"}, | |||
| 51 | + | } | |||
| 52 | + | for _, tc := range cases { | |||
| 53 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 54 | + | _, err := parseXML("bad.xml", strings.NewReader("<xcb header=\"test\">\n"+tc.body+"\n</xcb>")) | |||
| 55 | + | if err == nil || !strings.Contains(err.Error(), tc.want) || !strings.Contains(err.Error(), "bad.xml:2:") { | |||
| 56 | + | t.Fatalf("got %v, want file:line and %q", err, tc.want) | |||
| 57 | + | } | |||
| 58 | + | }) | |||
| 59 | + | } | |||
| 60 | + | } | |||
| 61 | + | func TestParseAST(t *testing.T) { | |||
| 62 | + | n, err := parseXML("ok.xml", strings.NewReader(`<xcb header="a"><doc><arbitrary/></doc><request name="R" opcode="1"><field type="CARD32" name="n"/><list type="CARD8" name="data"><op op="*"><fieldref>n</fieldref><value>4</value></op></list></request></xcb>`)) | |||
| 63 | + | if err != nil { | |||
| 64 | + | t.Fatal(err) | |||
| 65 | + | } | |||
| 66 | + | r := n.Children[1] | |||
| 67 | + | if r.Attr["name"] != "R" || r.Children[1].Children[0].Children[0].Text != "n" { | |||
| 68 | + | t.Fatal("AST lost order or expression") | |||
| 69 | + | } | |||
| 70 | + | } | |||
| 71 | + | func FuzzParseXML(f *testing.F) { | |||
| 72 | + | f.Add(`<xcb header="test"/>`) | |||
| 73 | + | f.Fuzz(func(t *testing.T, raw string) { _, _ = parseXML("fuzz.xml", strings.NewReader(raw)) }) | |||
| 74 | + | } | |||