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