216 lines
6.5 KiB
Go
216 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// plannedPart is one partition's copy plan: where it lives on each side (in
|
|
// bytes) and how its data moves.
|
|
type plannedPart struct {
|
|
Num int
|
|
SrcStartB, SrcSizeB int64
|
|
DstStartB, DstSizeB int64
|
|
Method string // "raw" | "fs-image" | "file-level"
|
|
Tool string // fs-image tool family
|
|
FSType string
|
|
ShrinkToB int64 // >0: shrink the fs to this many bytes before imaging
|
|
}
|
|
|
|
type targetPlan struct {
|
|
Script string // sfdisk restore script ("" when the source has no table / Windows-native)
|
|
IsFile bool
|
|
ImageSize int64 // file target: truncate to this
|
|
Parts []plannedPart
|
|
BootRegions []Region
|
|
RootPart int
|
|
ESPPart int
|
|
UEFI bool
|
|
Notes []string
|
|
}
|
|
|
|
// bootOpts is the JSON blob passed to reinstall_boot.
|
|
type bootOpts struct {
|
|
RootPart int `json:"rootPart"` // 1-based partition number holding "/"
|
|
ESPPart int `json:"espPart"` // 1-based ESP partition number, 0 if BIOS-only
|
|
UEFI bool `json:"uefi"`
|
|
DiskPath string `json:"diskPath"` // whole-disk path grub-install targets
|
|
}
|
|
|
|
type planOpts struct {
|
|
Parts map[int]bool // empty = all
|
|
Raw map[int]bool
|
|
FileLevel map[int]bool
|
|
FileAuto bool
|
|
AllowShrink bool
|
|
NewIDs bool
|
|
ImageSize int64 // explicit file-target size, 0 = auto
|
|
}
|
|
|
|
const gptTailSectors = 33 // secondary GPT header + entries
|
|
|
|
// planTargetLayout turns a probed source layout + the target's nature/size
|
|
// into a concrete copy plan, applying the sizing rules:
|
|
//
|
|
// - device target: never grow; if everything fits, keep the layout and
|
|
// leave trailing space untouched; if the last partition(s) overflow,
|
|
// shrink them (filesystem + partition) from the last inward, or fail.
|
|
// - file target: size the image to hold the layout (or ImageSize).
|
|
func planTargetLayout(src *DiskLayout, isFile bool, destSize int64, o planOpts) (*targetPlan, error) {
|
|
sector := src.LogicalSector
|
|
if sector <= 0 {
|
|
sector = 512
|
|
}
|
|
parts := sortedParts(src.Partitions)
|
|
var sel []Partition
|
|
for _, p := range parts {
|
|
if len(o.Parts) == 0 || o.Parts[p.Num] {
|
|
sel = append(sel, p)
|
|
}
|
|
}
|
|
if len(sel) == 0 && src.Scheme != "raw" {
|
|
return nil, fmt.Errorf("no partitions selected")
|
|
}
|
|
|
|
plan := &targetPlan{IsFile: isFile, BootRegions: append([]Region(nil), src.BootRegions...)}
|
|
|
|
// With --new-ids on an MBR disk, keep the raw bootstrap copy clear of the
|
|
// 4-byte disk signature at offset 0x1B8 so sfdisk's freshly generated one
|
|
// survives (GPT regenerates its GUID in the header, which we never
|
|
// raw-copy, so no clamp is needed there).
|
|
if o.NewIDs && src.Scheme == "mbr" {
|
|
for i := range plan.BootRegions {
|
|
r := &plan.BootRegions[i]
|
|
if r.Offset == 0 && r.Length > 0x1B8 {
|
|
r.Length = 0x1B8
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bytes the layout needs on disk = end of the last selected partition
|
|
// (+ secondary GPT for GPT disks).
|
|
lastEndB := int64(0)
|
|
for _, p := range sel {
|
|
if e := (p.Start + p.Size) * sector; e > lastEndB {
|
|
lastEndB = e
|
|
}
|
|
}
|
|
neededB := lastEndB
|
|
if src.Scheme == "gpt" {
|
|
neededB += gptTailSectors * sector
|
|
}
|
|
if src.Scheme == "raw" {
|
|
neededB = src.DiskSize
|
|
}
|
|
|
|
if isFile {
|
|
plan.ImageSize = o.ImageSize
|
|
if plan.ImageSize == 0 {
|
|
plan.ImageSize = roundUp(neededB+(1<<20), 1<<20)
|
|
}
|
|
destSize = plan.ImageSize
|
|
}
|
|
if destSize <= 0 {
|
|
return nil, fmt.Errorf("could not determine target size")
|
|
}
|
|
|
|
// --- shrink pass ----------------------------------------------------
|
|
sizeOverride := map[int]int64{}
|
|
if destSize < neededB {
|
|
short := neededB - destSize
|
|
for i := len(sel) - 1; i >= 0 && short > 0; i-- {
|
|
p := &sel[i]
|
|
curB := p.Size * sector
|
|
minB := p.FSMinBytes
|
|
shrinkable := minB > 0 && (isExtFS(p.FSType) || p.FSType == "ntfs")
|
|
if !shrinkable {
|
|
continue
|
|
}
|
|
minB = roundUp(minB+(16<<20), sector) // 16 MiB slack
|
|
if minB >= curB {
|
|
continue
|
|
}
|
|
reduce := curB - minB
|
|
if reduce > short {
|
|
reduce = roundUp(short, sector)
|
|
}
|
|
newB := curB - reduce
|
|
sizeOverride[p.Num] = newB / sector
|
|
plan.Notes = append(plan.Notes,
|
|
fmt.Sprintf("partition p%d (%s) shrinks %s -> %s to fit the target",
|
|
p.Num, p.FSType, humanBytes(curB), humanBytes(newB)))
|
|
short -= reduce
|
|
}
|
|
if short > 0 {
|
|
return nil, fmt.Errorf(
|
|
"target (%s) is smaller than the source layout needs (%s); the trailing partition(s) "+
|
|
"cannot shrink to make up the missing %s; use a larger target",
|
|
humanBytes(destSize), humanBytes(neededB), humanBytes(short))
|
|
}
|
|
if !o.AllowShrink {
|
|
var names []string
|
|
for n := range sizeOverride {
|
|
names = append(names, fmt.Sprintf("p%d", n))
|
|
}
|
|
return nil, fmt.Errorf(
|
|
"target is smaller than the source; partition(s) %s would have to be shrunk. "+
|
|
"Re-run with --allow-shrink (this resizes the SOURCE filesystem in place before imaging) "+
|
|
"or use --no-shrink off / a larger target",
|
|
strings.Join(names, ", "))
|
|
}
|
|
}
|
|
|
|
// --- partition-table script --------------------------------------
|
|
if src.Scheme != "raw" && strings.TrimSpace(src.SfdiskDump) != "" {
|
|
plan.Script = src.sfdiskRestoreScript(o.NewIDs, sizeOverride)
|
|
}
|
|
|
|
// --- per-partition method --------------------------------------
|
|
for _, p := range sel {
|
|
pp := plannedPart{
|
|
Num: p.Num,
|
|
SrcStartB: p.Start * sector,
|
|
SrcSizeB: p.Size * sector,
|
|
DstStartB: p.Start * sector,
|
|
DstSizeB: p.Size * sector,
|
|
FSType: p.FSType,
|
|
}
|
|
if ov, ok := sizeOverride[p.Num]; ok {
|
|
pp.DstSizeB = ov * sector
|
|
pp.ShrinkToB = ov * sector
|
|
}
|
|
method, tool := chooseCloneMethod(p, src.Tools, o.Raw, o.FileLevel, o.FileAuto)
|
|
pp.Method, pp.Tool = method, tool
|
|
if pp.ShrinkToB > 0 && method != "fs-image" {
|
|
return nil, fmt.Errorf(
|
|
"partition p%d (%s) must shrink to fit but no image cloner is available for it; "+
|
|
"install ntfsclone / partclone or use a larger target", p.Num, orDash(p.FSType))
|
|
}
|
|
plan.Parts = append(plan.Parts, pp)
|
|
}
|
|
|
|
// --- boot roles ------------------------------------------------
|
|
plan.RootPart, plan.ESPPart = pickBootParts(sel)
|
|
plan.UEFI = plan.ESPPart != 0
|
|
return plan, nil
|
|
}
|
|
|
|
func isExtFS(t string) bool { return t == "ext2" || t == "ext3" || t == "ext4" }
|
|
|
|
// pickBootParts guesses the "/" partition (largest Linux fs) and the ESP.
|
|
func pickBootParts(sel []Partition) (root, esp int) {
|
|
var bestSize int64
|
|
for _, p := range sel {
|
|
if p.isESP() && esp == 0 {
|
|
esp = p.Num
|
|
}
|
|
switch p.FSType {
|
|
case "ext2", "ext3", "ext4", "btrfs", "xfs":
|
|
if sz := p.Size; sz > bestSize {
|
|
bestSize, root = sz, p.Num
|
|
}
|
|
}
|
|
}
|
|
return root, esp
|
|
}
|