1package system
2
3import (
4 "context"
5 "errors"
6 "strings"
7
8 "github.com/bnema/archup/internal/domain/ports"
9)
10
11// DistributionType represents the type of Arch-based distribution
12type DistributionType int
13
14const (
15 // DistributionArch represents vanilla Arch Linux
16 DistributionArch DistributionType = iota
17 // DistributionCachyOS represents CachyOS
18 DistributionCachyOS
19 // DistributionEndeavourOS represents EndeavourOS
20 DistributionEndeavourOS
21 // DistributionGaruda represents Garuda Linux
22 DistributionGaruda
23 // DistributionManjaro represents Manjaro Linux
24 DistributionManjaro
25 // DistributionUnknown represents an unknown or unsupported distribution
26 DistributionUnknown
27)
28
29// String returns the string representation of the distribution type
30func (d DistributionType) String() string {
31 switch d {
32 case DistributionArch:
33 return "Arch Linux"
34 case DistributionCachyOS:
35 return "CachyOS"
36 case DistributionEndeavourOS:
37 return "EndeavourOS"
38 case DistributionGaruda:
39 return "Garuda Linux"
40 case DistributionManjaro:
41 return "Manjaro Linux"
42 case DistributionUnknown:
43 return "Unknown"
44 default:
45 return "Unknown"
46 }
47}
48
49// DetectDistribution detects which Arch-based distribution is running
50func DetectDistribution(ctx context.Context, fs ports.FileSystem) (DistributionType, error) {
51 // Check for vanilla Arch first
52 exists, err := fs.Exists("/etc/arch-release")
53 if err != nil {
54 return DistributionUnknown, errors.New("failed to detect distribution: " + err.Error())
55 }
56 if !exists {
57 return DistributionUnknown, errors.New("not running on Arch Linux or Arch ISO")
58 }
59
60 // Check for derivatives
61 if exists, _ := fs.Exists("/etc/cachyos-release"); exists {
62 return DistributionCachyOS, nil
63 }
64 if exists, _ := fs.Exists("/etc/eos-release"); exists {
65 return DistributionEndeavourOS, nil
66 }
67 if exists, _ := fs.Exists("/etc/garuda-release"); exists {
68 return DistributionGaruda, nil
69 }
70 if exists, _ := fs.Exists("/etc/manjaro-release"); exists {
71 return DistributionManjaro, nil
72 }
73
74 // If no derivative markers found, it's vanilla Arch
75 return DistributionArch, nil
76}
77
78// SystemConfig is an immutable value object representing system configuration
79// It encapsulates hostname, locale, timezone, and keymap settings
80type SystemConfig struct {
81 hostname string
82 timezone string
83 locale string
84 keymap string
85}
86
87// NewSystemConfig creates a new SystemConfig with validation
88func NewSystemConfig(hostname, timezone, locale, keymap string) (*SystemConfig, error) {
89 // Validate hostname
90 if err := ValidateHostname(hostname); err != nil {
91 return nil, err
92 }
93
94 // Validate timezone
95 if err := ValidateTimezone(timezone); err != nil {
96 return nil, err
97 }
98
99 // Validate locale
100 if err := ValidateLocale(locale); err != nil {
101 return nil, err
102 }
103
104 // Validate keymap
105 if err := ValidateKeymap(keymap); err != nil {
106 return nil, err
107 }
108
109 return &SystemConfig{
110 hostname: hostname,
111 timezone: timezone,
112 locale: locale,
113 keymap: keymap,
114 }, nil
115}
116
117// Hostname returns the configured hostname
118func (c *SystemConfig) Hostname() string {
119 return c.hostname
120}
121
122// Timezone returns the configured timezone
123func (c *SystemConfig) Timezone() string {
124 return c.timezone
125}
126
127// Locale returns the configured locale
128func (c *SystemConfig) Locale() string {
129 return c.locale
130}
131
132// Keymap returns the configured keymap
133func (c *SystemConfig) Keymap() string {
134 return c.keymap
135}
136
137// String returns a human-readable representation
138func (c *SystemConfig) String() string {
139 return "SystemConfig(" +
140 "hostname=" + c.hostname +
141 ", timezone=" + c.timezone +
142 ", locale=" + c.locale +
143 ", keymap=" + c.keymap +
144 ")"
145}
146
147// Equals checks if two SystemConfig objects are equal
148// Value objects should be compared by their fields
149func (c *SystemConfig) Equals(other *SystemConfig) bool {
150 if other == nil {
151 return false
152 }
153 return c.hostname == other.hostname &&
154 c.timezone == other.timezone &&
155 c.locale == other.locale &&
156 c.keymap == other.keymap
157}
158
159// Errors specific to SystemConfig
160var (
161 // ErrInvalidHostname is returned when hostname validation fails
162 ErrInvalidHostname = errors.New("invalid hostname: must be 1-63 chars, alphanumeric and hyphens, no leading/trailing hyphen")
163
164 // ErrInvalidTimezone is returned when timezone validation fails
165 ErrInvalidTimezone = errors.New("invalid timezone")
166
167 // ErrInvalidLocale is returned when locale validation fails
168 ErrInvalidLocale = errors.New("invalid locale")
169
170 // ErrInvalidKeymap is returned when keymap validation fails
171 ErrInvalidKeymap = errors.New("invalid keymap")
172
173 // ErrInvalidUsername is returned when username validation fails
174 ErrInvalidUsername = errors.New("invalid username: must be 1-32 chars, lowercase, start with letter")
175
176 // ErrInvalidPassword is returned when password validation fails
177 ErrInvalidPassword = errors.New("invalid password: must be at least 4 characters")
178)
179
180// ValidateHostname validates hostname according to RFC 1123
181// Valid hostnames: letters, digits, hyphens
182// Cannot start/end with hyphen, length 1-63 characters
183func ValidateHostname(hostname string) error {
184 if hostname == "" {
185 return ErrInvalidHostname
186 }
187
188 if len(hostname) > 63 {
189 return ErrInvalidHostname
190 }
191
192 if hostname[0] == '-' || hostname[len(hostname)-1] == '-' {
193 return ErrInvalidHostname
194 }
195
196 for _, ch := range hostname {
197 if !isHostnameChar(ch) {
198 return ErrInvalidHostname
199 }
200 }
201
202 return nil
203}
204
205// ValidateTimezone validates timezone string
206// Common zones: "UTC", "US/Eastern", "Europe/London", etc.
207// Empty timezone is allowed (means system default)
208func ValidateTimezone(timezone string) error {
209 // Empty timezone is allowed (system default)
210 if timezone == "" {
211 return nil
212 }
213
214 // Check for obviously invalid patterns
215 if strings.Contains(timezone, "..") || strings.HasPrefix(timezone, "/") {
216 return ErrInvalidTimezone
217 }
218
219 // Length check (reasonable limit)
220 if len(timezone) > 64 {
221 return ErrInvalidTimezone
222 }
223
224 return nil
225}
226
227// ValidateLocale validates locale string
228// Valid formats: "en_US.UTF-8", "en_US", "C.UTF-8", etc.
229// Empty locale is allowed (means system default)
230func ValidateLocale(locale string) error {
231 // Empty locale is allowed (system default)
232 if locale == "" {
233 return nil
234 }
235
236 // Basic format validation
237 if len(locale) > 64 {
238 return ErrInvalidLocale
239 }
240
241 // Should contain language code
242 if !strings.Contains(locale, "_") && !strings.Contains(locale, ".") && locale != "C" {
243 return ErrInvalidLocale
244 }
245
246 return nil
247}
248
249// ValidateKeymap validates keyboard layout
250// Common layouts: us, de, fr, dvorak, etc.
251// Can include variations like de-nodeadkeys
252func ValidateKeymap(keymap string) error {
253 if keymap == "" {
254 return ErrInvalidKeymap
255 }
256
257 if len(keymap) > 64 {
258 return ErrInvalidKeymap
259 }
260
261 // Allow letters, digits, hyphens
262 for _, ch := range keymap {
263 if !isKeymapChar(ch) {
264 return ErrInvalidKeymap
265 }
266 }
267
268 return nil
269}
270
271func isHostnameChar(ch rune) bool {
272 return (ch >= 'a' && ch <= 'z') ||
273 (ch >= 'A' && ch <= 'Z') ||
274 (ch >= '0' && ch <= '9') ||
275 ch == '-'
276}
277
278func isKeymapChar(ch rune) bool {
279 return (ch >= 'a' && ch <= 'z') ||
280 (ch >= '0' && ch <= '9') ||
281 ch == '-'
282}
283
284// ValidateUsername validates Linux username
285// Must be 1-32 chars, lowercase, start with letter, alphanumeric and underscores
286func ValidateUsername(username string) error {
287 if username == "" || len(username) > 32 {
288 return ErrInvalidUsername
289 }
290
291 // Must start with lowercase letter
292 if username[0] < 'a' || username[0] > 'z' {
293 return ErrInvalidUsername
294 }
295
296 // Must be lowercase alphanumeric or underscore
297 for _, ch := range username {
298 if !isUsernameChar(ch) {
299 return ErrInvalidUsername
300 }
301 }
302
303 return nil
304}
305
306func isUsernameChar(ch rune) bool {
307 return (ch >= 'a' && ch <= 'z') ||
308 (ch >= '0' && ch <= '9') ||
309 ch == '_'
310}
311
312// ValidatePassword validates password using user domain validation
313// Must be at least 4 characters
314func ValidatePassword(password string) error {
315 if password == "" {
316 return ErrInvalidPassword
317 }
318 if len(password) < 4 {
319 return ErrInvalidPassword
320 }
321 if len(password) > 128 {
322 return ErrInvalidPassword
323 }
324 return nil
325}