1package disk
2
3import (
4 "errors"
5 "fmt"
6 "strings"
7)
8
9// MountOptions is an immutable value object representing filesystem mount options
10type MountOptions struct {
11 options map[string]string // key-value pairs like "compress": "zstd", "noatime": ""
12}
13
14// NewMountOptions creates a new MountOptions with validation
15func NewMountOptions(options map[string]string) (*MountOptions, error) {
16 if options == nil {
17 return nil, errors.New("mount options cannot be nil")
18 }
19
20 // Validate each option
21 for key, value := range options {
22 if key == "" {
23 return nil, errors.New("mount option key cannot be empty")
24 }
25
26 // Check for invalid characters in keys
27 if strings.ContainsAny(key, " ,=") {
28 return nil, fmt.Errorf("invalid mount option key: %s", key)
29 }
30
31 // Check for invalid characters in values
32 if strings.ContainsAny(value, " ,") {
33 return nil, fmt.Errorf("invalid mount option value for key %s: %s", key, value)
34 }
35 }
36
37 // Create a copy to ensure immutability
38 optionsCopy := make(map[string]string, len(options))
39 for k, v := range options {
40 optionsCopy[k] = v
41 }
42
43 return &MountOptions{
44 options: optionsCopy,
45 }, nil
46}
47
48// NewBtrfsMountOptions creates standard Btrfs mount options
49// Used by the archup installer: noatime, compress=zstd
50func NewBtrfsMountOptions(subvolume string) (*MountOptions, error) {
51 options := map[string]string{
52 "noatime": "",
53 "compress": "zstd",
54 }
55
56 if subvolume != "" {
57 options["subvol"] = subvolume
58 }
59
60 return NewMountOptions(options)
61}
62
63// Options returns a copy of the options map
64func (m *MountOptions) Options() map[string]string {
65 optionsCopy := make(map[string]string, len(m.options))
66 for k, v := range m.options {
67 optionsCopy[k] = v
68 }
69 return optionsCopy
70}
71
72// Has checks if a specific option is set
73func (m *MountOptions) Has(key string) bool {
74 _, exists := m.options[key]
75 return exists
76}
77
78// Get returns the value for a specific option
79func (m *MountOptions) Get(key string) (string, bool) {
80 value, exists := m.options[key]
81 return value, exists
82}
83
84// ToString converts mount options to a comma-separated string
85// Format: "option1,option2=value,option3=value"
86func (m *MountOptions) ToString() string {
87 parts := make([]string, 0, len(m.options))
88
89 for key, value := range m.options {
90 if value == "" {
91 parts = append(parts, key)
92 } else {
93 parts = append(parts, fmt.Sprintf("%s=%s", key, value))
94 }
95 }
96
97 return strings.Join(parts, ",")
98}
99
100// String returns human-readable representation
101func (m *MountOptions) String() string {
102 return fmt.Sprintf("MountOptions(%s)", m.ToString())
103}
104
105// Equals checks if two MountOptions are equal
106func (m *MountOptions) Equals(other *MountOptions) bool {
107 if other == nil {
108 return false
109 }
110
111 if len(m.options) != len(other.options) {
112 return false
113 }
114
115 for key, value := range m.options {
116 otherValue, exists := other.options[key]
117 if !exists || value != otherValue {
118 return false
119 }
120 }
121
122 return true
123}
124
125// ParseMountOptions parses a comma-separated mount options string
126// Format: "option1,option2=value,option3=value"
127func ParseMountOptions(optionsStr string) (*MountOptions, error) {
128 if optionsStr == "" {
129 return NewMountOptions(map[string]string{})
130 }
131
132 options := make(map[string]string)
133 parts := strings.Split(optionsStr, ",")
134
135 for _, part := range parts {
136 part = strings.TrimSpace(part)
137 if part == "" {
138 continue
139 }
140
141 // Split by = for key=value format
142 if strings.Contains(part, "=") {
143 kv := strings.SplitN(part, "=", 2)
144 if len(kv) != 2 {
145 return nil, fmt.Errorf("invalid mount option format: %s", part)
146 }
147 key := strings.TrimSpace(kv[0])
148 value := strings.TrimSpace(kv[1])
149
150 if key == "" {
151 return nil, fmt.Errorf("empty option key in: %s", part)
152 }
153
154 options[key] = value
155 } else {
156 // Simple flag option (no value)
157 options[part] = ""
158 }
159 }
160
161 return NewMountOptions(options)
162}
163
164// MountPoint is an immutable value object representing a filesystem mount point
165type MountPoint struct {
166 path string
167 device string // e.g., /dev/sda1 or /dev/mapper/cryptroot
168 options *MountOptions
169}
170
171// NewMountPoint creates a new MountPoint with validation
172func NewMountPoint(path, device string, options *MountOptions) (*MountPoint, error) {
173 // Validate path
174 if path == "" {
175 return nil, errors.New("mount point path cannot be empty")
176 }
177
178 if !strings.HasPrefix(path, "/") {
179 return nil, errors.New("mount point path must start with /")
180 }
181
182 // Validate device
183 if device == "" {
184 return nil, errors.New("mount device cannot be empty")
185 }
186
187 // Validate options
188 if options == nil {
189 return nil, errors.New("mount options cannot be nil")
190 }
191
192 return &MountPoint{
193 path: path,
194 device: device,
195 options: options,
196 }, nil
197}
198
199// Path returns the mount point path
200func (mp *MountPoint) Path() string {
201 return mp.path
202}
203
204// Device returns the mount device
205func (mp *MountPoint) Device() string {
206 return mp.device
207}
208
209// Options returns the mount options
210func (mp *MountPoint) Options() *MountOptions {
211 return mp.options
212}
213
214// IsRoot returns true if this is the root mount point
215func (mp *MountPoint) IsRoot() bool {
216 return mp.path == "/"
217}
218
219// String returns human-readable representation
220func (mp *MountPoint) String() string {
221 return fmt.Sprintf("MountPoint(path=%s, device=%s, options=%s)",
222 mp.path, mp.device, mp.options.ToString())
223}
224
225// Equals checks if two MountPoint objects are equal
226func (mp *MountPoint) Equals(other *MountPoint) bool {
227 if other == nil {
228 return false
229 }
230 return mp.path == other.path &&
231 mp.device == other.device &&
232 mp.options.Equals(other.options)
233}
234
235// Errors specific to mount operations
236var (
237 // ErrInvalidMountPoint is returned when mount point validation fails
238 ErrInvalidMountPoint = errors.New("invalid mount point")
239
240 // ErrInvalidMountOptions is returned when mount options validation fails
241 ErrInvalidMountOptions = errors.New("invalid mount options")
242)
243
244// ValidateMountPoint validates a mount point path
245func ValidateMountPoint(path string) error {
246 if path == "" {
247 return fmt.Errorf("%w: path cannot be empty", ErrInvalidMountPoint)
248 }
249
250 if !strings.HasPrefix(path, "/") {
251 return fmt.Errorf("%w: path must start with /", ErrInvalidMountPoint)
252 }
253
254 // Check for obviously invalid patterns
255 if strings.Contains(path, "//") || strings.Contains(path, "..") {
256 return fmt.Errorf("%w: path contains invalid patterns", ErrInvalidMountPoint)
257 }
258
259 // Length check (reasonable limit)
260 if len(path) > 256 {
261 return fmt.Errorf("%w: path too long (max 256 chars)", ErrInvalidMountPoint)
262 }
263
264 return nil
265}