Source file src/os/readfrom_unix_test.go

     1  // Copyright 2024 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build freebsd || linux || solaris
     6  
     7  package os_test
     8  
     9  import (
    10  	"bytes"
    11  	"io"
    12  	"math/rand"
    13  	. "os"
    14  	"runtime"
    15  	"strconv"
    16  	"strings"
    17  	"syscall"
    18  	"testing"
    19  	"time"
    20  )
    21  
    22  type (
    23  	copyFileTestFunc func(*testing.T, int64) (*File, *File, []byte, *copyFileHook, string)
    24  	copyFileTestHook func(*testing.T) (*copyFileHook, string)
    25  )
    26  
    27  func TestCopyFile(t *testing.T) {
    28  	sizes := []int{
    29  		1,
    30  		42,
    31  		1025,
    32  		syscall.Getpagesize() + 1,
    33  		32769,
    34  	}
    35  	t.Run("Basic", func(t *testing.T) {
    36  		for _, size := range sizes {
    37  			t.Run(strconv.Itoa(size), func(t *testing.T) {
    38  				testCopyFiles(t, int64(size), -1)
    39  			})
    40  		}
    41  	})
    42  	t.Run("Limited", func(t *testing.T) {
    43  		t.Run("OneLess", func(t *testing.T) {
    44  			for _, size := range sizes {
    45  				t.Run(strconv.Itoa(size), func(t *testing.T) {
    46  					testCopyFiles(t, int64(size), int64(size)-1)
    47  				})
    48  			}
    49  		})
    50  		t.Run("Half", func(t *testing.T) {
    51  			for _, size := range sizes {
    52  				t.Run(strconv.Itoa(size), func(t *testing.T) {
    53  					testCopyFiles(t, int64(size), int64(size)/2)
    54  				})
    55  			}
    56  		})
    57  		t.Run("More", func(t *testing.T) {
    58  			for _, size := range sizes {
    59  				t.Run(strconv.Itoa(size), func(t *testing.T) {
    60  					testCopyFiles(t, int64(size), int64(size)+7)
    61  				})
    62  			}
    63  		})
    64  	})
    65  	t.Run("DoesntTryInAppendMode", func(t *testing.T) {
    66  		for _, newTest := range copyFileTests {
    67  			dst, src, data, hook, testName := newTest(t, 42)
    68  
    69  			dst2, err := OpenFile(dst.Name(), O_RDWR|O_APPEND, 0755)
    70  			if err != nil {
    71  				t.Fatalf("%s: %v", testName, err)
    72  			}
    73  			defer dst2.Close()
    74  
    75  			if _, err := io.Copy(dst2, src); err != nil {
    76  				t.Fatalf("%s: %v", testName, err)
    77  			}
    78  			switch runtime.GOOS {
    79  			case "illumos", "solaris": // sendfile() on SunOS allows target file with O_APPEND set.
    80  				if !hook.called {
    81  					t.Fatalf("%s: should have called the hook even with destination in O_APPEND mode", testName)
    82  				}
    83  			default:
    84  				if hook.called {
    85  					t.Fatalf("%s: hook shouldn't be called with destination in O_APPEND mode", testName)
    86  				}
    87  			}
    88  			mustSeekStart(t, dst2)
    89  			mustContainData(t, dst2, data) // through traditional means
    90  		}
    91  	})
    92  	t.Run("CopyFileItself", func(t *testing.T) {
    93  		for _, hookFunc := range copyFileHooks {
    94  			hook, testName := hookFunc(t)
    95  
    96  			f, err := CreateTemp("", "file-readfrom-itself-test")
    97  			if err != nil {
    98  				t.Fatalf("%s: failed to create tmp file: %v", testName, err)
    99  			}
   100  			t.Cleanup(func() {
   101  				f.Close()
   102  				Remove(f.Name())
   103  			})
   104  
   105  			data := []byte("hello world!")
   106  			if _, err := f.Write(data); err != nil {
   107  				t.Fatalf("%s: failed to create and feed the file: %v", testName, err)
   108  			}
   109  
   110  			if err := f.Sync(); err != nil {
   111  				t.Fatalf("%s: failed to save the file: %v", testName, err)
   112  			}
   113  
   114  			// Rewind it.
   115  			if _, err := f.Seek(0, io.SeekStart); err != nil {
   116  				t.Fatalf("%s: failed to rewind the file: %v", testName, err)
   117  			}
   118  
   119  			// Read data from the file itself.
   120  			if _, err := io.Copy(f, f); err != nil {
   121  				t.Fatalf("%s: failed to read from the file: %v", testName, err)
   122  			}
   123  
   124  			if hook.written != 0 || hook.handled || hook.err != nil {
   125  				t.Fatalf("%s: File.readFrom is expected not to use any zero-copy techniques when copying itself."+
   126  					"got hook.written=%d, hook.handled=%t, hook.err=%v; expected hook.written=0, hook.handled=false, hook.err=nil",
   127  					testName, hook.written, hook.handled, hook.err)
   128  			}
   129  
   130  			switch testName {
   131  			case "hookCopyFileRange":
   132  				// For copy_file_range(2), it fails and returns EINVAL when the source and target
   133  				// refer to the same file and their ranges overlap. The hook should be called to
   134  				// get the returned error and fall back to generic copy.
   135  				if !hook.called {
   136  					t.Fatalf("%s: should have called the hook", testName)
   137  				}
   138  			case "hookSendFile", "hookSendFileOverCopyFileRange":
   139  				// For sendfile(2), it allows the source and target to refer to the same file and overlap.
   140  				// The hook should not be called and just fall back to generic copy directly.
   141  				if hook.called {
   142  					t.Fatalf("%s: shouldn't have called the hook", testName)
   143  				}
   144  			default:
   145  				t.Fatalf("%s: unexpected test", testName)
   146  			}
   147  
   148  			// Rewind it.
   149  			if _, err := f.Seek(0, io.SeekStart); err != nil {
   150  				t.Fatalf("%s: failed to rewind the file: %v", testName, err)
   151  			}
   152  
   153  			data2, err := io.ReadAll(f)
   154  			if err != nil {
   155  				t.Fatalf("%s: failed to read from the file: %v", testName, err)
   156  			}
   157  
   158  			// It should wind up a double of the original data.
   159  			if s := strings.Repeat(string(data), 2); s != string(data2) {
   160  				t.Fatalf("%s: file contained %s, expected %s", testName, data2, s)
   161  			}
   162  		}
   163  	})
   164  	t.Run("NotRegular", func(t *testing.T) {
   165  		t.Run("BothPipes", func(t *testing.T) {
   166  			for _, hookFunc := range copyFileHooks {
   167  				hook, testName := hookFunc(t)
   168  
   169  				pr1, pw1, err := Pipe()
   170  				if err != nil {
   171  					t.Fatalf("%s: %v", testName, err)
   172  				}
   173  				defer pr1.Close()
   174  				defer pw1.Close()
   175  
   176  				pr2, pw2, err := Pipe()
   177  				if err != nil {
   178  					t.Fatalf("%s: %v", testName, err)
   179  				}
   180  				defer pr2.Close()
   181  				defer pw2.Close()
   182  
   183  				// The pipe is empty, and PIPE_BUF is large enough
   184  				// for this, by (POSIX) definition, so there is no
   185  				// need for an additional goroutine.
   186  				data := []byte("hello")
   187  				if _, err := pw1.Write(data); err != nil {
   188  					t.Fatalf("%s: %v", testName, err)
   189  				}
   190  				pw1.Close()
   191  
   192  				n, err := io.Copy(pw2, pr1)
   193  				if err != nil {
   194  					t.Fatalf("%s: %v", testName, err)
   195  				}
   196  				if n != int64(len(data)) {
   197  					t.Fatalf("%s: transferred %d, want %d", testName, n, len(data))
   198  				}
   199  				switch runtime.GOOS {
   200  				case "illumos", "solaris":
   201  					// On solaris, We rely on File.Stat to get the size of the source file,
   202  					// which doesn't work for pipe.
   203  					// On illumos, We skip anything other than regular files conservatively
   204  					// for the target file, therefore the hook shouldn't have been called.
   205  					if hook.called {
   206  						t.Fatalf("%s: shouldn't have called the hook with a source or a destination of pipe", testName)
   207  					}
   208  				default:
   209  					if !hook.called {
   210  						t.Fatalf("%s: should have called the hook with both source and destination of pipe", testName)
   211  					}
   212  				}
   213  				pw2.Close()
   214  				mustContainData(t, pr2, data)
   215  			}
   216  		})
   217  		t.Run("DstPipe", func(t *testing.T) {
   218  			for _, newTest := range copyFileTests {
   219  				dst, src, data, hook, testName := newTest(t, 255)
   220  				dst.Close()
   221  
   222  				pr, pw, err := Pipe()
   223  				if err != nil {
   224  					t.Fatalf("%s: %v", testName, err)
   225  				}
   226  				defer pr.Close()
   227  				defer pw.Close()
   228  
   229  				n, err := io.Copy(pw, src)
   230  				if err != nil {
   231  					t.Fatalf("%s: %v", testName, err)
   232  				}
   233  				if n != int64(len(data)) {
   234  					t.Fatalf("%s: transferred %d, want %d", testName, n, len(data))
   235  				}
   236  				switch runtime.GOOS {
   237  				case "illumos":
   238  					// On illumos, We skip anything other than regular files conservatively
   239  					// for the target file, therefore the hook shouldn't have been called.
   240  					if hook.called {
   241  						t.Fatalf("%s: shouldn't have called the hook with a destination of pipe", testName)
   242  					}
   243  				default:
   244  					if !hook.called {
   245  						t.Fatalf("%s: should have called the hook with a destination of pipe", testName)
   246  					}
   247  				}
   248  				pw.Close()
   249  				mustContainData(t, pr, data)
   250  			}
   251  		})
   252  		t.Run("SrcPipe", func(t *testing.T) {
   253  			for _, newTest := range copyFileTests {
   254  				dst, src, data, hook, testName := newTest(t, 255)
   255  				src.Close()
   256  
   257  				pr, pw, err := Pipe()
   258  				if err != nil {
   259  					t.Fatalf("%s: %v", testName, err)
   260  				}
   261  				defer pr.Close()
   262  				defer pw.Close()
   263  
   264  				// The pipe is empty, and PIPE_BUF is large enough
   265  				// for this, by (POSIX) definition, so there is no
   266  				// need for an additional goroutine.
   267  				if _, err := pw.Write(data); err != nil {
   268  					t.Fatalf("%s: %v", testName, err)
   269  				}
   270  				pw.Close()
   271  
   272  				n, err := io.Copy(dst, pr)
   273  				if err != nil {
   274  					t.Fatalf("%s: %v", testName, err)
   275  				}
   276  				if n != int64(len(data)) {
   277  					t.Fatalf("%s: transferred %d, want %d", testName, n, len(data))
   278  				}
   279  				switch runtime.GOOS {
   280  				case "illumos", "solaris":
   281  					// On SunOS, We rely on File.Stat to get the size of the source file,
   282  					// which doesn't work for pipe.
   283  					if hook.called {
   284  						t.Fatalf("%s: shouldn't have called the hook with a source of pipe", testName)
   285  					}
   286  				default:
   287  					if !hook.called {
   288  						t.Fatalf("%s: should have called the hook with a source of pipe", testName)
   289  					}
   290  				}
   291  				mustSeekStart(t, dst)
   292  				mustContainData(t, dst, data)
   293  			}
   294  		})
   295  	})
   296  	t.Run("Nil", func(t *testing.T) {
   297  		var nilFile *File
   298  		anyFile, err := CreateTemp("", "")
   299  		if err != nil {
   300  			t.Fatal(err)
   301  		}
   302  		defer Remove(anyFile.Name())
   303  		defer anyFile.Close()
   304  
   305  		if _, err := io.Copy(nilFile, nilFile); err != ErrInvalid {
   306  			t.Errorf("io.Copy(nilFile, nilFile) = %v, want %v", err, ErrInvalid)
   307  		}
   308  		if _, err := io.Copy(anyFile, nilFile); err != ErrInvalid {
   309  			t.Errorf("io.Copy(anyFile, nilFile) = %v, want %v", err, ErrInvalid)
   310  		}
   311  		if _, err := io.Copy(nilFile, anyFile); err != ErrInvalid {
   312  			t.Errorf("io.Copy(nilFile, anyFile) = %v, want %v", err, ErrInvalid)
   313  		}
   314  
   315  		if _, err := nilFile.ReadFrom(nilFile); err != ErrInvalid {
   316  			t.Errorf("nilFile.ReadFrom(nilFile) = %v, want %v", err, ErrInvalid)
   317  		}
   318  		if _, err := anyFile.ReadFrom(nilFile); err != ErrInvalid {
   319  			t.Errorf("anyFile.ReadFrom(nilFile) = %v, want %v", err, ErrInvalid)
   320  		}
   321  		if _, err := nilFile.ReadFrom(anyFile); err != ErrInvalid {
   322  			t.Errorf("nilFile.ReadFrom(anyFile) = %v, want %v", err, ErrInvalid)
   323  		}
   324  	})
   325  }
   326  
   327  func testCopyFile(t *testing.T, dst, src *File, data []byte, hook *copyFileHook, limit int64, testName string) {
   328  	// If we have a limit, wrap the reader.
   329  	var (
   330  		realsrc io.Reader
   331  		lr      *io.LimitedReader
   332  	)
   333  	if limit >= 0 {
   334  		lr = &io.LimitedReader{N: limit, R: src}
   335  		realsrc = lr
   336  		if limit < int64(len(data)) {
   337  			data = data[:limit]
   338  		}
   339  	} else {
   340  		realsrc = src
   341  	}
   342  
   343  	// Now call ReadFrom (through io.Copy), which will hopefully call
   344  	// poll.CopyFileRange or poll.SendFile.
   345  	n, err := io.Copy(dst, realsrc)
   346  	if err != nil {
   347  		t.Fatalf("%s: %v", testName, err)
   348  	}
   349  
   350  	// If we didn't have a limit or had a positive limit, we should have called
   351  	// poll.CopyFileRange or poll.SendFile with the right file descriptor arguments.
   352  	if limit != 0 && !hook.called {
   353  		t.Fatalf("%s: never called the hook", testName)
   354  	}
   355  	if hook.called && hook.dstfd != int(dst.Fd()) {
   356  		t.Fatalf("%s: wrong destination file descriptor: got %d, want %d", testName, hook.dstfd, dst.Fd())
   357  	}
   358  	if hook.called && hook.srcfd != int(src.Fd()) {
   359  		t.Fatalf("%s: wrong source file descriptor: got %d, want %d", testName, hook.srcfd, src.Fd())
   360  	}
   361  
   362  	// Check that the offsets after the transfer make sense, that the size
   363  	// of the transfer was reported correctly, and that the destination
   364  	// file contains exactly the bytes we expect it to contain.
   365  	dstoff, err := dst.Seek(0, io.SeekCurrent)
   366  	if err != nil {
   367  		t.Fatalf("%s: %v", testName, err)
   368  	}
   369  	srcoff, err := src.Seek(0, io.SeekCurrent)
   370  	if err != nil {
   371  		t.Fatalf("%s: %v", testName, err)
   372  	}
   373  	if dstoff != srcoff {
   374  		t.Errorf("%s: offsets differ: dstoff = %d, srcoff = %d", testName, dstoff, srcoff)
   375  	}
   376  	if dstoff != int64(len(data)) {
   377  		t.Errorf("%s: dstoff = %d, want %d", testName, dstoff, len(data))
   378  	}
   379  	if n != int64(len(data)) {
   380  		t.Errorf("%s: short ReadFrom: wrote %d bytes, want %d", testName, n, len(data))
   381  	}
   382  	mustSeekStart(t, dst)
   383  	mustContainData(t, dst, data)
   384  
   385  	// If we had a limit, check that it was updated.
   386  	if lr != nil {
   387  		if want := limit - n; lr.N != want {
   388  			t.Fatalf("%s: didn't update limit correctly: got %d, want %d", testName, lr.N, want)
   389  		}
   390  	}
   391  }
   392  
   393  // mustContainData ensures that the specified file contains exactly the
   394  // specified data.
   395  func mustContainData(t *testing.T, f *File, data []byte) {
   396  	t.Helper()
   397  
   398  	got := make([]byte, len(data))
   399  	if _, err := io.ReadFull(f, got); err != nil {
   400  		t.Fatal(err)
   401  	}
   402  	if !bytes.Equal(got, data) {
   403  		t.Fatalf("didn't get the same data back from %s", f.Name())
   404  	}
   405  	if _, err := f.Read(make([]byte, 1)); err != io.EOF {
   406  		t.Fatalf("not at EOF")
   407  	}
   408  }
   409  
   410  func mustSeekStart(t *testing.T, f *File) {
   411  	if _, err := f.Seek(0, io.SeekStart); err != nil {
   412  		t.Fatal(err)
   413  	}
   414  }
   415  
   416  // newCopyFileTest initializes a new test for copying data between files.
   417  // It creates source and destination files, and populates the source file
   418  // with random data of the specified size, then rewind it, so it can be
   419  // consumed by copy_file_range(2) or sendfile(2).
   420  func newCopyFileTest(t *testing.T, size int64) (dst, src *File, data []byte) {
   421  	src, data = createTempFile(t, "test-copy-file-src", size)
   422  
   423  	dst, err := CreateTemp(t.TempDir(), "test-copy-file-dst")
   424  	if err != nil {
   425  		t.Fatal(err)
   426  	}
   427  	t.Cleanup(func() { dst.Close() })
   428  
   429  	return
   430  }
   431  
   432  type copyFileHook struct {
   433  	called bool
   434  	dstfd  int
   435  	srcfd  int
   436  
   437  	written int64
   438  	handled bool
   439  	err     error
   440  }
   441  
   442  func createTempFile(tb testing.TB, name string, size int64) (*File, []byte) {
   443  	f, err := CreateTemp(tb.TempDir(), name)
   444  	if err != nil {
   445  		tb.Fatalf("failed to create temporary file: %v", err)
   446  	}
   447  	tb.Cleanup(func() {
   448  		f.Close()
   449  	})
   450  
   451  	randSeed := time.Now().Unix()
   452  	tb.Logf("random data seed: %d\n", randSeed)
   453  	prng := rand.New(rand.NewSource(randSeed))
   454  	data := make([]byte, size)
   455  	prng.Read(data)
   456  	if _, err := f.Write(data); err != nil {
   457  		tb.Fatalf("failed to create and feed the file: %v", err)
   458  	}
   459  	if err := f.Sync(); err != nil {
   460  		tb.Fatalf("failed to save the file: %v", err)
   461  	}
   462  	if _, err := f.Seek(0, io.SeekStart); err != nil {
   463  		tb.Fatalf("failed to rewind the file: %v", err)
   464  	}
   465  
   466  	return f, data
   467  }
   468  

View as plain text