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.