|  | // 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 erofs | 
|  |  | 
|  | import ( | 
|  | "io" | 
|  | "log" | 
|  | "math/rand" | 
|  | "os" | 
|  | "testing" | 
|  |  | 
|  | "github.com/stretchr/testify/assert" | 
|  | "github.com/stretchr/testify/require" | 
|  | "golang.org/x/sys/unix" | 
|  | ) | 
|  |  | 
|  | func TestKernelInterop(t *testing.T) { | 
|  | if os.Getenv("IN_KTEST") != "true" { | 
|  | t.Skip("Not in ktest") | 
|  | } | 
|  |  | 
|  | type testCase struct { | 
|  | name     string | 
|  | setup    func(w *Writer) error | 
|  | validate func(t *testing.T) error | 
|  | } | 
|  |  | 
|  | tests := []testCase{ | 
|  | { | 
|  | name: "SimpleFolder", | 
|  | setup: func(w *Writer) error { | 
|  | return w.Create(".", &Directory{ | 
|  | Base:     Base{GID: 123, UID: 124, Permissions: 0753}, | 
|  | Children: []string{}, | 
|  | }) | 
|  | }, | 
|  | validate: func(t *testing.T) error { | 
|  | var stat unix.Stat_t | 
|  | if err := unix.Stat("/test", &stat); err != nil { | 
|  | t.Errorf("failed to stat output: %v", err) | 
|  | } | 
|  | require.EqualValues(t, 124, stat.Uid, "wrong Uid") | 
|  | require.EqualValues(t, 123, stat.Gid, "wrong Gid") | 
|  | require.EqualValues(t, 0753, stat.Mode&^unix.S_IFMT, "wrong mode") | 
|  | return nil | 
|  | }, | 
|  | }, | 
|  | { | 
|  | name: "FolderHierarchy", | 
|  | setup: func(w *Writer) error { | 
|  | if err := w.Create(".", &Directory{ | 
|  | Base:     Base{GID: 123, UID: 124, Permissions: 0753}, | 
|  | Children: []string{"subdir"}, | 
|  | }); err != nil { | 
|  | return err | 
|  | } | 
|  | if err := w.Create("subdir", &Directory{ | 
|  | Base:     Base{GID: 123, UID: 124, Permissions: 0753}, | 
|  | Children: []string{}, | 
|  | }); err != nil { | 
|  | return err | 
|  | } | 
|  | return nil | 
|  | }, | 
|  | validate: func(t *testing.T) error { | 
|  | dirInfo, err := os.ReadDir("/test") | 
|  | 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, "subdir", dirInfo[0].Name(), "unexpected subdir") | 
|  | require.True(t, dirInfo[0].IsDir(), "subdir not a directory") | 
|  | subdirInfo, err := os.ReadDir("/test/subdir") | 
|  | assert.NoError(t, err, "cannot read empty subdir") | 
|  | require.Len(t, subdirInfo, 0, "unexpected subdirs in empty directory") | 
|  | return nil | 
|  | }, | 
|  | }, | 
|  | { | 
|  | name: "SmallFile", | 
|  | setup: func(w *Writer) error { | 
|  | if err := w.Create(".", &Directory{ | 
|  | Base:     Base{GID: 123, UID: 123, Permissions: 0755}, | 
|  | Children: []string{"test.bin"}, | 
|  | }); err != nil { | 
|  | return err | 
|  | } | 
|  | writer := w.CreateFile("test.bin", &FileMeta{ | 
|  | Base: Base{GID: 123, UID: 124, Permissions: 0644}, | 
|  | }) | 
|  | r := rand.New(rand.NewSource(0)) // Random but deterministic data | 
|  | if _, err := io.CopyN(writer, r, 128); err != nil { | 
|  | return err | 
|  | } | 
|  | if err := writer.Close(); err != nil { | 
|  | return err | 
|  | } | 
|  | return nil | 
|  | }, | 
|  | validate: func(t *testing.T) error { | 
|  | var stat unix.Stat_t | 
|  | err := unix.Stat("/test/test.bin", &stat) | 
|  | assert.NoError(t, err, "failed to stat file") | 
|  | require.EqualValues(t, 124, stat.Uid, "wrong Uid") | 
|  | require.EqualValues(t, 123, stat.Gid, "wrong Gid") | 
|  | require.EqualValues(t, 0644, stat.Mode&^unix.S_IFMT, "wrong mode") | 
|  | file, err := os.Open("/test/test.bin") | 
|  | assert.NoError(t, err, "failed to open test file") | 
|  | defer file.Close() | 
|  | r := io.LimitReader(rand.New(rand.NewSource(0)), 128) // 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: "Chardev", | 
|  | setup: func(w *Writer) error { | 
|  | if err := w.Create(".", &Directory{ | 
|  | Base:     Base{GID: 123, UID: 123, Permissions: 0755}, | 
|  | Children: []string{"ttyS0"}, | 
|  | }); err != nil { | 
|  | return err | 
|  | } | 
|  | err := w.Create("ttyS0", &CharacterDevice{ | 
|  | Base:  Base{GID: 0, UID: 0, Permissions: 0600}, | 
|  | Major: 4, | 
|  | Minor: 64, | 
|  | }) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | return nil | 
|  | }, | 
|  | validate: func(t *testing.T) error { | 
|  | var stat unix.Statx_t | 
|  | err := unix.Statx(0, "/test/ttyS0", 0, unix.STATX_ALL, &stat) | 
|  | assert.NoError(t, err, "failed to statx file") | 
|  | require.EqualValues(t, 0, stat.Uid, "wrong Uid") | 
|  | require.EqualValues(t, 0, stat.Gid, "wrong Gid") | 
|  | require.EqualValues(t, 0600, stat.Mode&^unix.S_IFMT, "wrong mode") | 
|  | require.EqualValues(t, unix.S_IFCHR, stat.Mode&unix.S_IFMT, "wrong file type") | 
|  | require.EqualValues(t, 4, stat.Rdev_major, "wrong dev major") | 
|  | require.EqualValues(t, 64, stat.Rdev_minor, "wrong dev minor") | 
|  | return nil | 
|  | }, | 
|  | }, | 
|  | { | 
|  | name: "LargeFile", | 
|  | setup: func(w *Writer) error { | 
|  | if err := w.Create(".", &Directory{ | 
|  | Base:     Base{GID: 123, UID: 123, Permissions: 0755}, | 
|  | Children: []string{"test.bin"}, | 
|  | }); err != nil { | 
|  | return err | 
|  | } | 
|  | writer := w.CreateFile("test.bin", &FileMeta{ | 
|  | Base: Base{GID: 123, UID: 124, Permissions: 0644}, | 
|  | }) | 
|  | r := rand.New(rand.NewSource(1)) // Random but deterministic data | 
|  | if _, err := io.CopyN(writer, r, 6500); err != nil { | 
|  | return err | 
|  | } | 
|  | if err := writer.Close(); err != nil { | 
|  | return err | 
|  | } | 
|  | return nil | 
|  | }, | 
|  | validate: func(t *testing.T) error { | 
|  | var stat unix.Stat_t | 
|  | rawContents, err := os.ReadFile("/dev/ram0") | 
|  | assert.NoError(t, err, "failed to read test data") | 
|  | log.Printf("%x", rawContents) | 
|  | err = unix.Stat("/test/test.bin", &stat) | 
|  | assert.NoError(t, err, "failed to stat file") | 
|  | require.EqualValues(t, 124, stat.Uid, "wrong Uid") | 
|  | require.EqualValues(t, 123, stat.Gid, "wrong Gid") | 
|  | require.EqualValues(t, 0644, stat.Mode&^unix.S_IFMT, "wrong mode") | 
|  | require.EqualValues(t, 6500, stat.Size, "wrong size") | 
|  | file, err := os.Open("/test/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: "MultipleMetaBlocks", | 
|  | setup: func(w *Writer) error { | 
|  | testFileNames := []string{"test1.bin", "test2.bin", "test3.bin"} | 
|  | if err := w.Create(".", &Directory{ | 
|  | Base:     Base{GID: 123, UID: 123, Permissions: 0755}, | 
|  | Children: testFileNames, | 
|  | }); err != nil { | 
|  | return err | 
|  | } | 
|  | for i, fileName := range testFileNames { | 
|  | writer := w.CreateFile(fileName, &FileMeta{ | 
|  | Base: Base{GID: 123, UID: 124, Permissions: 0644}, | 
|  | }) | 
|  | r := rand.New(rand.NewSource(int64(i))) // Random but deterministic data | 
|  | if _, err := io.CopyN(writer, r, 2053); err != nil { | 
|  | return err | 
|  | } | 
|  | if err := writer.Close(); err != nil { | 
|  | return err | 
|  | } | 
|  | } | 
|  | return nil | 
|  | }, | 
|  | validate: func(t *testing.T) error { | 
|  | testFileNames := []string{"test1.bin", "test2.bin", "test3.bin"} | 
|  | for i, fileName := range testFileNames { | 
|  | file, err := os.Open("/test/" + fileName) | 
|  | assert.NoError(t, err, "failed to open test file") | 
|  | defer file.Close() | 
|  | r := io.LimitReader(rand.New(rand.NewSource(int64(i))), 2053) // Random but deterministic data | 
|  | expected, _ := io.ReadAll(r) | 
|  | actual, err := io.ReadAll(file) | 
|  | assert.NoError(t, err, "failed to read test file") | 
|  | require.Equal(t, expected, actual, "content not identical") | 
|  | } | 
|  | return nil | 
|  | }, | 
|  | }, | 
|  | } | 
|  |  | 
|  | for _, test := range tests { | 
|  | t.Run(test.name, func(t *testing.T) { | 
|  | file, err := os.OpenFile("/dev/ram0", os.O_WRONLY, 0644) | 
|  | if err != nil { | 
|  | t.Fatalf("failed to create test image: %v", err) | 
|  | } | 
|  | defer file.Close() | 
|  | w, err := NewWriter(file) | 
|  | if err != nil { | 
|  | t.Fatalf("failed to initialize EROFS writer: %v", err) | 
|  | } | 
|  | if err := test.setup(w); err != nil { | 
|  | t.Fatalf("setup failed: %v", err) | 
|  | } | 
|  | if err := w.Close(); err != nil { | 
|  | t.Errorf("failed close: %v", err) | 
|  | } | 
|  | _ = file.Close() | 
|  | if err := os.MkdirAll("/test", 0755); err != nil { | 
|  | t.Error(err) | 
|  | } | 
|  | if err := unix.Mount("/dev/ram0", "/test", "erofs", unix.MS_NOEXEC|unix.MS_NODEV, ""); err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | if err := test.validate(t); err != nil { | 
|  | t.Errorf("validation failure: %v", err) | 
|  | } | 
|  | if err := unix.Unmount("/test", 0); err != nil { | 
|  | t.Fatalf("failed to unmount: %v", err) | 
|  | } | 
|  | }) | 
|  |  | 
|  | } | 
|  | } |