Introduce `://` syntax for Windows Devices in DeviceMapping.PathOnHost
**- What I did**

Introduced a new Windows Device string syntax for the `--devices` and the `api.types.container.DeviceMapping.PathOnHost`.

The syntax is `IDType://ID`, reflecting an evolution and generalisation of the existing `class/ID` syntax. The latter remains supported specifically for `class/X`: `IDType/ID` for other `IDType`s is not implemented to avoid future parsing ambiguity.

This syntax will function with both the `--device` parameter from https://github.com/docker/cli, and also when fed from Kubernetes (or other CRI client) via https://github.com/Mirantis/cri-dockerd, or [dockershim](https://kubernetes.io/blog/2022/02/17/dockershim-faq/) where supported. Kubernetes and CRI are both currently agnostic about this format, passing the strings down from user-controlled APIs unchanged.

~~`DeviceMapping` no long accepts-and-ignores values in the `PathInContainer` or `CgroupPermissions` fields for Windows Devices.~~ (This will be a separate to-be-created PR, as this was a more-impactful change than I realised)

For reference, containerd has just [implemented the same syntax](https://github.com/containerd/containerd/pull/6618) for both CRI and the `ctr` CLI, to be part of the 1.7 release series. When [a more-specific CRI type is implemented for Windows Devices](https://github.com/kubernetes/kubernetes/issues/97739), then cri-dockerd (or other CRI implementation) will be responsible for packing the required data into this format for consumption by the daemon.

Windows Devices are no-longer limited to Process Isolation mode, as there are IDTypes that are also supported for HyperV Isolation, and I don't think it's desirable to repeat the mapping of which IDTypes work with which Isolation mode here.

**Note**: Due to limitations of the HCS v1 API used when not using the containerd backend:
* IDTypes other than `class` or `vpci-class-guid` are not supported without the containerd backend.
* Assigning devices to HyperV-isolated containers is not supported without the containerd backend.

Because these depend on the backend in-use, they are reported as failures from the backend via `ContainerStart` when not supported. Previously, `ContainerCreate` rejected both of these cases unconditionally, so these restrictions are looser than they were before this change.

Practically, these limitations from using the HCS v1 API won't matter, since Windows Server 2019 and earlier do not support the other IDType mappings, even if using the HCS v2 API via containerd, and Windows Server 2022 and later is intended to default to using the containerd backend and the HCS v2 API in Docker 21.xx. The only other Windows Server product not already end-of-life is SAC 20H2, [which does support the newer ID types](https://github.com/kubernetes/kubernetes/issues/97739#issuecomment-767981773). That product, the final Windows Server SAC release, reaches end-of-life in August 2022.

**- How I did it**

Changed the code that split `DeviceMapping{PathOnHost: "class/X"}` into `WindowsDevice{IDType:"class", ID: "X"}` to also split `DeviceMapping{PathOnHost: "W://X"}` into `WindowsDevice{IDType:"W", ID: "X"}`

**- How to verify it**

Unit test coverage for the parser.

Integration tests covering `class/5B45201D-F2F2-4F3B-85BB-30FF1F953599`, `class://5B45201D-F2F2-4F3B-85BB-30FF1F953599` and `vpci-class-guid://5B45201D-F2F2-4F3B-85BB-30FF1F953599` for both Process Isolation and HyperV isolation, supported by both containerd and HCS v1-based backends, where possible.

*Note*: Integration tests were having issues with HyperV isolation, so they have been skipped except for the early-failure case. It turns out we currently have no integration tests for HyperV isolation running on CI. This is possibly a nested-VM issue on the CI side, so it may vary by build-host config. I saw one pass for the skipped tests on an RS5 node.

Manually test exactly like the existing `--device` CLI flag on Windows, except using `class://` where you would currently use `class/`, e.g. `class/5B45201D-F2F2-4F3B-85BB-30FF1F953599` and `class://5B45201D-F2F2-4F3B-85BB-30FF1F953599` should produce identical results.

hcshim defines [a bunch of other possible IDTypes](https://github.com/microsoft/hcsshim/blob/v0.9.2/internal/uvm/virtual_device.go#L14-L21), but platform support and use-cases will vary, so I can't really advise on how to test them. And we don't deal with them here anyway, they go on opaquely down to hcshim to be handled.

**- Description for the changelog**

Windows Devices described by string, e.g. `--devices` in the CLI, now support the syntax `IDType://ID` as well as `class/ID`. Supported IDTypes depend on the active backend and other parameters of the container being created.

**- A picture of a cute animal (not mandatory but encouraged)**

[![Baby Animal](https://static.wikia.nocookie.net/muppet/images/d/dc/Character.babyanimal.jpg)](https://muppet.fandom.com/wiki/Muppet_Babies_(puppets)?file=Character.babyanimal.jpg)
daemon/oci_windows_test.go
package daemon

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"gotest.tools/v3/fs"

	containertypes "github.com/docker/docker/api/types/container"
	"github.com/docker/docker/container"
	swarmagent "github.com/docker/swarmkit/agent"
	swarmapi "github.com/docker/swarmkit/api"
	specs "github.com/opencontainers/runtime-spec/specs-go"
	"golang.org/x/sys/windows/registry"
	"gotest.tools/v3/assert"
)

func TestSetWindowsCredentialSpecInSpec(t *testing.T) {
	// we need a temp directory to act as the daemon's root
	tmpDaemonRoot := fs.NewDir(t, t.Name()).Path()
	defer func() {
		assert.NilError(t, os.RemoveAll(tmpDaemonRoot))
	}()

	daemon := &Daemon{
		root: tmpDaemonRoot,
	}

	t.Run("it does nothing if there are no security options", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(&container.Container{}, spec)
		assert.NilError(t, err)
		assert.Check(t, spec.Windows == nil)

		err = daemon.setWindowsCredentialSpec(&container.Container{HostConfig: &containertypes.HostConfig{}}, spec)
		assert.NilError(t, err)
		assert.Check(t, spec.Windows == nil)

		err = daemon.setWindowsCredentialSpec(&container.Container{HostConfig: &containertypes.HostConfig{SecurityOpt: []string{}}}, spec)
		assert.NilError(t, err)
		assert.Check(t, spec.Windows == nil)
	})

	dummyContainerID := "dummy-container-ID"
	containerFactory := func(secOpt string) *container.Container {
		if !strings.Contains(secOpt, "=") {
			secOpt = "credentialspec=" + secOpt
		}
		return &container.Container{
			ID: dummyContainerID,
			HostConfig: &containertypes.HostConfig{
				SecurityOpt: []string{secOpt},
			},
		}
	}

	credSpecsDir := filepath.Join(tmpDaemonRoot, credentialSpecFileLocation)
	dummyCredFileContents := `{"We don't need no": "education"}`

	t.Run("happy path with a 'file://' option", func(t *testing.T) {
		spec := &specs.Spec{}

		// let's render a dummy cred file
		err := os.Mkdir(credSpecsDir, os.ModePerm)
		assert.NilError(t, err)
		dummyCredFileName := "dummy-cred-spec.json"
		dummyCredFilePath := filepath.Join(credSpecsDir, dummyCredFileName)
		err = os.WriteFile(dummyCredFilePath, []byte(dummyCredFileContents), 0644)
		defer func() {
			assert.NilError(t, os.Remove(dummyCredFilePath))
		}()
		assert.NilError(t, err)

		err = daemon.setWindowsCredentialSpec(containerFactory("file://"+dummyCredFileName), spec)
		assert.NilError(t, err)

		if assert.Check(t, spec.Windows != nil) {
			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
		}
	})

	t.Run("it's not allowed to use a 'file://' option with an absolute path", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(containerFactory(`file://C:\path\to\my\credspec.json`), spec)
		assert.ErrorContains(t, err, "invalid credential spec - file:// path cannot be absolute")

		assert.Check(t, spec.Windows == nil)
	})

	t.Run("it's not allowed to use a 'file://' option breaking out of the cred specs' directory", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(containerFactory(`file://..\credspec.json`), spec)
		assert.ErrorContains(t, err, fmt.Sprintf("invalid credential spec - file:// path must be under %s", credSpecsDir))

		assert.Check(t, spec.Windows == nil)
	})

	t.Run("when using a 'file://' option pointing to a file that doesn't exist, it fails gracefully", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(containerFactory("file://i-dont-exist.json"), spec)
		assert.ErrorContains(t, err, fmt.Sprintf("credential spec for container %s could not be read from file", dummyContainerID))
		assert.ErrorContains(t, err, "The system cannot find")

		assert.Check(t, spec.Windows == nil)
	})

	t.Run("happy path with a 'registry://' option", func(t *testing.T) {
		valueName := "my-cred-spec"
		key := &dummyRegistryKey{
			getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
				assert.Equal(t, valueName, name)
				return dummyCredFileContents, 0, nil
			},
		}
		defer setRegistryOpenKeyFunc(t, key)()

		spec := &specs.Spec{}
		assert.NilError(t, daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec))

		if assert.Check(t, spec.Windows != nil) {
			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
		}
		assert.Check(t, key.closed)
	})

	t.Run("when using a 'registry://' option and opening the registry key fails, it fails gracefully", func(t *testing.T) {
		dummyError := fmt.Errorf("dummy error")
		defer setRegistryOpenKeyFunc(t, &dummyRegistryKey{}, dummyError)()

		spec := &specs.Spec{}
		err := daemon.setWindowsCredentialSpec(containerFactory("registry://my-cred-spec"), spec)
		assert.ErrorContains(t, err, fmt.Sprintf("registry key %s could not be opened: %v", credentialSpecRegistryLocation, dummyError))

		assert.Check(t, spec.Windows == nil)
	})

	t.Run("when using a 'registry://' option pointing to a value that doesn't exist, it fails gracefully", func(t *testing.T) {
		valueName := "my-cred-spec"
		key := &dummyRegistryKey{
			getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
				assert.Equal(t, valueName, name)
				return "", 0, registry.ErrNotExist
			},
		}
		defer setRegistryOpenKeyFunc(t, key)()

		spec := &specs.Spec{}
		err := daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec)
		assert.ErrorContains(t, err, fmt.Sprintf("registry credential spec %q for container %s was not found", valueName, dummyContainerID))

		assert.Check(t, key.closed)
	})

	t.Run("when using a 'registry://' option and reading the registry value fails, it fails gracefully", func(t *testing.T) {
		dummyError := fmt.Errorf("dummy error")
		valueName := "my-cred-spec"
		key := &dummyRegistryKey{
			getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
				assert.Equal(t, valueName, name)
				return "", 0, dummyError
			},
		}
		defer setRegistryOpenKeyFunc(t, key)()

		spec := &specs.Spec{}
		err := daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec)
		assert.ErrorContains(t, err, fmt.Sprintf("error reading credential spec %q from registry for container %s: %v", valueName, dummyContainerID, dummyError))

		assert.Check(t, key.closed)
	})

	t.Run("happy path with a 'config://' option", func(t *testing.T) {
		configID := "my-cred-spec"

		dependencyManager := swarmagent.NewDependencyManager(nil)
		dependencyManager.Configs().Add(swarmapi.Config{
			ID: configID,
			Spec: swarmapi.ConfigSpec{
				Data: []byte(dummyCredFileContents),
			},
		})

		task := &swarmapi.Task{
			Spec: swarmapi.TaskSpec{
				Runtime: &swarmapi.TaskSpec_Container{
					Container: &swarmapi.ContainerSpec{
						Configs: []*swarmapi.ConfigReference{
							{
								ConfigID: configID,
							},
						},
					},
				},
			},
		}

		cntr := containerFactory("config://" + configID)
		cntr.DependencyStore = swarmagent.Restrict(dependencyManager, task)

		spec := &specs.Spec{}
		err := daemon.setWindowsCredentialSpec(cntr, spec)
		assert.NilError(t, err)

		if assert.Check(t, spec.Windows != nil) {
			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
		}
	})

	t.Run("using a 'config://' option on a container not managed by swarmkit is not allowed, and results in a generic error message to hide that purely internal API", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(containerFactory("config://whatever"), spec)
		assert.Equal(t, errInvalidCredentialSpecSecOpt, err)

		assert.Check(t, spec.Windows == nil)
	})

	t.Run("happy path with a 'raw://' option", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(containerFactory("raw://"+dummyCredFileContents), spec)
		assert.NilError(t, err)

		if assert.Check(t, spec.Windows != nil) {
			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
		}
	})

	t.Run("it's not case sensitive in the option names", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(containerFactory("CreDENtiaLSPeC=rAw://"+dummyCredFileContents), spec)
		assert.NilError(t, err)

		if assert.Check(t, spec.Windows != nil) {
			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
		}
	})

	t.Run("it rejects unknown options", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(containerFactory("credentialspe=config://whatever"), spec)
		assert.ErrorContains(t, err, "security option not supported: credentialspe")

		assert.Check(t, spec.Windows == nil)
	})

	t.Run("it rejects unsupported credentialspec options", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(containerFactory("idontexist://whatever"), spec)
		assert.Equal(t, errInvalidCredentialSpecSecOpt, err)

		assert.Check(t, spec.Windows == nil)
	})

	for _, option := range []string{"file", "registry", "config", "raw"} {
		t.Run(fmt.Sprintf("it rejects empty values for %s", option), func(t *testing.T) {
			spec := &specs.Spec{}

			err := daemon.setWindowsCredentialSpec(containerFactory(option+"://"), spec)
			assert.Equal(t, errInvalidCredentialSpecSecOpt, err)

			assert.Check(t, spec.Windows == nil)
		})
	}
}

