github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/data/file_data_test.go (about)

     1  // Copyright 2016 Keybase Inc. All rights reserved.
     2  // Use of this source code is governed by a BSD
     3  // license that can be found in the LICENSE file.
     4  
     5  package data
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"math"
    11  	"reflect"
    12  	"testing"
    13  
    14  	"github.com/keybase/client/go/kbfs/kbfsblock"
    15  	"github.com/keybase/client/go/kbfs/libkey"
    16  	libkeytest "github.com/keybase/client/go/kbfs/libkey/test"
    17  	"github.com/keybase/client/go/kbfs/tlf"
    18  	"github.com/keybase/client/go/libkb"
    19  	"github.com/keybase/client/go/logger"
    20  	"github.com/keybase/client/go/protocol/keybase1"
    21  	"github.com/stretchr/testify/require"
    22  	"golang.org/x/net/context"
    23  )
    24  
    25  func setupFileDataTest(t *testing.T, maxBlockSize int64,
    26  	maxPtrsPerBlock int) (*FileData, BlockCache, DirtyBlockCache, *DirtyFile) {
    27  	// Make a fake file.
    28  	ptr := BlockPointer{
    29  		ID:         kbfsblock.FakeID(42),
    30  		DirectType: DirectBlock,
    31  	}
    32  	id := tlf.FakeID(1, tlf.Private)
    33  	file := Path{
    34  		FolderBranch{Tlf: id},
    35  		[]PathNode{{ptr, NewPathPartString("file", nil)}},
    36  		nil,
    37  	}
    38  	chargedTo := keybase1.MakeTestUID(1).AsUserOrTeam()
    39  	bsplit := &BlockSplitterSimple{maxBlockSize, maxPtrsPerBlock, 10, 0}
    40  	kmd := libkeytest.NewEmptyKeyMetadata(id, 1)
    41  
    42  	cleanCache := NewBlockCacheStandard(1<<10, 1<<20)
    43  	dirtyBcache := SimpleDirtyBlockCacheStandard()
    44  	getter := func(ctx context.Context, _ libkey.KeyMetadata, ptr BlockPointer,
    45  		_ Path, _ BlockReqType) (*FileBlock, bool, error) {
    46  		isDirty := true
    47  		block, err := dirtyBcache.Get(ctx, id, ptr, MasterBranch)
    48  		if err != nil {
    49  			// Check the clean cache.
    50  			block, err = cleanCache.Get(ptr)
    51  			if err != nil {
    52  				return nil, false, err
    53  			}
    54  			isDirty = false
    55  		}
    56  		fblock, ok := block.(*FileBlock)
    57  		if !ok {
    58  			return nil, false,
    59  				fmt.Errorf("Block for %s is not a file block", ptr)
    60  		}
    61  		return fblock, isDirty, nil
    62  	}
    63  	cacher := func(ctx context.Context, ptr BlockPointer, block Block) error {
    64  		return dirtyBcache.Put(ctx, id, ptr, MasterBranch, block)
    65  	}
    66  
    67  	log := logger.NewTestLogger(t)
    68  	fd := NewFileData(
    69  		file, chargedTo, bsplit, kmd, getter, cacher, log,
    70  		libkb.NewVDebugLog(log))
    71  	df := NewDirtyFile(file, dirtyBcache)
    72  	return fd, cleanCache, dirtyBcache, df
    73  }
    74  
    75  type testFileDataLevel struct {
    76  	dirty    bool
    77  	children []testFileDataLevel
    78  	off      Int64Offset
    79  	size     int
    80  }
    81  
    82  type testFileDataHole struct {
    83  	start Int64Offset
    84  	end   Int64Offset
    85  }
    86  
    87  func testFileDataLevelFromData(t *testing.T, maxBlockSize Int64Offset,
    88  	maxPtrsPerBlock int, existingLevels int, fullDataLen Int64Offset,
    89  	holes []testFileDataHole, startWrite, endWrite,
    90  	holeShiftAfter Int64Offset, truncateExtend bool) testFileDataLevel {
    91  	// First fill in the leaf level.
    92  	var prevChildren []testFileDataLevel
    93  	var off Int64Offset
    94  	size := 0
    95  	nextHole := 0
    96  	for off < fullDataLen {
    97  		var nextOff Int64Offset
    98  		size = int(maxBlockSize)
    99  		makeFinalHole := false
   100  		switch {
   101  		case nextHole < len(holes) && off+maxBlockSize >= holes[nextHole].start:
   102  			size = int(holes[nextHole].start - off)
   103  			nextOff = holes[nextHole].end
   104  			nextHole++
   105  			makeFinalHole = nextHole >= len(holes) &&
   106  				nextOff >= fullDataLen
   107  		case off+maxBlockSize > fullDataLen:
   108  			size = int(fullDataLen - off)
   109  			nextOff = fullDataLen
   110  		default:
   111  			nextOff = off + maxBlockSize
   112  		}
   113  		dirty := off < endWrite && startWrite < off+Int64Offset(size)
   114  		// If this is a shrink, dirty the block containing startWrite.
   115  		if endWrite < 0 {
   116  			dirty = nextOff >= startWrite
   117  		}
   118  		newChild := testFileDataLevel{dirty, nil, off, size}
   119  		t.Logf("Expected leaf offset %d [dirty=%t]", off, dirty)
   120  		prevChildren = append(prevChildren, newChild)
   121  
   122  		if makeFinalHole {
   123  			// The final hole can only ever be dirty if there was a
   124  			// truncate to a new length.
   125  			newChild := testFileDataLevel{truncateExtend, nil, nextOff, 0}
   126  			t.Logf("Expected leaf offset %d (final)", nextOff)
   127  			prevChildren = append(prevChildren, newChild)
   128  		}
   129  
   130  		off = nextOff
   131  	}
   132  	if fullDataLen == 0 {
   133  		// Special case for a file that's been left empty.
   134  		newChild := testFileDataLevel{true, nil, 0, 0}
   135  		prevChildren = append(prevChildren, newChild)
   136  	}
   137  
   138  	// Now fill in any parents.  If this is a shrink, force the new
   139  	// data to have the same number of levels (we never remove levels
   140  	// of indirection at the moment).
   141  	newLevels := 1
   142  	for len(prevChildren) != 1 || (endWrite < 0 && newLevels < existingLevels) {
   143  		prevChildIndex := 0
   144  		var level []testFileDataLevel
   145  
   146  		numAtLevel := int(math.Ceil(float64(len(prevChildren)) /
   147  			float64(maxPtrsPerBlock)))
   148  		for i := 0; i < numAtLevel; i++ {
   149  			// Split the previous children up (if any) into
   150  			// maxPtrsPerBlock chunks.
   151  			var children []testFileDataLevel
   152  			var off Int64Offset
   153  			newIndex := prevChildIndex + maxPtrsPerBlock
   154  			if newIndex > len(prevChildren) {
   155  				newIndex = len(prevChildren)
   156  			}
   157  			off = prevChildren[prevChildIndex].off
   158  			children = prevChildren[prevChildIndex:newIndex]
   159  			prevChildIndex = newIndex
   160  			dirty := false
   161  			for _, child := range children {
   162  				// A parent is dirty if it has a dirty child.
   163  				if child.dirty {
   164  					dirty = true
   165  					break
   166  				}
   167  			}
   168  			// Also if a new block was made in a hole, any indirect
   169  			// parent that comes after the end of the write will be
   170  			// dirty, due to hole shifting.
   171  			if holeShiftAfter > 0 && off >= holeShiftAfter {
   172  				dirty = true
   173  				// If this is the bottom-most parent block after a
   174  				// hole shift, its rightmost child will also be marked
   175  				// dirty.
   176  				if newLevels == 1 {
   177  					children[len(children)-1].dirty = true
   178  				}
   179  			}
   180  			newChild := testFileDataLevel{dirty, children, off, 0}
   181  			level = append(level, newChild)
   182  		}
   183  		prevChildren = level
   184  		newLevels++
   185  	}
   186  
   187  	// Even in a shrink, the top block is always dirty.
   188  	currNode := &(prevChildren[0])
   189  	if endWrite < 0 {
   190  		currNode.dirty = true
   191  	}
   192  
   193  	// If we added new levels, we can expect the old topmost block to
   194  	// be dirty, since we have to upload it with a new block ID.
   195  	for i := 0; i <= newLevels-existingLevels; i++ {
   196  		t.Logf("Dirtying level %d %d %d", i, newLevels, existingLevels)
   197  		currNode.dirty = true
   198  		if len(currNode.children) == 0 {
   199  			break
   200  		}
   201  		currNode = &(currNode.children[0])
   202  	}
   203  
   204  	return prevChildren[0]
   205  }
   206  
   207  func (tfdl testFileDataLevel) check(t *testing.T, fd *FileData,
   208  	ptr BlockPointer, off Int64Offset, dirtyBcache DirtyBlockCache) (
   209  	dirtyPtrs map[BlockPointer]bool) {
   210  	dirtyPtrs = make(map[BlockPointer]bool)
   211  	levelString := fmt.Sprintf("ptr=%s, off=%d", ptr, off)
   212  	t.Logf("Checking %s", levelString)
   213  
   214  	require.Equal(t, tfdl.off, off, levelString)
   215  	if tfdl.dirty {
   216  		dirtyPtrs[ptr] = true
   217  		require.True(
   218  			t, dirtyBcache.IsDirty(fd.tree.file.Tlf, ptr, MasterBranch),
   219  			levelString)
   220  	}
   221  
   222  	fblock, isDirty, err := fd.getter(nil, nil, ptr, Path{}, BlockRead)
   223  	require.NoError(t, err, levelString)
   224  	require.Equal(t, tfdl.dirty, isDirty, levelString)
   225  	require.NotNil(t, fblock, levelString)
   226  
   227  	// We expect this to be a leaf block.
   228  	if len(tfdl.children) == 0 {
   229  		require.False(t, fblock.IsInd, levelString)
   230  		require.Len(t, fblock.IPtrs, 0, levelString)
   231  		require.Len(t, fblock.Contents, tfdl.size, levelString)
   232  		return dirtyPtrs
   233  	}
   234  
   235  	// Otherwise it's indirect, so check all the children.
   236  	require.True(t, fblock.IsInd, levelString)
   237  	require.Len(t, fblock.IPtrs, len(tfdl.children), levelString)
   238  	require.Len(t, fblock.Contents, 0, levelString)
   239  	for i, iptr := range fblock.IPtrs {
   240  		childDirtyPtrs := tfdl.children[i].check(
   241  			t, fd, iptr.BlockPointer, iptr.Off, dirtyBcache)
   242  		for ptr := range childDirtyPtrs {
   243  			dirtyPtrs[ptr] = true
   244  		}
   245  	}
   246  	return dirtyPtrs
   247  }
   248  
   249  func testFileDataCheckWrite(t *testing.T, fd *FileData,
   250  	dirtyBcache DirtyBlockCache, df *DirtyFile, data []byte, off Int64Offset,
   251  	topBlock *FileBlock, oldDe DirEntry, expectedSize uint64,
   252  	expectedUnrefs []BlockInfo, expectedDirtiedBytes int64,
   253  	expectedBytesExtended int64, expectedTopLevel testFileDataLevel) {
   254  	// Do the write.
   255  	ctx := context.Background()
   256  	newDe, dirtyPtrs, unrefs, newlyDirtiedChildBytes, bytesExtended, err :=
   257  		fd.Write(ctx, data, off, topBlock, oldDe, df)
   258  	require.NoError(t, err)
   259  
   260  	// Check the basics.
   261  	require.Equal(t, expectedSize, newDe.Size)
   262  	require.Equal(t, expectedDirtiedBytes, newlyDirtiedChildBytes)
   263  	require.Equal(t, expectedBytesExtended, bytesExtended)
   264  
   265  	// Go through each expected level and make sure we have the right
   266  	// set of dirty pointers and children.
   267  	expectedDirtyPtrs := expectedTopLevel.check(
   268  		t, fd, fd.rootBlockPointer(), 0, dirtyBcache)
   269  	dirtyPtrsMap := make(map[BlockPointer]bool)
   270  	for _, ptr := range dirtyPtrs {
   271  		dirtyPtrsMap[ptr] = true
   272  	}
   273  	require.True(t, reflect.DeepEqual(expectedDirtyPtrs, dirtyPtrsMap),
   274  		fmt.Sprintf("expected %v; got %v", expectedDirtyPtrs, dirtyPtrsMap))
   275  
   276  	// TODO: set the EncodedSize of the existing blocks to something
   277  	// non-zero so that we get some unrefs.
   278  	require.Len(t, unrefs, 0)
   279  }
   280  
   281  func testFileDataWriteExtendEmptyFile(t *testing.T, maxBlockSize Int64Offset,
   282  	maxPtrsPerBlock int, fullDataLen Int64Offset) {
   283  	fd, cleanBcache, dirtyBcache, df := setupFileDataTest(
   284  		t, int64(maxBlockSize), maxPtrsPerBlock)
   285  	topBlock := NewFileBlock().(*FileBlock)
   286  	err := cleanBcache.Put(
   287  		fd.rootBlockPointer(), fd.tree.file.Tlf, topBlock, TransientEntry,
   288  		SkipCacheHash)
   289  	require.NoError(t, err)
   290  	de := DirEntry{}
   291  	data := make([]byte, fullDataLen)
   292  	for i := 0; i < int(fullDataLen); i++ {
   293  		data[i] = byte(i)
   294  	}
   295  	expectedTopLevel := testFileDataLevelFromData(
   296  		t, maxBlockSize, maxPtrsPerBlock, 0, fullDataLen, nil, 0,
   297  		fullDataLen, 0, false)
   298  
   299  	testFileDataCheckWrite(
   300  		t, fd, dirtyBcache, df, data, 0, topBlock, de, uint64(fullDataLen),
   301  		nil, int64(fullDataLen), int64(fullDataLen), expectedTopLevel)
   302  
   303  	// Make sure we can read back the complete data.
   304  	gotData := make([]byte, fullDataLen)
   305  	nRead, err := fd.Read(context.Background(), gotData, 0)
   306  	require.NoError(t, err)
   307  	require.Equal(t, Int64Offset(nRead), fullDataLen)
   308  	require.True(t, bytes.Equal(data, gotData))
   309  }
   310  
   311  func testFileDataWriteNewLevel(t *testing.T, levels float64) {
   312  	capacity := math.Pow(2, levels)
   313  	halfCapacity := capacity/2 + 1
   314  	if levels == 1 {
   315  		halfCapacity = capacity - 1
   316  	}
   317  	// Fills half the leaf level.
   318  	testFileDataWriteExtendEmptyFile(t, 2, 2, Int64Offset(halfCapacity))
   319  	// Fills whole leaf level.
   320  	testFileDataWriteExtendEmptyFile(t, 2, 2, Int64Offset(capacity))
   321  
   322  }
   323  
   324  func TestFileDataWriteNewLevel(t *testing.T) {
   325  	for _, level := range []float64{1, 2, 3, 10} {
   326  		// capture range variable.
   327  		level := level
   328  		t.Run(fmt.Sprintf("%dLevels", int(level)), func(t *testing.T) {
   329  			testFileDataWriteNewLevel(t, level)
   330  		})
   331  	}
   332  }
   333  
   334  func testFileDataLevelExistingBlocks(t *testing.T, fd *FileData,
   335  	maxBlockSize Int64Offset, maxPtrsPerBlock int, existingData []byte,
   336  	holes []testFileDataHole, cleanBcache BlockCache) (*FileBlock, int) {
   337  	// First fill in the leaf blocks.
   338  	var off Int64Offset
   339  	existingDataLen := Int64Offset(len(existingData))
   340  	var prevChildren []*FileBlock
   341  	var leafOffs []Int64Offset
   342  	nextHole := 0
   343  	for off < existingDataLen {
   344  		endOff := off + maxBlockSize
   345  		var nextOff Int64Offset
   346  		makeFinalHole := false
   347  		switch {
   348  		case nextHole < len(holes) && endOff > holes[nextHole].start:
   349  			endOff = holes[nextHole].start
   350  			nextOff = holes[nextHole].end
   351  			nextHole++
   352  			makeFinalHole = nextHole >= len(holes) &&
   353  				nextOff >= existingDataLen
   354  		case endOff > existingDataLen:
   355  			endOff = existingDataLen
   356  			nextOff = existingDataLen
   357  		default:
   358  			nextOff = endOff
   359  		}
   360  
   361  		fblock := NewFileBlock().(*FileBlock)
   362  		fblock.Contents = existingData[off:endOff]
   363  		prevChildren = append(prevChildren, fblock)
   364  		t.Logf("Initial leaf offset %d, size %d", off, len(fblock.Contents))
   365  		leafOffs = append(leafOffs, off)
   366  
   367  		if makeFinalHole {
   368  			fblock := NewFileBlock().(*FileBlock)
   369  			prevChildren = append(prevChildren, fblock)
   370  			t.Logf("Initial leaf offset %d (final)", nextOff)
   371  			leafOffs = append(leafOffs, nextOff)
   372  		}
   373  
   374  		off = nextOff
   375  	}
   376  
   377  	// Now fill in any parents.
   378  	numLevels := 1
   379  	for len(prevChildren) != 1 {
   380  		prevChildIndex := 0
   381  		var level []*FileBlock
   382  		numAtLevel := int(math.Ceil(float64(len(prevChildren)) /
   383  			float64(maxPtrsPerBlock)))
   384  		for i := 0; i < numAtLevel; i++ {
   385  			// Split the previous children up (if any) into maxPtrsPerBlock
   386  			// chunks.
   387  			var children []*FileBlock
   388  			newIndex := prevChildIndex + maxPtrsPerBlock
   389  			if newIndex > len(prevChildren) {
   390  				newIndex = len(prevChildren)
   391  			}
   392  			children = prevChildren[prevChildIndex:newIndex]
   393  			fblock := NewFileBlock().(*FileBlock)
   394  			fblock.IsInd = true
   395  			dt := DirectBlock
   396  			if numLevels > 1 {
   397  				dt = IndirectBlock
   398  			}
   399  			for j, child := range children {
   400  				id, err := kbfsblock.MakeTemporaryID()
   401  				require.NoError(t, err)
   402  				ptr := BlockPointer{
   403  					ID:         id,
   404  					DirectType: dt,
   405  				}
   406  				var off Int64Offset
   407  				if child.IsInd {
   408  					off = child.IPtrs[0].Off
   409  				} else {
   410  					off = leafOffs[prevChildIndex+j]
   411  				}
   412  
   413  				fblock.IPtrs = append(fblock.IPtrs, IndirectFilePtr{
   414  					BlockInfo: BlockInfo{ptr, 0},
   415  					Off:       off,
   416  				})
   417  				err = cleanBcache.Put(
   418  					ptr, fd.tree.file.Tlf, child, TransientEntry, SkipCacheHash)
   419  				require.NoError(t, err)
   420  			}
   421  			prevChildIndex = newIndex
   422  			level = append(level, fblock)
   423  		}
   424  		prevChildren = level
   425  		numLevels++
   426  	}
   427  
   428  	if numLevels > 1 {
   429  		fd.tree.file.Path[len(fd.tree.file.Path)-1].DirectType = IndirectBlock
   430  	}
   431  
   432  	err := cleanBcache.Put(
   433  		fd.rootBlockPointer(), fd.tree.file.Tlf, prevChildren[0],
   434  		TransientEntry, SkipCacheHash)
   435  	require.NoError(t, err)
   436  	return prevChildren[0], numLevels
   437  }
   438  
   439  func testFileDataWriteExtendExistingFile(t *testing.T, maxBlockSize Int64Offset,
   440  	maxPtrsPerBlock int, existingLen, fullDataLen Int64Offset) {
   441  	fd, cleanBcache, dirtyBcache, df := setupFileDataTest(
   442  		t, int64(maxBlockSize), maxPtrsPerBlock)
   443  	data := make([]byte, fullDataLen)
   444  	for i := 0; i < int(fullDataLen); i++ {
   445  		data[i] = byte(i)
   446  	}
   447  	topBlock, levels := testFileDataLevelExistingBlocks(
   448  		t, fd, maxBlockSize, maxPtrsPerBlock, data[:existingLen], nil,
   449  		cleanBcache)
   450  	de := DirEntry{
   451  		EntryInfo: EntryInfo{
   452  			Size: uint64(existingLen),
   453  		},
   454  	}
   455  	expectedTopLevel := testFileDataLevelFromData(
   456  		t, maxBlockSize, maxPtrsPerBlock, levels, fullDataLen, nil, existingLen,
   457  		fullDataLen, 0, false)
   458  
   459  	extendedBytes := fullDataLen - existingLen
   460  	// Round up to find out the number of dirty bytes.
   461  	remainder := extendedBytes % maxBlockSize
   462  	dirtiedBytes := extendedBytes
   463  	if remainder > 0 {
   464  		dirtiedBytes += (maxBlockSize - remainder)
   465  	}
   466  	// Add a block's worth of dirty bytes if we're extending past the
   467  	// first full level, because the original block still gets dirtied
   468  	// because it needs to be inserted under a new ID.
   469  	if existingLen == maxBlockSize {
   470  		dirtiedBytes += maxBlockSize
   471  	}
   472  	testFileDataCheckWrite(
   473  		t, fd, dirtyBcache, df, data[existingLen:], existingLen,
   474  		topBlock, de, uint64(fullDataLen),
   475  		nil, int64(dirtiedBytes), int64(extendedBytes), expectedTopLevel)
   476  
   477  	// Make sure we can read back the complete data.
   478  	gotData := make([]byte, fullDataLen)
   479  	nRead, err := fd.Read(context.Background(), gotData, 0)
   480  	require.NoError(t, err)
   481  	require.Equal(t, nRead, int64(fullDataLen))
   482  	require.True(t, bytes.Equal(data, gotData))
   483  }
   484  
   485  func testFileDataExtendExistingLevels(t *testing.T, levels float64) {
   486  	capacity := math.Pow(2, levels)
   487  	halfCapacity := capacity / 2
   488  	// Starts with one lower level and adds a level.
   489  	testFileDataWriteExtendExistingFile(
   490  		t, 2, 2, Int64Offset(halfCapacity), Int64Offset(capacity))
   491  }
   492  
   493  func TestFileDataExtendExistingLevels(t *testing.T) {
   494  	for _, level := range []float64{1, 2, 3, 10} {
   495  		// capture range variable.
   496  		level := level
   497  		t.Run(fmt.Sprintf("%dLevels", int(level)), func(t *testing.T) {
   498  			testFileDataExtendExistingLevels(t, level)
   499  		})
   500  	}
   501  }
   502  
   503  func testFileDataOverwriteExistingFile(t *testing.T, maxBlockSize Int64Offset,
   504  	maxPtrsPerBlock int, fullDataLen Int64Offset, holes []testFileDataHole,
   505  	startWrite, endWrite Int64Offset, finalHoles []testFileDataHole) {
   506  	fd, cleanBcache, dirtyBcache, df := setupFileDataTest(
   507  		t, int64(maxBlockSize), maxPtrsPerBlock)
   508  	data := make([]byte, fullDataLen)
   509  	for i := 0; i < int(fullDataLen); i++ {
   510  		data[i] = byte(i)
   511  	}
   512  	holeShiftAfter := Int64Offset(0)
   513  	effectiveStartWrite := startWrite
   514  	for _, hole := range holes {
   515  		for i := hole.start; i < hole.end; i++ {
   516  			data[i] = byte(0)
   517  			if holeShiftAfter == 0 && startWrite <= i && i < endWrite {
   518  				holeShiftAfter = i
   519  				// If we're writing in a hole, we might extend the
   520  				// block on its left edge to its block boundary,
   521  				// which means that's effectively the start of the
   522  				// write.
   523  				effectiveStartWrite = hole.start
   524  			}
   525  		}
   526  	}
   527  	topBlock, levels := testFileDataLevelExistingBlocks(
   528  		t, fd, maxBlockSize, maxPtrsPerBlock, data, holes, cleanBcache)
   529  	de := DirEntry{
   530  		EntryInfo: EntryInfo{
   531  			Size: uint64(fullDataLen),
   532  		},
   533  	}
   534  
   535  	t.Logf("holeShiftAfter=%d", holeShiftAfter)
   536  	expectedTopLevel := testFileDataLevelFromData(
   537  		t, maxBlockSize, maxPtrsPerBlock, levels, fullDataLen, finalHoles,
   538  		effectiveStartWrite, endWrite, holeShiftAfter, false)
   539  
   540  	// Round up to find out the number of dirty bytes.
   541  	writtenBytes := endWrite - startWrite
   542  	remainder := writtenBytes % maxBlockSize
   543  	dirtiedBytes := writtenBytes
   544  	if remainder > 0 {
   545  		dirtiedBytes += (maxBlockSize - remainder)
   546  	}
   547  	// Capture extending an existing block to its block boundary when
   548  	// writing in a hole.
   549  	if effectiveStartWrite != startWrite &&
   550  		effectiveStartWrite%maxBlockSize > 0 {
   551  		dirtiedBytes += maxBlockSize
   552  	}
   553  
   554  	// The extended bytes are the size of the new blocks that were
   555  	// added.  This isn't exactly right, but for now just pick the
   556  	// start of the last hole and round it up to the next block.
   557  	extendedBytes := Int64Offset(0)
   558  	existingBytes := holes[len(holes)-1].start
   559  	remainder = existingBytes % maxBlockSize
   560  	if remainder > 0 {
   561  		existingBytes += (maxBlockSize - remainder)
   562  	}
   563  	if endWrite > existingBytes {
   564  		extendedBytes = endWrite - existingBytes
   565  		// Also ignore any bytes that are still in the hole.
   566  		if existingBytes < holeShiftAfter {
   567  			extendedBytes -= holeShiftAfter - existingBytes
   568  		}
   569  	}
   570  
   571  	newData := make([]byte, writtenBytes)
   572  	for i := startWrite; i < endWrite; i++ {
   573  		// The new data shifts each byte over by 1.
   574  		newData[i-startWrite] = byte(i + 1)
   575  	}
   576  	testFileDataCheckWrite(
   577  		t, fd, dirtyBcache, df, newData, startWrite,
   578  		topBlock, de, uint64(fullDataLen),
   579  		nil, int64(dirtiedBytes), int64(extendedBytes), expectedTopLevel)
   580  
   581  	copy(data[startWrite:endWrite], newData)
   582  
   583  	// Make sure we can read back the complete data.
   584  	gotData := make([]byte, fullDataLen)
   585  	nRead, err := fd.Read(context.Background(), gotData, 0)
   586  	require.NoError(t, err)
   587  	require.Equal(t, nRead, int64(fullDataLen))
   588  	require.True(t, bytes.Equal(data, gotData))
   589  }
   590  
   591  func TestFileDataWriteHole(t *testing.T) {
   592  	type test struct {
   593  		name  string
   594  		start Int64Offset
   595  		end   Int64Offset
   596  		final []testFileDataHole
   597  	}
   598  
   599  	tests := []test{
   600  		{"Start", 5, 8, []testFileDataHole{{8, 10}}},
   601  		// The first final hole starts at 6, instead of 5, because a write
   602  		// extends the existing block.
   603  		{"End", 8, 10, []testFileDataHole{{6, 8}, {10, 10}}},
   604  		// The first final hole starts at 6, instead of 5, because a write
   605  		// extends the existing block.
   606  		{"Middle", 7, 9, []testFileDataHole{{6, 7}, {9, 10}}},
   607  	}
   608  
   609  	for _, test := range tests {
   610  		// capture range variable.
   611  		test := test
   612  		t.Run(test.name, func(t *testing.T) {
   613  			testFileDataOverwriteExistingFile(t, 2, 2, 10,
   614  				[]testFileDataHole{{5, 10}}, test.start, test.end, test.final)
   615  		})
   616  	}
   617  }
   618  
   619  func testFileDataCheckTruncateExtend(t *testing.T, fd *FileData,
   620  	dirtyBcache DirtyBlockCache, df *DirtyFile, size uint64,
   621  	topBlock *FileBlock, oldDe DirEntry, expectedTopLevel testFileDataLevel) {
   622  	// Do the extending truncate.
   623  	ctx := context.Background()
   624  
   625  	_, parentBlocks, _, _, _, _, err :=
   626  		fd.GetFileBlockAtOffset(ctx, topBlock, Int64Offset(size), BlockWrite)
   627  	require.NoError(t, err)
   628  
   629  	newDe, dirtyPtrs, err := fd.TruncateExtend(
   630  		ctx, size, topBlock, parentBlocks, oldDe, df)
   631  	require.NoError(t, err)
   632  
   633  	// Check the basics.
   634  	require.Equal(t, size, newDe.Size)
   635  
   636  	// Go through each expected level and make sure we have the right
   637  	// set of dirty pointers and children.
   638  	expectedDirtyPtrs := expectedTopLevel.check(
   639  		t, fd, fd.rootBlockPointer(), 0, dirtyBcache)
   640  	dirtyPtrsMap := make(map[BlockPointer]bool)
   641  	for _, ptr := range dirtyPtrs {
   642  		dirtyPtrsMap[ptr] = true
   643  	}
   644  	require.True(t, reflect.DeepEqual(expectedDirtyPtrs, dirtyPtrsMap),
   645  		fmt.Sprintf("expected %v; got %v", expectedDirtyPtrs, dirtyPtrsMap))
   646  }
   647  
   648  func testFileDataTruncateExtendFile(t *testing.T, maxBlockSize Int64Offset,
   649  	maxPtrsPerBlock int, currDataLen Int64Offset, newSize uint64,
   650  	holes []testFileDataHole) {
   651  	fd, cleanBcache, dirtyBcache, df := setupFileDataTest(
   652  		t, int64(maxBlockSize), maxPtrsPerBlock)
   653  	data := make([]byte, currDataLen)
   654  	for i := 0; i < int(currDataLen); i++ {
   655  		data[i] = byte(i)
   656  	}
   657  	for _, hole := range holes {
   658  		for i := hole.start; i < hole.end; i++ {
   659  			data[i] = byte(0)
   660  		}
   661  	}
   662  	topBlock, levels := testFileDataLevelExistingBlocks(
   663  		t, fd, maxBlockSize, maxPtrsPerBlock, data, holes, cleanBcache)
   664  	de := DirEntry{
   665  		EntryInfo: EntryInfo{
   666  			Size: uint64(currDataLen),
   667  		},
   668  	}
   669  
   670  	expectedTopLevel := testFileDataLevelFromData(
   671  		t, maxBlockSize, maxPtrsPerBlock, levels, currDataLen,
   672  		append(holes, testFileDataHole{currDataLen, Int64Offset(newSize)}),
   673  		currDataLen, Int64Offset(newSize), 0, true)
   674  
   675  	testFileDataCheckTruncateExtend(
   676  		t, fd, dirtyBcache, df, newSize, topBlock, de, expectedTopLevel)
   677  
   678  	newZeroes := make([]byte, Int64Offset(newSize)-currDataLen)
   679  	data = append(data, newZeroes...)
   680  
   681  	// Make sure we can read back the complete data.
   682  	gotData := make([]byte, newSize)
   683  	nRead, err := fd.Read(context.Background(), gotData, 0)
   684  	require.NoError(t, err)
   685  	require.Equal(t, nRead, int64(newSize))
   686  	require.True(t, bytes.Equal(data, gotData))
   687  }
   688  
   689  func TestFileDataTruncateExtendLevel(t *testing.T) {
   690  	type test struct {
   691  		name    string
   692  		currLen Int64Offset
   693  		newSize uint64
   694  	}
   695  
   696  	tests := []test{
   697  		{"Same", 5, 8},
   698  		{"New", 3, 8},
   699  	}
   700  
   701  	for _, test := range tests {
   702  		// capture range variable.
   703  		test := test
   704  		t.Run(test.name, func(t *testing.T) {
   705  			testFileDataTruncateExtendFile(
   706  				t, 2, 2, test.currLen, test.newSize, nil)
   707  		})
   708  	}
   709  }
   710  
   711  func testFileDataCheckTruncateShrink(t *testing.T, fd *FileData,
   712  	dirtyBcache DirtyBlockCache, size uint64,
   713  	topBlock *FileBlock, oldDe DirEntry, expectedUnrefs []BlockInfo,
   714  	expectedDirtiedBytes int64, expectedTopLevel testFileDataLevel) {
   715  	// Do the extending truncate.
   716  	ctx := context.Background()
   717  
   718  	newDe, dirtyPtrs, unrefs, newlyDirtiedChildBytes, err := fd.TruncateShrink(
   719  		ctx, size, topBlock, oldDe)
   720  	require.NoError(t, err)
   721  
   722  	// Check the basics.
   723  	require.Equal(t, size, newDe.Size)
   724  	require.Equal(t, expectedDirtiedBytes, newlyDirtiedChildBytes)
   725  
   726  	// Go through each expected level and make sure we have the right
   727  	// set of dirty pointers and children.
   728  	expectedDirtyPtrs := expectedTopLevel.check(
   729  		t, fd, fd.rootBlockPointer(), 0, dirtyBcache)
   730  	dirtyPtrsMap := make(map[BlockPointer]bool)
   731  	for _, ptr := range dirtyPtrs {
   732  		dirtyPtrsMap[ptr] = true
   733  	}
   734  	require.True(t, reflect.DeepEqual(expectedDirtyPtrs, dirtyPtrsMap),
   735  		fmt.Sprintf("expected %v; got %v", expectedDirtyPtrs, dirtyPtrsMap))
   736  
   737  	// TODO: set the EncodedSize of the existing blocks to something
   738  	// non-zero so that we get some unrefs.
   739  	require.Len(t, unrefs, 0)
   740  }
   741  
   742  func testFileDataShrinkExistingFile(t *testing.T, maxBlockSize Int64Offset,
   743  	maxPtrsPerBlock int, existingLen int64, newSize uint64) {
   744  	fd, cleanBcache, dirtyBcache, _ := setupFileDataTest(
   745  		t, int64(maxBlockSize), maxPtrsPerBlock)
   746  	data := make([]byte, existingLen)
   747  	for i := 0; i < int(existingLen); i++ {
   748  		data[i] = byte(i)
   749  	}
   750  	topBlock, levels := testFileDataLevelExistingBlocks(
   751  		t, fd, maxBlockSize, maxPtrsPerBlock, data, nil, cleanBcache)
   752  	de := DirEntry{
   753  		EntryInfo: EntryInfo{
   754  			Size: uint64(existingLen),
   755  		},
   756  	}
   757  	expectedTopLevel := testFileDataLevelFromData(
   758  		t, maxBlockSize, maxPtrsPerBlock, levels, Int64Offset(newSize), nil,
   759  		Int64Offset(newSize),
   760  		Int64Offset(int64(newSize)-existingLen), /*negative*/
   761  		0, false)
   762  
   763  	// Round up to find out the number of dirty bytes.
   764  	dirtiedBytes := int64(newSize) % int64(maxBlockSize)
   765  	testFileDataCheckTruncateShrink(
   766  		t, fd, dirtyBcache, newSize, topBlock, de, nil, dirtiedBytes,
   767  		expectedTopLevel)
   768  
   769  	// Make sure we can read back the complete data.
   770  	gotData := make([]byte, newSize)
   771  	nRead, err := fd.Read(context.Background(), gotData, 0)
   772  	require.NoError(t, err)
   773  	require.Equal(t, nRead, int64(newSize))
   774  	require.True(t, bytes.Equal(data[:newSize], gotData))
   775  }
   776  
   777  func TestFileDataTruncateShrink(t *testing.T) {
   778  	type test struct {
   779  		name    string
   780  		currLen int64
   781  		newSize uint64
   782  	}
   783  
   784  	tests := []test{
   785  		{"WithinBlock", 6, 5},
   786  		{"WithinLevel", 8, 5},
   787  		{"ToZero", 8, 0},
   788  	}
   789  
   790  	for _, test := range tests {
   791  		// capture range variable.
   792  		test := test
   793  		t.Run(test.name, func(t *testing.T) {
   794  			testFileDataShrinkExistingFile(t, 2, 2, test.currLen, test.newSize)
   795  		})
   796  	}
   797  }
   798  
   799  func testFileDataWriteExtendExistingFileWithGap(t *testing.T,
   800  	maxBlockSize Int64Offset, maxPtrsPerBlock int, existingLen,
   801  	fullDataLen, startWrite Int64Offset, finalHoles []testFileDataHole) {
   802  	fd, cleanBcache, dirtyBcache, df := setupFileDataTest(
   803  		t, int64(maxBlockSize), maxPtrsPerBlock)
   804  	data := make([]byte, fullDataLen)
   805  	for i := Int64Offset(0); i < fullDataLen; i++ {
   806  		if i < existingLen || i >= startWrite {
   807  			data[i] = byte(i)
   808  		}
   809  	}
   810  	topBlock, levels := testFileDataLevelExistingBlocks(
   811  		t, fd, maxBlockSize, maxPtrsPerBlock, data[:existingLen], nil,
   812  		cleanBcache)
   813  	de := DirEntry{
   814  		EntryInfo: EntryInfo{
   815  			Size: uint64(existingLen),
   816  		},
   817  	}
   818  	// The write starts at `existingLen`, instead of `startWrite`,
   819  	// because we need to account for any bytes dirtied when extending
   820  	// the block to the left of the gap.
   821  	expectedTopLevel := testFileDataLevelFromData(
   822  		t, maxBlockSize, maxPtrsPerBlock, levels, fullDataLen,
   823  		finalHoles, existingLen, fullDataLen, 0, false)
   824  
   825  	extendedBytes := fullDataLen - existingLen
   826  	// Round up to find out the number of dirty bytes.
   827  	dirtiedBytes := fullDataLen - startWrite
   828  	remainder := dirtiedBytes % maxBlockSize
   829  	if remainder > 0 {
   830  		dirtiedBytes += (maxBlockSize - remainder)
   831  	}
   832  	// Dirty the current last block as well, if needed.
   833  	if existingLen%maxBlockSize > 0 {
   834  		dirtiedBytes += maxBlockSize
   835  	}
   836  	// Add a block's worth of dirty bytes if we're extending past the
   837  	// first full level, because the original block still gets dirtied
   838  	// because it needs to be inserted under a new ID.
   839  	if existingLen == maxBlockSize {
   840  		dirtiedBytes += maxBlockSize
   841  	}
   842  	testFileDataCheckWrite(
   843  		t, fd, dirtyBcache, df, data[startWrite:], startWrite,
   844  		topBlock, de, uint64(fullDataLen),
   845  		nil, int64(dirtiedBytes), int64(extendedBytes), expectedTopLevel)
   846  
   847  	// Make sure we can read back the complete data.
   848  	gotData := make([]byte, fullDataLen)
   849  	nRead, err := fd.Read(context.Background(), gotData, 0)
   850  	require.NoError(t, err)
   851  	require.Equal(t, nRead, int64(fullDataLen))
   852  	require.True(t, bytes.Equal(data, gotData))
   853  }
   854  
   855  // Test that we can write past the end of the last block of a file,
   856  // leaving a gap.  Regression tests for KBFS-1915.
   857  func TestFileDataWriteExtendExistingFileWithGap(t *testing.T) {
   858  	type test struct {
   859  		name       string
   860  		currLen    Int64Offset
   861  		newSize    Int64Offset
   862  		startWrite Int64Offset
   863  		finalHoles []testFileDataHole
   864  	}
   865  
   866  	tests := []test{
   867  		{"SwitchToIndirect", 1, 16, 10, []testFileDataHole{{2, 10}}},
   868  		{"FullExistingBlock", 6, 16, 10, []testFileDataHole{{6, 10}}},
   869  		{"FillExistingBlock", 5, 16, 10, []testFileDataHole{{6, 10}}},
   870  	}
   871  
   872  	for _, test := range tests {
   873  		// capture range variable.
   874  		test := test
   875  		t.Run(test.name, func(t *testing.T) {
   876  			testFileDataWriteExtendExistingFileWithGap(
   877  				t, 2, 2, test.currLen, test.newSize, test.startWrite,
   878  				test.finalHoles)
   879  		})
   880  	}
   881  }