o/blockdev: add options support

Allow passing options to Open(). This resolves a TODO left from when
blockdev was initially implemented and is now needed as Linux 6.12
rejects opening mounted block devices read-write, so we needed at least
a read-only option.

I also implemented the two options mentioned in the now-removed TODO
even though we're not using them yet.

These options are implemented generically to facilitate their use in
cross-platform code. Unsupported options are rejected at runtime. This
is similar to how Go's own stdlib does this.

Change-Id: I2548cb31e59a5c1198ca04537450bdf665878ca8
Reviewed-on: https://review.monogon.dev/c/monogon/+/3985
Reviewed-by: Jan Schär <jan@monogon.tech>
Tested-by: Jenkins CI
diff --git a/osbase/blockdev/blockdev.go b/osbase/blockdev/blockdev.go
index a8d55cb..5eb7fe8 100644
--- a/osbase/blockdev/blockdev.go
+++ b/osbase/blockdev/blockdev.go
@@ -7,10 +7,57 @@
 	"errors"
 	"fmt"
 	"io"
+	"os"
 )
 
 var ErrNotBlockDevice = errors.New("not a block device")
 
+// options aggregates all open options for all platforms.
+// If these were defined per-platform selecting the right ones per platform
+// would require multiple per-platform files at each call site.
+type options struct {
+	readOnly  bool
+	direct    bool
+	exclusive bool
+}
+
+func (o *options) collect(opts []Option) {
+	for _, f := range opts {
+		f(o)
+	}
+}
+
+func (o *options) genericFlags() int {
+	if o.readOnly {
+		return os.O_RDONLY
+	} else {
+		return os.O_RDWR
+	}
+}
+
+type Option func(*options)
+
+// WithReadonly opens the block device read-only. Any write calls will fail.
+// Passed as an option to Open.
+func WithReadonly(o *options) {
+	o.readOnly = true
+}
+
+// WithDirect opens the block device bypassing any caching by the kernel.
+// Note that additional alignment requirements might be imposed by the
+// underlying device.
+// Unsupported on non-Linux currently, will return an error.
+func WithDirect(o *options) {
+	o.direct = true
+}
+
+// WithExclusive tries to acquire a pseudo-exclusive lock (only with other
+// exclusive FDs) over the block device.
+// Unsupported on non-Linux currently, will return an error.
+func WithExclusive(o *options) {
+	o.exclusive = true
+}
+
 // BlockDev represents a generic block device made up of equally-sized blocks.
 // All offsets and intervals are expressed in bytes and must be aligned to
 // BlockSize and are recommended to be aligned to OptimalBlockSize if feasible.
diff --git a/osbase/blockdev/blockdev_darwin.go b/osbase/blockdev/blockdev_darwin.go
index 46fa1ce..c40e8c3 100644
--- a/osbase/blockdev/blockdev_darwin.go
+++ b/osbase/blockdev/blockdev_darwin.go
@@ -69,8 +69,18 @@
 }
 
 // Open opens a block device given a path to its inode.
-func Open(path string) (*Device, error) {
-	outFile, err := os.OpenFile(path, os.O_RDWR, 0640)
+func Open(path string, opts ...Option) (*Device, error) {
+	var o options
+	o.collect(opts)
+	flags := o.genericFlags()
+	if o.direct {
+		return nil, errors.New("WithDirect not supported on macOS")
+	}
+	if o.exclusive {
+		return nil, errors.New("WithExclusive not supported on macOS")
+	}
+
+	outFile, err := os.OpenFile(path, flags, 0640)
 	if err != nil {
 		return nil, fmt.Errorf("failed to open block device: %w", err)
 	}
diff --git a/osbase/blockdev/blockdev_linux.go b/osbase/blockdev/blockdev_linux.go
index b8fb558..fbcbf5b 100644
--- a/osbase/blockdev/blockdev_linux.go
+++ b/osbase/blockdev/blockdev_linux.go
@@ -182,9 +182,18 @@
 }
 
 // Open opens a block device given a path to its inode.
-// TODO: exclusive, O_DIRECT
-func Open(path string) (*Device, error) {
-	outFile, err := os.OpenFile(path, os.O_RDWR, 0640)
+func Open(path string, opts ...Option) (*Device, error) {
+	var o options
+	o.collect(opts)
+	flags := o.genericFlags()
+	if o.direct {
+		flags |= unix.O_DIRECT
+	}
+	if o.exclusive {
+		flags |= unix.O_EXCL
+	}
+
+	outFile, err := os.OpenFile(path, flags, 0640)
 	if err != nil {
 		return nil, fmt.Errorf("failed to open block device: %w", err)
 	}
diff --git a/osbase/blockdev/blockdev_windows.go b/osbase/blockdev/blockdev_windows.go
index 4541076..07bd528 100644
--- a/osbase/blockdev/blockdev_windows.go
+++ b/osbase/blockdev/blockdev_windows.go
@@ -83,8 +83,18 @@
 }
 
 // Open opens a block device given a path to its inode.
-func Open(path string) (*Device, error) {
-	outFile, err := os.OpenFile(path, os.O_RDWR, 0640)
+func Open(path string, opts ...Option) (*Device, error) {
+	var o options
+	o.collect(opts)
+	flags := o.genericFlags()
+	if o.direct {
+		return nil, errors.New("WithDirect not supported on Windows")
+	}
+	if o.exclusive {
+		return nil, errors.New("WithExclusive not supported on Windows")
+	}
+
+	outFile, err := os.OpenFile(path, flags, 0640)
 	if err != nil {
 		return nil, fmt.Errorf("failed to open block device: %w", err)
 	}