/* Helpers below */

type dummyRegistryKey struct {
	getStringValueFunc func(name string) (val string, valtype uint32, err error)
	closed             bool
}

func (k *dummyRegistryKey) GetStringValue(name string) (val string, valtype uint32, err error) {
	return k.getStringValueFunc(name)
}

func (k *dummyRegistryKey) Close() error {
	k.closed = true
	return nil
}

// setRegistryOpenKeyFunc replaces the registryOpenKeyFunc package variable, and returns a function
// to be called to revert the change when done with testing.
func setRegistryOpenKeyFunc(t *testing.T, key *dummyRegistryKey, err ...error) func() {
	previousRegistryOpenKeyFunc := registryOpenKeyFunc

	registryOpenKeyFunc = func(baseKey registry.Key, path string, access uint32) (registryKey, error) {
		// this should always be called with exactly the same arguments
		assert.Equal(t, registry.LOCAL_MACHINE, baseKey)
		assert.Equal(t, credentialSpecRegistryLocation, path)
		assert.Equal(t, uint32(registry.QUERY_VALUE), access)

		if len(err) > 0 {
			return nil, err[0]
		}
		return key, nil
	}

	return func() {
		registryOpenKeyFunc = previousRegistryOpenKeyFunc
	}
}

package daemon

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"gotest.tools/v3/fs"

	containertypes "github.com/docker/docker/api/types/container"
	"github.com/docker/docker/container"
	swarmagent "github.com/docker/swarmkit/agent"
	swarmapi "github.com/docker/swarmkit/api"
	specs "github.com/opencontainers/runtime-spec/specs-go"
	"golang.org/x/sys/windows/registry"
	"gotest.tools/v3/assert"
)

