// SPDX-License-Identifier: MIT // Package state persists the runtime snapshot prx needs to describe and undo // the currently applied routing configuration. package state import ( "encoding/json" "errors" "fmt" "os" "time" "prx/internal/fsx" ) // State is the on-disk snapshot. type State struct { Enabled bool `json:"enabled"` WANInterface string `json:"wan_interface"` Gateway4 string `json:"gateway_v4,omitempty"` Gateway6 string `json:"gateway_v6,omitempty"` Mark uint32 `json:"mark"` TableV4 int `json:"table_v4"` TableV6 int `json:"table_v6"` PriorityV4 int `json:"priority_v4"` PriorityV6 int `json:"priority_v6"` BypassTableV4 int `json:"bypass_table_v4"` BypassTableV6 int `json:"bypass_table_v6"` BypassPrioV4 int `json:"bypass_priority_v4"` BypassPrioV6 int `json:"bypass_priority_v6"` BypassCIDRs []string `json:"bypass_cidrs,omitempty"` EngineManaged bool `json:"engine_managed"` EngineWasRunning bool `json:"engine_was_running"` EnabledAt time.Time `json:"enabled_at"` Version string `json:"version,omitempty"` } // Load reads the state. A missing file yields a zero (disabled) state. // // The returned state is never nil, even alongside an error, so callers cannot // dereference a nil pointer after ignoring a read failure. func Load(path string) (*State, error) { raw, err := os.ReadFile(path) if err != nil { if errors.Is(err, os.ErrNotExist) { return &State{}, nil } return &State{}, fmt.Errorf("read state %s: %w", path, err) } var s State if err := json.Unmarshal(raw, &s); err != nil { return &State{}, fmt.Errorf("parse state %s: %w", path, err) } return &s, nil } // Save writes the state atomically, creating the parent directory. func Save(path string, s *State) error { raw, err := json.MarshalIndent(s, "", " ") if err != nil { return fmt.Errorf("encode state: %w", err) } raw = append(raw, '\n') if err := fsx.WriteFileAtomic(path, raw, 0o644); err != nil { return fmt.Errorf("write state %s: %w", path, err) } return nil } // Remove deletes the state file. A missing file is not an error. func Remove(path string) error { err := os.Remove(path) if err != nil && !errors.Is(err, os.ErrNotExist) { return err } return nil }