mirror of
https://github.com/YuzuZensai/TrollSSH.git
synced 2026-09-13 15:59:03 +00:00
♻️ refactor: split src into internal packages and cmd
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/YuzuZensai/TrollSSH/internal/logx"
|
||||
)
|
||||
|
||||
type PlaybackMode string
|
||||
|
||||
const (
|
||||
PlaybackLoop PlaybackMode = "loop"
|
||||
PlaybackRandom PlaybackMode = "random"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
MaxLoop int
|
||||
PlaybackMode PlaybackMode
|
||||
AllowUserControl bool
|
||||
SwitchDebounce time.Duration
|
||||
LoginDelay time.Duration
|
||||
MaxConnections int
|
||||
MaxTotalConnections int
|
||||
MaxAuthAttempts int
|
||||
HandshakeTimeout time.Duration
|
||||
MaxDimension int
|
||||
MaxTerminalCells int
|
||||
SessionTimeout time.Duration
|
||||
RenderCacheMB int
|
||||
BrightnessThreshold int
|
||||
Charset string
|
||||
Invert bool
|
||||
ForceGrayscale bool
|
||||
LogCredentials bool
|
||||
}
|
||||
|
||||
func warnInvalid(name, value string, fallback any) {
|
||||
logx.Warn(fmt.Sprintf("Invalid %s=%q, using default %v", name, logx.Sanitize(value), fallback))
|
||||
}
|
||||
|
||||
func envString(name, fallback string) string {
|
||||
value := strings.TrimSpace(os.Getenv(name))
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func envInt(name string, fallback, min, max int) int {
|
||||
raw := strings.TrimSpace(os.Getenv(name))
|
||||
if raw == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
warnInvalid(name, raw, fallback)
|
||||
return fallback
|
||||
}
|
||||
if parsed < min {
|
||||
return min
|
||||
}
|
||||
if parsed > max {
|
||||
return max
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func envBool(name string, fallback bool) bool {
|
||||
raw := strings.TrimSpace(os.Getenv(name))
|
||||
if raw == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := strconv.ParseBool(strings.ToLower(raw))
|
||||
if err != nil {
|
||||
warnInvalid(name, raw, fallback)
|
||||
return fallback
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func envDurationMs(name string, fallback time.Duration) time.Duration {
|
||||
raw := strings.TrimSpace(os.Getenv(name))
|
||||
if raw == "" {
|
||||
return fallback
|
||||
}
|
||||
ms, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
warnInvalid(name, raw, fallback)
|
||||
return fallback
|
||||
}
|
||||
if ms < 0 {
|
||||
ms = 0
|
||||
}
|
||||
return time.Duration(ms) * time.Millisecond
|
||||
}
|
||||
|
||||
func envPlaybackMode(name string, fallback PlaybackMode) PlaybackMode {
|
||||
raw := strings.TrimSpace(os.Getenv(name))
|
||||
if raw == "" {
|
||||
return fallback
|
||||
}
|
||||
switch PlaybackMode(strings.ToLower(raw)) {
|
||||
case PlaybackLoop:
|
||||
return PlaybackLoop
|
||||
case PlaybackRandom:
|
||||
return PlaybackRandom
|
||||
}
|
||||
warnInvalid(name, raw, fallback)
|
||||
return fallback
|
||||
}
|
||||
|
||||
func Load() Config {
|
||||
const maxInt = int(^uint(0) >> 1)
|
||||
return Config{
|
||||
Host: envString("HOST", "0.0.0.0"),
|
||||
Port: envInt("PORT", 22, 1, 65535),
|
||||
MaxLoop: envInt("MAX_LOOP", 5, 0, maxInt),
|
||||
PlaybackMode: envPlaybackMode("PLAYBACK_MODE", PlaybackLoop),
|
||||
AllowUserControl: envBool("ALLOW_USER_CONTROL", true),
|
||||
SwitchDebounce: envDurationMs("SWITCH_DEBOUNCE_MS", 120*time.Millisecond),
|
||||
LoginDelay: envDurationMs("LOGIN_DELAY", 1500*time.Millisecond),
|
||||
MaxConnections: envInt("MAX_CONNECTIONS", 10, 1, maxInt),
|
||||
MaxTotalConnections: envInt("MAX_TOTAL_CONNECTIONS", 1000, 1, maxInt),
|
||||
MaxAuthAttempts: envInt("MAX_AUTH_ATTEMPTS", 6, 1, maxInt),
|
||||
HandshakeTimeout: envDurationMs("HANDSHAKE_TIMEOUT", 10*time.Second),
|
||||
MaxDimension: envInt("MAX_DIMENSION", 512, 1, 4096),
|
||||
MaxTerminalCells: envInt("MAX_TERMINAL_CELLS", 500*512, 1, maxInt),
|
||||
SessionTimeout: envDurationMs("SESSION_TIMEOUT", 10*time.Minute),
|
||||
RenderCacheMB: envInt("RENDER_CACHE_MB", 256, 0, maxInt),
|
||||
BrightnessThreshold: envInt("BRIGHTNESS_THRESHOLD", 40, 0, 100),
|
||||
Charset: envString("CHARSET", "detailed"),
|
||||
Invert: envBool("INVERT", false),
|
||||
ForceGrayscale: envBool("FORCE_GRAYSCALE", false),
|
||||
LogCredentials: envBool("LOG_CREDENTIALS", false),
|
||||
}
|
||||
}
|
||||
|
||||
func LoadOptionalTextFile(filePath string) (string, bool) {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
text := strings.ReplaceAll(string(data), "\r\n", "\n")
|
||||
text = strings.ReplaceAll(text, "\n", "\r\n")
|
||||
return text, true
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLoadConfigDefaults(t *testing.T) {
|
||||
t.Setenv("HOST", "")
|
||||
t.Setenv("PORT", "")
|
||||
t.Setenv("PLAYBACK_MODE", "")
|
||||
t.Setenv("LOGIN_DELAY", "")
|
||||
cfg := Load()
|
||||
if cfg.Host != "0.0.0.0" {
|
||||
t.Errorf("host = %q", cfg.Host)
|
||||
}
|
||||
if cfg.Port != 22 {
|
||||
t.Errorf("port = %d", cfg.Port)
|
||||
}
|
||||
if cfg.PlaybackMode != PlaybackLoop {
|
||||
t.Errorf("playbackMode = %q", cfg.PlaybackMode)
|
||||
}
|
||||
if cfg.Charset != "detailed" {
|
||||
t.Errorf("charset = %q", cfg.Charset)
|
||||
}
|
||||
if cfg.LoginDelay != 1500*time.Millisecond {
|
||||
t.Errorf("loginDelay = %v", cfg.LoginDelay)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigClamping(t *testing.T) {
|
||||
t.Setenv("PORT", "999999")
|
||||
t.Setenv("BRIGHTNESS_THRESHOLD", "-5")
|
||||
cfg := Load()
|
||||
if cfg.Port != 65535 {
|
||||
t.Errorf("port clamp = %d", cfg.Port)
|
||||
}
|
||||
if cfg.BrightnessThreshold != 0 {
|
||||
t.Errorf("brightness clamp = %d", cfg.BrightnessThreshold)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigInvalidFallsBack(t *testing.T) {
|
||||
t.Setenv("PORT", "not-a-number")
|
||||
t.Setenv("INVERT", "yes-please")
|
||||
t.Setenv("PLAYBACK_MODE", "shuffle")
|
||||
cfg := Load()
|
||||
if cfg.Port != 22 {
|
||||
t.Errorf("port = %d, want default 22", cfg.Port)
|
||||
}
|
||||
if cfg.Invert {
|
||||
t.Error("invert should fall back to false")
|
||||
}
|
||||
if cfg.PlaybackMode != PlaybackLoop {
|
||||
t.Errorf("playbackMode = %q, want default loop", cfg.PlaybackMode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvDurationMs(t *testing.T) {
|
||||
t.Setenv("D", "250")
|
||||
if got := envDurationMs("D", time.Second); got != 250*time.Millisecond {
|
||||
t.Errorf("250 = %v, want 250ms", got)
|
||||
}
|
||||
t.Setenv("D", "-10")
|
||||
if got := envDurationMs("D", time.Second); got != 0 {
|
||||
t.Errorf("negative = %v, want 0", got)
|
||||
}
|
||||
t.Setenv("D", "banana")
|
||||
if got := envDurationMs("D", time.Second); got != time.Second {
|
||||
t.Errorf("invalid = %v, want fallback 1s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaybackModeRandom(t *testing.T) {
|
||||
t.Setenv("PLAYBACK_MODE", "RaNdOm")
|
||||
if Load().PlaybackMode != PlaybackRandom {
|
||||
t.Error("expected random")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user