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 Version string // module version from moxie.mod (e.g. "1.2.0"), may be empty
15 Library string // shared library path (e.g. "libfoo.so") for FFI packages
16 Types []ExternalTypeDef
17 Funcs []ExternalFuncDef
18 }
19
20 // ExternalTypeDef is a single codec type exported in a .mxh.
21 type ExternalTypeDef struct {
22 Name string
23 Fields []FieldDef
24 HasEncodeTo bool
25 HasDecodeFrom bool
26 }
27
28 // FieldDef is a single exported fixed-width struct field.
29 type FieldDef struct {
30 Name string
31 Type string // e.g. "uint32", "[20]byte"
32 }
33
34 // ExternalFuncDef declares an FFI function from a shared library.
35 type ExternalFuncDef struct {
36 Name string
37 Symbol string // C symbol name (defaults to Name if empty)
38 Params []ParamDef
39 Results []ParamDef
40 }
41
42 // ParamDef is a parameter or result in a function signature.
43 type ParamDef struct {
44 Name string
45 Type string
46 }
47
48 // ParseMXH reads a .mxh file from r and returns the parsed header.
49 func ParseMXH(r io.Reader) (*MXH, error) {
50 sc := bufio.NewScanner(r)
51
52 // First line must be the hash header.
53 if !sc.Scan() {
54 return nil, fmt.Errorf("mxh: empty file")
55 }
56 header := sc.Text()
57 hash, version, err := parseHashLine(header)
58 if err != nil {
59 return nil, err
60 }
61
62 mxh := &MXH{Hash: hash, Version: version}
63 var cur *ExternalTypeDef
64
65 for sc.Scan() {
66 line := strings.TrimSpace(sc.Text())
67 if line == "" || strings.HasPrefix(line, "//") {
68 continue
69 }
70
71 switch {
72 case strings.HasPrefix(line, "library "):
73 mxh.Library = parseLibraryLine(line)
74
75 case strings.HasPrefix(line, "func ") && !strings.HasPrefix(line, "func (") && !strings.HasPrefix(line, "func (*"):
76 if fd, err := parseFuncLine(line); err == nil {
77 mxh.Funcs = append(mxh.Funcs, fd)
78 }
79
80 case strings.HasPrefix(line, "type ") && strings.HasSuffix(line, " struct {"):
81 name := line[len("type ") : len(line)-len(" struct {")]
82 name = strings.TrimSpace(name)
83 mxh.Types = append(mxh.Types, ExternalTypeDef{Name: name})
84 cur = &mxh.Types[len(mxh.Types)-1]
85
86 case line == "}":
87 cur = nil
88
89 case strings.HasPrefix(line, "func (") && strings.Contains(line, ") EncodeTo("):
90 if cur == nil {
91 cur = findTypeBySig(mxh, line)
92 }
93 if cur != nil {
94 cur.HasEncodeTo = true
95 cur = nil
96 }
97
98 case strings.HasPrefix(line, "func (*") && strings.Contains(line, ") DecodeFrom("):
99 name := extractMethodReceiverName(line)
100 if t := findTypeByName(mxh, name); t != nil {
101 t.HasDecodeFrom = true
102 }
103
104 case cur != nil:
105 parts := strings.Fields(line)
106 if len(parts) == 2 {
107 cur.Fields = append(cur.Fields, FieldDef{Name: parts[0], Type: parts[1]})
108 }
109 }
110 }
111 if err := sc.Err(); err != nil {
112 return nil, fmt.Errorf("mxh: scan error: %w", err)
113 }
114 return mxh, nil
115 }
116
117 func parseHashLine(line string) (hash, version string, err error) {
118 // Expected: "// mxh v1 <hex>" or "// mxh v1 <hex> <version>"
119 const prefix = "// mxh v1 "
120 if !strings.HasPrefix(line, prefix) {
121 return "", "", fmt.Errorf("mxh: invalid header line %q (expected %q...)", line, prefix)
122 }
123 rest := strings.TrimSpace(line[len(prefix):])
124 if rest == "" {
125 return "", "", fmt.Errorf("mxh: empty hash in header line")
126 }
127 parts := strings.Fields(rest)
128 hash = parts[0]
129 if len(parts) > 1 {
130 version = parts[1]
131 }
132 return hash, version, nil
133 }
134
135 // findTypeBySig extracts the receiver type name from a value-receiver
136 // EncodeTo signature: "func (Foo) EncodeTo(..."
137 func findTypeBySig(mxh *MXH, line string) *ExternalTypeDef {
138 // "func (Name) EncodeTo..."
139 start := strings.Index(line, "(")
140 end := strings.Index(line, ")")
141 if start < 0 || end < 0 || end <= start+1 {
142 return nil
143 }
144 name := strings.TrimSpace(line[start+1 : end])
145 return findTypeByName(mxh, name)
146 }
147
148 // extractMethodReceiverName extracts the type name from a pointer-receiver
149 // signature: "func (*Foo) DecodeFrom(..."
150 func extractMethodReceiverName(line string) string {
151 start := strings.Index(line, "(*")
152 end := strings.Index(line, ")")
153 if start < 0 || end < 0 || end <= start+2 {
154 return ""
155 }
156 return strings.TrimSpace(line[start+2 : end])
157 }
158
159 func findTypeByName(mxh *MXH, name string) *ExternalTypeDef {
160 for i := range mxh.Types {
161 if mxh.Types[i].Name == name {
162 return &mxh.Types[i]
163 }
164 }
165 return nil
166 }
167
168 func parseLibraryLine(line string) string {
169 s := strings.TrimPrefix(line, "library ")
170 s = strings.TrimSpace(s)
171 if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
172 return s[1 : len(s)-1]
173 }
174 return s
175 }
176
177 // parseFuncLine parses: func Name(a int32, b int32) int32
178 // or: func Name(a int32, b int32) (int32, error)
179 // or: func Name(a int32) as "c_symbol_name"
180 func parseFuncLine(line string) (ExternalFuncDef, error) {
181 line = strings.TrimPrefix(line, "func ")
182 line = strings.TrimSpace(line)
183
184 // Extract function name.
185 paren := strings.IndexByte(line, '(')
186 if paren < 0 {
187 return ExternalFuncDef{}, fmt.Errorf("mxh func: no opening paren")
188 }
189 name := strings.TrimSpace(line[:paren])
190 rest := line[paren:]
191
192 // Find matching closing paren for params.
193 params, after, err := extractParenGroup(rest)
194 if err != nil {
195 return ExternalFuncDef{}, err
196 }
197
198 fd := ExternalFuncDef{Name: name}
199 fd.Params = parseParamList(params)
200
201 after = strings.TrimSpace(after)
202
203 // Check for "as" clause: as "c_name"
204 if asIdx := strings.Index(after, " as "); asIdx >= 0 {
205 sym := strings.TrimSpace(after[asIdx+4:])
206 if len(sym) >= 2 && sym[0] == '"' && sym[len(sym)-1] == '"' {
207 fd.Symbol = sym[1 : len(sym)-1]
208 }
209 after = strings.TrimSpace(after[:asIdx])
210 }
211
212 // Parse return types.
213 if after != "" {
214 if after[0] == '(' {
215 results, _, err := extractParenGroup(after)
216 if err == nil {
217 fd.Results = parseParamList(results)
218 }
219 } else {
220 fd.Results = []ParamDef{{Type: after}}
221 }
222 }
223
224 if fd.Symbol == "" {
225 fd.Symbol = fd.Name
226 }
227 return fd, nil
228 }
229
230 func extractParenGroup(s string) (string, string, error) {
231 if len(s) == 0 || s[0] != '(' {
232 return "", s, fmt.Errorf("expected '('")
233 }
234 depth := 0
235 for i := 0; i < len(s); i++ {
236 if s[i] == '(' {
237 depth++
238 } else if s[i] == ')' {
239 depth--
240 if depth == 0 {
241 return s[1:i], s[i+1:], nil
242 }
243 }
244 }
245 return "", "", fmt.Errorf("unmatched parenthesis")
246 }
247
248 func parseParamList(s string) []ParamDef {
249 s = strings.TrimSpace(s)
250 if s == "" {
251 return nil
252 }
253 var params []ParamDef
254 for _, part := range strings.Split(s, ",") {
255 part = strings.TrimSpace(part)
256 if part == "" {
257 continue
258 }
259 fields := strings.Fields(part)
260 switch len(fields) {
261 case 1:
262 params = append(params, ParamDef{Type: fields[0]})
263 case 2:
264 params = append(params, ParamDef{Name: fields[0], Type: fields[1]})
265 }
266 }
267 return params
268 }
269