main 6af0a61

validation.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 0

    ↑↓ moveenter openesc close