clonetool/disk_windows.go
2026-09-06 20:15:54 +02:00

233 lines
6.4 KiB
Go

//go:build windows
package main
import (
"encoding/binary"
"fmt"
"os"
"os/exec"
"strings"
"syscall"
)
const (
ioctlDiskGetDriveLayoutEx = 0x00070050 // IOCTL_DISK_GET_DRIVE_LAYOUT_EX
ioctlDiskUpdateProperties = 0x00070140 // IOCTL_DISK_UPDATE_PROPERTIES
)
func logicalSectorSize(path string) (int64, error) {
if !isDevicePath(path) {
return 512, nil
}
return alignmentFor(path), nil
}
// guidString renders a little-endian Win32 GUID (4-2-2 + 8 bytes) as the
// canonical upper-case string.
func guidString(b []byte) string {
if len(b) < 16 {
return ""
}
d1 := binary.LittleEndian.Uint32(b[0:4])
d2 := binary.LittleEndian.Uint16(b[4:6])
d3 := binary.LittleEndian.Uint16(b[6:8])
return strings.ToUpper(fmt.Sprintf("%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X",
d1, d2, d3, b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]))
}
// probeDiskLayout reads the partition table of a \\.\PhysicalDriveN via
// IOCTL_DISK_GET_DRIVE_LAYOUT_EX. Windows-native clone-disk is raw/VSS only:
// every partition is cloned block-wise and the table itself is reproduced by
// raw-copying the leading sectors (see computeBootRegionsWindows), so no
// sfdisk-style rebuild is needed.
func probeDiskLayout(path string) (*DiskLayout, error) {
d := &DiskLayout{DiskPath: path, Tools: probeToolsetWindows()}
info, err := statPath(path)
if err != nil {
return nil, err
}
if !info.Exists {
return nil, fmt.Errorf("%s does not exist", path)
}
d.DiskSize = info.Size
d.LogicalSector, _ = logicalSectorSize(path)
if d.LogicalSector == 0 {
d.LogicalSector = 512
}
d.PhysicalSector = d.LogicalSector
if !isDevicePath(path) {
// An image file: we cannot enumerate its table without extra tooling.
d.Scheme = "raw"
d.Warnings = append(d.Warnings, "image-file source on Windows: whole-image raw clone only")
return d, nil
}
p16, err := syscall.UTF16PtrFromString(path)
if err != nil {
return nil, err
}
h, err := syscall.CreateFile(p16, syscall.GENERIC_READ,
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE, nil, syscall.OPEN_EXISTING, 0, 0)
if err != nil {
return nil, &os.PathError{Op: "open", Path: path, Err: err}
}
defer syscall.CloseHandle(h)
buf := make([]byte, 64*1024)
var ret uint32
if err := syscall.DeviceIoControl(h, ioctlDiskGetDriveLayoutEx, nil, 0,
&buf[0], uint32(len(buf)), &ret, nil); err != nil {
return nil, fmt.Errorf("IOCTL_DISK_GET_DRIVE_LAYOUT_EX %s: %w", path, err)
}
style := binary.LittleEndian.Uint32(buf[0:4])
count := binary.LittleEndian.Uint32(buf[4:8])
switch style {
case 0:
d.Scheme, d.Label = "mbr", "dos"
sig := binary.LittleEndian.Uint32(buf[8:12])
d.LabelID = fmt.Sprintf("0x%08x", sig)
case 1:
d.Scheme, d.Label = "gpt", "gpt"
d.LabelID = guidString(buf[8:24])
default:
d.Scheme = "raw"
}
const hdr = 48
const peSize = 144
sector := d.LogicalSector
for i := uint32(0); i < count; i++ {
off := hdr + int(i)*peSize
if off+peSize > len(buf) {
break
}
e := buf[off : off+peSize]
start := int64(binary.LittleEndian.Uint64(e[8:16]))
length := int64(binary.LittleEndian.Uint64(e[16:24]))
num := int(binary.LittleEndian.Uint32(e[24:28]))
if length == 0 || num == 0 {
continue
}
p := Partition{
Num: num,
Start: start / sector,
Size: length / sector,
}
if style == 1 { // GPT union at offset 32
p.Type = guidString(e[32:48])
p.UUID = guidString(e[48:64])
p.Name = decodeUTF16(e[72:144])
} else if style == 0 { // MBR union at offset 32
p.Type = fmt.Sprintf("%02x", e[32])
if e[33] != 0 {
p.Attrs = "bootable"
}
}
d.Partitions = append(d.Partitions, p)
}
d.BootRegions = computeBootRegionsWindows(d)
return d, nil
}
func decodeUTF16(b []byte) string {
u := make([]uint16, 0, len(b)/2)
for i := 0; i+1 < len(b); i += 2 {
c := binary.LittleEndian.Uint16(b[i : i+2])
if c == 0 {
break
}
u = append(u, c)
}
return strings.TrimRight(syscall.UTF16ToString(u), "\x00")
}
// computeBootRegionsWindows: reproduce the partition table by raw-copying the
// sectors before the first partition, plus the trailing 33 sectors that hold
// the backup GPT.
func computeBootRegionsWindows(d *DiskLayout) []Region {
var regs []Region
if len(d.Partitions) == 0 {
return regs
}
first := d.Partitions[0].Start * d.LogicalSector
for _, p := range d.Partitions {
if s := p.Start * d.LogicalSector; s > 0 && s < first {
first = s
}
}
if first > 0 {
regs = append(regs, Region{Offset: 0, Length: first, Note: "protective MBR + primary GPT / MBR table + gap"})
}
if d.Scheme == "gpt" && d.DiskSize > 33*d.LogicalSector {
regs = append(regs, Region{
Offset: d.DiskSize - 33*d.LogicalSector,
Length: 33 * d.LogicalSector,
Note: "backup GPT",
})
}
return regs
}
func rereadPartTable(target string) {
if !isDevicePath(target) {
return
}
p16, err := syscall.UTF16PtrFromString(target)
if err != nil {
return
}
h, err := syscall.CreateFile(p16, syscall.GENERIC_READ|syscall.GENERIC_WRITE,
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE, nil, syscall.OPEN_EXISTING, 0, 0)
if err != nil {
return
}
defer syscall.CloseHandle(h)
var ret uint32
_ = syscall.DeviceIoControl(h, ioctlDiskUpdateProperties, nil, 0, nil, 0, &ret, nil)
}
// applyPartitionTable is a no-op on Windows: the table is reproduced via the
// raw boot regions and rereadPartTable() refreshes the kernel's view.
func applyPartitionTable(target, script string) error {
rereadPartTable(target)
return nil
}
func probeToolsetWindows() map[string]bool {
m := map[string]bool{}
for _, t := range []string{"vssadmin", "wmic", "bcdboot", "diskpart", "robocopy", "powershell"} {
if _, err := exec.LookPath(t); err == nil {
m[t] = true
}
}
return m
}
type diskAttachment struct {
blockPath string
cleanup func()
}
// attachDisk is a no-op for a raw \\.\PhysicalDriveN (partitions are addressed
// by byte offset within the whole-disk handle on Windows) and unsupported for
// an image file.
func attachDisk(path string, writable bool) (*diskAttachment, error) {
if isDevicePath(path) {
return &diskAttachment{blockPath: path, cleanup: func() {}}, nil
}
return nil, fmt.Errorf("filesystem-aware / file-level clone of an image file is not supported on Windows; use --raw")
}
func partNode(disk string, num int) string { return fmt.Sprintf("%s (partition %d)", disk, num) }
func lsblkFSType(string) string { return "" }
func mountAt(string, bool) (string, func(), error) {
return "", nil, fmt.Errorf("mount-based file-level clone is not supported on Windows; use --raw")
}