233 lines
6.4 KiB
Go
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")
|
|
}
|