1package disk
2
3import (
4 "errors"
5 "fmt"
6 "strings"
7)
8
9// Errors specific to disk validation
10var (
11 // ErrInvalidDiskPath is returned when disk path validation fails
12 ErrInvalidDiskPath = errors.New("invalid disk path")
13
14 // ErrInvalidPartitionPath is returned when partition path validation fails
15 ErrInvalidPartitionPath = errors.New("invalid partition path")
16)
17
18// DeterminePartitionPath determines the correct partition device path based on disk type
19// For NVMe disks: /dev/nvme0n1 → /dev/nvme0n1p1, /dev/nvme0n1p2
20// For SATA/SSD: /dev/sda → /dev/sda1, /dev/sda2
21// For MMC: /dev/mmcblk0 → /dev/mmcblk0p1, /dev/mmcblk0p2
22func DeterminePartitionPath(diskPath string, partitionNumber int) (string, error) {
23 if diskPath == "" {
24 return "", fmt.Errorf("%w: disk path cannot be empty", ErrInvalidDiskPath)
25 }
26
27 if partitionNumber < 1 || partitionNumber > 128 {
28 return "", fmt.Errorf("%w: partition number must be between 1 and 128", ErrInvalidPartitionPath)
29 }
30
31 // Check if disk path is valid
32 if !strings.HasPrefix(diskPath, "/dev/") {
33 return "", fmt.Errorf("%w: disk path must start with /dev/", ErrInvalidDiskPath)
34 }
35
36 // NVMe disks use 'p' prefix: nvme0n1p1, nvme0n1p2
37 if strings.Contains(diskPath, "nvme") {
38 return fmt.Sprintf("%sp%d", diskPath, partitionNumber), nil
39 }
40
41 // MMC disks use 'p' prefix: mmcblk0p1, mmcblk0p2
42 if strings.Contains(diskPath, "mmcblk") {
43 return fmt.Sprintf("%sp%d", diskPath, partitionNumber), nil
44 }
45
46 // SATA/SSD disks use direct numbering: sda1, sda2, vda1, vda2
47 return fmt.Sprintf("%s%d", diskPath, partitionNumber), nil
48}
49
50// ValidateDiskPath validates a disk device path
51func ValidateDiskPath(diskPath string) error {
52 if diskPath == "" {
53 return fmt.Errorf("%w: path cannot be empty", ErrInvalidDiskPath)
54 }
55
56 if !strings.HasPrefix(diskPath, "/dev/") {
57 return fmt.Errorf("%w: path must start with /dev/", ErrInvalidDiskPath)
58 }
59
60 // Check for common disk patterns
61 validPrefixes := []string{
62 "/dev/sd", // SATA/SCSI
63 "/dev/hd", // IDE (legacy)
64 "/dev/vd", // Virtio
65 "/dev/nvme", // NVMe
66 "/dev/mmcblk", // MMC
67 }
68
69 valid := false
70 for _, prefix := range validPrefixes {
71 if strings.HasPrefix(diskPath, prefix) {
72 valid = true
73 break
74 }
75 }
76
77 if !valid {
78 return fmt.Errorf("%w: unrecognized disk device type", ErrInvalidDiskPath)
79 }
80
81 // Length check
82 if len(diskPath) > 64 {
83 return fmt.Errorf("%w: path too long (max 64 chars)", ErrInvalidDiskPath)
84 }
85
86 return nil
87}
88
89// ValidatePartitionPath validates a partition device path
90func ValidatePartitionPath(partitionPath string) error {
91 if partitionPath == "" {
92 return fmt.Errorf("%w: path cannot be empty", ErrInvalidPartitionPath)
93 }
94
95 if !strings.HasPrefix(partitionPath, "/dev/") {
96 return fmt.Errorf("%w: path must start with /dev/", ErrInvalidPartitionPath)
97 }
98
99 // Length check
100 if len(partitionPath) > 64 {
101 return fmt.Errorf("%w: path too long (max 64 chars)", ErrInvalidPartitionPath)
102 }
103
104 return nil
105}
106
107// IsDiskPath checks if the given path looks like a disk (not a partition)
108func IsDiskPath(path string) bool {
109 // Partitions usually end with a digit or 'p' + digit
110 // Disks typically don't (except nvme and mmcblk which end with a digit)
111 if strings.Contains(path, "nvme") {
112 // NVMe disk: /dev/nvme0n1
113 // NVMe partition: /dev/nvme0n1p1
114 return !strings.Contains(path, "p")
115 }
116
117 if strings.Contains(path, "mmcblk") {
118 // MMC disk: /dev/mmcblk0
119 // MMC partition: /dev/mmcblk0p1
120 return !strings.Contains(path, "p")
121 }
122
123 // For SATA/SCSI: /dev/sda (disk) vs /dev/sda1 (partition)
124 // Check if last character is a digit
125 if len(path) == 0 {
126 return false
127 }
128
129 lastChar := path[len(path)-1]
130 return lastChar < '0' || lastChar > '9'
131}
132
133// ExtractDiskFromPartition extracts the disk path from a partition path
134// Examples:
135// - /dev/sda1 → /dev/sda
136// - /dev/nvme0n1p1 → /dev/nvme0n1
137// - /dev/mmcblk0p1 → /dev/mmcblk0
138func ExtractDiskFromPartition(partitionPath string) (string, error) {
139 if partitionPath == "" {
140 return "", fmt.Errorf("%w: path cannot be empty", ErrInvalidPartitionPath)
141 }
142
143 // NVMe partitions: must have 'p' separator (e.g., nvme0n1p1)
144 // If no 'p', it's already a disk (e.g., nvme0n1)
145 if strings.Contains(partitionPath, "nvme") {
146 if strings.Contains(partitionPath, "p") {
147 // Find last 'p' and remove everything after it
148 lastP := strings.LastIndex(partitionPath, "p")
149 return partitionPath[:lastP], nil
150 }
151 // No 'p' means it's already a disk path
152 return partitionPath, nil
153 }
154
155 // MMC partitions: must have 'p' separator (e.g., mmcblk0p1)
156 // If no 'p', it's already a disk (e.g., mmcblk0)
157 if strings.Contains(partitionPath, "mmcblk") {
158 if strings.Contains(partitionPath, "p") {
159 // Find last 'p' and remove everything after it
160 lastP := strings.LastIndex(partitionPath, "p")
161 return partitionPath[:lastP], nil
162 }
163 // No 'p' means it's already a disk path
164 return partitionPath, nil
165 }
166
167 // SATA/SCSI: remove trailing digits
168 // Find where digits start from the end
169 i := len(partitionPath) - 1
170 for i >= 0 && partitionPath[i] >= '0' && partitionPath[i] <= '9' {
171 i--
172 }
173
174 if i == len(partitionPath)-1 {
175 // No digits found - might already be a disk path
176 return partitionPath, nil
177 }
178
179 return partitionPath[:i+1], nil
180}