1package disk
2
3import (
4 "errors"
5 "fmt"
6 "strings"
7)
8
9// BtrfsSubvolume is an immutable value object representing a Btrfs subvolume
10type BtrfsSubvolume struct {
11 name string // e.g., "@", "@home", "@snapshots"
12 mountPoint string // e.g., "/", "/home", "/.snapshots"
13}
14
15// NewBtrfsSubvolume creates a new Btrfs subvolume with validation
16func NewBtrfsSubvolume(name, mountPoint string) (*BtrfsSubvolume, error) {
17 // Validate name
18 if err := ValidateSubvolumeName(name); err != nil {
19 return nil, err
20 }
21
22 // Validate mount point
23 if mountPoint != "" && !strings.HasPrefix(mountPoint, "/") {
24 return nil, errors.New("mount point must start with / or be empty")
25 }
26
27 return &BtrfsSubvolume{
28 name: name,
29 mountPoint: mountPoint,
30 }, nil
31}
32
33// Name returns the subvolume name
34func (s *BtrfsSubvolume) Name() string {
35 return s.name
36}
37
38// MountPoint returns the mount point
39func (s *BtrfsSubvolume) MountPoint() string {
40 return s.mountPoint
41}
42
43// String returns human-readable representation
44func (s *BtrfsSubvolume) String() string {
45 if s.mountPoint == "" {
46 return fmt.Sprintf("BtrfsSubvolume(name=%s)", s.name)
47 }
48 return fmt.Sprintf("BtrfsSubvolume(name=%s, mount=%s)", s.name, s.mountPoint)
49}
50
51// Equals checks if two subvolumes are equal
52func (s *BtrfsSubvolume) Equals(other *BtrfsSubvolume) bool {
53 if other == nil {
54 return false
55 }
56 return s.name == other.name && s.mountPoint == other.mountPoint
57}
58
59// BtrfsLayout is an entity that manages a collection of Btrfs subvolumes
60type BtrfsLayout struct {
61 subvolumes []*BtrfsSubvolume
62}
63
64// NewBtrfsLayout creates a new empty Btrfs layout
65func NewBtrfsLayout() *BtrfsLayout {
66 return &BtrfsLayout{
67 subvolumes: []*BtrfsSubvolume{},
68 }
69}
70
71// NewStandardBtrfsLayout creates a standard layout with @ and @home subvolumes
72// This matches the archup installer's default layout
73func NewStandardBtrfsLayout() (*BtrfsLayout, error) {
74 layout := NewBtrfsLayout()
75
76 // Create @ (root) subvolume
77 root, err := NewBtrfsSubvolume("@", "/")
78 if err != nil {
79 return nil, err
80 }
81
82 // Create @home subvolume
83 home, err := NewBtrfsSubvolume("@home", "/home")
84 if err != nil {
85 return nil, err
86 }
87
88 if err := layout.AddSubvolume(root); err != nil {
89 return nil, err
90 }
91
92 if err := layout.AddSubvolume(home); err != nil {
93 return nil, err
94 }
95
96 return layout, nil
97}
98
99// AddSubvolume adds a subvolume to the layout with validation
100func (l *BtrfsLayout) AddSubvolume(subvolume *BtrfsSubvolume) error {
101 if subvolume == nil {
102 return errors.New("subvolume cannot be nil")
103 }
104
105 // Check for duplicate names
106 for _, sv := range l.subvolumes {
107 if sv.name == subvolume.name {
108 return fmt.Errorf("subvolume %s already exists", subvolume.name)
109 }
110 }
111
112 // Check for duplicate mount points (except empty mount points)
113 if subvolume.mountPoint != "" {
114 for _, sv := range l.subvolumes {
115 if sv.mountPoint == subvolume.mountPoint {
116 return fmt.Errorf("mount point %s is already in use by subvolume %s", subvolume.mountPoint, sv.name)
117 }
118 }
119 }
120
121 l.subvolumes = append(l.subvolumes, subvolume)
122 return nil
123}
124
125// Subvolumes returns a copy of the subvolumes list
126func (l *BtrfsLayout) Subvolumes() []*BtrfsSubvolume {
127 subvolumes := make([]*BtrfsSubvolume, len(l.subvolumes))
128 copy(subvolumes, l.subvolumes)
129 return subvolumes
130}
131
132// SubvolumeCount returns the number of subvolumes
133func (l *BtrfsLayout) SubvolumeCount() int {
134 return len(l.subvolumes)
135}
136
137// FindSubvolumeByName finds a subvolume by its name
138func (l *BtrfsLayout) FindSubvolumeByName(name string) *BtrfsSubvolume {
139 for _, sv := range l.subvolumes {
140 if sv.name == name {
141 return sv
142 }
143 }
144 return nil
145}
146
147// FindSubvolumeByMountPoint finds a subvolume by its mount point
148func (l *BtrfsLayout) FindSubvolumeByMountPoint(mountPoint string) *BtrfsSubvolume {
149 for _, sv := range l.subvolumes {
150 if sv.mountPoint == mountPoint {
151 return sv
152 }
153 }
154 return nil
155}
156
157// HasRootSubvolume returns true if layout has a root (/) subvolume
158func (l *BtrfsLayout) HasRootSubvolume() bool {
159 return l.FindSubvolumeByMountPoint("/") != nil
160}
161
162// GetRootSubvolume returns the root subvolume if it exists
163func (l *BtrfsLayout) GetRootSubvolume() *BtrfsSubvolume {
164 return l.FindSubvolumeByMountPoint("/")
165}
166
167// ValidateLayout validates the Btrfs layout according to business rules
168func (l *BtrfsLayout) ValidateLayout() error {
169 // Must have at least one subvolume
170 if len(l.subvolumes) == 0 {
171 return errors.New("btrfs layout must have at least one subvolume")
172 }
173
174 // Must have a root subvolume
175 if !l.HasRootSubvolume() {
176 return errors.New("btrfs layout must have a root (/) subvolume")
177 }
178
179 // Root subvolume should be named @ by convention
180 rootSv := l.GetRootSubvolume()
181 if rootSv.name != "@" {
182 return errors.New("root subvolume should be named @")
183 }
184
185 return nil
186}
187
188// String returns human-readable representation
189func (l *BtrfsLayout) String() string {
190 return fmt.Sprintf("BtrfsLayout(subvolumes=%d)", len(l.subvolumes))
191}
192
193// Validation functions
194
195var (
196 // ErrInvalidSubvolumeName is returned when subvolume name validation fails
197 ErrInvalidSubvolumeName = errors.New("invalid subvolume name")
198
199 // ErrDuplicateSubvolume is returned when a subvolume already exists
200 ErrDuplicateSubvolume = errors.New("duplicate subvolume")
201)
202
203// ValidateSubvolumeName validates a Btrfs subvolume name
204// Valid names: start with @, alphanumeric plus hyphen/underscore
205// Examples: @, @home, @snapshots, @cache, @log
206func ValidateSubvolumeName(name string) error {
207 if name == "" {
208 return fmt.Errorf("%w: name cannot be empty", ErrInvalidSubvolumeName)
209 }
210
211 // Must start with @
212 if !strings.HasPrefix(name, "@") {
213 return fmt.Errorf("%w: name must start with @", ErrInvalidSubvolumeName)
214 }
215
216 // Must be at least @ (length 1)
217 if len(name) < 1 {
218 return fmt.Errorf("%w: name too short", ErrInvalidSubvolumeName)
219 }
220
221 // Maximum length (reasonable limit)
222 if len(name) > 64 {
223 return fmt.Errorf("%w: name too long (max 64 chars)", ErrInvalidSubvolumeName)
224 }
225
226 // Check characters after @
227 for i, ch := range name[1:] { // Skip the @
228 if !isValidSubvolumeChar(ch) {
229 return fmt.Errorf("%w: invalid character at position %d: %c", ErrInvalidSubvolumeName, i+1, ch)
230 }
231 }
232
233 // No spaces allowed
234 if strings.Contains(name, " ") {
235 return fmt.Errorf("%w: spaces not allowed", ErrInvalidSubvolumeName)
236 }
237
238 return nil
239}
240
241func isValidSubvolumeChar(ch rune) bool {
242 switch {
243 case ch >= 'a' && ch <= 'z':
244 return true
245 case ch >= 'A' && ch <= 'Z':
246 return true
247 case ch >= '0' && ch <= '9':
248 return true
249 case ch == '-' || ch == '_':
250 return true
251 default:
252 return false
253 }
254}