diff --git a/osversion_test.go b/osversion_test.go index a0c3302..e38f99a 100644 --- a/osversion_test.go +++ b/osversion_test.go @@ -161,6 +161,25 @@ func TestAvailableVersion(t *testing.T) { } } }) + + t.Run("macOS 12.3", func(t *testing.T) { + majorMinorVersion = 12 + cases := map[string]func() error{ + "BlockDeviceIdentifier": func() error { + _, err := (*VirtioBlockDeviceConfiguration)(nil).BlockDeviceIdentifier() + return err + }, + "SetBlockDeviceIdentifier": func() error { + return (*VirtioBlockDeviceConfiguration)(nil).SetBlockDeviceIdentifier("") + }, + } + for name, fn := range cases { + err := fn() + if !errors.Is(err, ErrUnsupportedOSVersion) { + t.Fatalf("unexpected error %v in %s", err, name) + } + } + }) } func Test_fetchMajorMinorVersion(t *testing.T) { diff --git a/storage.go b/storage.go index e9974cd..884cef3 100644 --- a/storage.go +++ b/storage.go @@ -4,6 +4,7 @@ package vz #cgo darwin CFLAGS: -x objective-c -fno-objc-arc #cgo darwin LDFLAGS: -lobjc -framework Foundation -framework Virtualization # include "virtualization.h" +# include "virtualization_12_3.h" # include "virtualization_13.h" */ import "C" @@ -104,6 +105,8 @@ type VirtioBlockDeviceConfiguration struct { *pointer *baseStorageDeviceConfiguration + + blockDeviceIdentifier string } // NewVirtioBlockDeviceConfiguration initialize a VZVirtioBlockDeviceConfiguration with a device attachment. @@ -130,6 +133,48 @@ func NewVirtioBlockDeviceConfiguration(attachment StorageDeviceAttachment) (*Vir return config, nil } +// BlockDeviceIdentifier returns the device identifier is a string identifying the Virtio block device. +// Empty string by default. +// +// The identifier can be retrieved in the guest via a VIRTIO_BLK_T_GET_ID request. +// +// This is only supported on macOS 12.3 and newer, error will be returned on older versions. +// +// see: https://developer.apple.com/documentation/virtualization/vzvirtioblockdeviceconfiguration/3917717-blockdeviceidentifier +func (v *VirtioBlockDeviceConfiguration) BlockDeviceIdentifier() (string, error) { + if err := macOSAvailable(12.3); err != nil { + return "", err + } + return v.blockDeviceIdentifier, nil +} + +// SetBlockDeviceIdentifier sets the device identifier is a string identifying the Virtio block device. +// +// The device identifier must be at most 20 bytes in length and ASCII-encodable. +// +// This is only supported on macOS 12.3 and newer, error will be returned on older versions. +// +// see: https://developer.apple.com/documentation/virtualization/vzvirtioblockdeviceconfiguration/3917717-blockdeviceidentifier +func (v *VirtioBlockDeviceConfiguration) SetBlockDeviceIdentifier(identifier string) error { + if err := macOSAvailable(12.3); err != nil { + return err + } + idChar := charWithGoString(identifier) + defer idChar.Free() + + nserrPtr := newNSErrorAsNil() + C.setBlockDeviceIdentifierVZVirtioBlockDeviceConfiguration( + objc.Ptr(v), + idChar.CString(), + &nserrPtr, + ) + if err := newNSError(nserrPtr); err != nil { + return err + } + v.blockDeviceIdentifier = identifier + return nil +} + // USBMassStorageDeviceConfiguration is a configuration of a USB Mass Storage storage device. // // This device configuration creates a storage device that conforms to the USB Mass Storage specification. diff --git a/storage_test.go b/storage_test.go new file mode 100644 index 0000000..0150bbd --- /dev/null +++ b/storage_test.go @@ -0,0 +1,64 @@ +package vz_test + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/Code-Hex/vz/v2" +) + +func TestBlockDeviceIdentifier(t *testing.T) { + if vz.Available(12.3) { + t.Skip("VirtioBlockDeviceConfiguration.SetBlockDeviceIdentifier is supported from macOS 12.3") + } + dir := t.TempDir() + path := filepath.Join(dir, "disk.img") + if err := vz.CreateDiskImage(path, 512); err != nil { + t.Fatal(err) + } + + attachment, err := vz.NewDiskImageStorageDeviceAttachment(path, false) + if err != nil { + t.Fatal(err) + } + config, err := vz.NewVirtioBlockDeviceConfiguration(attachment) + if err != nil { + t.Fatal(err) + } + got1, err := config.BlockDeviceIdentifier() + if err != nil { + t.Fatal(err) + } + if got1 != "" { + t.Fatalf("want empty by default: %q", got1) + } + + invalidID := strings.Repeat("h", 25) + if err := config.SetBlockDeviceIdentifier(invalidID); err == nil { + t.Fatal("want error") + } else { + nserr, ok := err.(*vz.NSError) + if !ok { + t.Fatalf("unexpected error: %v", err) + } + if nserr.Domain != "VZErrorDomain" { + t.Errorf("unexpected NSError domain: %v", nserr) + } + if nserr.Code != int(vz.ErrorInvalidVirtualMachineConfiguration) { + t.Errorf("unexpected NSError code: %v", nserr) + } + } + + want := "hello" + if err := config.SetBlockDeviceIdentifier(want); err != nil { + t.Fatal(err) + } + got2, err := config.BlockDeviceIdentifier() + if err != nil { + t.Fatal(err) + } + if got2 != want { + t.Fatalf("want %q but got %q", want, got2) + } +} diff --git a/virtualization_12_3.h b/virtualization_12_3.h new file mode 100644 index 0000000..5d210cd --- /dev/null +++ b/virtualization_12_3.h @@ -0,0 +1,16 @@ +// +// virtualization_12_3.h +// +// Created by codehex. +// + +#pragma once + +#import "virtualization_helper.h" +#import + +// FIXME(codehex): this is dirty hack to avoid clang-format error like below +// "Configuration file(s) do(es) not support C++: /github.com/Code-Hex/vz/.clang-format" +#define NSURLComponents NSURLComponents + +void setBlockDeviceIdentifierVZVirtioBlockDeviceConfiguration(void *blockDeviceConfig, const char *identifier, void **error); \ No newline at end of file diff --git a/virtualization_12_3.m b/virtualization_12_3.m new file mode 100644 index 0000000..5d968c8 --- /dev/null +++ b/virtualization_12_3.m @@ -0,0 +1,24 @@ +// +// virtualization_12_3.m +// +// Created by codehex. +// + +#import "virtualization_12_3.h" + +void setBlockDeviceIdentifierVZVirtioBlockDeviceConfiguration(void *blockDeviceConfig, const char *identifier, void **error) +{ + if (@available(macOS 12.3, *)) { + NSString *identifierNSString = [NSString stringWithUTF8String:identifier]; + BOOL valid = [VZVirtioBlockDeviceConfiguration + validateBlockDeviceIdentifier:identifierNSString + error:(NSError *_Nullable *_Nullable)error]; + if (!valid) { + return; + } + [(VZVirtioBlockDeviceConfiguration *)blockDeviceConfig setBlockDeviceIdentifier:identifierNSString]; + return; + } + + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} \ No newline at end of file