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