1package system
2
3import (
4 "context"
5 "errors"
6 "strings"
7
8 "github.com/bnema/archup/internal/domain/ports"
9)
10
11// SystemValidationRules contains business rules for system configuration
12type SystemValidationRules struct{}
13
14// NewSystemValidationRules creates a new instance
15func NewSystemValidationRules() *SystemValidationRules {
16 return &SystemValidationRules{}
17}
18
19// ValidateCombination validates a combination of system configuration settings
20// This enforces cross-field business rules
21func (r *SystemValidationRules) ValidateCombination(config *SystemConfig, cpuInfo *CPUInfo) error {
22 if config == nil {
23 return errors.New("system config cannot be nil")
24 }
25
26 if cpuInfo == nil {
27 return errors.New("CPU info cannot be nil")
28 }
29
30 // Rule: certain timezones may have locale implications
31 // This is more of a guideline/warning in practice
32
33 return nil
34}
35
36// SanitizeHostname removes or replaces invalid characters from hostname
37// Returns a sanitized hostname that passes validation
38func (r *SystemValidationRules) SanitizeHostname(hostname string) string {
39 // Convert to lowercase
40 hostname = strings.ToLower(hostname)
41
42 // Replace invalid characters with hyphen
43 var result strings.Builder
44 for _, ch := range hostname {
45 if (ch >= 'a' && ch <= 'z') ||
46 (ch >= '0' && ch <= '9') ||
47 ch == '-' {
48 result.WriteRune(ch)
49 } else {
50 result.WriteRune('-')
51 }
52 }
53
54 hostname = result.String()
55
56 // Remove leading/trailing hyphens
57 hostname = strings.Trim(hostname, "-")
58
59 // Collapse consecutive hyphens
60 for strings.Contains(hostname, "--") {
61 hostname = strings.ReplaceAll(hostname, "--", "-")
62 }
63
64 // Limit to 63 characters
65 if len(hostname) > 63 {
66 hostname = hostname[:63]
67 }
68
69 // Remove trailing hyphen if it exists (from truncation)
70 hostname = strings.TrimRight(hostname, "-")
71
72 return hostname
73}
74
75// SuggestedLocaleForTimezone returns a commonly paired locale for a given timezone
76// Returns empty string if no specific suggestion
77func (r *SystemValidationRules) SuggestedLocaleForTimezone(timezone string) string {
78 // Common timezone -> locale pairings (not enforced, just suggestions)
79 suggestions := map[string]string{
80 "America/New_York": "en_US.UTF-8",
81 "America/Chicago": "en_US.UTF-8",
82 "America/Denver": "en_US.UTF-8",
83 "America/Los_Angeles": "en_US.UTF-8",
84 "Europe/London": "en_GB.UTF-8",
85 "Europe/Paris": "fr_FR.UTF-8",
86 "Europe/Berlin": "de_DE.UTF-8",
87 "Europe/Madrid": "es_ES.UTF-8",
88 "Asia/Tokyo": "ja_JP.UTF-8",
89 "Australia/Sydney": "en_AU.UTF-8",
90 "UTC": "en_US.UTF-8",
91 }
92
93 if locale, exists := suggestions[timezone]; exists {
94 return locale
95 }
96
97 return ""
98}
99
100// SuggestedKeymapForLocale returns a commonly used keymap for a given locale
101// Returns empty string if no specific suggestion
102func (r *SystemValidationRules) SuggestedKeymapForLocale(locale string) string {
103 // Common locale -> keymap pairings (not enforced, just suggestions)
104 suggestions := map[string]string{
105 "en_US.UTF-8": "us",
106 "en_GB.UTF-8": "gb",
107 "de_DE.UTF-8": "de",
108 "fr_FR.UTF-8": "fr",
109 "es_ES.UTF-8": "es",
110 "it_IT.UTF-8": "it",
111 "ja_JP.UTF-8": "jp",
112 }
113
114 if keymap, exists := suggestions[locale]; exists {
115 return keymap
116 }
117
118 return ""
119}
120
121// IsValidLocaleVariant checks if a locale is a valid UTF-8 or other standard variant
122func (r *SystemValidationRules) IsValidLocaleVariant(locale string) bool {
123 // Common valid variants
124 validVariants := map[string]bool{
125 "UTF-8": true,
126 "utf8": true,
127 "UTF8": true,
128 "ISO8859-1": true,
129 "ISO-8859-1": true,
130 }
131
132 // Check if locale contains a known valid variant
133 for variant := range validVariants {
134 if strings.Contains(strings.ToUpper(locale), strings.ToUpper(variant)) {
135 return true
136 }
137 }
138
139 // Accept if it's just a locale code without variant
140 if !strings.Contains(locale, ".") && locale != "C" {
141 return false
142 }
143
144 return true
145}
146
147// HostnameRequirements returns a description of hostname requirements
148func (r *SystemValidationRules) HostnameRequirements() string {
149 return "Hostname must be 1-63 characters, contain only letters, digits, and hyphens. Cannot start or end with hyphen."
150}
151
152// TimezoneRequirements returns a description of timezone requirements
153func (r *SystemValidationRules) TimezoneRequirements() string {
154 return "Timezone can be empty (system default) or a valid IANA timezone like UTC, Europe/London, America/New_York"
155}
156
157// LocaleRequirements returns a description of locale requirements
158func (r *SystemValidationRules) LocaleRequirements() string {
159 return "Locale can be empty (system default) or a valid locale code like en_US.UTF-8"
160}
161
162// KeymapRequirements returns a description of keymap requirements
163func (r *SystemValidationRules) KeymapRequirements() string {
164 return "Keymap must be a valid keyboard layout code like us, de, fr, or dvorak. Can include variants like de-nodeadkeys"
165}
166
167// ValidateArchLinux checks if the system is running Arch Linux
168// Returns an error if not running on Arch Linux or Arch ISO
169func (r *SystemValidationRules) ValidateArchLinux(ctx context.Context, fs ports.FileSystem) error {
170 exists, err := fs.Exists("/etc/arch-release")
171 if err != nil {
172 return errors.New("failed to check for Arch Linux: " + err.Error())
173 }
174 if !exists {
175 return errors.New("must be running on Arch Linux or Arch ISO")
176 }
177 return nil
178}
179
180// ValidateNotDerivative checks if the system is vanilla Arch (not a derivative)
181// Returns an error if a derivative is detected (CachyOS, EndeavourOS, Garuda, Manjaro)
182func (r *SystemValidationRules) ValidateNotDerivative(ctx context.Context, fs ports.FileSystem) error {
183 derivatives := map[string]string{
184 "/etc/cachyos-release": "CachyOS",
185 "/etc/eos-release": "EndeavourOS",
186 "/etc/garuda-release": "Garuda",
187 "/etc/manjaro-release": "Manjaro",
188 }
189
190 for marker, name := range derivatives {
191 exists, err := fs.Exists(marker)
192 if err != nil {
193 // Continue checking other markers even if one fails
194 continue
195 }
196 if exists {
197 return errors.New("must be vanilla Arch (detected " + name + ")")
198 }
199 }
200
201 return nil
202}
203
204// ValidateArchitecture checks if the system architecture is x86_64
205// Returns an error if not running on x86_64 architecture
206func (r *SystemValidationRules) ValidateArchitecture(arch string) error {
207 if arch == "" {
208 return errors.New("architecture cannot be empty")
209 }
210 if arch != "x86_64" {
211 return errors.New("must be x86_64 architecture (detected: " + arch + ")")
212 }
213 return nil
214}
215
216// ValidateUEFIBoot checks if the system is booted in UEFI mode
217// Returns an error if not booted in UEFI mode (legacy BIOS is not supported)
218func (r *SystemValidationRules) ValidateUEFIBoot(ctx context.Context, fs ports.FileSystem) error {
219 exists, err := fs.Exists("/sys/firmware/efi/efivars")
220 if err != nil {
221 return errors.New("failed to check UEFI boot mode: " + err.Error())
222 }
223 if !exists {
224 return errors.New("must be UEFI boot mode (legacy BIOS not supported)")
225 }
226 return nil
227}
228
229// DetectSecureBoot checks if Secure Boot is enabled
230// Returns true if enabled, false if disabled, and an error if detection fails
231func (r *SystemValidationRules) DetectSecureBoot(ctx context.Context, exec ports.CommandExecutor) (bool, error) {
232 output, err := exec.Execute(ctx, "bootctl", "status")
233 if err != nil {
234 // If bootctl fails, we can't determine Secure Boot status
235 // This is not necessarily an error - might not be available
236 return false, nil
237 }
238
239 // Check if output contains "Secure Boot: enabled"
240 if strings.Contains(string(output), "Secure Boot: enabled") {
241 return true, nil
242 }
243
244 return false, nil
245}
246
247// ValidateSecureBootDisabled checks that Secure Boot is disabled
248// Returns an error if Secure Boot is enabled
249func (r *SystemValidationRules) ValidateSecureBootDisabled(ctx context.Context, exec ports.CommandExecutor) error {
250 enabled, err := r.DetectSecureBoot(ctx, exec)
251 if err != nil {
252 return errors.New("failed to detect Secure Boot status: " + err.Error())
253 }
254 if enabled {
255 return errors.New("secure boot must be disabled")
256 }
257 return nil
258}