mxh.go raw
1 // Package parse provides a parser for .mxh protocol header files.
2 package parse
3
4 import (
5 "bufio"
6 "fmt"
7 "io"
8 "strings"
9 )
10
11 // MXH is the parsed content of a .mxh file.
12 type MXH struct {
13 Hash string // FNV-1a hex hash of the body (from header line)
14 Types []ExternalTypeDef
15 }
16
17 // ExternalTypeDef is a single codec type exported in a .mxh.
18 type ExternalTypeDef struct {
19 Name string
20 Fields []FieldDef
21 HasEncodeTo bool
22 HasDecodeFrom bool
23 }
24
25 // FieldDef is a single exported fixed-width struct field.
26 type FieldDef struct {
27 Name string
28 Type string // e.g. "uint32", "[20]byte"
29 }
30
31 // ParseMXH reads a .mxh file from r and returns the parsed header.
32 func ParseMXH(r io.Reader) (*MXH, error) {
33 sc := bufio.NewScanner(r)
34
35 // First line must be the hash header.
36 if !sc.Scan() {
37 return nil, fmt.Errorf("mxh: empty file")
38 }
39 header := sc.Text()
40 hash, err := parseHashLine(header)
41 if err != nil {
42 return nil, err
43 }
44
45 mxh := &MXH{Hash: hash}
46 var cur *ExternalTypeDef
47
48 for sc.Scan() {
49 line := strings.TrimSpace(sc.Text())
50 if line == "" || strings.HasPrefix(line, "//") {
51 continue
52 }
53
54 switch {
55 case strings.HasPrefix(line, "type ") && strings.HasSuffix(line, " struct {"):
56 // type Foo struct {
57 name := line[len("type ") : len(line)-len(" struct {")]
58 name = strings.TrimSpace(name)
59 mxh.Types = append(mxh.Types, ExternalTypeDef{Name: name})
60 cur = &mxh.Types[len(mxh.Types)-1]
61
62 case line == "}":
63 cur = nil
64
65 case strings.HasPrefix(line, "func (") && strings.Contains(line, ") EncodeTo("):
66 if cur == nil {
67 // method signature for most recent type
68 cur = findTypeBySig(mxh, line)
69 }
70 if cur != nil {
71 cur.HasEncodeTo = true
72 cur = nil
73 }
74
75 case strings.HasPrefix(line, "func (*") && strings.Contains(line, ") DecodeFrom("):
76 name := extractMethodReceiverName(line)
77 if t := findTypeByName(mxh, name); t != nil {
78 t.HasDecodeFrom = true
79 }
80
81 case cur != nil:
82 // Field line inside struct block: " Name Type"
83 parts := strings.Fields(line)
84 if len(parts) == 2 {
85 cur.Fields = append(cur.Fields, FieldDef{Name: parts[0], Type: parts[1]})
86 }
87 }
88 }
89 if err := sc.Err(); err != nil {
90 return nil, fmt.Errorf("mxh: scan error: %w", err)
91 }
92 return mxh, nil
93 }
94
95 func parseHashLine(line string) (string, error) {
96 // Expected: "// mxh v1 <hex>"
97 const prefix = "// mxh v1 "
98 if !strings.HasPrefix(line, prefix) {
99 return "", fmt.Errorf("mxh: invalid header line %q (expected %q...)", line, prefix)
100 }
101 hash := strings.TrimSpace(line[len(prefix):])
102 if hash == "" {
103 return "", fmt.Errorf("mxh: empty hash in header line")
104 }
105 return hash, nil
106 }
107
108 // findTypeBySig extracts the receiver type name from a value-receiver
109 // EncodeTo signature: "func (Foo) EncodeTo(..."
110 func findTypeBySig(mxh *MXH, line string) *ExternalTypeDef {
111 // "func (Name) EncodeTo..."
112 start := strings.Index(line, "(")
113 end := strings.Index(line, ")")
114 if start < 0 || end < 0 || end <= start+1 {
115 return nil
116 }
117 name := strings.TrimSpace(line[start+1 : end])
118 return findTypeByName(mxh, name)
119 }
120
121 // extractMethodReceiverName extracts the type name from a pointer-receiver
122 // signature: "func (*Foo) DecodeFrom(..."
123 func extractMethodReceiverName(line string) string {
124 start := strings.Index(line, "(*")
125 end := strings.Index(line, ")")
126 if start < 0 || end < 0 || end <= start+2 {
127 return ""
128 }
129 return strings.TrimSpace(line[start+2 : end])
130 }
131
132 func findTypeByName(mxh *MXH, name string) *ExternalTypeDef {
133 for i := range mxh.Types {
134 if mxh.Types[i].Name == name {
135 return &mxh.Types[i]
136 }
137 }
138 return nil
139 }
140