268 lines
7.2 KiB
Go
268 lines
7.2 KiB
Go
// Copyright 2019 The gVisor Authors.
|
|
//
|
|
// 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 ext
|
|
|
|
import (
|
|
"bytes"
|
|
"math/rand"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"gvisor.dev/gvisor/pkg/binary"
|
|
"gvisor.dev/gvisor/pkg/sentry/fsimpl/ext/disklayout"
|
|
)
|
|
|
|
const (
|
|
// mockExtentBlkSize is the mock block size used for testing.
|
|
// No block has more than 1 header + 4 entries.
|
|
mockExtentBlkSize = uint64(64)
|
|
)
|
|
|
|
// The tree described below looks like:
|
|
//
|
|
// 0.{Head}[Idx][Idx]
|
|
// / \
|
|
// / \
|
|
// 1.{Head}[Ext][Ext] 2.{Head}[Idx]
|
|
// / | \
|
|
// [Phy] [Phy, Phy] 3.{Head}[Ext]
|
|
// |
|
|
// [Phy, Phy, Phy]
|
|
//
|
|
// Legend:
|
|
// - Head = ExtentHeader
|
|
// - Idx = ExtentIdx
|
|
// - Ext = Extent
|
|
// - Phy = Physical Block
|
|
//
|
|
// Please note that ext4 might not construct extent trees looking like this.
|
|
// This is purely for testing the tree traversal logic.
|
|
var (
|
|
node3 = &disklayout.ExtentNode{
|
|
Header: disklayout.ExtentHeader{
|
|
Magic: disklayout.ExtentMagic,
|
|
NumEntries: 1,
|
|
MaxEntries: 4,
|
|
Height: 0,
|
|
},
|
|
Entries: []disklayout.ExtentEntryPair{
|
|
{
|
|
Entry: &disklayout.Extent{
|
|
FirstFileBlock: 3,
|
|
Length: 3,
|
|
StartBlockLo: 6,
|
|
},
|
|
Node: nil,
|
|
},
|
|
},
|
|
}
|
|
|
|
node2 = &disklayout.ExtentNode{
|
|
Header: disklayout.ExtentHeader{
|
|
Magic: disklayout.ExtentMagic,
|
|
NumEntries: 1,
|
|
MaxEntries: 4,
|
|
Height: 1,
|
|
},
|
|
Entries: []disklayout.ExtentEntryPair{
|
|
{
|
|
Entry: &disklayout.ExtentIdx{
|
|
FirstFileBlock: 3,
|
|
ChildBlockLo: 2,
|
|
},
|
|
Node: node3,
|
|
},
|
|
},
|
|
}
|
|
|
|
node1 = &disklayout.ExtentNode{
|
|
Header: disklayout.ExtentHeader{
|
|
Magic: disklayout.ExtentMagic,
|
|
NumEntries: 2,
|
|
MaxEntries: 4,
|
|
Height: 0,
|
|
},
|
|
Entries: []disklayout.ExtentEntryPair{
|
|
{
|
|
Entry: &disklayout.Extent{
|
|
FirstFileBlock: 0,
|
|
Length: 1,
|
|
StartBlockLo: 3,
|
|
},
|
|
Node: nil,
|
|
},
|
|
{
|
|
Entry: &disklayout.Extent{
|
|
FirstFileBlock: 1,
|
|
Length: 2,
|
|
StartBlockLo: 4,
|
|
},
|
|
Node: nil,
|
|
},
|
|
},
|
|
}
|
|
|
|
node0 = &disklayout.ExtentNode{
|
|
Header: disklayout.ExtentHeader{
|
|
Magic: disklayout.ExtentMagic,
|
|
NumEntries: 2,
|
|
MaxEntries: 4,
|
|
Height: 2,
|
|
},
|
|
Entries: []disklayout.ExtentEntryPair{
|
|
{
|
|
Entry: &disklayout.ExtentIdx{
|
|
FirstFileBlock: 0,
|
|
ChildBlockLo: 0,
|
|
},
|
|
Node: node1,
|
|
},
|
|
{
|
|
Entry: &disklayout.ExtentIdx{
|
|
FirstFileBlock: 3,
|
|
ChildBlockLo: 1,
|
|
},
|
|
Node: node2,
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
// TestExtentReader stress tests extentReader functionality. It performs random
|
|
// length reads from all possible positions in the extent tree.
|
|
func TestExtentReader(t *testing.T) {
|
|
mockExtentFile, want := extentTreeSetUp(t, node0)
|
|
n := len(want)
|
|
|
|
for from := 0; from < n; from++ {
|
|
got := make([]byte, n-from)
|
|
|
|
if read, err := mockExtentFile.ReadAt(got, int64(from)); err != nil {
|
|
t.Fatalf("file read operation from offset %d to %d only read %d bytes: %v", from, n, read, err)
|
|
}
|
|
|
|
if diff := cmp.Diff(got, want[from:]); diff != "" {
|
|
t.Fatalf("file data from offset %d to %d mismatched (-want +got):\n%s", from, n, diff)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBuildExtentTree tests the extent tree building logic.
|
|
func TestBuildExtentTree(t *testing.T) {
|
|
mockExtentFile, _ := extentTreeSetUp(t, node0)
|
|
|
|
opt := cmpopts.IgnoreUnexported(disklayout.ExtentIdx{}, disklayout.ExtentHeader{})
|
|
if diff := cmp.Diff(&mockExtentFile.root, node0, opt); diff != "" {
|
|
t.Errorf("extent tree mismatch (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
// extentTreeSetUp writes the passed extent tree to a mock disk as an extent
|
|
// tree. It also constucts a mock extent file with the same tree built in it.
|
|
// It also writes random data file data and returns it.
|
|
func extentTreeSetUp(t *testing.T, root *disklayout.ExtentNode) (*extentFile, []byte) {
|
|
t.Helper()
|
|
|
|
mockDisk := make([]byte, mockExtentBlkSize*10)
|
|
mockExtentFile := &extentFile{
|
|
regFile: regularFile{
|
|
inode: inode{
|
|
fs: &filesystem{
|
|
dev: bytes.NewReader(mockDisk),
|
|
},
|
|
diskInode: &disklayout.InodeNew{
|
|
InodeOld: disklayout.InodeOld{
|
|
SizeLo: uint32(mockExtentBlkSize) * getNumPhyBlks(root),
|
|
},
|
|
},
|
|
blkSize: mockExtentBlkSize,
|
|
},
|
|
},
|
|
}
|
|
|
|
fileData := writeTree(&mockExtentFile.regFile.inode, mockDisk, node0, mockExtentBlkSize)
|
|
|
|
if err := mockExtentFile.buildExtTree(); err != nil {
|
|
t.Fatalf("inode.buildExtTree failed: %v", err)
|
|
}
|
|
return mockExtentFile, fileData
|
|
}
|
|
|
|
// writeTree writes the tree represented by `root` to the inode and disk. It
|
|
// also writes random file data on disk.
|
|
func writeTree(in *inode, disk []byte, root *disklayout.ExtentNode, mockExtentBlkSize uint64) []byte {
|
|
rootData := binary.Marshal(nil, binary.LittleEndian, root.Header)
|
|
for _, ep := range root.Entries {
|
|
rootData = binary.Marshal(rootData, binary.LittleEndian, ep.Entry)
|
|
}
|
|
|
|
copy(in.diskInode.Data(), rootData)
|
|
|
|
var fileData []byte
|
|
for _, ep := range root.Entries {
|
|
if root.Header.Height == 0 {
|
|
fileData = append(fileData, writeFileDataToExtent(disk, ep.Entry.(*disklayout.Extent))...)
|
|
} else {
|
|
fileData = append(fileData, writeTreeToDisk(disk, ep)...)
|
|
}
|
|
}
|
|
return fileData
|
|
}
|
|
|
|
// writeTreeToDisk is the recursive step for writeTree which writes the tree
|
|
// on the disk only. Also writes random file data on disk.
|
|
func writeTreeToDisk(disk []byte, curNode disklayout.ExtentEntryPair) []byte {
|
|
nodeData := binary.Marshal(nil, binary.LittleEndian, curNode.Node.Header)
|
|
for _, ep := range curNode.Node.Entries {
|
|
nodeData = binary.Marshal(nodeData, binary.LittleEndian, ep.Entry)
|
|
}
|
|
|
|
copy(disk[curNode.Entry.PhysicalBlock()*mockExtentBlkSize:], nodeData)
|
|
|
|
var fileData []byte
|
|
for _, ep := range curNode.Node.Entries {
|
|
if curNode.Node.Header.Height == 0 {
|
|
fileData = append(fileData, writeFileDataToExtent(disk, ep.Entry.(*disklayout.Extent))...)
|
|
} else {
|
|
fileData = append(fileData, writeTreeToDisk(disk, ep)...)
|
|
}
|
|
}
|
|
return fileData
|
|
}
|
|
|
|
// writeFileDataToExtent writes random bytes to the blocks on disk that the
|
|
// passed extent points to.
|
|
func writeFileDataToExtent(disk []byte, ex *disklayout.Extent) []byte {
|
|
phyExStartBlk := ex.PhysicalBlock()
|
|
phyExStartOff := phyExStartBlk * mockExtentBlkSize
|
|
phyExEndOff := phyExStartOff + uint64(ex.Length)*mockExtentBlkSize
|
|
rand.Read(disk[phyExStartOff:phyExEndOff])
|
|
return disk[phyExStartOff:phyExEndOff]
|
|
}
|
|
|
|
// getNumPhyBlks returns the number of physical blocks covered under the node.
|
|
func getNumPhyBlks(node *disklayout.ExtentNode) uint32 {
|
|
var res uint32
|
|
for _, ep := range node.Entries {
|
|
if node.Header.Height == 0 {
|
|
res += uint32(ep.Entry.(*disklayout.Extent).Length)
|
|
} else {
|
|
res += getNumPhyBlks(ep.Node)
|
|
}
|
|
}
|
|
return res
|
|
}
|