detached 6af0a61

mount.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 0

    ↑↓ moveenter openesc close