func TestSetWindowsCredentialSpecInSpec(t *testing.T) {
	// we need a temp directory to act as the daemon's root
	tmpDaemonRoot := fs.NewDir(t, t.Name()).Path()
	defer func() {
		assert.NilError(t, os.RemoveAll(tmpDaemonRoot))
	}()

	daemon := &Daemon{
		root: tmpDaemonRoot,
	}

	t.Run("it does nothing if there are no security options", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(&container.Container{}, spec)
		assert.NilError(t, err)
		assert.Check(t, spec.Windows == nil)

		err = daemon.setWindowsCredentialSpec(&container.Container{HostConfig: &containertypes.HostConfig{}}, spec)
		assert.NilError(t, err)
		assert.Check(t, spec.Windows == nil)

		err = daemon.setWindowsCredentialSpec(&container.Container{HostConfig: &containertypes.HostConfig{SecurityOpt: []string{}}}, spec)
		assert.NilError(t, err)
		assert.Check(t, spec.Windows == nil)
	})

	dummyContainerID := "dummy-container-ID"
	containerFactory := func(secOpt string) *container.Container {
		if !strings.Contains(secOpt, "=") {
			secOpt = "credentialspec=" + secOpt
		}
		return &container.Container{
			ID: dummyContainerID,
			HostConfig: &containertypes.HostConfig{
				SecurityOpt: []string{secOpt},
			},
		}
	}

	credSpecsDir := filepath.Join(tmpDaemonRoot, credentialSpecFileLocation)
	dummyCredFileContents := `{"We don't need no": "education"}`

	t.Run("happy path with a 'file://' option", func(t *testing.T) {
		spec := &specs.Spec{}

		// let's render a dummy cred file
		err := os.Mkdir(credSpecsDir, os.ModePerm)
		assert.NilError(t, err)
		dummyCredFileName := "dummy-cred-spec.json"
		dummyCredFilePath := filepath.Join(credSpecsDir, dummyCredFileName)
		err = os.WriteFile(dummyCredFilePath, []byte(dummyCredFileContents), 0644)
		defer func() {
			assert.NilError(t, os.Remove(dummyCredFilePath))
		}()
		assert.NilError(t, err)

		err = daemon.setWindowsCredentialSpec(containerFactory("file://"+dummyCredFileName), spec)
		assert.NilError(t, err)

		if assert.Check(t, spec.Windows != nil) {
			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
		}
	})

	t.Run("it's not allowed to use a 'file://' option with an absolute path", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(containerFactory(`file://C:\path\to\my\credspec.json`), spec)
		assert.ErrorContains(t, err, "invalid credential spec - file:// path cannot be absolute")

		assert.Check(t, spec.Windows == nil)
	})

	t.Run("it's not allowed to use a 'file://' option breaking out of the cred specs' directory", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(containerFactory(`file://..\credspec.json`), spec)
		assert.ErrorContains(t, err, fmt.Sprintf("invalid credential spec - file:// path must be under %s", credSpecsDir))

		assert.Check(t, spec.Windows == nil)
	})

	t.Run("when using a 'file://' option pointing to a file that doesn't exist, it fails gracefully", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(containerFactory("file://i-dont-exist.json"), spec)
		assert.ErrorContains(t, err, fmt.Sprintf("credential spec for container %s could not be read from file", dummyContainerID))
		assert.ErrorContains(t, err, "The system cannot find")

		assert.Check(t, spec.Windows == nil)
	})

	t.Run("happy path with a 'registry://' option", func(t *testing.T) {
		valueName := "my-cred-spec"
		key := &dummyRegistryKey{
			getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
				assert.Equal(t, valueName, name)
				return dummyCredFileContents, 0, nil
			},
		}
		defer setRegistryOpenKeyFunc(t, key)()

		spec := &specs.Spec{}
		assert.NilError(t, daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec))

		if assert.Check(t, spec.Windows != nil) {
			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
		}
		assert.Check(t, key.closed)
	})

	t.Run("when using a 'registry://' option and opening the registry key fails, it fails gracefully", func(t *testing.T) {
		dummyError := fmt.Errorf("dummy error")
		defer setRegistryOpenKeyFunc(t, &dummyRegistryKey{}, dummyError)()

		spec := &specs.Spec{}
		err := daemon.setWindowsCredentialSpec(containerFactory("registry://my-cred-spec"), spec)
		assert.ErrorContains(t, err, fmt.Sprintf("registry key %s could not be opened: %v", credentialSpecRegistryLocation, dummyError))

		assert.Check(t, spec.Windows == nil)
	})

	t.Run("when using a 'registry://' option pointing to a value that doesn't exist, it fails gracefully", func(t *testing.T) {
		valueName := "my-cred-spec"
		key := &dummyRegistryKey{
			getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
				assert.Equal(t, valueName, name)
				return "", 0, registry.ErrNotExist
			},
		}
		defer setRegistryOpenKeyFunc(t, key)()

		spec := &specs.Spec{}
		err := daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec)
		assert.ErrorContains(t, err, fmt.Sprintf("registry credential spec %q for container %s was not found", valueName, dummyContainerID))

		assert.Check(t, key.closed)
	})

	t.Run("when using a 'registry://' option and reading the registry value fails, it fails gracefully", func(t *testing.T) {
		dummyError := fmt.Errorf("dummy error")
		valueName := "my-cred-spec"
		key := &dummyRegistryKey{
			getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
				assert.Equal(t, valueName, name)
				return "", 0, dummyError
			},
		}
		defer setRegistryOpenKeyFunc(t, key)()

		spec := &specs.Spec{}
		err := daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec)
		assert.ErrorContains(t, err, fmt.Sprintf("error reading credential spec %q from registry for container %s: %v", valueName, dummyContainerID, dummyError))

		assert.Check(t, key.closed)
	})

	t.Run("happy path with a 'config://' option", func(t *testing.T) {
		configID := "my-cred-spec"

		dependencyManager := swarmagent.NewDependencyManager(nil)
		dependencyManager.Configs().Add(swarmapi.Config{
			ID: configID,
			Spec: swarmapi.ConfigSpec{
				Data: []byte(dummyCredFileContents),
			},
		})

		task := &swarmapi.Task{
			Spec: swarmapi.TaskSpec{
				Runtime: &swarmapi.TaskSpec_Container{
					Container: &swarmapi.ContainerSpec{
						Configs: []*swarmapi.ConfigReference{
							{
								ConfigID: configID,
							},
						},
					},
				},
			},
		}

		cntr := containerFactory("config://" + configID)
		cntr.DependencyStore = swarmagent.Restrict(dependencyManager, task)

		spec := &specs.Spec{}
		err := daemon.setWindowsCredentialSpec(cntr, spec)
		assert.NilError(t, err)

		if assert.Check(t, spec.Windows != nil) {
			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
		}
	})

	t.Run("using a 'config://' option on a container not managed by swarmkit is not allowed, and results in a generic error message to hide that purely internal API", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(containerFactory("config://whatever"), spec)
		assert.Equal(t, errInvalidCredentialSpecSecOpt, err)

		assert.Check(t, spec.Windows == nil)
	})

	t.Run("happy path with a 'raw://' option", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(containerFactory("raw://"+dummyCredFileContents), spec)
		assert.NilError(t, err)

		if assert.Check(t, spec.Windows != nil) {
			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
		}
	})

	t.Run("it's not case sensitive in the option names", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(containerFactory("CreDENtiaLSPeC=rAw://"+dummyCredFileContents), spec)
		assert.NilError(t, err)

		if assert.Check(t, spec.Windows != nil) {
			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
		}
	})

	t.Run("it rejects unknown options", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(containerFactory("credentialspe=config://whatever"), spec)
		assert.ErrorContains(t, err, "security option not supported: credentialspe")

		assert.Check(t, spec.Windows == nil)
	})

	t.Run("it rejects unsupported credentialspec options", func(t *testing.T) {
		spec := &specs.Spec{}

		err := daemon.setWindowsCredentialSpec(containerFactory("idontexist://whatever"), spec)
		assert.Equal(t, errInvalidCredentialSpecSecOpt, err)

		assert.Check(t, spec.Windows == nil)
	})

	for _, option := range []string{"file", "registry", "config", "raw"} {
		t.Run(fmt.Sprintf("it rejects empty values for %s", option), func(t *testing.T) {
			spec := &specs.Spec{}

			err := daemon.setWindowsCredentialSpec(containerFactory(option+"://"), spec)
			assert.Equal(t, errInvalidCredentialSpecSecOpt, err)

			assert.Check(t, spec.Windows == nil)
		})
	}
}

