o/blockdev: add windows implementation

This adds a relatively bare bones, but working implementation of
blockdev for Windows.  Has been manually tested on Windows 23H2.

Change-Id: I7a2bf0a46e973f838506a321ad815629f62a3b35
Reviewed-on: https://review.monogon.dev/c/monogon/+/3696
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
Vouch-Run-CI: Lorenz Brun <lorenz@monogon.tech>
Tested-by: Jenkins CI
diff --git a/osbase/blockdev/BUILD.bazel b/osbase/blockdev/BUILD.bazel
index f476e8b..a720d80 100644
--- a/osbase/blockdev/BUILD.bazel
+++ b/osbase/blockdev/BUILD.bazel
@@ -7,6 +7,7 @@
         "blockdev.go",
         "blockdev_darwin.go",
         "blockdev_linux.go",
+        "blockdev_windows.go",
         "memory.go",
     ],
     importpath = "source.monogon.dev/osbase/blockdev",
@@ -24,6 +25,9 @@
         "@io_bazel_rules_go//go/platform:linux": [
             "@org_golang_x_sys//unix",
         ],
+        "@io_bazel_rules_go//go/platform:windows": [
+            "@org_golang_x_sys//windows",
+        ],
         "//conditions:default": [],
     }),
 )
diff --git a/osbase/blockdev/blockdev_windows.go b/osbase/blockdev/blockdev_windows.go
new file mode 100644
index 0000000..9e3d51c
--- /dev/null
+++ b/osbase/blockdev/blockdev_windows.go
@@ -0,0 +1,197 @@
+//go:build windows
+
+package blockdev
+
+import (
+	"errors"
+	"fmt"
+	"math/bits"
+	"os"
+	"runtime"
+	"syscall"
+	"unsafe"
+
+	"golang.org/x/sys/windows"
+)
+
+const (
+	IOCTL_DISK_BASE                  = 0x00000007
+	IOCTL_DISK_GET_DRIVE_GEOMETRY_EX = (IOCTL_DISK_BASE << 16) | (0x0028 << 2)
+)
+
+type DISK_GEOMETRY struct {
+	Cylinders         uint64
+	MediaType         uint32
+	TracksPerCylinder uint32
+	SectorsPerTrack   uint32
+	BytesPerSector    uint32
+}
+
+type DISK_GEOMETRY_EX_RAW struct {
+	Geometry DISK_GEOMETRY
+	DiskSize uint64
+}
+
+type Device struct {
+	backend    *os.File
+	rawConn    syscall.RawConn
+	blockSize  int64
+	blockCount int64
+}
+
+func (d *Device) ReadAt(p []byte, off int64) (n int, err error) {
+	return d.backend.ReadAt(p, off)
+}
+
+func (d *Device) WriteAt(p []byte, off int64) (n int, err error) {
+	return d.backend.WriteAt(p, off)
+}
+
+func (d *Device) Close() error {
+	return d.backend.Close()
+}
+
+func (d *Device) BlockCount() int64 {
+	return d.blockCount
+}
+
+func (d *Device) BlockSize() int64 {
+	return d.blockSize
+}
+
+func (d *Device) OptimalBlockSize() int64 {
+	return d.blockSize
+}
+
+func (d *Device) Discard(startByte int64, endByte int64) error {
+	// Can be implemented using DKIOCUNMAP, but needs x/sys/unix extension.
+	// Not mandatory, so this is fine for now.
+	return errors.ErrUnsupported
+}
+
+func (d *Device) Zero(startByte int64, endByte int64) error {
+	// It doesn't look like MacOS even has any zeroing acceleration, so just
+	// use the generic one.
+	return GenericZero(d, startByte, endByte)
+}
+
+func (d *Device) Sync() error {
+	return d.backend.Sync()
+}
+
+// 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)
+	if err != nil {
+		return nil, fmt.Errorf("failed to open block device: %w", err)
+	}
+	return FromFileHandle(outFile)
+}
+
+// FromFileHandle creates a blockdev from a device handle. The device handle is
+// not duplicated, closing the returned Device will close it. If the handle is
+// not a block device, i.e does not implement block device ioctls, an error is
+// returned.
+func FromFileHandle(handle *os.File) (*Device, error) {
+	outFileC, err := handle.SyscallConn()
+	if err != nil {
+		return nil, fmt.Errorf("error getting SyscallConn: %w", err)
+	}
+	buf := make([]uint8, 0x80)
+	var n uint32
+
+	var p runtime.Pinner
+	p.Pin(&buf[0])
+	defer p.Unpin()
+
+	outFileC.Control(func(fd uintptr) {
+		err = windows.DeviceIoControl(windows.Handle(fd), IOCTL_DISK_GET_DRIVE_GEOMETRY_EX, nil, 0, &buf[0], uint32(len(buf)), &n, nil)
+	})
+	if errors.Is(err, windows.ERROR_INVALID_PARAMETER) {
+		return nil, ErrNotBlockDevice
+	} else if err != nil {
+		return nil, fmt.Errorf("when querying disk block size: %w", err)
+	}
+
+	diskGeometryBase := (*DISK_GEOMETRY_EX_RAW)(unsafe.Pointer(&buf[0]))
+	blockSize := int64(diskGeometryBase.Geometry.BytesPerSector)
+	blockCount := int64(diskGeometryBase.DiskSize) / blockSize
+	if int64(diskGeometryBase.DiskSize)%blockSize != 0 {
+		return nil, fmt.Errorf("block device size is not an integer multiple of its block size (%d %% %d = %d)", diskGeometryBase.DiskSize, blockSize, diskGeometryBase.DiskSize%uint64(blockSize))
+	}
+
+	return &Device{
+		backend:    handle,
+		rawConn:    outFileC,
+		blockSize:  blockSize,
+		blockCount: blockCount,
+	}, nil
+}
+
+type File struct {
+	backend    *os.File
+	rawConn    syscall.RawConn
+	blockSize  int64
+	blockCount int64
+}
+
+func CreateFile(name string, blockSize int64, blockCount int64) (*File, error) {
+	if blockSize < 512 {
+		return nil, fmt.Errorf("blockSize must be bigger than 512 bytes")
+	}
+	if bits.OnesCount64(uint64(blockSize)) != 1 {
+		return nil, fmt.Errorf("blockSize must be a power of two")
+	}
+	out, err := os.Create(name)
+	if err != nil {
+		return nil, fmt.Errorf("when creating backing file: %w", err)
+	}
+	rawConn, err := out.SyscallConn()
+	if err != nil {
+		return nil, fmt.Errorf("unable to get SyscallConn: %w", err)
+	}
+	return &File{
+		backend:    out,
+		blockSize:  blockSize,
+		rawConn:    rawConn,
+		blockCount: blockCount,
+	}, nil
+}
+
+func (d *File) ReadAt(p []byte, off int64) (n int, err error) {
+	return d.backend.ReadAt(p, off)
+}
+
+func (d *File) WriteAt(p []byte, off int64) (n int, err error) {
+	return d.backend.WriteAt(p, off)
+}
+
+func (d *File) Close() error {
+	return d.backend.Close()
+}
+
+func (d *File) BlockCount() int64 {
+	return d.blockCount
+}
+
+func (d *File) BlockSize() int64 {
+	return d.blockSize
+}
+
+func (d *File) OptimalBlockSize() int64 {
+	return d.blockSize
+}
+
+func (d *File) Discard(startByte int64, endByte int64) error {
+	// Can be supported in the future via FSCTL_SET_ZERO_DATA.
+	return errors.ErrUnsupported
+}
+
+func (d *File) Zero(startByte int64, endByte int64) error {
+	// Can possibly be accelerated in the future via FSCTL_SET_ZERO_DATA.
+	return GenericZero(d, startByte, endByte)
+}
+
+func (d *File) Sync() error {
+	return d.backend.Sync()
+}