1package disk
2
3import (
4 "errors"
5 "fmt"
6)
7
8// Disk is an entity representing a physical disk with partitions
9// It manages the collection of partitions and validates partition layout
10type Disk struct {
11 device string
12 sizeGB int64
13 partitions []*Partition
14}
15
16// NewDisk creates a new Disk entity with validation
17func NewDisk(device string, sizeGB int64) (*Disk, error) {
18 // Validate device
19 if device == "" {
20 return nil, errors.New("disk device cannot be empty")
21 }
22
23 // Validate size
24 if sizeGB < 1 {
25 return nil, errors.New("disk size must be at least 1GB")
26 }
27
28 // Reasonable limit (max 100TB)
29 if sizeGB > 104857600 {
30 return nil, errors.New("disk size is unreasonably large")
31 }
32
33 return &Disk{
34 device: device,
35 sizeGB: sizeGB,
36 partitions: []*Partition{},
37 }, nil
38}
39
40// Device returns the device path
41func (d *Disk) Device() string {
42 return d.device
43}
44
45// SizeGB returns the disk size in gigabytes
46func (d *Disk) SizeGB() int64 {
47 return d.sizeGB
48}
49
50// SizeMB returns the disk size in megabytes
51func (d *Disk) SizeMB() int64 {
52 return d.sizeGB * 1024
53}
54
55// Partitions returns a copy of the partitions list
56func (d *Disk) Partitions() []*Partition {
57 partitions := make([]*Partition, len(d.partitions))
58 copy(partitions, d.partitions)
59 return partitions
60}
61
62// PartitionCount returns the number of partitions
63func (d *Disk) PartitionCount() int {
64 return len(d.partitions)
65}
66
67// AddPartition adds a partition to the disk with validation
68func (d *Disk) AddPartition(partition *Partition) error {
69 if partition == nil {
70 return errors.New("partition cannot be nil")
71 }
72
73 // Check partition device is on this disk
74 if !isPartitionOfDisk(d.device, partition.device) {
75 return fmt.Errorf("partition %s is not on disk %s", partition.device, d.device)
76 }
77
78 // Check for duplicate device
79 for _, p := range d.partitions {
80 if p.device == partition.device {
81 return fmt.Errorf("partition %s already exists", partition.device)
82 }
83 }
84
85 // Check for duplicate mount points (except swap which has no mount point)
86 if partition.mountPoint != "" {
87 for _, p := range d.partitions {
88 if p.mountPoint == partition.mountPoint {
89 return fmt.Errorf("mount point %s is already in use", partition.mountPoint)
90 }
91 }
92 }
93
94 // Validate total size doesn't exceed disk size
95 if totalPartitionSize := d.calculateTotalPartitionSize() + partition.SizeGB(); totalPartitionSize > d.sizeGB {
96 return fmt.Errorf("partition size would exceed disk capacity (total: %dGB, disk: %dGB)",
97 totalPartitionSize, d.sizeGB)
98 }
99
100 d.partitions = append(d.partitions, partition)
101 return nil
102}
103
104// FindPartitionByMountPoint finds a partition by its mount point
105func (d *Disk) FindPartitionByMountPoint(mountPoint string) *Partition {
106 for _, p := range d.partitions {
107 if p.mountPoint == mountPoint {
108 return p
109 }
110 }
111 return nil
112}
113
114// FindPartitionByDevice finds a partition by its device path
115func (d *Disk) FindPartitionByDevice(device string) *Partition {
116 for _, p := range d.partitions {
117 if p.device == device {
118 return p
119 }
120 }
121 return nil
122}
123
124// HasRootPartition returns true if disk has a root (/) partition
125func (d *Disk) HasRootPartition() bool {
126 return d.FindPartitionByMountPoint("/") != nil
127}
128
129// HasBootPartition returns true if disk has a boot partition
130func (d *Disk) HasBootPartition() bool {
131 return d.FindPartitionByMountPoint("/boot") != nil || d.FindPartitionByMountPoint("/boot/efi") != nil
132}
133
134// HasEFIPartition returns true if disk has an EFI partition
135func (d *Disk) HasEFIPartition() bool {
136 return d.FindPartitionByMountPoint("/boot/efi") != nil
137}
138
139// GetEFIPartition returns the EFI partition if it exists
140func (d *Disk) GetEFIPartition() *Partition {
141 return d.FindPartitionByMountPoint("/boot/efi")
142}
143
144// GetRootPartition returns the root partition if it exists
145func (d *Disk) GetRootPartition() *Partition {
146 return d.FindPartitionByMountPoint("/")
147}
148
149// GetBootPartition returns the boot partition if it exists
150func (d *Disk) GetBootPartition() *Partition {
151 if p := d.FindPartitionByMountPoint("/boot/efi"); p != nil {
152 return p
153 }
154 return d.FindPartitionByMountPoint("/boot")
155}
156
157// CalculateUsedSpace returns total space used by partitions in GB
158func (d *Disk) CalculateUsedSpace() int64 {
159 return d.calculateTotalPartitionSize()
160}
161
162// CalculateFreeSpace returns remaining space in GB
163func (d *Disk) CalculateFreeSpace() int64 {
164 return d.sizeGB - d.calculateTotalPartitionSize()
165}
166
167// ValidateLayout validates the partition layout according to business rules
168func (d *Disk) ValidateLayout() error {
169 // At minimum, must have root partition
170 if !d.HasRootPartition() {
171 return errors.New("disk must have a root (/) partition")
172 }
173
174 rootPartition := d.GetRootPartition()
175
176 // Root partition must be at least 20GB for comfortable installation
177 if rootPartition.SizeGB() < 20 {
178 return errors.New("root partition must be at least 20GB")
179 }
180
181 // For UEFI boot, must have EFI partition
182 // (This is a business rule - we assume UEFI for now)
183 if !d.HasEFIPartition() {
184 return errors.New("disk must have an EFI partition (/boot/efi) for UEFI boot")
185 }
186
187 // EFI partition must be FAT32
188 efiPartition := d.GetEFIPartition()
189 if efiPartition.Filesystem() != FilesystemFAT32 {
190 return errors.New("EFI partition must use FAT32 filesystem")
191 }
192
193 // EFI partition must be at least 256MB (typical 512MB)
194 if efiPartition.SizeGB() < 1 {
195 // Less than 1GB is ok for EFI if it's at least 256MB
196 if efiPartition.SizeMB() < 256 {
197 return errors.New("EFI partition must be at least 256MB")
198 }
199 }
200
201 // No duplicate mount points
202 mountPoints := make(map[string]bool)
203 for _, p := range d.partitions {
204 if p.mountPoint != "" {
205 if mountPoints[p.mountPoint] {
206 return fmt.Errorf("duplicate mount point: %s", p.mountPoint)
207 }
208 mountPoints[p.mountPoint] = true
209 }
210 }
211
212 return nil
213}
214
215// String returns human-readable representation
216func (d *Disk) String() string {
217 return fmt.Sprintf("Disk(device=%s, size=%dGB, partitions=%d)", d.device, d.sizeGB, len(d.partitions))
218}
219
220// Private helper methods
221
222func (d *Disk) calculateTotalPartitionSize() int64 {
223 var total int64
224 for _, p := range d.partitions {
225 total += p.SizeGB()
226 }
227 return total
228}
229
230// isPartitionOfDisk checks if a partition device belongs to a disk
231// e.g., /dev/sda1 belongs to /dev/sda, /dev/nvme0n1p1 belongs to /dev/nvme0n1
232func isPartitionOfDisk(diskDevice, partitionDevice string) bool {
233 if len(partitionDevice) <= len(diskDevice) {
234 return false
235 }
236
237 // Check prefix matches
238 if partitionDevice[:len(diskDevice)] != diskDevice {
239 return false
240 }
241
242 // Next character can be 'p' (for nvme: nvme0n1p1) or a digit (for sda: sda1)
243 nextChar := partitionDevice[len(diskDevice)]
244 return (nextChar >= '0' && nextChar <= '9') || nextChar == 'p'
245}