internal/app/config.go
raw ยท 3573 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"`
PhysicalLocks []string `json:"physical_locks,omitempty"`
}
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")
}
physical := map[string]bool{}
for _, id := range l.PhysicalLocks {
if !identifier.MatchString(id) || physical[id] {
return fmt.Errorf("invalid or duplicate physical lock %q", id)
}
physical[id] = true
}
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.PhysicalLocks {
l.PhysicalLocks[j] = strings.TrimSpace(l.PhysicalLocks[j])
}
for j := range l.Readers {
l.Readers[j] = strings.TrimSpace(l.Readers[j])
}
}
}