| // Copyright 2020 The Monogon Project Authors. | 
 | // | 
 | // SPDX-License-Identifier: Apache-2.0 | 
 | // | 
 | // 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 loop | 
 |  | 
 | import ( | 
 | 	"encoding/binary" | 
 | 	"io" | 
 | 	"math" | 
 | 	"os" | 
 | 	"runtime" | 
 | 	"syscall" | 
 | 	"testing" | 
 | 	"unsafe" | 
 |  | 
 | 	"github.com/stretchr/testify/assert" | 
 | 	"github.com/stretchr/testify/require" | 
 | 	"golang.org/x/sys/unix" | 
 | ) | 
 |  | 
 | // Write a test file with a very specific pattern (increasing little-endian 16 | 
 | // bit unsigned integers) to detect offset correctness. File is always 128KiB | 
 | // large (2^16 * 2 bytes). | 
 | func makeTestFile() *os.File { | 
 | 	f, err := os.CreateTemp("/tmp", "") | 
 | 	if err != nil { | 
 | 		panic(err) | 
 | 	} | 
 | 	for i := 0; i <= math.MaxUint16; i++ { | 
 | 		if err := binary.Write(f, binary.LittleEndian, uint16(i)); err != nil { | 
 | 			panic(err) | 
 | 		} | 
 | 	} | 
 | 	if _, err := f.Seek(0, io.SeekStart); err != nil { | 
 | 		panic(err) | 
 | 	} | 
 | 	return f | 
 | } | 
 |  | 
 | func getBlkdevSize(f *os.File) (size uint64) { | 
 | 	if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), unix.BLKGETSIZE64, uintptr(unsafe.Pointer(&size))); err != 0 { | 
 | 		panic(err) | 
 | 	} | 
 | 	return | 
 | } | 
 |  | 
 | func getOffsetFromContent(dev *Device) (firstIndex uint16) { | 
 | 	if err := binary.Read(dev.dev, binary.LittleEndian, &firstIndex); err != nil { | 
 | 		panic(err) | 
 | 	} | 
 | 	firstIndex *= 2 // 2 bytes per index | 
 | 	return | 
 | } | 
 |  | 
 | func setupCreate(t *testing.T, config Config) *Device { | 
 | 	f := makeTestFile() | 
 | 	dev, err := Create(f, config) | 
 | 	defer f.Close() | 
 | 	assert.NoError(t, err) | 
 | 	t.Cleanup(func() { | 
 | 		if dev != nil { | 
 | 			dev.Remove() | 
 | 		} | 
 | 		os.Remove(f.Name()) | 
 | 	}) | 
 | 	if dev == nil { | 
 | 		t.FailNow() | 
 | 	} | 
 | 	return dev | 
 | } | 
 |  | 
 | func TestDeviceAccessors(t *testing.T) { | 
 | 	if os.Getenv("IN_KTEST") != "true" { | 
 | 		t.Skip("Not in ktest") | 
 | 	} | 
 | 	dev := setupCreate(t, Config{}) | 
 |  | 
 | 	devPath, err := dev.DevPath() | 
 | 	assert.NoError(t, err) | 
 | 	require.Equal(t, "/dev/loop0", devPath) | 
 |  | 
 | 	var stat unix.Stat_t | 
 | 	assert.NoError(t, unix.Stat("/dev/loop0", &stat)) | 
 | 	devNum, err := dev.Dev() | 
 | 	assert.NoError(t, err) | 
 | 	require.Equal(t, stat.Rdev, devNum) | 
 |  | 
 | 	backingFile, err := dev.BackingFilePath() | 
 | 	assert.NoError(t, err) | 
 | 	// The filename of the temporary file is not available in this context, but | 
 | 	// we know that the file needs to be in /tmp, which should be a good-enough | 
 | 	// test. | 
 | 	assert.Contains(t, backingFile, "/tmp/") | 
 | } | 
 |  | 
 | func TestCreate(t *testing.T) { | 
 | 	if os.Getenv("IN_KTEST") != "true" { | 
 | 		t.Skip("Not in ktest") | 
 | 	} | 
 | 	t.Parallel() | 
 | 	tests := []struct { | 
 | 		name     string | 
 | 		config   Config | 
 | 		validate func(t *testing.T, dev *Device) | 
 | 	}{ | 
 | 		{"NoOpts", Config{}, func(t *testing.T, dev *Device) { | 
 | 			require.Equal(t, uint64(128*1024), getBlkdevSize(dev.dev)) | 
 | 			require.Equal(t, uint16(0), getOffsetFromContent(dev)) | 
 |  | 
 | 			_, err := dev.dev.WriteString("test") | 
 | 			assert.NoError(t, err) | 
 | 		}}, | 
 | 		{"DirectIO", Config{Flags: FlagDirectIO}, func(t *testing.T, dev *Device) { | 
 | 			require.Equal(t, uint64(128*1024), getBlkdevSize(dev.dev)) | 
 |  | 
 | 			_, err := dev.dev.WriteString("test") | 
 | 			assert.NoError(t, err) | 
 | 		}}, | 
 | 		{"ReadOnly", Config{Flags: FlagReadOnly}, func(t *testing.T, dev *Device) { | 
 | 			_, err := dev.dev.WriteString("test") | 
 | 			assert.Error(t, err) | 
 | 		}}, | 
 | 		{"Mapping", Config{BlockSize: 512, SizeLimit: 2048, Offset: 4096}, func(t *testing.T, dev *Device) { | 
 | 			assert.Equal(t, uint16(4096), getOffsetFromContent(dev)) | 
 | 			assert.Equal(t, uint64(2048), getBlkdevSize(dev.dev)) | 
 | 		}}, | 
 | 	} | 
 | 	for _, test := range tests { | 
 | 		t.Run(test.name, func(t *testing.T) { | 
 | 			dev := setupCreate(t, test.config) | 
 | 			test.validate(t, dev) | 
 | 			assert.NoError(t, dev.Remove()) | 
 | 		}) | 
 | 	} | 
 | } | 
 |  | 
 | func TestOpenBadDevice(t *testing.T) { | 
 | 	if os.Getenv("IN_KTEST") != "true" { | 
 | 		t.Skip("Not in ktest") | 
 | 	} | 
 | 	dev, err := Open("/dev/null") | 
 | 	require.Error(t, err) | 
 | 	if dev != nil { // Prevent leaks in case this test fails | 
 | 		dev.Close() | 
 | 	} | 
 | } | 
 |  | 
 | func TestOpen(t *testing.T) { | 
 | 	if os.Getenv("IN_KTEST") != "true" { | 
 | 		t.Skip("Not in ktest") | 
 | 	} | 
 | 	f := makeTestFile() | 
 | 	defer os.Remove(f.Name()) | 
 | 	defer f.Close() | 
 | 	dev, err := Create(f, Config{}) | 
 | 	assert.NoError(t, err) | 
 | 	path, err := dev.DevPath() | 
 | 	assert.NoError(t, err) | 
 | 	assert.NoError(t, dev.Close()) | 
 | 	reopenedDev, err := Open(path) | 
 | 	assert.NoError(t, err) | 
 | 	defer reopenedDev.Remove() | 
 | 	reopenedDevPath, err := reopenedDev.DevPath() | 
 | 	assert.NoError(t, err) | 
 | 	require.Equal(t, path, reopenedDevPath) // Still needs to be the same device | 
 | } | 
 |  | 
 | func TestResize(t *testing.T) { | 
 | 	if os.Getenv("IN_KTEST") != "true" { | 
 | 		t.Skip("Not in ktest") | 
 | 	} | 
 | 	f, err := os.CreateTemp("/tmp", "") | 
 | 	assert.NoError(t, err) | 
 | 	empty1K := make([]byte, 1024) | 
 | 	for i := 0; i < 64; i++ { | 
 | 		_, err := f.Write(empty1K) | 
 | 		assert.NoError(t, err) | 
 | 	} | 
 | 	dev, err := Create(f, Config{}) | 
 | 	assert.NoError(t, err) | 
 | 	require.Equal(t, uint64(64*1024), getBlkdevSize(dev.dev)) | 
 | 	for i := 0; i < 32; i++ { | 
 | 		_, err := f.Write(empty1K) | 
 | 		assert.NoError(t, err) | 
 | 	} | 
 | 	assert.NoError(t, f.Sync()) | 
 | 	assert.NoError(t, dev.RefreshSize()) | 
 | 	require.Equal(t, uint64(96*1024), getBlkdevSize(dev.dev)) | 
 | } | 
 |  | 
 | func TestStructSize(t *testing.T) { | 
 | 	if runtime.GOOS != "linux" && runtime.GOARCH != "amd64" { | 
 | 		t.Skip("Reference value not available") | 
 | 	} | 
 | 	require.Equal(t, uintptr(304), unsafe.Sizeof(loopConfig{})) | 
 | } |