From cfdeb03b051f58d2f2b253c1056ae3f92eea9c52 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Wed, 17 Jul 2024 21:18:21 +0400 Subject: [PATCH] feat: implement GPT editing Lots of ideas from v1, coupled with new data structures and simplified ideas plus proper testing. Signed-off-by: Andrey Smirnov --- .dockerignore | 3 +- .kres.yaml | 2 + Dockerfile | 3 +- Makefile | 4 +- blkid/internal/filesystems/ext/ext.go | 5 +- blkid/internal/filesystems/iso9660/iso9660.go | 6 +- blkid/internal/filesystems/luks/luks.go | 6 +- blkid/internal/filesystems/lvm2/lvm2.go | 6 +- .../internal/filesystems/squashfs/squashfs.go | 6 +- blkid/internal/filesystems/swap/swap.go | 6 +- .../filesystems/talosmeta/talosmeta.go | 6 +- blkid/internal/filesystems/vfat/vfat.go | 9 +- blkid/internal/filesystems/xfs/xfs.go | 6 +- blkid/internal/filesystems/zfs/zfs.go | 6 +- blkid/internal/partitions/gpt/gpt.go | 98 +-- blkid/internal/partitions/gpt/header_extra.go | 21 - blkid/internal/partitions/gpt/lba.go | 20 - blkid/internal/partitions/gpt/uuid.go | 17 - blkid/internal/utils/utils.go | 25 - blkid/probe_linux.go | 4 +- block/device_linux_test.go | 5 + block/device_partitions_linux.go | 97 +++ .../internal => internal}/cstruct/cstruct.go | 0 .../gpt => internal/gptstructs}/entry.go | 4 +- .../gpt => internal/gptstructs}/entry.h | 0 internal/gptstructs/gptstructs.go | 13 + .../gpt => internal/gptstructs}/header.go | 4 +- .../gpt => internal/gptstructs}/header.h | 0 internal/gptstructs/header_extra.go | 110 +++ internal/gptutil/gptutil.go | 50 ++ internal/gptutil/gptutil_test.go | 23 + internal/ioutil/ioutil.go | 34 + partitioning/gpt/gpt.go | 644 ++++++++++++++++++ partitioning/gpt/gpt_test.go | 292 ++++++++ partitioning/gpt/options.go | 68 ++ partitioning/gpt/testdata/allocate.gdisk | 21 + partitioning/gpt/testdata/allocate.sfdisk | 11 + partitioning/gpt/testdata/empty-no-mbr.gdisk | 18 + partitioning/gpt/testdata/empty.gdisk | 17 + partitioning/gpt/testdata/empty.sfdisk | 6 + partitioning/gpt/testdata/grow.gdisk | 19 + partitioning/gpt/testdata/grow.sfdisk | 9 + partitioning/gpt/testdata/mix-allocate.gdisk | 22 + partitioning/gpt/testdata/mix-allocate.sfdisk | 12 + 44 files changed, 1527 insertions(+), 211 deletions(-) delete mode 100644 blkid/internal/partitions/gpt/header_extra.go delete mode 100644 blkid/internal/partitions/gpt/lba.go delete mode 100644 blkid/internal/partitions/gpt/uuid.go create mode 100644 block/device_partitions_linux.go rename {blkid/internal => internal}/cstruct/cstruct.go (100%) rename {blkid/internal/partitions/gpt => internal/gptstructs}/entry.go (93%) rename {blkid/internal/partitions/gpt => internal/gptstructs}/entry.h (100%) create mode 100644 internal/gptstructs/gptstructs.go rename {blkid/internal/partitions/gpt => internal/gptstructs}/header.go (97%) rename {blkid/internal/partitions/gpt => internal/gptstructs}/header.h (100%) create mode 100644 internal/gptstructs/header_extra.go create mode 100644 internal/gptutil/gptutil.go create mode 100644 internal/gptutil/gptutil_test.go create mode 100644 internal/ioutil/ioutil.go create mode 100644 partitioning/gpt/gpt.go create mode 100644 partitioning/gpt/gpt_test.go create mode 100644 partitioning/gpt/options.go create mode 100644 partitioning/gpt/testdata/allocate.gdisk create mode 100644 partitioning/gpt/testdata/allocate.sfdisk create mode 100644 partitioning/gpt/testdata/empty-no-mbr.gdisk create mode 100644 partitioning/gpt/testdata/empty.gdisk create mode 100644 partitioning/gpt/testdata/empty.sfdisk create mode 100644 partitioning/gpt/testdata/grow.gdisk create mode 100644 partitioning/gpt/testdata/grow.sfdisk create mode 100644 partitioning/gpt/testdata/mix-allocate.gdisk create mode 100644 partitioning/gpt/testdata/mix-allocate.sfdisk diff --git a/.dockerignore b/.dockerignore index bfdbdd1..2e4e713 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,9 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2024-05-06T05:59:06Z by kres d15226e. +# Generated on 2024-07-17T17:13:05Z by kres ac94478-dirty. * +!internal !blkid !block !go.mod diff --git a/.kres.yaml b/.kres.yaml index 9e3e122..c34d7c1 100644 --- a/.kres.yaml +++ b/.kres.yaml @@ -34,6 +34,8 @@ spec: kind: custom.Step name: zfs-img spec: + makefile: + enabled: true docker: enabled: true stages: diff --git a/Dockerfile b/Dockerfile index 6fd5137..e505aa5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2024-07-15T11:59:50Z by kres ac94478. +# Generated on 2024-07-17T17:17:17Z by kres ac94478-dirty. ARG TOOLCHAIN @@ -56,6 +56,7 @@ COPY go.sum go.sum RUN cd . RUN --mount=type=cache,target=/go/pkg go mod download RUN --mount=type=cache,target=/go/pkg go mod verify +COPY ./internal ./internal COPY ./blkid ./blkid COPY ./block ./block RUN --mount=type=cache,target=/go/pkg go list -mod=readonly all >/dev/null diff --git a/Makefile b/Makefile index ac9ea07..14b2e30 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2024-07-15T11:59:50Z by kres ac94478. +# Generated on 2024-07-17T17:17:17Z by kres ac94478-dirty. # common variables @@ -175,6 +175,8 @@ unit-tests: ## Performs unit tests unit-tests-race: ## Performs unit tests with race detection enabled. @$(MAKE) target-$@ TARGET_ARGS="--allow security.insecure" +zfs-img: + .PHONY: lint lint: lint-golangci-lint lint-gofumpt lint-govulncheck ## Run all linters for the project. diff --git a/blkid/internal/filesystems/ext/ext.go b/blkid/internal/filesystems/ext/ext.go index 0cdc67a..9bdfeaf 100644 --- a/blkid/internal/filesystems/ext/ext.go +++ b/blkid/internal/filesystems/ext/ext.go @@ -5,7 +5,7 @@ // Package ext probes extfs filesystems. package ext -//go:generate go run ../../cstruct/cstruct.go -pkg ext -struct SuperBlock -input superblock.h -endianness LittleEndian +//go:generate go run ../../../../internal/cstruct/cstruct.go -pkg ext -struct SuperBlock -input superblock.h -endianness LittleEndian import ( "bytes" @@ -16,6 +16,7 @@ import ( "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/utils" + "github.com/siderolabs/go-blockdevice/v2/internal/ioutil" ) const sbOffset = 0x400 @@ -49,7 +50,7 @@ func (p *Probe) Name() string { func (p *Probe) Probe(r probe.Reader, _ magic.Magic) (*probe.Result, error) { buf := make([]byte, SUPERBLOCK_SIZE) - if err := utils.ReadFullAt(r, buf, sbOffset); err != nil { + if err := ioutil.ReadFullAt(r, buf, sbOffset); err != nil { return nil, err } diff --git a/blkid/internal/filesystems/iso9660/iso9660.go b/blkid/internal/filesystems/iso9660/iso9660.go index 5bfaffa..bd35976 100644 --- a/blkid/internal/filesystems/iso9660/iso9660.go +++ b/blkid/internal/filesystems/iso9660/iso9660.go @@ -5,7 +5,7 @@ // Package iso9660 probes ISO9660 filesystems. package iso9660 -//go:generate go run ../../cstruct/cstruct.go -pkg iso9660 -struct VolumeDescriptor -input volume.h -endianness NativeEndian +//go:generate go run ../../../../internal/cstruct/cstruct.go -pkg iso9660 -struct VolumeDescriptor -input volume.h -endianness NativeEndian import ( "strings" @@ -15,7 +15,7 @@ import ( "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/utils" + "github.com/siderolabs/go-blockdevice/v2/internal/ioutil" ) const ( @@ -62,7 +62,7 @@ vdLoop: for i := range vdMax { buf := make([]byte, VOLUMEDESCRIPTOR_SIZE) - if err := utils.ReadFullAt(r, buf, superblockOffset+sectorSize*int64(i)); err != nil { + if err := ioutil.ReadFullAt(r, buf, superblockOffset+sectorSize*int64(i)); err != nil { break } diff --git a/blkid/internal/filesystems/luks/luks.go b/blkid/internal/filesystems/luks/luks.go index e30674c..e221f37 100644 --- a/blkid/internal/filesystems/luks/luks.go +++ b/blkid/internal/filesystems/luks/luks.go @@ -5,7 +5,7 @@ // Package luks probes LUKS encrypted filesystems. package luks -//go:generate go run ../../cstruct/cstruct.go -pkg luks -struct Luks2Header -input luks2_header.h -endianness BigEndian +//go:generate go run ../../../../internal/cstruct/cstruct.go -pkg luks -struct Luks2Header -input luks2_header.h -endianness BigEndian import ( "bytes" @@ -15,7 +15,7 @@ import ( "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/utils" + "github.com/siderolabs/go-blockdevice/v2/internal/ioutil" ) var luksMagic = magic.Magic{ @@ -40,7 +40,7 @@ func (p *Probe) Name() string { func (p *Probe) Probe(r probe.Reader, _ magic.Magic) (*probe.Result, error) { buf := make([]byte, LUKS2HEADER_SIZE) - if err := utils.ReadFullAt(r, buf, 0); err != nil { + if err := ioutil.ReadFullAt(r, buf, 0); err != nil { return nil, err } diff --git a/blkid/internal/filesystems/lvm2/lvm2.go b/blkid/internal/filesystems/lvm2/lvm2.go index 1f722eb..af54ff8 100644 --- a/blkid/internal/filesystems/lvm2/lvm2.go +++ b/blkid/internal/filesystems/lvm2/lvm2.go @@ -5,12 +5,12 @@ // Package lvm2 probes LVM2 PVs. package lvm2 -//go:generate go run ../../cstruct/cstruct.go -pkg lvm2 -struct LVM2Header -input lvm2_header.h -endianness LittleEndian +//go:generate go run ../../../../internal/cstruct/cstruct.go -pkg lvm2 -struct LVM2Header -input lvm2_header.h -endianness LittleEndian import ( "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/utils" + "github.com/siderolabs/go-blockdevice/v2/internal/ioutil" ) var ( @@ -44,7 +44,7 @@ func (p *Probe) Name() string { func (p *Probe) probe(r probe.Reader, offset int64) (LVM2Header, error) { buf := make([]byte, LVM2HEADER_SIZE) - if err := utils.ReadFullAt(r, buf, offset); err != nil { + if err := ioutil.ReadFullAt(r, buf, offset); err != nil { return nil, err } diff --git a/blkid/internal/filesystems/squashfs/squashfs.go b/blkid/internal/filesystems/squashfs/squashfs.go index 59f3684..4564bb6 100644 --- a/blkid/internal/filesystems/squashfs/squashfs.go +++ b/blkid/internal/filesystems/squashfs/squashfs.go @@ -5,12 +5,12 @@ // Package squashfs probes Squash filesystems. package squashfs -//go:generate go run ../../cstruct/cstruct.go -pkg squashfs -struct SuperBlock -input superblock.h -endianness LittleEndian +//go:generate go run ../../../../internal/cstruct/cstruct.go -pkg squashfs -struct SuperBlock -input superblock.h -endianness LittleEndian import ( "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/utils" + "github.com/siderolabs/go-blockdevice/v2/internal/ioutil" ) var squashfsMagic1 = magic.Magic{ // big endian @@ -43,7 +43,7 @@ func (p *Probe) Name() string { func (p *Probe) Probe(r probe.Reader, _ magic.Magic) (*probe.Result, error) { buf := make([]byte, SUPERBLOCK_SIZE) - if err := utils.ReadFullAt(r, buf, 0); err != nil { + if err := ioutil.ReadFullAt(r, buf, 0); err != nil { return nil, err } diff --git a/blkid/internal/filesystems/swap/swap.go b/blkid/internal/filesystems/swap/swap.go index cda0d0d..92247c9 100644 --- a/blkid/internal/filesystems/swap/swap.go +++ b/blkid/internal/filesystems/swap/swap.go @@ -6,7 +6,7 @@ package swap // TODO: is it little or host endian? -//go:generate go run ../../cstruct/cstruct.go -pkg swap -struct SwapHeader -input swap_header.h -endianness LittleEndian +//go:generate go run ../../../../internal/cstruct/cstruct.go -pkg swap -struct SwapHeader -input swap_header.h -endianness LittleEndian import ( "bytes" @@ -16,7 +16,7 @@ import ( "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/utils" + "github.com/siderolabs/go-blockdevice/v2/internal/ioutil" ) var ( @@ -99,7 +99,7 @@ func (p *Probe) Name() string { func (p *Probe) Probe(r probe.Reader, m magic.Magic) (*probe.Result, error) { buf := make([]byte, SWAPHEADER_SIZE) - if err := utils.ReadFullAt(r, buf, 1024); err != nil { + if err := ioutil.ReadFullAt(r, buf, 1024); err != nil { return nil, err } diff --git a/blkid/internal/filesystems/talosmeta/talosmeta.go b/blkid/internal/filesystems/talosmeta/talosmeta.go index d67ce17..aff16db 100644 --- a/blkid/internal/filesystems/talosmeta/talosmeta.go +++ b/blkid/internal/filesystems/talosmeta/talosmeta.go @@ -10,7 +10,7 @@ import ( "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/utils" + "github.com/siderolabs/go-blockdevice/v2/internal/ioutil" ) // META constants, from talos/internal/pkg/meta/internal/adv/talos. @@ -45,7 +45,7 @@ func (p *Probe) Probe(r probe.Reader, _ magic.Magic) (*probe.Result, error) { buf := make([]byte, 4) for _, offset := range []int64{0, length} { - if err := utils.ReadFullAt(r, buf, offset); err != nil { + if err := ioutil.ReadFullAt(r, buf, offset); err != nil { return nil, err } @@ -53,7 +53,7 @@ func (p *Probe) Probe(r probe.Reader, _ magic.Magic) (*probe.Result, error) { continue } - if err := utils.ReadFullAt(r, buf, offset+length-4); err != nil { + if err := ioutil.ReadFullAt(r, buf, offset+length-4); err != nil { return nil, err } diff --git a/blkid/internal/filesystems/vfat/vfat.go b/blkid/internal/filesystems/vfat/vfat.go index 4ed0fe8..0b31cf1 100644 --- a/blkid/internal/filesystems/vfat/vfat.go +++ b/blkid/internal/filesystems/vfat/vfat.go @@ -5,14 +5,15 @@ // Package vfat probes FAT12/FAT16/FAT32 filesystems. package vfat -//go:generate go run ../../cstruct/cstruct.go -pkg vfat -struct MSDOSSB -input msdos.h -endianness LittleEndian +//go:generate go run ../../../../internal/cstruct/cstruct.go -pkg vfat -struct MSDOSSB -input msdos.h -endianness LittleEndian -//go:generate go run ../../cstruct/cstruct.go -pkg vfat -struct VFATSB -input vfat.h -endianness LittleEndian +//go:generate go run ../../../../internal/cstruct/cstruct.go -pkg vfat -struct VFATSB -input vfat.h -endianness LittleEndian import ( "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/utils" + "github.com/siderolabs/go-blockdevice/v2/internal/ioutil" ) var ( @@ -72,11 +73,11 @@ func (p *Probe) Probe(r probe.Reader, _ magic.Magic) (*probe.Result, error) { vfatBuf := make([]byte, VFATSB_SIZE) msdosBuf := make([]byte, MSDOSSB_SIZE) - if err := utils.ReadFullAt(r, vfatBuf, 0); err != nil { + if err := ioutil.ReadFullAt(r, vfatBuf, 0); err != nil { return nil, err } - if err := utils.ReadFullAt(r, msdosBuf, 0); err != nil { + if err := ioutil.ReadFullAt(r, msdosBuf, 0); err != nil { return nil, err } diff --git a/blkid/internal/filesystems/xfs/xfs.go b/blkid/internal/filesystems/xfs/xfs.go index 9bc7e7b..01e141c 100644 --- a/blkid/internal/filesystems/xfs/xfs.go +++ b/blkid/internal/filesystems/xfs/xfs.go @@ -5,7 +5,7 @@ // Package xfs probes XFS filesystems. package xfs -//go:generate go run ../../cstruct/cstruct.go -pkg xfs -struct SuperBlock -input superblock.h -endianness BigEndian +//go:generate go run ../../../../internal/cstruct/cstruct.go -pkg xfs -struct SuperBlock -input superblock.h -endianness BigEndian import ( "bytes" @@ -16,7 +16,7 @@ import ( "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/utils" + "github.com/siderolabs/go-blockdevice/v2/internal/ioutil" ) var xfsMagic = magic.Magic{ @@ -41,7 +41,7 @@ func (p *Probe) Name() string { func (p *Probe) Probe(r probe.Reader, _ magic.Magic) (*probe.Result, error) { buf := make([]byte, SUPERBLOCK_SIZE) - if err := utils.ReadFullAt(r, buf, 0); err != nil { + if err := ioutil.ReadFullAt(r, buf, 0); err != nil { return nil, err } diff --git a/blkid/internal/filesystems/zfs/zfs.go b/blkid/internal/filesystems/zfs/zfs.go index 385078a..282913e 100644 --- a/blkid/internal/filesystems/zfs/zfs.go +++ b/blkid/internal/filesystems/zfs/zfs.go @@ -5,14 +5,14 @@ // Package zfs probes ZFS filesystems. package zfs -//go:generate go run ../../cstruct/cstruct.go -pkg zfs -struct ZFSUB -input zfs.h -endianness LittleEndian +//go:generate go run ../../../../internal/cstruct/cstruct.go -pkg zfs -struct ZFSUB -input zfs.h -endianness LittleEndian import ( "fmt" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/utils" + "github.com/siderolabs/go-blockdevice/v2/internal/ioutil" ) // https://github.com/util-linux/util-linux/blob/c0207d354ee47fb56acfa64b03b5b559bb301280/libblkid/src/superblocks/zfs.c @@ -66,7 +66,7 @@ func (p *Probe) Probe(r probe.Reader, _ magic.Magic) (*probe.Result, error) { size - zfsStartOffset - lastLabelOffset, } { labelBuf := make([]byte, zfsVdevLabelSize) - if err := utils.ReadFullAt(r, labelBuf, int64(labelOffset)); err != nil { + if err := ioutil.ReadFullAt(r, labelBuf, int64(labelOffset)); err != nil { return nil, err } diff --git a/blkid/internal/partitions/gpt/gpt.go b/blkid/internal/partitions/gpt/gpt.go index 17bc230..4c57122 100644 --- a/blkid/internal/partitions/gpt/gpt.go +++ b/blkid/internal/partitions/gpt/gpt.go @@ -7,7 +7,6 @@ package gpt import ( "bytes" - "hash/crc32" "github.com/google/uuid" "github.com/siderolabs/go-pointer" @@ -15,13 +14,10 @@ import ( "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/utils" + "github.com/siderolabs/go-blockdevice/v2/internal/gptstructs" + "github.com/siderolabs/go-blockdevice/v2/internal/gptutil" ) -//go:generate go run ../../cstruct/cstruct.go -pkg gpt -struct Header -input header.h -endianness LittleEndian - -//go:generate go run ../../cstruct/cstruct.go -pkg gpt -struct Entry -input entry.h -endianness LittleEndian - // nullMagic matches always. var nullMagic = magic.Magic{} @@ -38,27 +34,24 @@ func (p *Probe) Name() string { return "gpt" } -const ( - primaryLBA = 1 - headerSignature = 0x5452415020494645 // "EFI PART" -) +const primaryLBA = 1 // Probe runs the further inspection and returns the result if successful. func (p *Probe) Probe(r probe.Reader, _ magic.Magic) (*probe.Result, error) { - lastLBA, ok := lastLBA(r) + lastLBA, ok := gptutil.LastLBA(r) if !ok { return nil, nil //nolint:nilnil } // try reading primary header - hdr, entries, err := readHeader(r, primaryLBA, lastLBA) + hdr, entries, err := gptstructs.ReadHeader(r, primaryLBA, lastLBA) if err != nil { return nil, err } if hdr == nil { // try reading backup header - hdr, entries, err = readHeader(r, lastLBA, lastLBA) + hdr, entries, err = gptstructs.ReadHeader(r, lastLBA, lastLBA) if err != nil { return nil, err } @@ -69,7 +62,7 @@ func (p *Probe) Probe(r probe.Reader, _ magic.Magic) (*probe.Result, error) { return nil, nil //nolint:nilnil } - ptUUID, err := uuid.FromBytes(guidToUUID(hdr.Get_disk_guid())) + ptUUID, err := uuid.FromBytes(gptutil.GUIDToUUID(hdr.Get_disk_guid())) if err != nil { return nil, err } @@ -107,12 +100,12 @@ func (p *Probe) Probe(r probe.Reader, _ magic.Magic) (*probe.Result, error) { continue } - partUUID, err := uuid.FromBytes(guidToUUID(entry.Get_unique_partition_guid())) + partUUID, err := uuid.FromBytes(gptutil.GUIDToUUID(entry.Get_unique_partition_guid())) if err != nil { return nil, err } - typeUUID, err := uuid.FromBytes(guidToUUID(entry.Get_partition_type_guid())) + typeUUID, err := uuid.FromBytes(gptutil.GUIDToUUID(entry.Get_partition_type_guid())) if err != nil { return nil, err } @@ -140,76 +133,3 @@ func (p *Probe) Probe(r probe.Reader, _ magic.Magic) (*probe.Result, error) { return result, nil } - -func readHeader(r probe.Reader, lba, lastLBA uint64) (*Header, []Entry, error) { - sectorSize := r.GetSectorSize() - buf := make([]byte, sectorSize) - - if err := utils.ReadFullAt(r, buf, int64(lba)*int64(sectorSize)); err != nil { - return nil, nil, err - } - - hdr := Header(buf) - - // verify the header signature - if hdr.Get_signature() != headerSignature { - return nil, nil, nil - } - - // sanity check the header size - headerSize := hdr.Get_header_size() - if headerSize < HEADER_SIZE || uint(headerSize) > sectorSize { - return nil, nil, nil - } - - // verify the header checksum - if hdr.Get_header_crc32() != hdr.calculateChecksum() { - return nil, nil, nil - } - - // verify LBA - if hdr.Get_my_lba() != lba { - return nil, nil, nil - } - - firstUsableLBA := hdr.Get_first_usable_lba() - lastUsableLBA := hdr.Get_last_usable_lba() - - // verify the usable LBA range - if lastUsableLBA < firstUsableLBA || firstUsableLBA > lastLBA || lastUsableLBA > lastLBA { - return nil, nil, nil - } - - // header should be outside the usable range - if firstUsableLBA < lba && lba < lastUsableLBA { - return nil, nil, nil - } - - // read the partition entries - if hdr.Get_sizeof_partition_entry() != ENTRY_SIZE { - return nil, nil, nil - } - - if hdr.Get_num_partition_entries() == 0 || hdr.Get_num_partition_entries() > 128 { - return nil, nil, nil - } - - // read partition entries, verify checksum - entriesBuffer := make([]byte, hdr.Get_num_partition_entries()*ENTRY_SIZE) - - if err := utils.ReadFullAt(r, entriesBuffer, int64(hdr.Get_partition_entries_lba())*int64(sectorSize)); err != nil { - return nil, nil, err - } - - entriesChecksum := crc32.ChecksumIEEE(entriesBuffer) - if entriesChecksum != hdr.Get_partition_entry_array_crc32() { - return nil, nil, nil - } - - entries := make([]Entry, hdr.Get_num_partition_entries()) - for i := range entries { - entries[i] = Entry(entriesBuffer[i*ENTRY_SIZE : (i+1)*ENTRY_SIZE]) - } - - return &hdr, entries, nil -} diff --git a/blkid/internal/partitions/gpt/header_extra.go b/blkid/internal/partitions/gpt/header_extra.go deleted file mode 100644 index a481d80..0000000 --- a/blkid/internal/partitions/gpt/header_extra.go +++ /dev/null @@ -1,21 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package gpt - -import ( - "hash/crc32" - "slices" -) - -func (h Header) calculateChecksum() uint32 { - b := slices.Clone(h[:HEADER_SIZE]) - - b[16] = 0 - b[17] = 0 - b[18] = 0 - b[19] = 0 - - return crc32.ChecksumIEEE(b) -} diff --git a/blkid/internal/partitions/gpt/lba.go b/blkid/internal/partitions/gpt/lba.go deleted file mode 100644 index fc4ede1..0000000 --- a/blkid/internal/partitions/gpt/lba.go +++ /dev/null @@ -1,20 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package gpt - -import ( - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" -) - -func lastLBA(r probe.Reader) (uint64, bool) { - sectorSize := r.GetSectorSize() - size := r.GetSize() - - if uint64(sectorSize) > size { - return 0, false - } - - return (size / uint64(sectorSize)) - 1, true -} diff --git a/blkid/internal/partitions/gpt/uuid.go b/blkid/internal/partitions/gpt/uuid.go deleted file mode 100644 index 6882ac4..0000000 --- a/blkid/internal/partitions/gpt/uuid.go +++ /dev/null @@ -1,17 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package gpt - -func guidToUUID(g []byte) []byte { - return append( - []byte{ - g[3], g[2], g[1], g[0], - g[5], g[4], - g[7], g[6], - g[8], g[9], - }, - g[10:16]..., - ) -} diff --git a/blkid/internal/utils/utils.go b/blkid/internal/utils/utils.go index f88f446..7dbc9ee 100644 --- a/blkid/internal/utils/utils.go +++ b/blkid/internal/utils/utils.go @@ -7,7 +7,6 @@ package utils import ( "hash/crc32" - "io" "sync" ) @@ -24,27 +23,3 @@ func CRC32c(buf []byte) uint32 { func IsPowerOf2[T uint8 | uint16 | uint32 | uint64](num T) bool { return (num != 0 && ((num & (num - 1)) == 0)) } - -// ReadFullAt is io.ReadFull for io.ReaderAt. -func ReadFullAt(r io.ReaderAt, buf []byte, offset int64) error { - for n := 0; n < len(buf); { - m, err := r.ReadAt(buf[n:], offset) - - n += m - offset += int64(m) - - if err != nil { - if err == io.EOF && n == len(buf) { - return nil - } - - if err == io.EOF { - err = io.ErrUnexpectedEOF - } - - return err - } - } - - return nil -} diff --git a/blkid/probe_linux.go b/blkid/probe_linux.go index 318ad1a..e69f8ff 100644 --- a/blkid/probe_linux.go +++ b/blkid/probe_linux.go @@ -15,7 +15,7 @@ import ( "github.com/siderolabs/go-blockdevice/v2/blkid/internal/chain" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/utils" + "github.com/siderolabs/go-blockdevice/v2/internal/ioutil" ) type probeReader struct { @@ -116,7 +116,7 @@ func (i *Info) probe(f *os.File, chain chain.Chain, offset, length uint64, optio buf := make([]byte, magicReadSize) - if err := utils.ReadFullAt(f, buf, int64(offset)); err != nil { + if err := ioutil.ReadFullAt(f, buf, int64(offset)); err != nil { return nil, nil, fmt.Errorf("error reading magic buffer: %w", err) } diff --git a/block/device_linux_test.go b/block/device_linux_test.go index 91cd4af..2331eb9 100644 --- a/block/device_linux_test.go +++ b/block/device_linux_test.go @@ -113,6 +113,11 @@ func TestDevice(t *testing.T) { require.NoError(t, err) assert.False(t, isWhole) + + partitionNum, err := devWhole.GetKernelLastPartitionNum() + require.NoError(t, err) + + assert.Equal(t, 6, partitionNum) }) t.Run("get whole disk", func(t *testing.T) { diff --git a/block/device_partitions_linux.go b/block/device_partitions_linux.go new file mode 100644 index 0000000..12d07ad --- /dev/null +++ b/block/device_partitions_linux.go @@ -0,0 +1,97 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package block + +import ( + "os" + "path/filepath" + "runtime" + "strconv" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +// KernelPartitionAdd invokes the BLKPG_ADD_PARTITION ioctl. +func (d *Device) KernelPartitionAdd(no int, start, length uint64) error { + return d.inform(unix.BLKPG_ADD_PARTITION, int32(no), int64(start), int64(length)) +} + +// KernelPartitionResize invokes the BLKPG_RESIZE_PARTITION ioctl. +func (d *Device) KernelPartitionResize(no int, first, length uint64) error { + return d.inform(unix.BLKPG_RESIZE_PARTITION, int32(no), int64(first), int64(length)) +} + +// KernelPartitionDelete invokes the BLKPG_DEL_PARTITION ioctl. +func (d *Device) KernelPartitionDelete(no int) error { + return d.inform(unix.BLKPG_DEL_PARTITION, int32(no), 0, 0) +} + +func (d *Device) inform(op int32, no int32, start, length int64) error { + data := &unix.BlkpgPartition{ + Start: start, + Length: length, + Pno: no, + } + + arg := &unix.BlkpgIoctlArg{ + Op: op, + Datalen: int32(unsafe.Sizeof(*data)), + Data: (*byte)(unsafe.Pointer(data)), + } + + _, _, errno := syscall.Syscall( + syscall.SYS_IOCTL, + d.f.Fd(), + unix.BLKPG, + uintptr(unsafe.Pointer(arg)), + ) + + runtime.KeepAlive(d) + + if errno == 0 { + return nil + } + + return errno +} + +// GetKernelLastPartitionNum returns the maximum partition number in the kernel. +func (d *Device) GetKernelLastPartitionNum() (int, error) { + sysFsPath, err := d.sysFsPath() + if err != nil { + return 0, err + } + + contents, err := os.ReadDir(sysFsPath) + if err != nil { + return 0, err + } + + var max int + + for _, entry := range contents { + if !entry.IsDir() { + continue + } + + contents := readSysFsFile(filepath.Join(sysFsPath, entry.Name(), "partition")) + if len(contents) == 0 { + continue + } + + partNum, err := strconv.Atoi(contents) + if err != nil { + continue + } + + if partNum > max { + max = partNum + } + } + + return max, nil +} diff --git a/blkid/internal/cstruct/cstruct.go b/internal/cstruct/cstruct.go similarity index 100% rename from blkid/internal/cstruct/cstruct.go rename to internal/cstruct/cstruct.go diff --git a/blkid/internal/partitions/gpt/entry.go b/internal/gptstructs/entry.go similarity index 93% rename from blkid/internal/partitions/gpt/entry.go rename to internal/gptstructs/entry.go index 77a7b56..aff75aa 100644 --- a/blkid/internal/partitions/gpt/entry.go +++ b/internal/gptstructs/entry.go @@ -2,9 +2,9 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -// Code generated by "cstruct -pkg gpt -struct Entry -input entry.h -endianness LittleEndian"; DO NOT EDIT. +// Code generated by "cstruct -pkg gptstructs -struct Entry -input entry.h -endianness LittleEndian"; DO NOT EDIT. -package gpt +package gptstructs import "encoding/binary" diff --git a/blkid/internal/partitions/gpt/entry.h b/internal/gptstructs/entry.h similarity index 100% rename from blkid/internal/partitions/gpt/entry.h rename to internal/gptstructs/entry.h diff --git a/internal/gptstructs/gptstructs.go b/internal/gptstructs/gptstructs.go new file mode 100644 index 0000000..5cd8ee3 --- /dev/null +++ b/internal/gptstructs/gptstructs.go @@ -0,0 +1,13 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package gptstructs provides encoded definitions for GPT on-disk structures. +package gptstructs + +//go:generate go run ../cstruct/cstruct.go -pkg gptstructs -struct Header -input header.h -endianness LittleEndian + +//go:generate go run ../cstruct/cstruct.go -pkg gptstructs -struct Entry -input entry.h -endianness LittleEndian + +// NumEntries is the number of entries in the GPT. +const NumEntries = 128 diff --git a/blkid/internal/partitions/gpt/header.go b/internal/gptstructs/header.go similarity index 97% rename from blkid/internal/partitions/gpt/header.go rename to internal/gptstructs/header.go index 4d25f89..1faba19 100644 --- a/blkid/internal/partitions/gpt/header.go +++ b/internal/gptstructs/header.go @@ -2,9 +2,9 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -// Code generated by "cstruct -pkg gpt -struct Header -input header.h -endianness LittleEndian"; DO NOT EDIT. +// Code generated by "cstruct -pkg gptstructs -struct Header -input header.h -endianness LittleEndian"; DO NOT EDIT. -package gpt +package gptstructs import "encoding/binary" diff --git a/blkid/internal/partitions/gpt/header.h b/internal/gptstructs/header.h similarity index 100% rename from blkid/internal/partitions/gpt/header.h rename to internal/gptstructs/header.h diff --git a/internal/gptstructs/header_extra.go b/internal/gptstructs/header_extra.go new file mode 100644 index 0000000..15f7588 --- /dev/null +++ b/internal/gptstructs/header_extra.go @@ -0,0 +1,110 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package gptstructs + +import ( + "hash/crc32" + "io" + "slices" + + "github.com/siderolabs/go-blockdevice/v2/internal/ioutil" +) + +// HeaderSignature is the signature of the GPT header. +const HeaderSignature = 0x5452415020494645 // "EFI PART" + +// CalculateChecksum calculates the checksum of the header. +func (h Header) CalculateChecksum() uint32 { + b := slices.Clone(h[:HEADER_SIZE]) + + b[16] = 0 + b[17] = 0 + b[18] = 0 + b[19] = 0 + + return crc32.ChecksumIEEE(b) +} + +// HeaderReader is an interface for reading GPT headers. +type HeaderReader interface { + io.ReaderAt + GetSectorSize() uint +} + +// ReadHeader reads the GPT header and partition entries. +// +// It does sanity checks on the header and partition entries. +func ReadHeader(r HeaderReader, lba, lastLBA uint64) (*Header, []Entry, error) { + sectorSize := r.GetSectorSize() + buf := make([]byte, sectorSize) + + if err := ioutil.ReadFullAt(r, buf, int64(lba)*int64(sectorSize)); err != nil { + return nil, nil, err + } + + hdr := Header(buf) + + // verify the header signature + if hdr.Get_signature() != HeaderSignature { + return nil, nil, nil + } + + // sanity check the header size + headerSize := hdr.Get_header_size() + if headerSize < HEADER_SIZE || uint(headerSize) > sectorSize { + return nil, nil, nil + } + + // verify the header checksum + if hdr.Get_header_crc32() != hdr.CalculateChecksum() { + return nil, nil, nil + } + + // verify LBA + if hdr.Get_my_lba() != lba { + return nil, nil, nil + } + + firstUsableLBA := hdr.Get_first_usable_lba() + lastUsableLBA := hdr.Get_last_usable_lba() + + // verify the usable LBA range + if lastUsableLBA < firstUsableLBA || firstUsableLBA > lastLBA || lastUsableLBA > lastLBA { + return nil, nil, nil + } + + // header should be outside the usable range + if firstUsableLBA < lba && lba < lastUsableLBA { + return nil, nil, nil + } + + // read the partition entries + if hdr.Get_sizeof_partition_entry() != ENTRY_SIZE { + return nil, nil, nil + } + + if hdr.Get_num_partition_entries() == 0 || hdr.Get_num_partition_entries() > NumEntries { + return nil, nil, nil + } + + // read partition entries, verify checksum + entriesBuffer := make([]byte, hdr.Get_num_partition_entries()*ENTRY_SIZE) + + if err := ioutil.ReadFullAt(r, entriesBuffer, int64(hdr.Get_partition_entries_lba())*int64(sectorSize)); err != nil { + return nil, nil, err + } + + entriesChecksum := crc32.ChecksumIEEE(entriesBuffer) + if entriesChecksum != hdr.Get_partition_entry_array_crc32() { + return nil, nil, nil + } + + entries := make([]Entry, hdr.Get_num_partition_entries()) + for i := range entries { + entries[i] = Entry(entriesBuffer[i*ENTRY_SIZE : (i+1)*ENTRY_SIZE]) + } + + return &hdr, entries, nil +} diff --git a/internal/gptutil/gptutil.go b/internal/gptutil/gptutil.go new file mode 100644 index 0000000..97b8f0a --- /dev/null +++ b/internal/gptutil/gptutil.go @@ -0,0 +1,50 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package gptutil implements helper functions for GPT tables. +package gptutil + +// DiskSizer is an interface for block devices that can provide their sector size and total size. +type DiskSizer interface { + GetSectorSize() uint + GetSize() uint64 +} + +// LastLBA returns the last logical block address of the device. +func LastLBA(r DiskSizer) (uint64, bool) { + sectorSize := r.GetSectorSize() + size := r.GetSize() + + if uint64(sectorSize) > size { + return 0, false + } + + return (size / uint64(sectorSize)) - 1, true +} + +// GUIDToUUID converts a GPT GUID to a UUID. +func GUIDToUUID(g []byte) []byte { + return append( + []byte{ + g[3], g[2], g[1], g[0], + g[5], g[4], + g[7], g[6], + g[8], g[9], + }, + g[10:16]..., + ) +} + +// UUIDToGUID converts a UUID to a GPT GUID. +func UUIDToGUID(u []byte) []byte { + return append( + []byte{ + u[3], u[2], u[1], u[0], + u[5], u[4], + u[7], u[6], + u[8], u[9], + }, + u[10:16]..., + ) +} diff --git a/internal/gptutil/gptutil_test.go b/internal/gptutil/gptutil_test.go new file mode 100644 index 0000000..22f9ab6 --- /dev/null +++ b/internal/gptutil/gptutil_test.go @@ -0,0 +1,23 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package gptutil_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/siderolabs/go-blockdevice/v2/internal/gptutil" +) + +func TestGUIDToUUID(t *testing.T) { + uuid := []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77} + + guid := []byte{0x67, 0x45, 0x23, 0x01, 0xab, 0x89, 0xef, 0xcd, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77} + + assert.Equal(t, uuid, gptutil.GUIDToUUID(guid)) + assert.Equal(t, guid, gptutil.GUIDToUUID(uuid)) + assert.Equal(t, uuid, gptutil.GUIDToUUID(gptutil.UUIDToGUID(uuid))) +} diff --git a/internal/ioutil/ioutil.go b/internal/ioutil/ioutil.go new file mode 100644 index 0000000..bb6615a --- /dev/null +++ b/internal/ioutil/ioutil.go @@ -0,0 +1,34 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package ioutil provides IO utility functions. +package ioutil + +import ( + "io" +) + +// ReadFullAt is io.ReadFull for io.ReaderAt. +func ReadFullAt(r io.ReaderAt, buf []byte, offset int64) error { + for n := 0; n < len(buf); { + m, err := r.ReadAt(buf[n:], offset) + + n += m + offset += int64(m) + + if err != nil { + if err == io.EOF && n == len(buf) { + return nil + } + + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + + return err + } + } + + return nil +} diff --git a/partitioning/gpt/gpt.go b/partitioning/gpt/gpt.go new file mode 100644 index 0000000..9b72f60 --- /dev/null +++ b/partitioning/gpt/gpt.go @@ -0,0 +1,644 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package gpt implements read/write support for GPT partition tables. +package gpt + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "hash/crc32" + "io" + "math" + "os" + "slices" + + "github.com/google/uuid" + "github.com/siderolabs/gen/xslices" + "golang.org/x/sys/unix" + "golang.org/x/text/encoding/unicode" + + "github.com/siderolabs/go-blockdevice/v2/block" + "github.com/siderolabs/go-blockdevice/v2/internal/gptstructs" + "github.com/siderolabs/go-blockdevice/v2/internal/gptutil" + "github.com/siderolabs/go-blockdevice/v2/internal/ioutil" +) + +// Device is an interface around actual block device. +type Device interface { + io.ReaderAt + io.WriterAt + + GetSectorSize() uint + GetSize() uint64 + GetIOSize() (uint, error) + Sync() error + + GetKernelLastPartitionNum() (int, error) + KernelPartitionAdd(no int, start, length uint64) error + KernelPartitionResize(no int, first, length uint64) error + KernelPartitionDelete(no int) error +} + +// Table is a wrapper type around GPT partition table. +type Table struct { + dev Device + // partition entries are indexed with the partition number. + // + // if the partition is missing, its entry is `nil`. + entries []*Partition + + lastLBA uint64 + + primaryHeaderLBA, secondaryHeaderLBA uint64 + primaryPartitionsLBA, secondaryPartitionsLBA uint64 + firstUsableLBA, lastUsableLBA uint64 + + diskGUID uuid.UUID + + options Options + + alignment uint64 + sectorSize uint +} + +// Partition is a single partition entry in GPT. +type Partition struct { + Name string + + TypeGUID uuid.UUID + PartGUID uuid.UUID + + FirstLBA uint64 + LastLBA uint64 + + Flags uint64 +} + +type deviceWrapper struct { + *os.File + *block.Device + + size uint64 +} + +func (wrapper *deviceWrapper) GetSize() uint64 { + return wrapper.size +} + +// DeviceFromBlockDevice creates a new Device from a block.Device. +func DeviceFromBlockDevice(dev *block.Device, f *os.File) (Device, error) { + size, err := dev.GetSize() + if err != nil { + return nil, err + } + + return &deviceWrapper{ + File: f, + Device: dev, + size: size, + }, nil +} + +// New creates a new (empty) partition table for a specified device. +func New(dev Device, opts ...Option) (*Table, error) { + var options Options + + for _, opt := range opts { + opt(&options) + } + + lastLBA, ok := gptutil.LastLBA(dev) + if !ok { + return nil, errors.New("failed to calculate last LBA (device too small?)") + } + + if lastLBA < 33 { + return nil, errors.New("device too small for GPT") + } + + diskGUID := options.DiskGUID + if diskGUID == uuid.Nil { + diskGUID = uuid.New() + } + + t := &Table{ + dev: dev, + options: options, + diskGUID: diskGUID, + } + + t.init(lastLBA) + + return t, nil +} + +// Read reads the partition table from the device. +func Read(dev Device, opts ...Option) (*Table, error) { + var options Options + + for _, opt := range opts { + opt(&options) + } + + lastLBA, ok := gptutil.LastLBA(dev) + if !ok { + return nil, errors.New("failed to calculate last LBA (device too small?)") + } + + if lastLBA < 33 { + return nil, errors.New("device too small for GPT") + } + + hdr, entries, err := gptstructs.ReadHeader(dev, 1, lastLBA) + if err != nil { + return nil, err + } + + if hdr == nil { + hdr, entries, err = gptstructs.ReadHeader(dev, lastLBA, lastLBA) + if err != nil { + return nil, err + } + } + + if hdr == nil { + return nil, errors.New("no GPT header found") + } + + diskGUID, err := uuid.FromBytes(gptutil.GUIDToUUID(hdr.Get_disk_guid())) + if err != nil { + return nil, err + } + + t := &Table{ + dev: dev, + options: options, + diskGUID: diskGUID, + } + + t.init(lastLBA) + + // decode entries + partitions := make([]*Partition, len(entries)) + + zeroGUID := make([]byte, 16) + utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM) + + lastFilledIdx := -1 + + for idx, entry := range entries { + if entry.Get_starting_lba() < t.firstUsableLBA || entry.Get_ending_lba() > t.lastUsableLBA { + continue + } + + // skip zero GUIDs + if bytes.Equal(entry.Get_partition_type_guid(), zeroGUID) { + continue + } + + partUUID, err := uuid.FromBytes(gptutil.GUIDToUUID(entry.Get_unique_partition_guid())) + if err != nil { + return nil, err + } + + typeUUID, err := uuid.FromBytes(gptutil.GUIDToUUID(entry.Get_partition_type_guid())) + if err != nil { + return nil, err + } + + name, err := utf16.NewDecoder().Bytes(entry.Get_partition_name()) + if err != nil { + return nil, err + } + + name = bytes.TrimRight(name, "\x00") + + partitions[idx] = &Partition{ + Name: string(name), + + TypeGUID: typeUUID, + PartGUID: partUUID, + + FirstLBA: entry.Get_starting_lba(), + LastLBA: entry.Get_ending_lba(), + + Flags: entry.Get_attributes(), + } + + lastFilledIdx = idx + } + + if lastFilledIdx >= 0 { + t.entries = partitions[:lastFilledIdx+1] + } + + return t, nil +} + +func (t *Table) init(lastLBA uint64) { + t.lastLBA = lastLBA + t.sectorSize = t.dev.GetSectorSize() + + lbasForEntries := (gptstructs.ENTRY_SIZE*gptstructs.NumEntries + t.sectorSize - 1) / t.sectorSize + + t.primaryHeaderLBA = uint64(1) + t.secondaryHeaderLBA = lastLBA + + t.primaryPartitionsLBA = t.primaryHeaderLBA + 1 + uint64(t.options.SkipLBAs) + t.secondaryPartitionsLBA = t.secondaryHeaderLBA - uint64(lbasForEntries) + + t.firstUsableLBA = t.primaryPartitionsLBA + uint64(lbasForEntries) + t.lastUsableLBA = t.secondaryPartitionsLBA - 1 + + ioSize, err := t.dev.GetIOSize() + if err != nil { + ioSize = t.sectorSize + } + + alignmentSize := max(ioSize, 2048*512) + t.alignment = uint64((alignmentSize + t.sectorSize - 1) / t.sectorSize) +} + +// Clear the partition table. +func (t *Table) Clear() { + t.entries = nil +} + +// Compact the partition table by removing empty entries. +func (t *Table) Compact() { + t.entries = xslices.FilterInPlace(t.entries, func(e *Partition) bool { + return e != nil + }) +} + +type allocatableRange struct { + lowLBA uint64 + highLBA uint64 + + partitionIdx int + + size uint64 +} + +// allocatableRanges returns the slices of LBA ranges that are not allocated to any partition. +func (t *Table) allocatableRanges() []allocatableRange { + partitionIdx := 0 + lowLBA := t.firstUsableLBA + + var ranges []allocatableRange + + for { + for partitionIdx < len(t.entries) { + if t.entries[partitionIdx] == nil { + partitionIdx++ + } + + break + } + + var highLBA uint64 + + if partitionIdx < len(t.entries) { + highLBA = t.entries[partitionIdx].FirstLBA - 1 + } else { + highLBA = t.lastUsableLBA + } + + lowLBA = (lowLBA + t.alignment - 1) / t.alignment * t.alignment + + if highLBA > lowLBA { + ranges = append(ranges, allocatableRange{ + lowLBA: lowLBA, + highLBA: highLBA, + partitionIdx: partitionIdx, + size: (highLBA - lowLBA + 1) * uint64(t.sectorSize), + }) + } + + if highLBA == t.lastUsableLBA { + break + } + + lowLBA = t.entries[partitionIdx].LastLBA + 1 + partitionIdx++ + } + + return ranges +} + +// LargestContiguousAllocatable returns the size of the largest contiguous allocatable range. +func (t *Table) LargestContiguousAllocatable() uint64 { + ranges := t.allocatableRanges() + + var largest uint64 + + for _, r := range ranges { + if r.size > largest { + largest = r.size + } + } + + return largest +} + +// AllocatePartition adds a new partition to the table. +// +// If successful, returns the partition number (1-indexed) and the partition entry created. +func (t *Table) AllocatePartition(size uint64, name string, partType uuid.UUID, opts ...PartitionOption) (int, Partition, error) { + var options PartitionOptions + + for _, o := range opts { + o(&options) + } + + if size < uint64(t.sectorSize) { + return 0, Partition{}, errors.New("partition size must be greater than sector size") + } + + if options.UniqueGUID == uuid.Nil { + options.UniqueGUID = uuid.New() + } + + var smallestRange allocatableRange + + for _, allocatableRange := range t.allocatableRanges() { + if allocatableRange.size >= size && (smallestRange.size == 0 || allocatableRange.size < smallestRange.size) { + smallestRange = allocatableRange + } + } + + if smallestRange.size == 0 { + return 0, Partition{}, errors.New("no allocatable range found") + } + + entry := &Partition{ + Name: name, + TypeGUID: partType, + PartGUID: options.UniqueGUID, + FirstLBA: smallestRange.lowLBA, + LastLBA: smallestRange.lowLBA + size/uint64(t.sectorSize) - 1, + Flags: options.Flags, + } + + if smallestRange.partitionIdx > 0 && t.entries[smallestRange.partitionIdx-1] == nil { + t.entries[smallestRange.partitionIdx-1] = entry + } else { + t.entries = slices.Insert( + t.entries, + smallestRange.partitionIdx, + entry, + ) + } + + return smallestRange.partitionIdx + 1, *entry, nil +} + +// AvailablePartitionGrowth returns the number of bytes that can be added to the partition. +func (t *Table) AvailablePartitionGrowth(partition int) (uint64, error) { + if partition < 0 || partition >= len(t.entries) { + return 0, fmt.Errorf("partition %d out of range", partition) + } + + if t.entries[partition] == nil { + return 0, fmt.Errorf("partition %d is not allocated", partition) + } + + for _, allocatableRange := range t.allocatableRanges() { + if allocatableRange.partitionIdx == partition+1 { + return allocatableRange.size, nil + } + } + + return 0, nil +} + +// GrowPartition grows the partition by the specified number of bytes. +func (t *Table) GrowPartition(partition int, size uint64) error { + allowedGrowth, err := t.AvailablePartitionGrowth(partition) + if err != nil { + return err + } + + if size > allowedGrowth { + return fmt.Errorf("requested growth %d exceeds available growth %d", size, allowedGrowth) + } + + entry := t.entries[partition] + entry.LastLBA += size / uint64(t.sectorSize) + + return nil +} + +// DeletePartition deletes a partition from the table. +func (t *Table) DeletePartition(partition int) error { + if partition < 0 || partition >= len(t.entries) { + return fmt.Errorf("partition %d out of range", partition) + } + + t.entries[partition] = nil + + return nil +} + +// Partitions returns the list of partitions in the table. +// +// The returned list should not be modified. +// Partitions in the list are zero-indexed, while +// Linux kernel partitions are one-indexed. +func (t *Table) Partitions() []*Partition { + return slices.Clone(t.entries) +} + +// Write writes the partition table to the device. +func (t *Table) Write() error { + // build entries + entriesBuf := make([]byte, gptstructs.ENTRY_SIZE*gptstructs.NumEntries) + + utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM) + + for i, entry := range t.entries { + if entry == nil { + // zeroed entry + continue + } + + // write partition entry + entryBuf := gptstructs.Entry(entriesBuf[i*gptstructs.ENTRY_SIZE : (i+1)*gptstructs.ENTRY_SIZE]) + entryBuf.Put_partition_type_guid(gptutil.UUIDToGUID(entry.TypeGUID[:])) + entryBuf.Put_unique_partition_guid(gptutil.UUIDToGUID(entry.PartGUID[:])) + entryBuf.Put_starting_lba(entry.FirstLBA) + entryBuf.Put_ending_lba(entry.LastLBA) + entryBuf.Put_attributes(entry.Flags) + + nameBuf, err := utf16.NewEncoder().Bytes([]byte(entry.Name)) + if err != nil { + return fmt.Errorf("failed to encode partition name: %w", err) + } + + if len(nameBuf) > 72 { + return fmt.Errorf("partition name %q too long: %d bytes", entry.Name, len(nameBuf)) + } + + entryBuf.Put_partition_name(nameBuf) + } + + entriesChecksum := crc32.ChecksumIEEE(entriesBuf) + + // GPT header should occupy whole sector + header := gptstructs.Header(make([]byte, t.sectorSize)) + header.Put_signature(gptstructs.HeaderSignature) + header.Put_revision(0x00010000) + header.Put_header_size(gptstructs.HEADER_SIZE) + header.Put_first_usable_lba(t.firstUsableLBA) + header.Put_last_usable_lba(t.lastUsableLBA) + header.Put_disk_guid(gptutil.UUIDToGUID(t.diskGUID[:])) + header.Put_num_partition_entries(gptstructs.NumEntries) + header.Put_sizeof_partition_entry(gptstructs.ENTRY_SIZE) + header.Put_partition_entry_array_crc32(entriesChecksum) + + // now, primary and secondary headers/entries + primaryHeader := slices.Clone(header) + primaryHeader.Put_my_lba(t.primaryHeaderLBA) + primaryHeader.Put_alternate_lba(t.secondaryHeaderLBA) + primaryHeader.Put_partition_entries_lba(t.primaryPartitionsLBA) + primaryHeader.Put_header_crc32(primaryHeader.CalculateChecksum()) + + _, err := t.dev.WriteAt(primaryHeader, int64(t.primaryHeaderLBA)*int64(t.sectorSize)) + if err != nil { + return fmt.Errorf("failed to write primary header: %w", err) + } + + _, err = t.dev.WriteAt(entriesBuf, int64(t.primaryPartitionsLBA)*int64(t.sectorSize)) + if err != nil { + return fmt.Errorf("failed to write primary entries: %w", err) + } + + secondaryHeader := slices.Clone(header) + secondaryHeader.Put_my_lba(t.secondaryHeaderLBA) + secondaryHeader.Put_alternate_lba(t.primaryHeaderLBA) + secondaryHeader.Put_partition_entries_lba(t.secondaryPartitionsLBA) + secondaryHeader.Put_header_crc32(secondaryHeader.CalculateChecksum()) + + _, err = t.dev.WriteAt(secondaryHeader, int64(t.secondaryHeaderLBA)*int64(t.sectorSize)) + if err != nil { + return fmt.Errorf("failed to write secondary header: %w", err) + } + + _, err = t.dev.WriteAt(entriesBuf, int64(t.secondaryPartitionsLBA)*int64(t.sectorSize)) + if err != nil { + return fmt.Errorf("failed to write secondary entries: %w", err) + } + + if !t.options.SkipPMBR { + // write protective MBR + if err = t.writePMBR(); err != nil { + return err + } + } + + if err = t.dev.Sync(); err != nil { + return fmt.Errorf("failed to sync device: %w", err) + } + + return t.syncKernel() +} + +func (t *Table) writePMBR() error { + protectiveMBR := make([]byte, 512) + + if err := ioutil.ReadFullAt(t.dev, protectiveMBR, 0); err != nil { + return fmt.Errorf("failed to read protective MBR: %w", err) + } + + // boot signature + protectiveMBR[510], protectiveMBR[511] = 0x55, 0xAA + protectiveMBR[511] = 0xAA + + // PMBR protective entry. + b := protectiveMBR[446 : 446+16] + + if t.options.MarkPMBRBootable { + // Some BIOSes in legacy mode won't boot from a disk unless there is at least one + // partition in the MBR marked bootable. Mark this partition as bootable. + b[0] = 0x80 + } else { + b[0] = 0x00 + } + + // Partition type: EFI data partition. + b[4] = 0xee + + // CHS for the start of the partition + copy(b[1:4], []byte{0x00, 0x02, 0x00}) + + // CHS for the end of the partition + copy(b[5:8], []byte{0xff, 0xff, 0xff}) + + // Partition start LBA. + binary.LittleEndian.PutUint32(b[8:12], 1) + + // Partition length in sectors. + // This might overflow uint32, so check accordingly + if t.lastLBA > math.MaxUint32 { + binary.LittleEndian.PutUint32(b[12:16], uint32(math.MaxUint32)) + } else { + binary.LittleEndian.PutUint32(b[12:16], uint32(t.lastLBA)) + } + + _, err := t.dev.WriteAt(protectiveMBR, 0) + if err != nil { + return fmt.Errorf("failed to write protective MBR: %w", err) + } + + return nil +} + +func (t *Table) syncKernel() error { + kernelPartitionNum, err := t.dev.GetKernelLastPartitionNum() + if err != nil { + return fmt.Errorf("failed to get kernel last partition number: %w", err) + } + + partitionNum := max(kernelPartitionNum, len(t.entries)) + + for no := 1; no <= partitionNum; no++ { + var myEntry *Partition + if no <= len(t.entries) { + myEntry = t.entries[no-1] + } + + // try to delete the partition first + err := t.dev.KernelPartitionDelete(no) + + switch { + case errors.Is(err, unix.ENXIO): + // partition doesn't exist, ok + case errors.Is(err, unix.EBUSY) && myEntry != nil: + // proceed to resize + err = t.dev.KernelPartitionResize(no, + myEntry.FirstLBA*uint64(t.sectorSize), + (myEntry.LastLBA-myEntry.FirstLBA+1)*uint64(t.sectorSize)) + if err != nil { + return fmt.Errorf("failed to resize partition %d: %w", no, err) + } + + continue + case err != nil: + return fmt.Errorf("failed to delete partition %d: %w", no, err) + } + + err = t.dev.KernelPartitionAdd(no, + myEntry.FirstLBA*uint64(t.sectorSize), + (myEntry.LastLBA-myEntry.FirstLBA+1)*uint64(t.sectorSize), + ) + if err != nil { + return fmt.Errorf("failed to add partition %d: %w", no, err) + } + } + + return nil +} diff --git a/partitioning/gpt/gpt_test.go b/partitioning/gpt/gpt_test.go new file mode 100644 index 0000000..3eb5a8e --- /dev/null +++ b/partitioning/gpt/gpt_test.go @@ -0,0 +1,292 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//go:build linux + +package gpt_test + +import ( + "embed" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/freddierice/go-losetup/v2" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/siderolabs/go-blockdevice/v2/block" + "github.com/siderolabs/go-blockdevice/v2/partitioning/gpt" +) + +const ( + MiB = 1024 * 1024 + GiB = 1024 * MiB +) + +func sfdiskDump(t *testing.T, devPath string) string { + t.Helper() + + cmd := exec.Command("sfdisk", "--dump", devPath) + cmd.Stderr = os.Stderr + out, err := cmd.Output() + assert.NoError(t, err) + + output := string(out) + output = regexp.MustCompile(`device:[^\n]+\n`).ReplaceAllString(output, "") + output = regexp.MustCompile(`/dev/[^:]+:\s+`).ReplaceAllString(output, "") + + t.Log("sfdisk output:\n", output) + + return output +} + +func gdiskDump(t *testing.T, devPath string) string { + t.Helper() + + cmd := exec.Command("gdisk", "-l", devPath) + cmd.Stderr = os.Stderr + out, err := cmd.Output() + assert.NoError(t, err) + + output := string(out) + output = regexp.MustCompile(`^GPT [^\n]+\n\n`).ReplaceAllString(output, "") + output = regexp.MustCompile(`Disk /dev[^:+]+:`).ReplaceAllString(output, "") + output = strings.ReplaceAll(output, "\a", "") + + t.Log("gdisk output:\n", output) + + return output +} + +//go:embed testdata/* +var testdataFs embed.FS + +func loadTestdata(t *testing.T, name string) string { + t.Helper() + + data, err := testdataFs.ReadFile(filepath.Join("testdata", name)) + require.NoError(t, err) + + return string(data) +} + +func allocateError(_ int, _ gpt.Partition, err error) error { + return err +} + +func TestGPT(t *testing.T) { + if os.Geteuid() != 0 { + t.Skip("test requires root privileges") + } + + partType1 := uuid.MustParse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B") + partType2 := uuid.MustParse("E6D6D379-F507-44C2-A23C-238F2A3DF928") + + for _, test := range []struct { //nolint:govet + name string + + opts []gpt.Option + + diskSize uint64 + + allocator func(*testing.T, *gpt.Table) + + expectedSfdiskDump string + expectedGdiskDump string + }{ + { + name: "empty", + diskSize: 2 * GiB, + opts: []gpt.Option{ + gpt.WithDiskGUID(uuid.MustParse("D815C311-BDED-43FE-A91A-DCBE0D8025D5")), + }, + + expectedSfdiskDump: loadTestdata(t, "empty.sfdisk"), + expectedGdiskDump: loadTestdata(t, "empty.gdisk"), + }, + { + name: "empty without PMBR", + diskSize: 2 * GiB, + opts: []gpt.Option{ + gpt.WithDiskGUID(uuid.MustParse("D815C311-BDED-43FE-A91A-DCBE0D8025D5")), + gpt.WithSkipPMBR(), + }, + + expectedGdiskDump: loadTestdata(t, "empty-no-mbr.gdisk"), + }, + { + name: "simple allocate", + diskSize: 6 * GiB, + opts: []gpt.Option{ + gpt.WithDiskGUID(uuid.MustParse("B6D003E5-7D1D-45E3-9F4B-4A2430B46D4A")), + }, + allocator: func(t *testing.T, table *gpt.Table) { + t.Helper() + + require.NoError(t, allocateError(table.AllocatePartition(1*GiB, "1G", partType1, + gpt.WithUniqueGUID(uuid.MustParse("DA66737E-1ED4-4DDF-B98C-70CEBFE3ADA0")), + ))) + require.NoError(t, allocateError(table.AllocatePartition(100*MiB, "100M", partType1, + gpt.WithUniqueGUID(uuid.MustParse("3D0FE86B-7791-4659-B564-FC49A542866D")), + ))) + require.NoError(t, allocateError(table.AllocatePartition(2.5*GiB, "2.5G", partType2, + gpt.WithUniqueGUID(uuid.MustParse("EE1A711E-DE12-4D9F-98FF-672F7AD638F8")), + ))) + require.NoError(t, allocateError(table.AllocatePartition(1*GiB, "1G", partType2, + gpt.WithUniqueGUID(uuid.MustParse("15E609C8-9775-4E86-AF59-8A87E7C03FAB")), + ))) + }, + + expectedSfdiskDump: loadTestdata(t, "allocate.sfdisk"), + expectedGdiskDump: loadTestdata(t, "allocate.gdisk"), + }, + { + name: "allocate with deletes", + diskSize: 6 * GiB, + opts: []gpt.Option{ + gpt.WithDiskGUID(uuid.MustParse("B6D003E5-7D1D-45E3-9F4B-4A2430B46D4A")), + }, + allocator: func(t *testing.T, table *gpt.Table) { + t.Helper() + + // allocate 3 1G partitions first, and delete the middle one + + require.NoError(t, allocateError(table.AllocatePartition(1*GiB, "1G1", partType1, + gpt.WithUniqueGUID(uuid.MustParse("DA66737E-1ED4-4DDF-B98C-70CEBFE3ADA0")), + ))) + require.NoError(t, allocateError(table.AllocatePartition(1*GiB, "1G2", partType1))) + require.NoError(t, allocateError(table.AllocatePartition(1*GiB, "1G3", partType2, + gpt.WithUniqueGUID(uuid.MustParse("3D0FE86B-7791-4659-B564-FC49A542866D")), + ))) + + require.NoError(t, table.DeletePartition(1)) + + // allocate smaller partitions to fill the gap + require.NoError(t, allocateError(table.AllocatePartition(200*MiB, "200M", partType2, + gpt.WithUniqueGUID(uuid.MustParse("EE1A711E-DE12-4D9F-98FF-672F7AD638F8")), + ))) + require.NoError(t, allocateError(table.AllocatePartition(400*MiB, "400M", partType2, + gpt.WithUniqueGUID(uuid.MustParse("15E609C8-9775-4E86-AF59-8A87E7C03FAB")), + ))) + + // partition that doesn't fit the gap will be appended to the end + require.NoError(t, allocateError(table.AllocatePartition(500*MiB, "500M", partType2, + gpt.WithUniqueGUID(uuid.MustParse("15E609C8-9775-4E86-AF59-8A87E7C03FAC")), + ))) + }, + + expectedSfdiskDump: loadTestdata(t, "mix-allocate.sfdisk"), + expectedGdiskDump: loadTestdata(t, "mix-allocate.gdisk"), + }, + { + name: "resize", + diskSize: 6 * GiB, + opts: []gpt.Option{ + gpt.WithDiskGUID(uuid.MustParse("B6D003E5-7D1D-45E3-9F4B-4A2430B46D4A")), + }, + allocator: func(t *testing.T, table *gpt.Table) { + t.Helper() + + // allocate 2 1G partitions first, and grow the last one + require.NoError(t, allocateError(table.AllocatePartition(1*GiB, "1G", partType1, + gpt.WithUniqueGUID(uuid.MustParse("DA66737E-1ED4-4DDF-B98C-70CEBFE3ADA0")), + ))) + require.NoError(t, allocateError(table.AllocatePartition(1*GiB, "GROW", partType2, + gpt.WithUniqueGUID(uuid.MustParse("3D0FE86B-7791-4659-B564-FC49A542866D")), + ))) + + // attempt to grow the first one + growth, err := table.AvailablePartitionGrowth(0) + require.NoError(t, err) + + assert.EqualValues(t, 0, growth) + + // grow the second one + growth, err = table.AvailablePartitionGrowth(1) + require.NoError(t, err) + + assert.EqualValues(t, 4*GiB-(2048+33)*512, growth) + + require.NoError(t, table.GrowPartition(1, growth)) + }, + + expectedSfdiskDump: loadTestdata(t, "grow.sfdisk"), + expectedGdiskDump: loadTestdata(t, "grow.gdisk"), + }, + } { + t.Run(test.name, func(t *testing.T) { + tmpDir := t.TempDir() + + rawImage := filepath.Join(tmpDir, "image.raw") + + f, err := os.Create(rawImage) + require.NoError(t, err) + + require.NoError(t, f.Truncate(int64(test.diskSize))) + require.NoError(t, f.Close()) + + var loDev losetup.Device + + loDev, err = losetup.Attach(rawImage, 0, false) + require.NoError(t, err) + + t.Cleanup(func() { + assert.NoError(t, loDev.Detach()) + }) + + disk, err := os.OpenFile(loDev.Path(), os.O_RDWR, 0) + require.NoError(t, err) + + t.Cleanup(func() { + assert.NoError(t, disk.Close()) + }) + + blkdev := block.NewFromFile(disk) + + gptdev, err := gpt.DeviceFromBlockDevice(blkdev, disk) + require.NoError(t, err) + + table, err := gpt.New(gptdev, test.opts...) + require.NoError(t, err) + + assert.EqualValues(t, test.diskSize-(2048+33)*512, table.LargestContiguousAllocatable()) + + if test.allocator != nil { + test.allocator(t, table) + } + + require.NoError(t, table.Write()) + + if test.expectedSfdiskDump != "" { + assert.Equal(t, test.expectedSfdiskDump, sfdiskDump(t, loDev.Path())) + } + + if test.expectedGdiskDump != "" { + assert.Equal(t, test.expectedGdiskDump, gdiskDump(t, loDev.Path())) + } + + // re-read the table and check if it's the same + table2, err := gpt.Read(gptdev, test.opts...) + require.NoError(t, err) + + assert.Equal(t, table.Partitions(), table2.Partitions()) + + // re-write the partition table + require.NoError(t, table2.Write()) + + if test.expectedSfdiskDump != "" { + assert.Equal(t, test.expectedSfdiskDump, sfdiskDump(t, loDev.Path())) + } + + if test.expectedGdiskDump != "" { + assert.Equal(t, test.expectedGdiskDump, gdiskDump(t, loDev.Path())) + } + }) + } +} diff --git a/partitioning/gpt/options.go b/partitioning/gpt/options.go new file mode 100644 index 0000000..fce0091 --- /dev/null +++ b/partitioning/gpt/options.go @@ -0,0 +1,68 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package gpt + +import "github.com/google/uuid" + +// Options is a set of options for creating a new partition table. +type Options struct { + SkipPMBR bool + MarkPMBRBootable bool + + // Number of LBAs to skip before the writing partition entries. + SkipLBAs uint + + // DiskGUID is a GUID for the disk. + // + // If not set, on partition table creation, a new GUID is generated. + DiskGUID uuid.UUID +} + +// Option is a function that sets some option. +type Option func(*Options) + +// WithSkipPMBR is an option to skip writing protective MBR. +func WithSkipPMBR() Option { + return func(o *Options) { + o.SkipPMBR = true + } +} + +// WithMarkPMBRBootable is an option to mark protective MBR bootable. +func WithMarkPMBRBootable() Option { + return func(o *Options) { + o.MarkPMBRBootable = true + } +} + +// WithSkipLBAs is an option to skip writing partition entries. +func WithSkipLBAs(n uint) Option { + return func(o *Options) { + o.SkipLBAs = n + } +} + +// WithDiskGUID is an option to set disk GUID. +func WithDiskGUID(guid uuid.UUID) Option { + return func(o *Options) { + o.DiskGUID = guid + } +} + +// PartitionOptions configure a partition. +type PartitionOptions struct { + UniqueGUID uuid.UUID + Flags uint64 +} + +// PartitionOption is a function that sets some option. +type PartitionOption func(*PartitionOptions) + +// WithUniqueGUID is an option to set a unique GUID for the partition. +func WithUniqueGUID(guid uuid.UUID) PartitionOption { + return func(o *PartitionOptions) { + o.UniqueGUID = guid + } +} diff --git a/partitioning/gpt/testdata/allocate.gdisk b/partitioning/gpt/testdata/allocate.gdisk new file mode 100644 index 0000000..4d5738a --- /dev/null +++ b/partitioning/gpt/testdata/allocate.gdisk @@ -0,0 +1,21 @@ +Partition table scan: + MBR: protective + BSD: not present + APM: not present + GPT: present + +Found valid GPT with protective MBR; using GPT. + 12582912 sectors, 6.0 GiB +Sector size (logical/physical): 512/512 bytes +Disk identifier (GUID): B6D003E5-7D1D-45E3-9F4B-4A2430B46D4A +Partition table holds up to 128 entries +Main partition table begins at sector 2 and ends at sector 33 +First usable sector is 34, last usable sector is 12582878 +Partitions will be aligned on 2048-sector boundaries +Total free space is 2940861 sectors (1.4 GiB) + +Number Start (sector) End (sector) Size Code Name + 1 2048 2099199 1024.0 MiB EF00 1G + 2 2099200 2303999 100.0 MiB EF00 100M + 3 2304000 7546879 2.5 GiB 8E00 2.5G + 4 7546880 9644031 1024.0 MiB 8E00 1G diff --git a/partitioning/gpt/testdata/allocate.sfdisk b/partitioning/gpt/testdata/allocate.sfdisk new file mode 100644 index 0000000..b5d7b25 --- /dev/null +++ b/partitioning/gpt/testdata/allocate.sfdisk @@ -0,0 +1,11 @@ +label: gpt +label-id: B6D003E5-7D1D-45E3-9F4B-4A2430B46D4A +unit: sectors +first-lba: 34 +last-lba: 12582878 +sector-size: 512 + +start= 2048, size= 2097152, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, uuid=DA66737E-1ED4-4DDF-B98C-70CEBFE3ADA0, name="1G" +start= 2099200, size= 204800, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, uuid=3D0FE86B-7791-4659-B564-FC49A542866D, name="100M" +start= 2304000, size= 5242880, type=E6D6D379-F507-44C2-A23C-238F2A3DF928, uuid=EE1A711E-DE12-4D9F-98FF-672F7AD638F8, name="2.5G" +start= 7546880, size= 2097152, type=E6D6D379-F507-44C2-A23C-238F2A3DF928, uuid=15E609C8-9775-4E86-AF59-8A87E7C03FAB, name="1G" diff --git a/partitioning/gpt/testdata/empty-no-mbr.gdisk b/partitioning/gpt/testdata/empty-no-mbr.gdisk new file mode 100644 index 0000000..19da49a --- /dev/null +++ b/partitioning/gpt/testdata/empty-no-mbr.gdisk @@ -0,0 +1,18 @@ +Partition table scan: + MBR: not present + BSD: not present + APM: not present + GPT: present + +Found valid GPT with corrupt MBR; using GPT and will write new +protective MBR on save. + 4194304 sectors, 2.0 GiB +Sector size (logical/physical): 512/512 bytes +Disk identifier (GUID): D815C311-BDED-43FE-A91A-DCBE0D8025D5 +Partition table holds up to 128 entries +Main partition table begins at sector 2 and ends at sector 33 +First usable sector is 34, last usable sector is 4194270 +Partitions will be aligned on 2048-sector boundaries +Total free space is 4194237 sectors (2.0 GiB) + +Number Start (sector) End (sector) Size Code Name diff --git a/partitioning/gpt/testdata/empty.gdisk b/partitioning/gpt/testdata/empty.gdisk new file mode 100644 index 0000000..ff7456a --- /dev/null +++ b/partitioning/gpt/testdata/empty.gdisk @@ -0,0 +1,17 @@ +Partition table scan: + MBR: protective + BSD: not present + APM: not present + GPT: present + +Found valid GPT with protective MBR; using GPT. + 4194304 sectors, 2.0 GiB +Sector size (logical/physical): 512/512 bytes +Disk identifier (GUID): D815C311-BDED-43FE-A91A-DCBE0D8025D5 +Partition table holds up to 128 entries +Main partition table begins at sector 2 and ends at sector 33 +First usable sector is 34, last usable sector is 4194270 +Partitions will be aligned on 2048-sector boundaries +Total free space is 4194237 sectors (2.0 GiB) + +Number Start (sector) End (sector) Size Code Name diff --git a/partitioning/gpt/testdata/empty.sfdisk b/partitioning/gpt/testdata/empty.sfdisk new file mode 100644 index 0000000..63b8c4c --- /dev/null +++ b/partitioning/gpt/testdata/empty.sfdisk @@ -0,0 +1,6 @@ +label: gpt +label-id: D815C311-BDED-43FE-A91A-DCBE0D8025D5 +unit: sectors +first-lba: 34 +last-lba: 4194270 +sector-size: 512 diff --git a/partitioning/gpt/testdata/grow.gdisk b/partitioning/gpt/testdata/grow.gdisk new file mode 100644 index 0000000..2a1fddb --- /dev/null +++ b/partitioning/gpt/testdata/grow.gdisk @@ -0,0 +1,19 @@ +Partition table scan: + MBR: protective + BSD: not present + APM: not present + GPT: present + +Found valid GPT with protective MBR; using GPT. + 12582912 sectors, 6.0 GiB +Sector size (logical/physical): 512/512 bytes +Disk identifier (GUID): B6D003E5-7D1D-45E3-9F4B-4A2430B46D4A +Partition table holds up to 128 entries +Main partition table begins at sector 2 and ends at sector 33 +First usable sector is 34, last usable sector is 12582878 +Partitions will be aligned on 2048-sector boundaries +Total free space is 2014 sectors (1007.0 KiB) + +Number Start (sector) End (sector) Size Code Name + 1 2048 2099199 1024.0 MiB EF00 1G + 2 2099200 12582878 5.0 GiB 8E00 GROW diff --git a/partitioning/gpt/testdata/grow.sfdisk b/partitioning/gpt/testdata/grow.sfdisk new file mode 100644 index 0000000..ce8af8e --- /dev/null +++ b/partitioning/gpt/testdata/grow.sfdisk @@ -0,0 +1,9 @@ +label: gpt +label-id: B6D003E5-7D1D-45E3-9F4B-4A2430B46D4A +unit: sectors +first-lba: 34 +last-lba: 12582878 +sector-size: 512 + +start= 2048, size= 2097152, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, uuid=DA66737E-1ED4-4DDF-B98C-70CEBFE3ADA0, name="1G" +start= 2099200, size= 10483679, type=E6D6D379-F507-44C2-A23C-238F2A3DF928, uuid=3D0FE86B-7791-4659-B564-FC49A542866D, name="GROW" diff --git a/partitioning/gpt/testdata/mix-allocate.gdisk b/partitioning/gpt/testdata/mix-allocate.gdisk new file mode 100644 index 0000000..79f6890 --- /dev/null +++ b/partitioning/gpt/testdata/mix-allocate.gdisk @@ -0,0 +1,22 @@ +Partition table scan: + MBR: protective + BSD: not present + APM: not present + GPT: present + +Found valid GPT with protective MBR; using GPT. + 12582912 sectors, 6.0 GiB +Sector size (logical/physical): 512/512 bytes +Disk identifier (GUID): B6D003E5-7D1D-45E3-9F4B-4A2430B46D4A +Partition table holds up to 128 entries +Main partition table begins at sector 2 and ends at sector 33 +First usable sector is 34, last usable sector is 12582878 +Partitions will be aligned on 2048-sector boundaries +Total free space is 6135741 sectors (2.9 GiB) + +Number Start (sector) End (sector) Size Code Name + 1 2048 2099199 1024.0 MiB EF00 1G1 + 2 2099200 2508799 200.0 MiB 8E00 200M + 3 2508800 3327999 400.0 MiB 8E00 400M + 4 4196352 6293503 1024.0 MiB 8E00 1G3 + 5 6293504 7317503 500.0 MiB 8E00 500M diff --git a/partitioning/gpt/testdata/mix-allocate.sfdisk b/partitioning/gpt/testdata/mix-allocate.sfdisk new file mode 100644 index 0000000..836097e --- /dev/null +++ b/partitioning/gpt/testdata/mix-allocate.sfdisk @@ -0,0 +1,12 @@ +label: gpt +label-id: B6D003E5-7D1D-45E3-9F4B-4A2430B46D4A +unit: sectors +first-lba: 34 +last-lba: 12582878 +sector-size: 512 + +start= 2048, size= 2097152, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, uuid=DA66737E-1ED4-4DDF-B98C-70CEBFE3ADA0, name="1G1" +start= 2099200, size= 409600, type=E6D6D379-F507-44C2-A23C-238F2A3DF928, uuid=EE1A711E-DE12-4D9F-98FF-672F7AD638F8, name="200M" +start= 2508800, size= 819200, type=E6D6D379-F507-44C2-A23C-238F2A3DF928, uuid=15E609C8-9775-4E86-AF59-8A87E7C03FAB, name="400M" +start= 4196352, size= 2097152, type=E6D6D379-F507-44C2-A23C-238F2A3DF928, uuid=3D0FE86B-7791-4659-B564-FC49A542866D, name="1G3" +start= 6293504, size= 1024000, type=E6D6D379-F507-44C2-A23C-238F2A3DF928, uuid=15E609C8-9775-4E86-AF59-8A87E7C03FAC, name="500M"