| package fat32 | 
 |  | 
 | import ( | 
 | 	"bytes" | 
 | 	"fmt" | 
 | 	"io" | 
 | 	"math/rand" | 
 | 	"os" | 
 | 	"strings" | 
 | 	"testing" | 
 | 	"time" | 
 |  | 
 | 	"github.com/stretchr/testify/assert" | 
 | 	"github.com/stretchr/testify/require" | 
 | 	"golang.org/x/mod/semver" | 
 | 	"golang.org/x/sys/unix" | 
 | ) | 
 |  | 
 | func TestKernelInterop(t *testing.T) { | 
 | 	if os.Getenv("IN_KTEST") != "true" { | 
 | 		t.Skip("Not in ktest") | 
 | 	} | 
 |  | 
 | 	// ONCHANGE(//third_party/linux): Drop this once we move to a Kernel version | 
 | 	// newer than 5.19 which will have FAT btime support. | 
 | 	kernelVersion, err := os.ReadFile("/proc/sys/kernel/osrelease") | 
 | 	if err != nil { | 
 | 		t.Fatalf("unable to determine kernel version: %v", err) | 
 | 	} | 
 | 	haveBtime := semver.Compare("v"+string(kernelVersion), "v5.19.0") >= 0 | 
 |  | 
 | 	type testCase struct { | 
 | 		name     string | 
 | 		setup    func(root *Inode) error | 
 | 		validate func(t *testing.T) error | 
 | 	} | 
 |  | 
 | 	// Random timestamp in UTC, divisible by 10ms | 
 | 	testTimestamp1 := time.Date(2022, 03, 04, 5, 6, 7, 10, time.UTC) | 
 | 	// Random timestamp in UTC, divisible by 2s | 
 | 	testTimestamp2 := time.Date(2022, 03, 04, 5, 6, 8, 0, time.UTC) | 
 | 	// Random timestamp in UTC, divisible by 10ms | 
 | 	testTimestamp3 := time.Date(2052, 03, 02, 5, 6, 7, 10, time.UTC) | 
 | 	// Random timestamp in UTC, divisible by 2s | 
 | 	testTimestamp4 := time.Date(2052, 10, 04, 5, 3, 4, 0, time.UTC) | 
 |  | 
 | 	testContent1 := "testcontent1" | 
 |  | 
 | 	tests := []testCase{ | 
 | 		{ | 
 | 			name: "SimpleFolder", | 
 | 			setup: func(root *Inode) error { | 
 | 				root.Children = []*Inode{{ | 
 | 					Name:       "testdir", | 
 | 					Attrs:      AttrDirectory, | 
 | 					CreateTime: testTimestamp1, | 
 | 					ModTime:    testTimestamp2, | 
 | 				}} | 
 | 				return nil | 
 | 			}, | 
 | 			validate: func(t *testing.T) error { | 
 | 				var stat unix.Statx_t | 
 | 				if err := unix.Statx(0, "/dut/testdir", 0, unix.STATX_TYPE|unix.STATX_MTIME|unix.STATX_BTIME, &stat); err != nil { | 
 | 					availableFiles, err := os.ReadDir("/dut") | 
 | 					var availableFileNames []string | 
 | 					for _, f := range availableFiles { | 
 | 						availableFileNames = append(availableFileNames, f.Name()) | 
 | 					} | 
 | 					if err != nil { | 
 | 						t.Fatalf("Failed to list filesystem root directory: %v", err) | 
 | 					} | 
 | 					t.Fatalf("Failed to stat output: %v (available: %v)", err, strings.Join(availableFileNames, ", ")) | 
 | 				} | 
 | 				if stat.Mode&unix.S_IFDIR == 0 { | 
 | 					t.Errorf("testdir is expected to be a directory, but has mode %v", stat.Mode) | 
 | 				} | 
 | 				btime := time.Unix(stat.Btime.Sec, int64(stat.Btime.Nsec)) | 
 | 				if !btime.Equal(testTimestamp1) && haveBtime { | 
 | 					t.Errorf("testdir btime expected %v, got %v", testTimestamp1, btime) | 
 | 				} | 
 | 				mtime := time.Unix(stat.Mtime.Sec, int64(stat.Mtime.Nsec)) | 
 | 				if !mtime.Equal(testTimestamp2) { | 
 | 					t.Errorf("testdir mtime expected %v, got %v", testTimestamp2, mtime) | 
 | 				} | 
 | 				return nil | 
 | 			}, | 
 | 		}, | 
 | 		{ | 
 | 			name: "SimpleFile", | 
 | 			setup: func(root *Inode) error { | 
 | 				root.Children = []*Inode{{ | 
 | 					Name:       "testfile", | 
 | 					CreateTime: testTimestamp3, | 
 | 					ModTime:    testTimestamp4, | 
 | 					Content:    strings.NewReader(testContent1), | 
 | 				}} | 
 | 				return nil | 
 | 			}, | 
 | 			validate: func(t *testing.T) error { | 
 | 				var stat unix.Statx_t | 
 | 				if err := unix.Statx(0, "/dut/testfile", 0, unix.STATX_TYPE|unix.STATX_MTIME|unix.STATX_BTIME, &stat); err != nil { | 
 | 					t.Fatalf("failed to stat output: %v", err) | 
 | 				} | 
 | 				if stat.Mode&unix.S_IFREG == 0 { | 
 | 					t.Errorf("testfile is expected to be a file, but has mode %v", stat.Mode) | 
 | 				} | 
 | 				btime := time.Unix(stat.Btime.Sec, int64(stat.Btime.Nsec)) | 
 | 				if !btime.Equal(testTimestamp3) && haveBtime { | 
 | 					t.Errorf("testfile ctime expected %v, got %v", testTimestamp3, btime) | 
 | 				} | 
 | 				mtime := time.Unix(stat.Mtime.Sec, int64(stat.Mtime.Nsec)) | 
 | 				if !mtime.Equal(testTimestamp4) { | 
 | 					t.Errorf("testfile mtime expected %v, got %v", testTimestamp3, mtime) | 
 | 				} | 
 | 				contents, err := os.ReadFile("/dut/testfile") | 
 | 				if err != nil { | 
 | 					t.Fatalf("failed to read back test file: %v", err) | 
 | 				} | 
 | 				if string(contents) != testContent1 { | 
 | 					t.Errorf("testfile contains %x, got %x", contents, []byte(testContent1)) | 
 | 				} | 
 | 				return nil | 
 | 			}, | 
 | 		}, | 
 | 		{ | 
 | 			name: "FolderHierarchy", | 
 | 			setup: func(i *Inode) error { | 
 | 				i.Children = []*Inode{{ | 
 | 					Name:       "l1", | 
 | 					Attrs:      AttrDirectory, | 
 | 					CreateTime: testTimestamp1, | 
 | 					ModTime:    testTimestamp2, | 
 | 					Children: []*Inode{{ | 
 | 						Name:       "l2", | 
 | 						Attrs:      AttrDirectory, | 
 | 						CreateTime: testTimestamp1, | 
 | 						ModTime:    testTimestamp2, | 
 | 					}}, | 
 | 				}} | 
 | 				return nil | 
 | 			}, | 
 | 			validate: func(t *testing.T) error { | 
 | 				dirInfo, err := os.ReadDir("/dut/l1") | 
 | 				if err != nil { | 
 | 					t.Fatalf("Failed to read top-level directory: %v", err) | 
 | 				} | 
 | 				require.Len(t, dirInfo, 1, "more subdirs than expected") | 
 | 				require.Equal(t, "l2", dirInfo[0].Name(), "unexpected subdir") | 
 | 				require.True(t, dirInfo[0].IsDir(), "l1 not a directory") | 
 | 				subdirInfo, err := os.ReadDir("/dut/l1/l2") | 
 | 				assert.NoError(t, err, "cannot read empty subdir") | 
 | 				require.Len(t, subdirInfo, 0, "unexpected subdirs in empty directory") | 
 | 				return nil | 
 | 			}, | 
 | 		}, | 
 | 		{ | 
 | 			name: "LargeFile", | 
 | 			setup: func(i *Inode) error { | 
 | 				content := make([]byte, 6500) | 
 | 				io.ReadFull(rand.New(rand.NewSource(1)), content) | 
 | 				i.Children = []*Inode{{ | 
 | 					Name:    "test.bin", | 
 | 					Content: bytes.NewReader(content), | 
 | 				}} | 
 | 				return nil | 
 | 			}, | 
 | 			validate: func(t *testing.T) error { | 
 | 				var stat unix.Stat_t | 
 | 				err := unix.Stat("/dut/test.bin", &stat) | 
 | 				assert.NoError(t, err, "failed to stat file") | 
 | 				require.EqualValues(t, 6500, stat.Size, "wrong size") | 
 | 				file, err := os.Open("/dut/test.bin") | 
 | 				assert.NoError(t, err, "failed to open test file") | 
 | 				defer file.Close() | 
 | 				r := io.LimitReader(rand.New(rand.NewSource(1)), 6500) // Random but deterministic data | 
 | 				expected, _ := io.ReadAll(r) | 
 | 				actual, err := io.ReadAll(file) | 
 | 				assert.NoError(t, err, "failed to read test file") | 
 | 				assert.Equal(t, expected, actual, "content not identical") | 
 | 				return nil | 
 | 			}, | 
 | 		}, | 
 | 		{ | 
 | 			name: "Unicode", | 
 | 			setup: func(i *Inode) error { | 
 | 				i.Children = []*Inode{{ | 
 | 					Name:    "✨😂", // Really exercise that UTF-16 conversion | 
 | 					Content: strings.NewReader("😂"), | 
 | 				}} | 
 | 				return nil | 
 | 			}, | 
 | 			validate: func(t *testing.T) error { | 
 | 				file, err := os.Open("/dut/✨😂") | 
 | 				if err != nil { | 
 | 					availableFiles, err := os.ReadDir("/dut") | 
 | 					var availableFileNames []string | 
 | 					for _, f := range availableFiles { | 
 | 						availableFileNames = append(availableFileNames, f.Name()) | 
 | 					} | 
 | 					if err != nil { | 
 | 						t.Fatalf("Failed to list filesystem root directory: %v", err) | 
 | 					} | 
 | 					t.Fatalf("Failed to open unicode file: %v (available files: %v)", err, strings.Join(availableFileNames, ", ")) | 
 | 				} | 
 | 				defer file.Close() | 
 | 				contents, err := io.ReadAll(file) | 
 | 				if err != nil { | 
 | 					t.Errorf("Wrong content: expected %x, got %x", []byte("😂"), contents) | 
 | 				} | 
 | 				return nil | 
 | 			}, | 
 | 		}, | 
 | 		{ | 
 | 			name: "MultipleMetaClusters", | 
 | 			setup: func(root *Inode) error { | 
 | 				// Only test up to 2048 files as Linux gets VERY slow if going | 
 | 				// up to the maximum of approximately 32K | 
 | 				for i := 0; i < 2048; i++ { | 
 | 					root.Children = append(root.Children, &Inode{ | 
 | 						Name:    fmt.Sprintf("verylongtestfilename%d", i), | 
 | 						Content: strings.NewReader("random test content"), | 
 | 					}) | 
 | 				} | 
 | 				return nil | 
 | 			}, | 
 | 			validate: func(t *testing.T) error { | 
 | 				files, err := os.ReadDir("/dut") | 
 | 				if err != nil { | 
 | 					t.Errorf("failed to list directory: %v", err) | 
 | 				} | 
 | 				if len(files) != 2048 { | 
 | 					t.Errorf("wrong number of files: expected %d, got %d", 2048, len(files)) | 
 | 				} | 
 | 				return nil | 
 | 			}, | 
 | 		}, | 
 | 	} | 
 |  | 
 | 	for _, test := range tests { | 
 | 		t.Run(test.name, func(t *testing.T) { | 
 | 			file, err := os.OpenFile("/dev/ram0", os.O_WRONLY|os.O_TRUNC, 0644) | 
 | 			if err != nil { | 
 | 				t.Fatalf("failed to create test image: %v", err) | 
 | 			} | 
 | 			size, err := unix.IoctlGetInt(int(file.Fd()), unix.BLKGETSIZE64) | 
 | 			if err != nil { | 
 | 				t.Fatalf("failed to get ramdisk size: %v", err) | 
 | 			} | 
 | 			blockSize, err := unix.IoctlGetInt(int(file.Fd()), unix.BLKBSZGET) | 
 | 			if err != nil { | 
 | 				t.Fatalf("failed to get ramdisk block size: %v", err) | 
 | 			} | 
 | 			defer file.Close() | 
 | 			rootInode := Inode{ | 
 | 				Attrs: AttrDirectory, | 
 | 			} | 
 | 			if err := test.setup(&rootInode); err != nil { | 
 | 				t.Fatalf("setup failed: %v", err) | 
 | 			} | 
 | 			if err := WriteFS(file, rootInode, Options{ | 
 | 				ID:         1234, | 
 | 				Label:      "KTEST", | 
 | 				BlockSize:  uint16(blockSize), | 
 | 				BlockCount: uint32(size / blockSize), | 
 | 			}); err != nil { | 
 | 				t.Fatalf("failed to write fileystem: %v", err) | 
 | 			} | 
 | 			_ = file.Close() | 
 | 			if err := os.MkdirAll("/dut", 0755); err != nil { | 
 | 				t.Error(err) | 
 | 			} | 
 | 			// TODO(lorenz): Set CONFIG_FAT_DEFAULT_UTF8 for Monogon Kernel | 
 | 			if err := unix.Mount("/dev/ram0", "/dut", "vfat", unix.MS_NOEXEC|unix.MS_NODEV, "utf8=1"); err != nil { | 
 | 				t.Fatal(err) | 
 | 			} | 
 | 			defer unix.Unmount("/dut", 0) | 
 | 			test.validate(t) | 
 | 		}) | 
 |  | 
 | 	} | 
 | } |