// SPDX-License-Identifier: Apache-2.0
/*
 * Copyright (C) 2020 Aleksa Sarai <cyphar@cyphar.com>
 * Copyright (C) 2020 SUSE LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package devices

import (
	"bytes"
	"reflect"
	"testing"

	"github.com/opencontainers/runc/libcontainer/devices"
)

func TestDeviceEmulatorLoad(t *testing.T) {
	tests := []struct {
		name, list string
		expected   *Emulator
	}{
		{
			name: "BlacklistMode",
			list: `a *:* rwm`,
			expected: &Emulator{
				defaultAllow: true,
			},
		},
		{
			name: "WhitelistBasic",
			list: `c 4:2 rw`,
			expected: &Emulator{
				defaultAllow: false,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 4,
						minor: 2,
					}: devices.Permissions("rw"),
				},
			},
		},
		{
			name: "WhitelistWildcard",
			list: `b 0:* m`,
			expected: &Emulator{
				defaultAllow: false,
				rules: deviceRules{
					{
						node:  devices.BlockDevice,
						major: 0,
						minor: devices.Wildcard,
					}: devices.Permissions("m"),
				},
			},
		},
		{
			name: "WhitelistDuplicate",
			list: `c *:* rwm
c 1:1 r`,
			expected: &Emulator{
				defaultAllow: false,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: devices.Wildcard,
						minor: devices.Wildcard,
					}: devices.Permissions("rwm"),
					// To match the kernel, we allow redundant rules.
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 1,
					}: devices.Permissions("r"),
				},
			},
		},
		{
			name: "WhitelistComplicated",
			list: `c *:* m
b *:* m
c 1:3 rwm
c 1:5 rwm
c 1:7 rwm
c 1:8 rwm
c 1:9 rwm
c 5:0 rwm
c 5:2 rwm
c 136:* rwm
c 10:200 rwm`,
			expected: &Emulator{
				defaultAllow: false,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: devices.Wildcard,
						minor: devices.Wildcard,
					}: devices.Permissions("m"),
					{
						node:  devices.BlockDevice,
						major: devices.Wildcard,
						minor: devices.Wildcard,
					}: devices.Permissions("m"),
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 3,
					}: devices.Permissions("rwm"),
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 5,
					}: devices.Permissions("rwm"),
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 7,
					}: devices.Permissions("rwm"),
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 8,
					}: devices.Permissions("rwm"),
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 9,
					}: devices.Permissions("rwm"),
					{
						node:  devices.CharDevice,
						major: 5,
						minor: 0,
					}: devices.Permissions("rwm"),
					{
						node:  devices.CharDevice,
						major: 5,
						minor: 2,
					}: devices.Permissions("rwm"),
					{
						node:  devices.CharDevice,
						major: 136,
						minor: devices.Wildcard,
					}: devices.Permissions("rwm"),
					{
						node:  devices.CharDevice,
						major: 10,
						minor: 200,
					}: devices.Permissions("rwm"),
				},
			},
		},
		// Some invalid lists.
		{
			name:     "InvalidFieldNumber",
			list:     `b 1:0`,
			expected: nil,
		},
		{
			name:     "InvalidDeviceType",
			list:     `p *:* rwm`,
			expected: nil,
		},
		{
			name:     "InvalidMajorNumber1",
			list:     `p -1:3 rwm`,
			expected: nil,
		},
		{
			name:     "InvalidMajorNumber2",
			list:     `c foo:27 rwm`,
			expected: nil,
		},
		{
			name:     "InvalidMinorNumber1",
			list:     `b 1:-4 rwm`,
			expected: nil,
		},
		{
			name:     "InvalidMinorNumber2",
			list:     `b 1:foo rwm`,
			expected: nil,
		},
		{
			name:     "InvalidPermissions",
			list:     `b 1:7 rwk`,
			expected: nil,
		},
	}

	for _, test := range tests {
		test := test // capture range variable
		t.Run(test.name, func(t *testing.T) {
			list := bytes.NewBufferString(test.list)
			emu, err := EmulatorFromList(list)
			if err != nil && test.expected != nil {
				t.Fatalf("unexpected failure when creating emulator: %v", err)
			} else if err == nil && test.expected == nil {
				t.Fatalf("unexpected success when creating emulator: %#v", emu)
			}

			if !reflect.DeepEqual(emu, test.expected) {
				t.Errorf("final emulator state mismatch: %#v != %#v", emu, test.expected)
			}
		})
	}
}

func testDeviceEmulatorApply(t *testing.T, baseDefaultAllow bool) {
	tests := []struct {
		name           string
		rule           devices.Rule
		base, expected *Emulator
	}{
		// Switch between default modes.
		{
			name: "SwitchToOtherMode",
			rule: devices.Rule{
				Type:        devices.WildcardDevice,
				Major:       devices.Wildcard,
				Minor:       devices.Wildcard,
				Permissions: devices.Permissions("rwm"),
				Allow:       !baseDefaultAllow,
			},
			base: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: devices.Wildcard,
						minor: devices.Wildcard,
					}: devices.Permissions("rwm"),
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 1,
					}: devices.Permissions("r"),
				},
			},
			expected: &Emulator{
				defaultAllow: !baseDefaultAllow,
				rules:        nil,
			},
		},
		{
			name: "SwitchToSameModeNoop",
			rule: devices.Rule{
				Type:        devices.WildcardDevice,
				Major:       devices.Wildcard,
				Minor:       devices.Wildcard,
				Permissions: devices.Permissions("rwm"),
				Allow:       baseDefaultAllow,
			},
			base: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules:        nil,
			},
			expected: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules:        nil,
			},
		},
		{
			name: "SwitchToSameMode",
			rule: devices.Rule{
				Type:        devices.WildcardDevice,
				Major:       devices.Wildcard,
				Minor:       devices.Wildcard,
				Permissions: devices.Permissions("rwm"),
				Allow:       baseDefaultAllow,
			},
			base: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: devices.Wildcard,
						minor: devices.Wildcard,
					}: devices.Permissions("rwm"),
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 1,
					}: devices.Permissions("r"),
				},
			},
			expected: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules:        nil,
			},
		},
		// Rule addition logic.
		{
			name: "RuleAdditionBasic",
			rule: devices.Rule{
				Type:        devices.CharDevice,
				Major:       42,
				Minor:       1337,
				Permissions: devices.Permissions("rm"),
				Allow:       !baseDefaultAllow,
			},
			base: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 2,
						minor: 1,
					}: devices.Permissions("rwm"),
					{
						node:  devices.BlockDevice,
						major: 1,
						minor: 5,
					}: devices.Permissions("r"),
				},
			},
			expected: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 2,
						minor: 1,
					}: devices.Permissions("rwm"),
					{
						node:  devices.BlockDevice,
						major: 1,
						minor: 5,
					}: devices.Permissions("r"),
					{
						node:  devices.CharDevice,
						major: 42,
						minor: 1337,
					}: devices.Permissions("rm"),
				},
			},
		},
		{
			name: "RuleAdditionBasicDuplicate",
			rule: devices.Rule{
				Type:        devices.CharDevice,
				Major:       42,
				Minor:       1337,
				Permissions: devices.Permissions("rm"),
				Allow:       !baseDefaultAllow,
			},
			base: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 42,
						minor: devices.Wildcard,
					}: devices.Permissions("rwm"),
				},
			},
			expected: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 42,
						minor: devices.Wildcard,
					}: devices.Permissions("rwm"),
					// To match the kernel, we allow redundant rules.
					{
						node:  devices.CharDevice,
						major: 42,
						minor: 1337,
					}: devices.Permissions("rm"),
				},
			},
		},
		{
			name: "RuleAdditionBasicDuplicateNoop",
			rule: devices.Rule{
				Type:        devices.CharDevice,
				Major:       42,
				Minor:       1337,
				Permissions: devices.Permissions("rm"),
				Allow:       !baseDefaultAllow,
			},
			base: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 42,
						minor: 1337,
					}: devices.Permissions("rm"),
				},
			},
			expected: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 42,
						minor: 1337,
					}: devices.Permissions("rm"),
				},
			},
		},
		{
			name: "RuleAdditionMerge",
			rule: devices.Rule{
				Type:        devices.BlockDevice,
				Major:       5,
				Minor:       12,
				Permissions: devices.Permissions("rm"),
				Allow:       !baseDefaultAllow,
			},
			base: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 2,
						minor: 1,
					}: devices.Permissions("rwm"),
					{
						node:  devices.BlockDevice,
						major: 5,
						minor: 12,
					}: devices.Permissions("rw"),
				},
			},
			expected: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 2,
						minor: 1,
					}: devices.Permissions("rwm"),
					{
						node:  devices.BlockDevice,
						major: 5,
						minor: 12,
					}: devices.Permissions("rwm"),
				},
			},
		},
		{
			name: "RuleAdditionMergeWildcard",
			rule: devices.Rule{
				Type:        devices.BlockDevice,
				Major:       5,
				Minor:       devices.Wildcard,
				Permissions: devices.Permissions("rm"),
				Allow:       !baseDefaultAllow,
			},
			base: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 2,
						minor: 1,
					}: devices.Permissions("rwm"),
					{
						node:  devices.BlockDevice,
						major: 5,
						minor: devices.Wildcard,
					}: devices.Permissions("rw"),
				},
			},
			expected: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 2,
						minor: 1,
					}: devices.Permissions("rwm"),
					{
						node:  devices.BlockDevice,
						major: 5,
						minor: devices.Wildcard,
					}: devices.Permissions("rwm"),
				},
			},
		},
		{
			name: "RuleAdditionMergeNoop",
			rule: devices.Rule{
				Type:        devices.BlockDevice,
				Major:       5,
				Minor:       12,
				Permissions: devices.Permissions("r"),
				Allow:       !baseDefaultAllow,
			},
			base: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 2,
						minor: 1,
					}: devices.Permissions("rwm"),
					{
						node:  devices.BlockDevice,
						major: 5,
						minor: 12,
					}: devices.Permissions("rw"),
				},
			},
			expected: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 2,
						minor: 1,
					}: devices.Permissions("rwm"),
					{
						node:  devices.BlockDevice,
						major: 5,
						minor: 12,
					}: devices.Permissions("rw"),
				},
			},
		},
		// Rule removal logic.
		{
			name: "RuleRemovalBasic",
			rule: devices.Rule{
				Type:        devices.CharDevice,
				Major:       42,
				Minor:       1337,
				Permissions: devices.Permissions("rm"),
				Allow:       baseDefaultAllow,
			},
			base: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 42,
						minor: 1337,
					}: devices.Permissions("rm"),
					{
						node:  devices.BlockDevice,
						major: 1,
						minor: 5,
					}: devices.Permissions("r"),
				},
			},
			expected: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.BlockDevice,
						major: 1,
						minor: 5,
					}: devices.Permissions("r"),
				},
			},
		},
		{
			name: "RuleRemovalNonexistent",
			rule: devices.Rule{
				Type:        devices.CharDevice,
				Major:       4,
				Minor:       1,
				Permissions: devices.Permissions("rw"),
				Allow:       baseDefaultAllow,
			},
			base: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.BlockDevice,
						major: 1,
						minor: 5,
					}: devices.Permissions("r"),
				},
			},
			expected: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.BlockDevice,
						major: 1,
						minor: 5,
					}: devices.Permissions("r"),
				},
			},
		},
		{
			name: "RuleRemovalFull",
			rule: devices.Rule{
				Type:        devices.CharDevice,
				Major:       42,
				Minor:       1337,
				Permissions: devices.Permissions("rw"),
				Allow:       baseDefaultAllow,
			},
			base: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 42,
						minor: 1337,
					}: devices.Permissions("w"),
					{
						node:  devices.BlockDevice,
						major: 1,
						minor: 5,
					}: devices.Permissions("r"),
				},
			},
			expected: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.BlockDevice,
						major: 1,
						minor: 5,
					}: devices.Permissions("r"),
				},
			},
		},
		{
			name: "RuleRemovalPartial",
			rule: devices.Rule{
				Type:        devices.CharDevice,
				Major:       42,
				Minor:       1337,
				Permissions: devices.Permissions("r"),
				Allow:       baseDefaultAllow,
			},
			base: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 42,
						minor: 1337,
					}: devices.Permissions("rm"),
					{
						node:  devices.BlockDevice,
						major: 1,
						minor: 5,
					}: devices.Permissions("r"),
				},
			},
			expected: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 42,
						minor: 1337,
					}: devices.Permissions("m"),
					{
						node:  devices.BlockDevice,
						major: 1,
						minor: 5,
					}: devices.Permissions("r"),
				},
			},
		},
		// Check our non-canonical behaviour when it comes to try to "punch
		// out" holes in a wildcard rule.
		{
			name: "RuleRemovalWildcardPunchoutImpossible",
			rule: devices.Rule{
				Type:        devices.CharDevice,
				Major:       42,
				Minor:       1337,
				Permissions: devices.Permissions("r"),
				Allow:       baseDefaultAllow,
			},
			base: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 42,
						minor: devices.Wildcard,
					}: devices.Permissions("rm"),
					{
						node:  devices.CharDevice,
						major: 42,
						minor: 1337,
					}: devices.Permissions("r"),
				},
			},
			expected: nil,
		},
		{
			name: "RuleRemovalWildcardPunchoutPossible",
			rule: devices.Rule{
				Type:        devices.CharDevice,
				Major:       42,
				Minor:       1337,
				Permissions: devices.Permissions("r"),
				Allow:       baseDefaultAllow,
			},
			base: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 42,
						minor: devices.Wildcard,
					}: devices.Permissions("wm"),
					{
						node:  devices.CharDevice,
						major: 42,
						minor: 1337,
					}: devices.Permissions("r"),
				},
			},
			expected: &Emulator{
				defaultAllow: baseDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 42,
						minor: devices.Wildcard,
					}: devices.Permissions("wm"),
				},
			},
		},
	}

	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			err := test.base.Apply(test.rule)
			if err != nil && test.expected != nil {
				t.Fatalf("unexpected failure when applying apply rule: %v", err)
			} else if err == nil && test.expected == nil {
				t.Fatalf("unexpected success when applying apply rule: %#v", test.base)
			}

			if test.expected != nil && !reflect.DeepEqual(test.base, test.expected) {
				t.Errorf("final emulator state mismatch: %#v != %#v", test.base, test.expected)
			}
		})
	}
}

func TestDeviceEmulatorWhitelistApply(t *testing.T) {
	testDeviceEmulatorApply(t, false)
}

func TestDeviceEmulatorBlacklistApply(t *testing.T) {
	testDeviceEmulatorApply(t, true)
}

func testDeviceEmulatorTransition(t *testing.T, sourceDefaultAllow bool) {
	tests := []struct {
		name           string
		source, target *Emulator
		expected       []*devices.Rule
	}{
		// No-op changes.
		{
			name: "Noop",
			source: &Emulator{
				defaultAllow: sourceDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 42,
						minor: devices.Wildcard,
					}: devices.Permissions("wm"),
				},
			},
			target: &Emulator{
				defaultAllow: sourceDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 42,
						minor: devices.Wildcard,
					}: devices.Permissions("wm"),
				},
			},
			// Identical white-lists produce no extra rules.
			expected: nil,
		},
		// Switching modes.
		{
			name: "SwitchToOtherMode",
			source: &Emulator{
				defaultAllow: sourceDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 2,
					}: devices.Permissions("rwm"),
				},
			},
			target: &Emulator{
				defaultAllow: !sourceDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.BlockDevice,
						major: 42,
						minor: devices.Wildcard,
					}: devices.Permissions("wm"),
				},
			},
			expected: []*devices.Rule{
				// Clear-all rule.
				{
					Type:        devices.WildcardDevice,
					Major:       devices.Wildcard,
					Minor:       devices.Wildcard,
					Permissions: devices.Permissions("rwm"),
					Allow:       !sourceDefaultAllow,
				},
				// The actual rule-set.
				{
					Type:        devices.BlockDevice,
					Major:       42,
					Minor:       devices.Wildcard,
					Permissions: devices.Permissions("wm"),
					Allow:       sourceDefaultAllow,
				},
			},
		},
		// Rule changes.
		{
			name: "RuleAddition",
			source: &Emulator{
				defaultAllow: sourceDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 2,
					}: devices.Permissions("rwm"),
				},
			},
			target: &Emulator{
				defaultAllow: sourceDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 2,
					}: devices.Permissions("rwm"),
					{
						node:  devices.BlockDevice,
						major: 42,
						minor: 1337,
					}: devices.Permissions("rwm"),
				},
			},
			expected: []*devices.Rule{
				{
					Type:        devices.BlockDevice,
					Major:       42,
					Minor:       1337,
					Permissions: devices.Permissions("rwm"),
					Allow:       !sourceDefaultAllow,
				},
			},
		},
		{
			name: "RuleRemoval",
			source: &Emulator{
				defaultAllow: sourceDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 2,
					}: devices.Permissions("rwm"),
					{
						node:  devices.BlockDevice,
						major: 42,
						minor: 1337,
					}: devices.Permissions("rwm"),
				},
			},
			target: &Emulator{
				defaultAllow: sourceDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 2,
					}: devices.Permissions("rwm"),
				},
			},
			expected: []*devices.Rule{
				{
					Type:        devices.BlockDevice,
					Major:       42,
					Minor:       1337,
					Permissions: devices.Permissions("rwm"),
					Allow:       sourceDefaultAllow,
				},
			},
		},
		{
			name: "RuleMultipleAdditionRemoval",
			source: &Emulator{
				defaultAllow: sourceDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 2,
					}: devices.Permissions("rwm"),
					{
						node:  devices.BlockDevice,
						major: 3,
						minor: 9,
					}: devices.Permissions("rw"),
				},
			},
			target: &Emulator{
				defaultAllow: sourceDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 2,
					}: devices.Permissions("rwm"),
				},
			},
			expected: []*devices.Rule{
				{
					Type:        devices.BlockDevice,
					Major:       3,
					Minor:       9,
					Permissions: devices.Permissions("rw"),
					Allow:       sourceDefaultAllow,
				},
			},
		},
		// Modifying the access permissions.
		{
			name: "RulePartialAddition",
			source: &Emulator{
				defaultAllow: sourceDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 2,
					}: devices.Permissions("r"),
				},
			},
			target: &Emulator{
				defaultAllow: sourceDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 2,
					}: devices.Permissions("rwm"),
				},
			},
			expected: []*devices.Rule{
				{
					Type:        devices.CharDevice,
					Major:       1,
					Minor:       2,
					Permissions: devices.Permissions("wm"),
					Allow:       !sourceDefaultAllow,
				},
			},
		},
		{
			name: "RulePartialRemoval",
			source: &Emulator{
				defaultAllow: sourceDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 2,
					}: devices.Permissions("rw"),
				},
			},
			target: &Emulator{
				defaultAllow: sourceDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 2,
					}: devices.Permissions("w"),
				},
			},
			expected: []*devices.Rule{
				{
					Type:        devices.CharDevice,
					Major:       1,
					Minor:       2,
					Permissions: devices.Permissions("r"),
					Allow:       sourceDefaultAllow,
				},
			},
		},
		{
			name: "RulePartialBoth",
			source: &Emulator{
				defaultAllow: sourceDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 2,
					}: devices.Permissions("rw"),
				},
			},
			target: &Emulator{
				defaultAllow: sourceDefaultAllow,
				rules: deviceRules{
					{
						node:  devices.CharDevice,
						major: 1,
						minor: 2,
					}: devices.Permissions("rm"),
				},
			},
			expected: []*devices.Rule{
				{
					Type:        devices.CharDevice,
					Major:       1,
					Minor:       2,
					Permissions: devices.Permissions("w"),
					Allow:       sourceDefaultAllow,
				},
				{
					Type:        devices.CharDevice,
					Major:       1,
					Minor:       2,
					Permissions: devices.Permissions("m"),
					Allow:       !sourceDefaultAllow,
				},
			},
		},
	}

	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			// If we are in black-list mode, we need to prepend the relevant
			// clear-all rule (the expected rule lists are written with
			// white-list mode in mind), and then make a full copy of the
			// target rules.
			if sourceDefaultAllow && test.source.defaultAllow == test.target.defaultAllow {
				test.expected = []*devices.Rule{{
					Type:        devices.WildcardDevice,
					Major:       devices.Wildcard,
					Minor:       devices.Wildcard,
					Permissions: devices.Permissions("rwm"),
					Allow:       test.target.defaultAllow,
				}}
				for _, rule := range test.target.rules.orderedEntries() {
					test.expected = append(test.expected, &devices.Rule{
						Type:        rule.meta.node,
						Major:       rule.meta.major,
						Minor:       rule.meta.minor,
						Permissions: rule.perms,
						Allow:       !test.target.defaultAllow,
					})
				}
			}

			rules, err := test.source.Transition(test.target)
			if err != nil {
				t.Fatalf("unexpected error while calculating transition rules: %#v", err)
			}

			if !reflect.DeepEqual(rules, test.expected) {
				t.Errorf("rules don't match expected set: %#v != %#v", rules, test.expected)
			}

			// Apply the rules to the source to see if it actually transitions
			// correctly. This is all emulated but it's a good thing to
			// double-check.
			for _, rule := range rules {
				if err := test.source.Apply(*rule); err != nil {
					t.Fatalf("error while applying transition rule [%#v]: %v", rule, err)
				}
			}
			if !reflect.DeepEqual(test.source, test.target) {
				t.Errorf("transition incomplete after applying all rules: %#v != %#v", test.source, test.target)
			}
		})
	}
}

func TestDeviceEmulatorTransitionFromBlacklist(t *testing.T) {
	testDeviceEmulatorTransition(t, true)
}

func TestDeviceEmulatorTransitionFromWhitelist(t *testing.T) {
	testDeviceEmulatorTransition(t, false)
}
