diff --git a/.conform.yaml b/.conform.yaml index f7870b3..cde6d97 100644 --- a/.conform.yaml +++ b/.conform.yaml @@ -1,37 +1,48 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2024-02-05T11:19:51Z by kres 0e666ea. +# Generated on 2024-02-29T11:53:44Z by kres latest. ---- policies: -- type: commit - spec: - dco: true - gpg: - required: true - identity: - gitHubOrganization: siderolabs - spellcheck: - locale: US - maximumOfOneCommit: true - header: - length: 89 - imperative: true - case: lower - invalidLastCharacters: . - body: - required: true - conventional: - types: ["chore","docs","perf","refactor","style","test","release"] - scopes: [".*"] -- type: license - spec: - skipPaths: - - .git/ - - testdata/ - includeSuffixes: - - .go - excludeSuffixes: - - .pb.go - - .pb.gw.go - header: "// This Source Code Form is subject to the terms of the Mozilla Public\u000A// License, v. 2.0. If a copy of the MPL was not distributed with this\u000A// file, You can obtain one at http://mozilla.org/MPL/2.0/.\u000A" + - type: commit + spec: + dco: true + gpg: + required: true + identity: + gitHubOrganization: siderolabs + spellcheck: + locale: US + maximumOfOneCommit: true + header: + length: 89 + imperative: true + case: lower + invalidLastCharacters: . + body: + required: true + conventional: + types: + - chore + - docs + - perf + - refactor + - style + - test + - release + scopes: + - .* + - type: license + spec: + root: . + skipPaths: + - .git/ + - testdata/ + includeSuffixes: + - .go + excludeSuffixes: + - .pb.go + - .pb.gw.go + header: | + // 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/. diff --git a/.kres.yaml b/.kres.yaml index 7ddd525..c0796cb 100644 --- a/.kres.yaml +++ b/.kres.yaml @@ -19,6 +19,8 @@ spec: - cryptsetup - dosfstools - e2fsprogs + - parted + - util-linux - xfsprogs --- kind: service.CodeCov diff --git a/Dockerfile b/Dockerfile index 9589d3b..0a279bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2024-02-23T15:08:58Z by kres latest. +# Generated on 2024-03-01T13:44:11Z by kres latest. ARG TOOLCHAIN @@ -11,7 +11,7 @@ FROM scratch AS generate # base toolchain image FROM ${TOOLCHAIN} AS toolchain -RUN apk --update --no-cache add bash curl build-base protoc protobuf-dev cdrkit cryptsetup dosfstools e2fsprogs xfsprogs +RUN apk --update --no-cache add bash curl build-base protoc protobuf-dev cdrkit cryptsetup dosfstools e2fsprogs parted util-linux xfsprogs # build tools FROM --platform=${BUILDPLATFORM} toolchain AS tools diff --git a/blkid/blkdid.go b/blkid/blkdid.go index e7f0ddd..0e04942 100644 --- a/blkid/blkdid.go +++ b/blkid/blkdid.go @@ -19,15 +19,46 @@ type Info struct { //nolint:govet // Overall size of the probed device (in bytes). Size uint64 + // Sector size of the device (in bytes). + SectorSize uint + // Optimal I/O size for the device (in bytes). - IOSize uint64 + IOSize uint + + // ProbeResult is the result of probing the device. + ProbeResult - // TODO: API might be different. + // Parts is the result of probing the nested filesystem/partitions. + Parts []NestedProbeResult +} + +// ProbeResult is a result of probing a single filesystem/partition. +type ProbeResult struct { //nolint:govet Name string UUID *uuid.UUID Label *string BlockSize uint32 FilesystemBlockSize uint32 - FilesystemSize uint64 + ProbedSize uint64 +} + +// NestedResult is result of probing a nested filesystem/partition. +// +// It annotates the ProbeResult with the partition information. +type NestedResult struct { + PartitionUUID *uuid.UUID + PartitionType *uuid.UUID + PartitionLabel *string + PartitionIndex uint // 1-based index + + PartitionOffset, PartitionSize uint64 +} + +// NestedProbeResult is a result of probing a nested filesystem/partition. +type NestedProbeResult struct { //nolint:govet + NestedResult + ProbeResult + + Parts []NestedProbeResult } diff --git a/blkid/blkid_linux.go b/blkid/blkid_linux.go index b34647e..cdaa18f 100644 --- a/blkid/blkid_linux.go +++ b/blkid/blkid_linux.go @@ -57,15 +57,18 @@ func Probe(f *os.File) (*Info, error) { } else { return nil, fmt.Errorf("failed to get block device I/O size: %w", err) } + + info.SectorSize = info.BlockDevice.GetSectorSize() case unix.S_IFREG: // regular file (an image?), so use different settings info.Size = uint64(st.Size()) info.IOSize = block.DefaultBlockSize + info.SectorSize = block.DefaultBlockSize default: return nil, fmt.Errorf("unsupported file type: %s", st.Mode().Type()) } - if err := info.probe(f, 0, info.Size); err != nil { + if err := info.fillProbeResult(f); err != nil { return nil, fmt.Errorf("failed to probe: %w", err) } diff --git a/blkid/blkid_linux_test.go b/blkid/blkid_linux_test.go index c0318d3..c4cd8d7 100644 --- a/blkid/blkid_linux_test.go +++ b/blkid/blkid_linux_test.go @@ -11,9 +11,13 @@ import ( "os" "os/exec" "path/filepath" + "strings" "testing" "github.com/freddierice/go-losetup/v2" + "github.com/google/uuid" + "github.com/siderolabs/gen/xslices" + "github.com/siderolabs/go-pointer" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -21,7 +25,10 @@ import ( "github.com/siderolabs/go-blockdevice/v2/block" ) -const MiB = 1024 * 1024 +const ( + MiB = 1024 * 1024 + GiB = 1024 * MiB +) func xfsSetup(t *testing.T, path string) { t.Helper() @@ -98,7 +105,7 @@ func isoSetup(useJoilet bool) func(t *testing.T, path string) { } //nolint:gocognit -func TestProbePath(t *testing.T) { +func TestProbePathFilesystems(t *testing.T) { for _, test := range []struct { //nolint:govet name string @@ -283,9 +290,323 @@ func TestProbePath(t *testing.T) { assert.Contains(t, test.expectedFSBlockSize, info.FilesystemBlockSize) } - assert.Equal(t, test.expectedFSSize, info.FilesystemSize) + assert.Equal(t, test.expectedFSSize, info.ProbedSize) }) }) } } } + +func setupGPT(t *testing.T, path string) { + t.Helper() + + script := strings.TrimSpace(` +label: gpt +label-id: DDDA0816-8B53-47BF-A813-9EBB1F73AAA2 +size= 204800, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, uuid=3C047FF8-E35C-4918-A061-B4C1E5A291E5, name="EFI" +size= 2048, type=21686148-6449-6E6F-744E-656564454649, uuid=942D2017-052E-4216-B4E4-2110507E4CD4, name="BIOS", attrs="LegacyBIOSBootable" +size= 2048000, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=E8516F6B-F03E-45AE-8D9D-9958456EE7E4, name="BOOT" +size= 2048, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=CE6B2D56-7A70-4546-926C-7A9B41607347, name="META" +size= 204800, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=7F5FCD6C-A703-40D2-8796-E5CF7F3A9EB5, name="STATE" + type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=0F06E81A-E78D-426B-A078-30A01AAB3FB7, name="EPHEMERAL" +`) + + cmd := exec.Command("sfdisk", path) + cmd.Stdin = strings.NewReader(script) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + require.NoError(t, cmd.Run()) +} + +func wipe1MB(f func(*testing.T, string)) func(*testing.T, string) { + return func(t *testing.T, path string) { + t.Helper() + + f(t, path) + + f, err := os.OpenFile(path, os.O_RDWR, 0) + require.NoError(t, err) + + _, err = f.Write(make([]byte, 1*MiB)) + require.NoError(t, err) + + require.NoError(t, f.Close()) + } +} + +var expectedParts = []blkid.NestedProbeResult{ + { + NestedResult: blkid.NestedResult{ + PartitionUUID: pointer.To(uuid.MustParse("3C047FF8-E35C-4918-A061-B4C1E5A291E5")), + PartitionType: pointer.To(uuid.MustParse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B")), + PartitionLabel: pointer.To("EFI"), + PartitionIndex: 1, + PartitionOffset: 1 * MiB, + PartitionSize: 100 * MiB, + }, + }, + { + NestedResult: blkid.NestedResult{ + PartitionUUID: pointer.To(uuid.MustParse("942D2017-052E-4216-B4E4-2110507E4CD4")), + PartitionType: pointer.To(uuid.MustParse("21686148-6449-6E6F-744E-656564454649")), + PartitionLabel: pointer.To("BIOS"), + PartitionIndex: 2, + PartitionOffset: 101 * MiB, + PartitionSize: 1 * MiB, + }, + }, + { + NestedResult: blkid.NestedResult{ + PartitionUUID: pointer.To(uuid.MustParse("E8516F6B-F03E-45AE-8D9D-9958456EE7E4")), + PartitionType: pointer.To(uuid.MustParse("0FC63DAF-8483-4772-8E79-3D69D8477DE4")), + PartitionLabel: pointer.To("BOOT"), + PartitionIndex: 3, + PartitionOffset: 102 * MiB, + PartitionSize: 1000 * MiB, + }, + }, + { + NestedResult: blkid.NestedResult{ + PartitionUUID: pointer.To(uuid.MustParse("CE6B2D56-7A70-4546-926C-7A9B41607347")), + PartitionType: pointer.To(uuid.MustParse("0FC63DAF-8483-4772-8E79-3D69D8477DE4")), + PartitionLabel: pointer.To("META"), + PartitionIndex: 4, + PartitionOffset: 1102 * MiB, + PartitionSize: 1 * MiB, + }, + }, + { + NestedResult: blkid.NestedResult{ + PartitionUUID: pointer.To(uuid.MustParse("7F5FCD6C-A703-40D2-8796-E5CF7F3A9EB5")), + PartitionType: pointer.To(uuid.MustParse("0FC63DAF-8483-4772-8E79-3D69D8477DE4")), + PartitionLabel: pointer.To("STATE"), + PartitionIndex: 5, + PartitionOffset: 1103 * MiB, + PartitionSize: 100 * MiB, + }, + }, + { + NestedResult: blkid.NestedResult{ + PartitionUUID: pointer.To(uuid.MustParse("0F06E81A-E78D-426B-A078-30A01AAB3FB7")), + PartitionType: pointer.To(uuid.MustParse("0FC63DAF-8483-4772-8E79-3D69D8477DE4")), + PartitionLabel: pointer.To("EPHEMERAL"), + PartitionIndex: 6, + PartitionOffset: 1203 * MiB, + PartitionSize: 844 * MiB, + }, + }, +} + +func TestProbePathGPT(t *testing.T) { + for _, test := range []struct { //nolint:govet + name string + + size uint64 + setup func(*testing.T, string) + + expectedUUID uuid.UUID + expectedParts []blkid.NestedProbeResult + }{ + { + name: "good GPT", + + size: 2 * GiB, + setup: setupGPT, + + expectedUUID: uuid.MustParse("DDDA0816-8B53-47BF-A813-9EBB1F73AAA2"), + expectedParts: expectedParts, + }, + { + name: "corrupted GPT", + + size: 2 * GiB, + setup: wipe1MB(setupGPT), + + expectedUUID: uuid.MustParse("DDDA0816-8B53-47BF-A813-9EBB1F73AAA2"), + expectedParts: expectedParts, + }, + } { + for _, useLoopDevice := range []bool{false, true} { + t.Run(fmt.Sprintf("loop=%v", useLoopDevice), func(t *testing.T) { + t.Run(test.name, func(t *testing.T) { + if useLoopDevice && os.Geteuid() != 0 { + t.Skip("test requires root privileges") + } + + 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.size))) + require.NoError(t, f.Close()) + + var probePath string + + if useLoopDevice { + var loDev losetup.Device + + loDev, err = losetup.Attach(rawImage, 0, false) + require.NoError(t, err) + + t.Cleanup(func() { + assert.NoError(t, loDev.Detach()) + }) + + probePath = loDev.Path() + } else { + probePath = rawImage + } + + test.setup(t, probePath) + + info, err := blkid.ProbePath(probePath) + require.NoError(t, err) + + if useLoopDevice { + assert.NotNil(t, info.BlockDevice) + } else { + assert.Nil(t, info.BlockDevice) + } + + assert.EqualValues(t, block.DefaultBlockSize, info.IOSize) + + if test.size != 0 { + assert.EqualValues(t, test.size, info.Size) + } + + assert.Equal(t, "gpt", info.Name) + assert.EqualValues(t, block.DefaultBlockSize, info.BlockSize) + assert.Equal(t, test.size-1*MiB-33*block.DefaultBlockSize, info.ProbedSize) + + require.NotNil(t, info.UUID) + assert.Equal(t, test.expectedUUID, *info.UUID) + + assert.Equal(t, test.expectedParts, info.Parts) + }) + }) + } + } +} + +func setupNestedGPT(t *testing.T, path string) { + t.Helper() + + setupGPT(t, path) + + require.NoError(t, exec.Command("partprobe", path).Run()) + + vfatSetup(t, path+"p1") + extfsSetup(t, path+"p3") + xfsSetup(t, path+"p6") +} + +func TestProbePathNested(t *testing.T) { + if os.Geteuid() != 0 { + t.Skip("test requires root privileges") + } + + if hostname, _ := os.Hostname(); hostname == "buildkitsandbox" { //nolint: errcheck + t.Skip("test not supported under buildkit as partition devices are not propagated from /dev") + } + + for _, test := range []struct { //nolint:govet + name string + + size uint64 + setup func(*testing.T, string) + + expectedUUID uuid.UUID + expectedParts []blkid.NestedProbeResult + }{ + { + name: "good GPT, ext4fs, xfs, vfat, none", + + size: 2 * GiB, + setup: setupNestedGPT, + + expectedUUID: uuid.MustParse("DDDA0816-8B53-47BF-A813-9EBB1F73AAA2"), + expectedParts: expectedParts, + }, + } { + 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.size))) + 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()) + }) + + probePath := loDev.Path() + + test.setup(t, probePath) + + info, err := blkid.ProbePath(probePath) + require.NoError(t, err) + + assert.NotNil(t, info.BlockDevice) + + assert.EqualValues(t, block.DefaultBlockSize, info.IOSize) + + if test.size != 0 { + assert.EqualValues(t, test.size, info.Size) + } + + assert.Equal(t, "gpt", info.Name) + assert.EqualValues(t, block.DefaultBlockSize, info.BlockSize) + assert.Equal(t, test.size-1*MiB-33*block.DefaultBlockSize, info.ProbedSize) + + require.NotNil(t, info.UUID) + assert.Equal(t, test.expectedUUID, *info.UUID) + + // extract only partition information and compare it separately + partitionsOnly := xslices.Map(info.Parts, func(p blkid.NestedProbeResult) blkid.NestedProbeResult { + return blkid.NestedProbeResult{ + NestedResult: p.NestedResult, + } + }) + + assert.Equal(t, test.expectedParts, partitionsOnly) + + // EFI: vfat + assert.Equal(t, "vfat", info.Parts[0].Name) + assert.EqualValues(t, 512, info.Parts[0].BlockSize) + assert.EqualValues(t, 2048, info.Parts[0].FilesystemBlockSize) + assert.EqualValues(t, 0x63f9c00, info.Parts[0].ProbedSize) + + // empty + assert.Equal(t, blkid.ProbeResult{}, info.Parts[1].ProbeResult) + + // BOOT: ext4 + assert.Equal(t, "extfs", info.Parts[2].Name) + assert.Contains(t, []uint32{1024, 4096}, info.Parts[2].BlockSize) + assert.Contains(t, []uint32{1024, 4096}, info.Parts[2].FilesystemBlockSize) + assert.EqualValues(t, 1000*MiB, info.Parts[2].ProbedSize) + + // empty + assert.Equal(t, blkid.ProbeResult{}, info.Parts[3].ProbeResult) + assert.Equal(t, blkid.ProbeResult{}, info.Parts[4].ProbeResult) + + // EPHEMERAL: xfs + assert.Equal(t, "xfs", info.Parts[5].Name) + assert.EqualValues(t, 512, info.Parts[5].BlockSize) + assert.EqualValues(t, 4096, info.Parts[5].FilesystemBlockSize) + assert.EqualValues(t, 0x30c00000, info.Parts[5].ProbedSize) + }) + } +} diff --git a/blkid/internal/probers/probers.go b/blkid/internal/chain/chain.go similarity index 57% rename from blkid/internal/probers/probers.go rename to blkid/internal/chain/chain.go index 59e528b..9973889 100644 --- a/blkid/internal/probers/probers.go +++ b/blkid/internal/chain/chain.go @@ -2,37 +2,25 @@ // 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 probers provides a list of probers for different filesystems and volume managers. -package probers +// Package chain provides a list of probers for different filesystems and volume managers. +package chain import ( - "io" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/filesystems/bluestore" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/filesystems/ext" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/filesystems/iso9660" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/filesystems/luks" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/filesystems/vfat" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/filesystems/xfs" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/result" + "github.com/siderolabs/go-blockdevice/v2/blkid/internal/partitions/gpt" + "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" ) -// Prober is an interface for probing filesystems and volume managers. -type Prober interface { - // Name returns the name of the filesystem or volume manager. - Name() string - // Magic returns the magic value for the filesystem or volume manager. - Magic() []*magic.Magic - // Probe runs the further inspection and returns the result if successful. - Probe(io.ReaderAt) (*result.Result, error) -} - -// ProberChain is a list of probers. -type ProberChain []Prober +// Chain is a list of probers. +type Chain []probe.Prober // MaxMagicSize returns the maximum size of the magic value in the chain. -func (chain ProberChain) MaxMagicSize() int { +func (chain Chain) MaxMagicSize() int { max := 0 for _, prober := range chain { @@ -47,8 +35,8 @@ func (chain ProberChain) MaxMagicSize() int { } // MagicMatches returns the prober that matches the magic value in the buffer. -func (chain ProberChain) MagicMatches(buf []byte) []Prober { - var matches []Prober +func (chain Chain) MagicMatches(buf []byte) []probe.Prober { + var matches []probe.Prober for _, prober := range chain { for _, magic := range prober.Magic() { @@ -63,12 +51,13 @@ func (chain ProberChain) MagicMatches(buf []byte) []Prober { return matches } -// Chain returns a list of probers for the filesystems and volume managers. -func Chain() ProberChain { - return ProberChain{ +// Default returns a list of probers for the filesystems and volume managers. +func Default() Chain { + return Chain{ &xfs.Probe{}, &ext.Probe{}, &vfat.Probe{}, + &gpt.Probe{}, &luks.Probe{}, &iso9660.Probe{}, &bluestore.Probe{}, diff --git a/blkid/internal/probers/probers_test.go b/blkid/internal/chain/chain_test.go similarity index 68% rename from blkid/internal/probers/probers_test.go rename to blkid/internal/chain/chain_test.go index a2f0393..5744a07 100644 --- a/blkid/internal/probers/probers_test.go +++ b/blkid/internal/chain/chain_test.go @@ -2,16 +2,16 @@ // 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 probers_test +package chain_test import ( "testing" "github.com/stretchr/testify/assert" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probers" + "github.com/siderolabs/go-blockdevice/v2/blkid/internal/chain" ) func TestMaxMagicSize(t *testing.T) { - assert.Equal(t, 32774, probers.Chain().MaxMagicSize()) + assert.Equal(t, 32774, chain.Default().MaxMagicSize()) } diff --git a/blkid/internal/filesystems/bluestore/bluestore.go b/blkid/internal/filesystems/bluestore/bluestore.go index 1b955e0..913c756 100644 --- a/blkid/internal/filesystems/bluestore/bluestore.go +++ b/blkid/internal/filesystems/bluestore/bluestore.go @@ -6,10 +6,8 @@ package bluestore import ( - "io" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/result" + "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" ) var blueStoreMagic = magic.Magic{ @@ -31,6 +29,6 @@ func (p *Probe) Name() string { } // Probe runs the further inspection and returns the result if successful. -func (p *Probe) Probe(io.ReaderAt) (*result.Result, error) { - return &result.Result{}, nil +func (p *Probe) Probe(probe.Reader) (*probe.Result, error) { + return &probe.Result{}, nil } diff --git a/blkid/internal/filesystems/ext/ext.go b/blkid/internal/filesystems/ext/ext.go index de54de3..1e7fa0c 100644 --- a/blkid/internal/filesystems/ext/ext.go +++ b/blkid/internal/filesystems/ext/ext.go @@ -9,13 +9,12 @@ package ext import ( "bytes" - "io" "github.com/google/uuid" "github.com/siderolabs/go-pointer" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/result" + "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/utils" ) @@ -47,7 +46,7 @@ func (p *Probe) Name() string { } // Probe runs the further inspection and returns the result if successful. -func (p *Probe) Probe(r io.ReaderAt) (*result.Result, error) { +func (p *Probe) Probe(r probe.Reader) (*probe.Result, error) { buf := make([]byte, SUPERBLOCK_SIZE) if _, err := r.ReadAt(buf, sbOffset); err != nil { @@ -69,12 +68,12 @@ func (p *Probe) Probe(r io.ReaderAt) (*result.Result, error) { return nil, err } - res := &result.Result{ + res := &probe.Result{ UUID: &uuid, BlockSize: sb.BlockSize(), FilesystemBlockSize: sb.BlockSize(), - FilesystemSize: sb.FilesystemSize(), + ProbedSize: sb.FilesystemSize(), } lbl := sb.Get_s_volume_name() diff --git a/blkid/internal/filesystems/iso9660/iso9660.go b/blkid/internal/filesystems/iso9660/iso9660.go index 4a07233..46195f4 100644 --- a/blkid/internal/filesystems/iso9660/iso9660.go +++ b/blkid/internal/filesystems/iso9660/iso9660.go @@ -8,14 +8,13 @@ package iso9660 //go:generate go run ../../cstruct/cstruct.go -pkg iso9660 -struct VolumeDescriptor -input volume.h -endianness NativeEndian import ( - "io" "strings" "github.com/siderolabs/go-pointer" "golang.org/x/text/encoding/unicode" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/result" + "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" ) const ( @@ -55,7 +54,7 @@ func isonum16(b []byte) uint16 { } // Probe runs the further inspection and returns the result if successful. -func (p *Probe) Probe(r io.ReaderAt) (*result.Result, error) { +func (p *Probe) Probe(r probe.Reader) (*probe.Result, error) { var pvd, joilet VolumeDescriptor vdLoop: @@ -91,10 +90,10 @@ vdLoop: logicalBlockSize := isonum16(pvd.Get_logical_block_size()) spaceSize := isonum16(pvd.Get_space_size()) - res := &result.Result{ + res := &probe.Result{ BlockSize: uint32(logicalBlockSize), FilesystemBlockSize: uint32(logicalBlockSize), - FilesystemSize: uint64(spaceSize) * uint64(logicalBlockSize), + ProbedSize: uint64(spaceSize) * uint64(logicalBlockSize), } if joilet != nil { diff --git a/blkid/internal/filesystems/luks/luks.go b/blkid/internal/filesystems/luks/luks.go index e07564c..f679d6d 100644 --- a/blkid/internal/filesystems/luks/luks.go +++ b/blkid/internal/filesystems/luks/luks.go @@ -9,13 +9,12 @@ package luks import ( "bytes" - "io" "github.com/google/uuid" "github.com/siderolabs/go-pointer" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/result" + "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" ) var luksMagic = magic.Magic{ @@ -37,7 +36,7 @@ func (p *Probe) Name() string { } // Probe runs the further inspection and returns the result if successful. -func (p *Probe) Probe(r io.ReaderAt) (*result.Result, error) { +func (p *Probe) Probe(r probe.Reader) (*probe.Result, error) { buf := make([]byte, LUKS2HEADER_SIZE) if _, err := r.ReadAt(buf, 0); err != nil { @@ -50,7 +49,7 @@ func (p *Probe) Probe(r io.ReaderAt) (*result.Result, error) { return nil, nil //nolint:nilnil } - res := &result.Result{} + res := &probe.Result{} lbl := hdr.Get_label() if lbl[0] != 0 { diff --git a/blkid/internal/filesystems/vfat/vfat.go b/blkid/internal/filesystems/vfat/vfat.go index 2dd5074..cbdcba1 100644 --- a/blkid/internal/filesystems/vfat/vfat.go +++ b/blkid/internal/filesystems/vfat/vfat.go @@ -10,10 +10,8 @@ package vfat //go:generate go run ../../cstruct/cstruct.go -pkg vfat -struct VFATSB -input vfat.h -endianness LittleEndian import ( - "io" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/result" + "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/utils" ) @@ -70,7 +68,7 @@ func (p *Probe) Name() string { } // Probe runs the further inspection and returns the result if successful. -func (p *Probe) Probe(r io.ReaderAt) (*result.Result, error) { +func (p *Probe) Probe(r probe.Reader) (*probe.Result, error) { vfatBuf := make([]byte, VFATSB_SIZE) msdosBuf := make([]byte, MSDOSSB_SIZE) @@ -96,10 +94,10 @@ func (p *Probe) Probe(r io.ReaderAt) (*result.Result, error) { sectorSize := uint32(msdosSB.Get_ms_sector_size()) - res := &result.Result{ + res := &probe.Result{ BlockSize: sectorSize, FilesystemBlockSize: uint32(vfatSB.Get_vs_cluster_size()) * sectorSize, - FilesystemSize: uint64(sectorCount) * uint64(sectorSize), + ProbedSize: uint64(sectorCount) * uint64(sectorSize), } return res, nil diff --git a/blkid/internal/filesystems/xfs/xfs.go b/blkid/internal/filesystems/xfs/xfs.go index a3244a5..5262f12 100644 --- a/blkid/internal/filesystems/xfs/xfs.go +++ b/blkid/internal/filesystems/xfs/xfs.go @@ -9,13 +9,12 @@ package xfs import ( "bytes" - "io" "github.com/google/uuid" "github.com/siderolabs/go-pointer" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/result" + "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" ) var xfsMagic = magic.Magic{ @@ -37,7 +36,7 @@ func (p *Probe) Name() string { } // Probe runs the further inspection and returns the result if successful. -func (p *Probe) Probe(r io.ReaderAt) (*result.Result, error) { +func (p *Probe) Probe(r probe.Reader) (*probe.Result, error) { buf := make([]byte, SUPERBLOCK_SIZE) if _, err := r.ReadAt(buf, 0); err != nil { @@ -54,12 +53,12 @@ func (p *Probe) Probe(r io.ReaderAt) (*result.Result, error) { return nil, err } - res := &result.Result{ + res := &probe.Result{ UUID: &uuid, BlockSize: uint32(sb.Get_sb_sectsize()), FilesystemBlockSize: sb.Get_sb_blocksize(), - FilesystemSize: sb.FilesystemSize(), + ProbedSize: sb.FilesystemSize(), } lbl := sb.Get_sb_fname() diff --git a/blkid/internal/partitions/gpt/entry.go b/blkid/internal/partitions/gpt/entry.go new file mode 100644 index 0000000..69e707f --- /dev/null +++ b/blkid/internal/partitions/gpt/entry.go @@ -0,0 +1,47 @@ +// 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/. + +// Code generated by "cstruct -pkg gpt -struct Entry -input entry.h -endianness LittleEndian"; DO NOT EDIT. + +package gpt + +import "encoding/binary" + +var _ = binary.LittleEndian + +// Entry is a byte slice representing the entry.h C header. +type Entry []byte + +// Get_partition_type_guid returns type UUID. +func (s Entry) Get_partition_type_guid() []byte { + return s[0:16] +} + +// Get_unique_partition_guid returns partition UUID. +func (s Entry) Get_unique_partition_guid() []byte { + return s[16:32] +} + +// Get_starting_lba returns starting_lba. +func (s Entry) Get_starting_lba() uint64 { + return binary.LittleEndian.Uint64(s[32:40]) +} + +// Get_ending_lba returns ending_lba. +func (s Entry) Get_ending_lba() uint64 { + return binary.LittleEndian.Uint64(s[40:48]) +} + +// Get_attributes returns attributes. +func (s Entry) Get_attributes() uint64 { + return binary.LittleEndian.Uint64(s[48:56]) +} + +// Get_partition_name returns partition_name. +func (s Entry) Get_partition_name() []byte { + return s[56:128] +} + +// ENTRY_SIZE is the size of the Entry struct. +const ENTRY_SIZE = 128 diff --git a/blkid/internal/partitions/gpt/entry.h b/blkid/internal/partitions/gpt/entry.h new file mode 100644 index 0000000..3bd17b3 --- /dev/null +++ b/blkid/internal/partitions/gpt/entry.h @@ -0,0 +1,14 @@ +#include + +struct gpt_entry { + uint8_t partition_type_guid[16]; /* type UUID */ + uint8_t unique_partition_guid[16]; /* partition UUID */ + uint64_t starting_lba; + uint64_t ending_lba; + + /*struct gpt_entry_attributes attributes;*/ + + uint64_t attributes; + + uint8_t partition_name[72]; /* UTF-16LE string*/ +} __attribute__ ((packed)); diff --git a/blkid/internal/partitions/gpt/gpt.go b/blkid/internal/partitions/gpt/gpt.go new file mode 100644 index 0000000..2aa0244 --- /dev/null +++ b/blkid/internal/partitions/gpt/gpt.go @@ -0,0 +1,214 @@ +// 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 probes GPT partition tables. +package gpt + +import ( + "bytes" + "hash/crc32" + + "github.com/google/uuid" + "github.com/siderolabs/go-pointer" + "golang.org/x/text/encoding/unicode" + + "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" + "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" +) + +//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{} + +// Probe for the partition table. +type Probe struct{} + +// Magic returns the magic value for the partition table. +func (p *Probe) Magic() []*magic.Magic { + return []*magic.Magic{&nullMagic} +} + +// Name returns the name of the partition table. +func (p *Probe) Name() string { + return "gpt" +} + +const ( + primaryLBA = 1 + headerSignature = 0x5452415020494645 // "EFI PART" +) + +// Probe runs the further inspection and returns the result if successful. +func (p *Probe) Probe(r probe.Reader) (*probe.Result, error) { + lastLBA, ok := lastLBA(r) + if !ok { + return nil, nil //nolint:nilnil + } + + // try reading primary header + hdr, entries, err := readHeader(r, primaryLBA, lastLBA) + if err != nil { + return nil, err + } + + if hdr == nil { + // try reading backup header + hdr, entries, err = readHeader(r, lastLBA, lastLBA) + if err != nil { + return nil, err + } + } + + if hdr == nil { + // no header, skip + return nil, nil //nolint:nilnil + } + + ptUUID, err := uuid.FromBytes(guidToUUID(hdr.Get_disk_guid())) + if err != nil { + return nil, err + } + + sectorSize := r.GetSectorSize() + + result := &probe.Result{ + UUID: &ptUUID, + + BlockSize: uint32(sectorSize), + ProbedSize: uint64(sectorSize) * (hdr.Get_last_usable_lba() - hdr.Get_first_usable_lba() + 1), + } + + partIdx := uint(1) + firstUsableLBA := hdr.Get_first_usable_lba() + lastUsableLBA := hdr.Get_last_usable_lba() + + zeroGUID := make([]byte, 16) + utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM) + + for _, entry := range entries { + offset := entry.Get_starting_lba() * uint64(sectorSize) + size := (entry.Get_ending_lba() - entry.Get_starting_lba() + 1) * uint64(sectorSize) + + if entry.Get_starting_lba() < firstUsableLBA || entry.Get_ending_lba() > lastUsableLBA { + partIdx++ + + continue + } + + // skip zero GUIDs + if bytes.Equal(entry.Get_partition_type_guid(), zeroGUID) { + partIdx++ + + continue + } + + partUUID, err := uuid.FromBytes(guidToUUID(entry.Get_unique_partition_guid())) + if err != nil { + return nil, err + } + + typeUUID, err := uuid.FromBytes(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") + + result.Parts = append(result.Parts, probe.Partition{ + UUID: &partUUID, + TypeUUID: &typeUUID, + Label: pointer.To(string(name)), + + Index: partIdx, + + Offset: offset, + Size: size, + }) + + partIdx++ + } + + return result, nil +} + +func readHeader(r probe.Reader, lba, lastLBA uint64) (*Header, []Entry, error) { + sectorSize := r.GetSectorSize() + buf := make([]byte, sectorSize) + + if _, err := r.ReadAt(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 := r.ReadAt(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.go b/blkid/internal/partitions/gpt/header.go new file mode 100644 index 0000000..e20081f --- /dev/null +++ b/blkid/internal/partitions/gpt/header.go @@ -0,0 +1,87 @@ +// 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/. + +// Code generated by "cstruct -pkg gpt -struct Header -input header.h -endianness LittleEndian"; DO NOT EDIT. + +package gpt + +import "encoding/binary" + +var _ = binary.LittleEndian + +// Header is a byte slice representing the header.h C header. +type Header []byte + +// Get_signature returns "EFI PART". +func (s Header) Get_signature() uint64 { + return binary.LittleEndian.Uint64(s[0:8]) +} + +// Get_revision returns revision. +func (s Header) Get_revision() uint32 { + return binary.LittleEndian.Uint32(s[8:12]) +} + +// Get_header_size returns usually 92 bytes. +func (s Header) Get_header_size() uint32 { + return binary.LittleEndian.Uint32(s[12:16]) +} + +// Get_header_crc32 returns header_crc32. +func (s Header) Get_header_crc32() uint32 { + return binary.LittleEndian.Uint32(s[16:20]) +} + +// Get_reserved1 returns reserved1. +func (s Header) Get_reserved1() uint32 { + return binary.LittleEndian.Uint32(s[20:24]) +} + +// Get_my_lba returns location of this header copy. +func (s Header) Get_my_lba() uint64 { + return binary.LittleEndian.Uint64(s[24:32]) +} + +// Get_alternate_lba returns location of the other header copy. +func (s Header) Get_alternate_lba() uint64 { + return binary.LittleEndian.Uint64(s[32:40]) +} + +// Get_first_usable_lba returns first usable LBA for partitions. +func (s Header) Get_first_usable_lba() uint64 { + return binary.LittleEndian.Uint64(s[40:48]) +} + +// Get_last_usable_lba returns last usable LBA for partitions. +func (s Header) Get_last_usable_lba() uint64 { + return binary.LittleEndian.Uint64(s[48:56]) +} + +// Get_disk_guid returns disk UUID. +func (s Header) Get_disk_guid() []byte { + return s[56:72] +} + +// Get_partition_entries_lba returns always 2 in primary header copy. +func (s Header) Get_partition_entries_lba() uint64 { + return binary.LittleEndian.Uint64(s[72:80]) +} + +// Get_num_partition_entries returns num_partition_entries. +func (s Header) Get_num_partition_entries() uint32 { + return binary.LittleEndian.Uint32(s[80:84]) +} + +// Get_sizeof_partition_entry returns sizeof_partition_entry. +func (s Header) Get_sizeof_partition_entry() uint32 { + return binary.LittleEndian.Uint32(s[84:88]) +} + +// Get_partition_entry_array_crc32 returns partition_entry_array_crc32. +func (s Header) Get_partition_entry_array_crc32() uint32 { + return binary.LittleEndian.Uint32(s[88:92]) +} + +// HEADER_SIZE is the size of the Header struct. +const HEADER_SIZE = 92 diff --git a/blkid/internal/partitions/gpt/header.h b/blkid/internal/partitions/gpt/header.h new file mode 100644 index 0000000..b392cc6 --- /dev/null +++ b/blkid/internal/partitions/gpt/header.h @@ -0,0 +1,33 @@ +#include + +struct gpt_header { + uint64_t signature; /* "EFI PART" */ + uint32_t revision; + uint32_t header_size; /* usually 92 bytes */ + uint32_t header_crc32; /* checksum of header with this + * field zeroed during calculation */ + uint32_t reserved1; + + uint64_t my_lba; /* location of this header copy */ + uint64_t alternate_lba; /* location of the other header copy */ + uint64_t first_usable_lba; /* first usable LBA for partitions */ + uint64_t last_usable_lba; /* last usable LBA for partitions */ + + uint8_t disk_guid[16]; /* disk UUID */ + + uint64_t partition_entries_lba; /* always 2 in primary header copy */ + uint32_t num_partition_entries; + uint32_t sizeof_partition_entry; + uint32_t partition_entry_array_crc32; + + /* + * The rest of the block is reserved by UEFI and must be zero. EFI + * standard handles this by: + * + * uint8_t reserved2[ BLKSSZGET - 92 ]; + * + * This definition is useless in practice. It is necessary to read + * whole block from the device rather than sizeof(struct gpt_header) + * only. + */ +} __attribute__ ((packed)); diff --git a/blkid/internal/partitions/gpt/header_extra.go b/blkid/internal/partitions/gpt/header_extra.go new file mode 100644 index 0000000..a481d80 --- /dev/null +++ b/blkid/internal/partitions/gpt/header_extra.go @@ -0,0 +1,21 @@ +// 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 new file mode 100644 index 0000000..fc4ede1 --- /dev/null +++ b/blkid/internal/partitions/gpt/lba.go @@ -0,0 +1,20 @@ +// 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 new file mode 100644 index 0000000..6882ac4 --- /dev/null +++ b/blkid/internal/partitions/gpt/uuid.go @@ -0,0 +1,17 @@ +// 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/probe/probe.go b/blkid/internal/probe/probe.go new file mode 100644 index 0000000..2ce79cd --- /dev/null +++ b/blkid/internal/probe/probe.go @@ -0,0 +1,56 @@ +// 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 probe defines common probe interfaces. +package probe + +import ( + "io" + + "github.com/google/uuid" + + "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" +) + +// Reader is a context for probing filesystems and volume managers. +type Reader interface { + io.ReaderAt + + GetSectorSize() uint + GetSize() uint64 +} + +// Prober is an interface for probing filesystems and volume managers. +type Prober interface { + // Name returns the name of the filesystem or volume manager. + Name() string + // Magic returns the magic value for the filesystem or volume manager. + Magic() []*magic.Magic + // Probe runs the further inspection and returns the result if successful. + Probe(Reader) (*Result, error) +} + +// Result is a probe result. +type Result struct { + UUID *uuid.UUID + Label *string + + Parts []Partition + + BlockSize uint32 + FilesystemBlockSize uint32 + ProbedSize uint64 +} + +// Partition is a probe sub-result. +type Partition struct { + UUID *uuid.UUID + TypeUUID *uuid.UUID + Label *string + + Index uint // 1-based index + + Offset uint64 + Size uint64 +} diff --git a/blkid/internal/result/result.go b/blkid/internal/result/result.go deleted file mode 100644 index a90b329..0000000 --- a/blkid/internal/result/result.go +++ /dev/null @@ -1,18 +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 result describes probe result. -package result - -import "github.com/google/uuid" - -// Result is a probe result. -type Result struct { - UUID *uuid.UUID - Label *string - - BlockSize uint32 - FilesystemBlockSize uint32 - FilesystemSize uint64 -} diff --git a/blkid/probe_linux.go b/blkid/probe_linux.go index 513c349..99cf2d8 100644 --- a/blkid/probe_linux.go +++ b/blkid/probe_linux.go @@ -11,47 +11,129 @@ import ( "io" "os" - "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probers" + "github.com/siderolabs/go-blockdevice/v2/blkid/internal/chain" + "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" ) -func (i *Info) probe(f *os.File, offset, length uint64) error { - if offset+length > i.Size { - return fmt.Errorf("probing range is out of bounds: offset %d + len %d > size %d", offset, length, i.Size) +type probeReader struct { + io.ReaderAt + + sectorSize uint + size uint64 +} + +func (r *probeReader) GetSectorSize() uint { + return r.sectorSize +} + +func (r *probeReader) GetSize() uint64 { + return r.size +} + +func (i *Info) fillProbeResult(f *os.File) error { + chain := chain.Default() + + res, matched, err := i.probe(f, chain, 0, i.Size) + if err != nil { + return fmt.Errorf("error probing: %w", err) + } + + if res == nil { + return nil + } + + i.Name = matched.Name() + i.UUID = res.UUID + i.BlockSize = res.BlockSize + i.FilesystemBlockSize = res.FilesystemBlockSize + i.ProbedSize = res.ProbedSize + i.Label = res.Label + + if err = i.fillNested(f, chain, 0, &i.Parts, res.Parts); err != nil { + return fmt.Errorf("error probing nested: %w", err) + } + + return nil +} + +func (i *Info) fillNested(f *os.File, chain chain.Chain, offset uint64, out *[]NestedProbeResult, parts []probe.Partition) error { + if len(parts) == 0 { + return nil + } + + *out = make([]NestedProbeResult, len(parts)) + + for idx, part := range parts { + (*out)[idx].PartitionIndex = part.Index + (*out)[idx].PartitionUUID = part.UUID + (*out)[idx].PartitionType = part.TypeUUID + (*out)[idx].PartitionLabel = part.Label + + (*out)[idx].PartitionOffset = part.Offset + (*out)[idx].PartitionSize = part.Size + + res, matched, err := i.probe(f, chain, offset+part.Offset, part.Size) + if err != nil { + return fmt.Errorf("error probing nested: %w", err) + } + + if res == nil { + continue + } + + (*out)[idx].Name = matched.Name() + (*out)[idx].UUID = res.UUID + (*out)[idx].BlockSize = res.BlockSize + (*out)[idx].FilesystemBlockSize = res.FilesystemBlockSize + (*out)[idx].ProbedSize = res.ProbedSize + (*out)[idx].Label = res.Label + + if err = i.fillNested(f, chain, offset+part.Offset, &(*out)[idx].Parts, res.Parts); err != nil { + return fmt.Errorf("error probing nested: %w", err) + } } - // read enough data to cover the maximum magic size - chain := probers.Chain() + return nil +} + +func (i *Info) probe(f *os.File, chain chain.Chain, offset, length uint64) (*probe.Result, probe.Prober, error) { + if offset+length > i.Size { + return nil, nil, fmt.Errorf("probing range is out of bounds: offset %d + len %d > size %d", offset, length, i.Size) + } if length < uint64(chain.MaxMagicSize()) { - return fmt.Errorf("probing range is too small: len %d < max magic size %d", length, chain.MaxMagicSize()) + return nil, nil, fmt.Errorf("probing range is too small: len %d < max magic size %d", length, chain.MaxMagicSize()) } - magicReadSize := max(uint64(chain.MaxMagicSize()), i.IOSize) - magicReadSize = min(magicReadSize, length) + magicReadSize := max(uint(chain.MaxMagicSize()), i.IOSize) + + if uint64(magicReadSize) > length { + magicReadSize = uint(length) + } buf := make([]byte, magicReadSize) _, err := f.ReadAt(buf, int64(offset)) if err != nil { - return fmt.Errorf("error reading magic buffer: %w", err) + return nil, nil, fmt.Errorf("error reading magic buffer: %w", err) + } + + pR := &probeReader{ + ReaderAt: io.NewSectionReader(f, int64(offset), int64(length)), + + sectorSize: i.SectorSize, + size: length, } for _, matched := range chain.MagicMatches(buf) { - res, err := matched.Probe(io.NewSectionReader(f, int64(offset), int64(length))) + res, err := matched.Probe(pR) if err != nil || res == nil { // skip failed probes continue } - i.Name = matched.Name() - i.UUID = res.UUID - i.BlockSize = res.BlockSize - i.FilesystemBlockSize = res.FilesystemBlockSize - i.FilesystemSize = res.FilesystemSize - i.Label = res.Label - - break + return res, matched, nil } - return nil + return nil, nil, nil } diff --git a/block/device_linux.go b/block/device_linux.go index 26c4051..b5f7ab7 100644 --- a/block/device_linux.go +++ b/block/device_linux.go @@ -21,9 +21,9 @@ func (d *Device) GetSize() (uint64, error) { } // GetIOSize returns blockdevice optimal I/O size in bytes. -func (d *Device) GetIOSize() (uint64, error) { +func (d *Device) GetIOSize() (uint, error) { for _, ioctl := range []int{unix.BLKIOOPT, unix.BLKIOMIN, unix.BLKBSZGET} { - var size uint64 + var size uint if _, _, errno := unix.Syscall(unix.SYS_IOCTL, d.f.Fd(), uintptr(ioctl), uintptr(unsafe.Pointer(&size))); errno != 0 { continue } @@ -35,3 +35,14 @@ func (d *Device) GetIOSize() (uint64, error) { return DefaultBlockSize, nil } + +// GetSectorSize returns blockdevice sector size in bytes. +func (d *Device) GetSectorSize() uint { + var size uint + + if _, _, errno := unix.Syscall(unix.SYS_IOCTL, d.f.Fd(), uintptr(unix.BLKSSZGET), uintptr(unsafe.Pointer(&size))); errno != 0 { + return DefaultBlockSize + } + + return size +} diff --git a/block/utils.go b/block/utils.go index 4f4df37..6b61246 100644 --- a/block/utils.go +++ b/block/utils.go @@ -4,6 +4,6 @@ package block -func isPowerOf2[T uint8 | uint16 | uint32 | uint64](num T) bool { +func isPowerOf2[T uint8 | uint16 | uint32 | uint64 | uint](num T) bool { return (num != 0 && ((num & (num - 1)) == 0)) } diff --git a/go.mod b/go.mod index 9e4ec18..ca7bab7 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.0 require ( github.com/freddierice/go-losetup/v2 v2.0.1 github.com/google/uuid v1.6.0 + github.com/siderolabs/gen v0.4.7 github.com/siderolabs/go-pointer v1.0.0 github.com/stretchr/testify v1.8.4 golang.org/x/sys v0.16.0 diff --git a/go.sum b/go.sum index 3350486..97fcd2c 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/siderolabs/gen v0.4.7 h1:lM69UYggT7yzpubf7hEFaNujPdY55Y9zvQf/NC18GvA= +github.com/siderolabs/gen v0.4.7/go.mod h1:4PBYMdXxTg292IDRq4CGn5AymyDxJVEDvobVKDqFBEA= github.com/siderolabs/go-pointer v1.0.0 h1:6TshPKep2doDQJAAtHUuHWXbca8ZfyRySjSBT/4GsMU= github.com/siderolabs/go-pointer v1.0.0/go.mod h1:HTRFUNYa3R+k0FFKNv11zgkaCLzEkWVzoYZ433P3kHc= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=