/* Helpers below */

type dummyRegistryKey struct {
	getStringValueFunc func(name string) (val string, valtype uint32, err error)
	closed             bool
}

func (k *dummyRegistryKey) GetStringValue(name string) (val string, valtype uint32, err error) {
	return k.getStringValueFunc(name)
}

func (k *dummyRegistryKey) Close() error {
	k.closed = true
	return nil
}

// setRegistryOpenKeyFunc replaces the registryOpenKeyFunc package variable, and returns a function
// to be called to revert the change when done with testing.
func setRegistryOpenKeyFunc(t *testing.T, key *dummyRegistryKey, err ...error) func() {
	previousRegistryOpenKeyFunc := registryOpenKeyFunc

	registryOpenKeyFunc = func(baseKey registry.Key, path string, access uint32) (registryKey, error) {
		// this should always be called with exactly the same arguments
		assert.Equal(t, registry.LOCAL_MACHINE, baseKey)
		assert.Equal(t, credentialSpecRegistryLocation, path)
		assert.Equal(t, uint32(registry.QUERY_VALUE), access)

		if len(err) > 0 {
			return nil, err[0]
		}
		return key, nil
	}

	return func() {
		registryOpenKeyFunc = previousRegistryOpenKeyFunc
	}
}

func TestSetupWindowsDevices(t *testing.T) {
	t.Run("it does nothing if there are no devices", func(t *testing.T) {
		devices, err := setupWindowsDevices(nil)
		assert.NilError(t, err)
		assert.Equal(t, len(devices), 0)
	})

	t.Run("it fails if any devices are blank", func(t *testing.T) {
		devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: ""}})
		assert.ErrorContains(t, err, "invalid device assignment path")
		assert.ErrorContains(t, err, "''")
		assert.Equal(t, len(devices), 0)
	})

	t.Run("it fails if all devices do not contain '/' or '://'", func(t *testing.T) {
		devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "anything"}, {PathOnHost: "goes"}})
		assert.ErrorContains(t, err, "invalid device assignment path")
		assert.ErrorContains(t, err, "'anything'")
		assert.Equal(t, len(devices), 0)
	})

	t.Run("it fails if any devices do not contain '/' or '://'", func(t *testing.T) {
		devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: "goes"}})
		assert.ErrorContains(t, err, "invalid device assignment path")
		assert.ErrorContains(t, err, "'goes'")
		assert.Equal(t, len(devices), 0)
	})

	t.Run("it fails if all '/'-separated devices do not have IDType 'class'", func(t *testing.T) {
		devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "klass/anything"}, {PathOnHost: "klass/goes"}})
		assert.ErrorContains(t, err, "invalid device assignment path")
		assert.ErrorContains(t, err, "'klass/anything'")
		assert.Equal(t, len(devices), 0)
	})

	t.Run("it fails if any '/'-separated devices do not have IDType 'class'", func(t *testing.T) {
		devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: "klass/goes"}})
		assert.ErrorContains(t, err, "invalid device assignment path")
		assert.ErrorContains(t, err, "'klass/goes'")
		assert.Equal(t, len(devices), 0)
	})

	t.Run("it fails if any '://'-separated devices have IDType ''", func(t *testing.T) {
		devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: "://goes"}})
		assert.ErrorContains(t, err, "invalid device assignment path")
		assert.ErrorContains(t, err, "'://goes'")
		assert.Equal(t, len(devices), 0)
	})

	t.Run("it creates devices if all '/'-separated devices have IDType 'class'", func(t *testing.T) {
		devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: "class/goes"}})
		expectedDevices := []specs.WindowsDevice{{IDType: "class", ID: "anything"}, {IDType: "class", ID: "goes"}}
		assert.NilError(t, err)
		assert.Equal(t, len(devices), len(expectedDevices))
		for i := range expectedDevices {
			assert.Equal(t, devices[i], expectedDevices[i])
		}
	})

	t.Run("it creates devices if all '://'-separated devices have non-blank IDType", func(t *testing.T) {
		devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class://anything"}, {PathOnHost: "klass://goes"}})
		expectedDevices := []specs.WindowsDevice{{IDType: "class", ID: "anything"}, {IDType: "klass", ID: "goes"}}
		assert.NilError(t, err)
		assert.Equal(t, len(devices), len(expectedDevices))
		for i := range expectedDevices {
			assert.Equal(t, devices[i], expectedDevices[i])
		}
	})

	t.Run("it creates devices when given a mix of '/'-separated and '://'-separated devices", func(t *testing.T) {
		devices, err := setupWindowsDevices([]containertypes.DeviceMapping{{PathOnHost: "class/anything"}, {PathOnHost: "klass://goes"}})
		expectedDevices := []specs.WindowsDevice{{IDType: "class", ID: "anything"}, {IDType: "klass", ID: "goes"}}
		assert.NilError(t, err)
		assert.Equal(t, len(devices), len(expectedDevices))
		for i := range expectedDevices {
			assert.Equal(t, devices[i], expectedDevices[i])
		}
	})
}

oh! interesting; should've looked in this file itself ☺️. No worries; it was mostly a nit (and something that stood out, because we generally don't use this format, but apparently we did in this file); I'm fine keeping it as-is