penguin/homekey-go

homekey-go

internal/app/config.go

raw ยท 3162 bytes

package app

import (
	"encoding/json"
	"fmt"
	"homekey.local/provisioner/internal/blegateway"
	"os"
	"regexp"
	"strings"
)

type LockConfig struct {
	ID      string   `json:"id"`
	Name    string   `json:"name"`
	Port    int      `json:"port"`
	Finish  string   `json:"finish"`
	Readers []string `json:"readers"`
}
type ReaderConfig struct {
	ID      string `json:"id"`
	Name    string `json:"name,omitempty"`
	Address string `json:"address"`
	Adapter string `json:"adapter"`
}
type Config struct {
	Interface string         `json:"interface"`
	Locks     []LockConfig   `json:"locks"`
	Readers   []ReaderConfig `json:"readers"`
}

var identifier = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$`)

func Load(path string) (Config, error) {
	var c Config
	b, err := os.ReadFile(path)
	if err != nil {
		return c, err
	}
	// Supervisor may retain removed legacy options. Only the new fields are used.
	if err = json.Unmarshal(b, &c); err != nil {
		return c, err
	}
	c.Normalize()
	return c, c.Validate()
}
func (c Config) Validate() error {
	readers := map[string]bool{}
	addresses := map[string]bool{}
	for _, r := range c.Readers {
		if !identifier.MatchString(r.ID) || readers[r.ID] {
			return fmt.Errorf("invalid or duplicate reader ID %q", r.ID)
		}
		if err := blegateway.ValidateAddress(r.Address); err != nil {
			return err
		}
		address := strings.ToUpper(r.Address)
		if addresses[address] {
			return fmt.Errorf("duplicate reader address %s", address)
		}
		if !regexp.MustCompile(`^hci[0-9]+$`).MatchString(r.Adapter) {
			return fmt.Errorf("invalid reader adapter")
		}
		readers[r.ID] = true
		addresses[address] = true
	}
	ids := map[string]bool{}
	ports := map[int]bool{}
	for _, l := range c.Locks {
		if !identifier.MatchString(l.ID) || ids[l.ID] {
			return fmt.Errorf("invalid or duplicate lock ID %q", l.ID)
		}
		if strings.TrimSpace(l.Name) == "" {
			return fmt.Errorf("lock name is required")
		}
		if l.Port < 1 || l.Port > 65535 || ports[l.Port] || l.Port == 8099 {
			return fmt.Errorf("invalid, reserved or duplicate lock port %d", l.Port)
		}
		switch l.Finish {
		case "silver", "black", "gold", "tan":
		default:
			return fmt.Errorf("invalid lock finish")
		}
		seen := map[string]bool{}
		for _, r := range l.Readers {
			if !readers[r] || seen[r] {
				return fmt.Errorf("unknown or duplicate reader %q for lock %s", r, l.ID)
			}
			seen[r] = true
		}
		ids[l.ID] = true
		ports[l.Port] = true
	}
	return nil
}

func (c *Config) Normalize() {
	c.Interface = strings.TrimSpace(c.Interface)
	if c.Locks == nil {
		c.Locks = []LockConfig{}
	}
	if c.Readers == nil {
		c.Readers = []ReaderConfig{}
	}
	for i := range c.Readers {
		r := &c.Readers[i]
		r.ID = strings.TrimSpace(r.ID)
		r.Name = strings.TrimSpace(r.Name)
		r.Address = strings.ToUpper(strings.TrimSpace(r.Address))
		r.Adapter = strings.TrimSpace(r.Adapter)
	}
	for i := range c.Locks {
		l := &c.Locks[i]
		l.ID = strings.TrimSpace(l.ID)
		l.Name = strings.TrimSpace(l.Name)
		l.Finish = strings.TrimSpace(l.Finish)
		if l.Readers == nil {
			l.Readers = []string{}
		}
		for j := range l.Readers {
			l.Readers[j] = strings.TrimSpace(l.Readers[j])
		}
	}
}