package station import ( "fmt" "math" "os" "regexp" "strings" "encoding/json/jsontext" jsonv2 "encoding/json/v2" ) type loaded struct { host string port int callsign string authKey string talkGroup int } type fileShape struct { Host jsontext.Value `json:"host"` Port jsontext.Value `json:"port"` Callsign jsontext.Value `json:"callsign"` AuthKey jsontext.Value `json:"authKey"` TalkGroup jsontext.Value `json:"talkGroup"` } type templateFile struct { Host string `json:"host"` Port int `json:"port"` Callsign string `json:"callsign"` AuthKey string `json:"authKey"` TalkGroup int `json:"talkGroup"` } var callsignPattern = regexp.MustCompile(`^[A-Z0-9][A-Z]{0,2}\d[A-Z0-9]{0,3}[A-Z](?:-[A-Z0-9]{1,3})?$`) func loadConfig(path string) (loaded, error) { body, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { if err := writeTemplate(path); err != nil { return loaded{}, err } return loaded{}, fmt.Errorf("created %s", path) } return loaded{}, fmt.Errorf("could not be read") } return parseConfig(body) } func writeTemplate(path string) error { body, err := jsonv2.Marshal(templateFile{}) if err != nil { return err } body = append(body, '\n') f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) if err != nil { return err } _, werr := f.Write(body) cerr := f.Close() if werr != nil { return werr } if cerr != nil { return cerr } return os.Chmod(path, 0o600) } func parseConfig(body []byte) (loaded, error) { var shape fileShape if err := jsonv2.Unmarshal(body, &shape); err != nil { return loaded{}, fmt.Errorf("could not be read") } host, err := stringField("host", shape.Host) if err != nil { return loaded{}, err } port, err := intField("port", shape.Port, 1, 65535) if err != nil { return loaded{}, err } call, err := stringField("callsign", shape.Callsign) if err != nil { return loaded{}, err } key, err := authField(shape.AuthKey) if err != nil { return loaded{}, err } tg, err := intField("talkGroup", shape.TalkGroup, 1, int(math.MaxUint32)) if err != nil { return loaded{}, err } host = strings.TrimSpace(host) if host == "" { return loaded{}, fmt.Errorf("host is empty") } call = strings.ToUpper(strings.TrimSpace(call)) if call == "" { return loaded{}, fmt.Errorf("callsign is empty") } if !callsignPattern.MatchString(call) { return loaded{}, fmt.Errorf("callsign is invalid") } return loaded{host: host, port: port, callsign: call, authKey: key, talkGroup: tg}, nil } func stringField(name string, v jsontext.Value) (string, error) { if len(v) == 0 { return "", nil } if v.Kind() != jsontext.KindString { return "", fmt.Errorf("%s has the wrong type", name) } var s string if err := jsonv2.Unmarshal(v, &s); err != nil { return "", fmt.Errorf("%s has the wrong type", name) } return s, nil } func authField(v jsontext.Value) (string, error) { s, err := stringField("authKey", v) if err != nil { return "", err } if strings.TrimSpace(s) == "" { return "", fmt.Errorf("authKey is empty") } return s, nil } func intField(name string, v jsontext.Value, min, max int) (int, error) { if len(v) == 0 { return 0, fmt.Errorf("%s is invalid", name) } if v.Kind() != jsontext.KindNumber { return 0, fmt.Errorf("%s has the wrong type", name) } var n int if err := jsonv2.Unmarshal(v, &n); err != nil { return 0, fmt.Errorf("%s is invalid", name) } if n < min || n > max { return 0, fmt.Errorf("%s is invalid", name) } return n, nil }