Repository: ceph/go-ceph
Pull request #348 (2020-08-19 20:52:47)

cephfs: add Pwritev & Preadv File methods
First patch adds a new type to internal/cutil for managing iovec's from go. I put it here because it seemed generic and out of place just in cephfs and it turns out they are used in rbd (for a couple of aio calls though... not something I'm jumping to add right now).

Second patch adds Preadv and Pwritev to the File type. These allow you to read or write into multiple byte-slice buffers in one call.

Fixes: #259 
Fixes: #258 



## Checklist
- [x] Added tests for features and functional changes
- [x] Public functions and types are documented
- [x] Standard formatting is applied to Go code



Patch:
diff --git a/cephfs/file.go b/cephfs/file.go
index 1ad4bb8..f8c1d95 100644
--- a/cephfs/file.go
+++ b/cephfs/file.go
@@ -13,6 +13,8 @@ import "C"
 import (
 	"io"
 	"unsafe"
+
+	"github.com/ceph/go-ceph/internal/cutil"
 )
 
 const (
@@ -128,6 +130,37 @@ func (f *File) ReadAt(buf []byte, offset int64) (int, error) {
 	return f.read(buf, offset)
 }
 
+// Preadv will read data from the file, starting at the given offset,
+// into the byte-slice data buffers sequentially.
+// The number of bytes read will be returned.
+// When nothing is left to read from the file the return values will be:
+// 0, io.EOF.
+//
+// Implements:
+//  int ceph_preadv(struct ceph_mount_info *cmount, int fd, const struct iovec *iov, int iovcnt,
+//                  int64_t offset);
+func (f *File) Preadv(data [][]byte, offset int64) (int, error) {
+	if err := f.validate(); err != nil {
+		return 0, err
+	}
+	iov := cutil.ByteSlicesToIovec(data)
+	defer iov.Free()
+
+	ret := C.ceph_preadv(
+		f.mount.mount,
+		f.fd,
+		(*C.struct_iovec)(iov.Pointer()),
+		C.int(iov.Len()),
+		C.int64_t(offset))
+	switch {
+	case ret < 0:
+		return 0, getError(ret)
+	case ret == 0:
+		return 0, io.EOF
+	}
+	return int(ret), nil
+}
+
 // write directly wraps the ceph_write call. Because write is such a common
 // operation we deviate from the ceph naming and expose Write and WriteAt
 // wrappers for external callers of the library.
@@ -163,6 +196,32 @@ func (f *File) WriteAt(buf []byte, offset int64) (int, error) {
 	return f.write(buf, offset)
 }
 
+// Pwritev writes data from the slice of byte-slice buffers to the file at the
+// specified offset.
+// The number of bytes written is returned.
+//
+// Implements:
+//  int ceph_pwritev(struct ceph_mount_info *cmount, int fd, const struct iovec *iov, int iovcnt,
+//                   int64_t offset);
+func (f *File) Pwritev(data [][]byte, offset int64) (int, error) {
+	if err := f.validate(); err != nil {
+		return 0, err
+	}
+	iov := cutil.ByteSlicesToIovec(data)
+	defer iov.Free()
+
+	ret := C.ceph_pwritev(
+		f.mount.mount,
+		f.fd,
+		(*C.struct_iovec)(iov.Pointer()),
+		C.int(iov.Len()),
+		C.int64_t(offset))
+	if ret < 0 {
+		return 0, getError(ret)
+	}
+	return int(ret), nil
+}
+
 // Seek will reposition the file stream based on the given offset.
 //
 // Implements:
diff --git a/internal/cutil/iovec.go b/internal/cutil/iovec.go
new file mode 100644
index 0000000..d89de06
--- /dev/null
+++ b/internal/cutil/iovec.go
@@ -0,0 +1,78 @@
+package cutil
+
+/*
+#include <stdlib.h>
+#include <sys/uio.h>
+*/
+import "C"
+
+import (
+	"unsafe"
+)
+
+var iovecSize uintptr
+
+// StructIovecPtr is an unsafe pointer wrapping C's `*struct iovec`.
+type StructIovecPtr unsafe.Pointer
+
+// Iovec helps manage struct iovec arrays needed by some C functions.
+type Iovec struct {
+	// cvec represents an array of struct iovec C memory
+	cvec unsafe.Pointer
+	// length of the array (in elements)
+	length int
+}
+
+// NewIovec creates an Iovec, and underlying C memory, of the specified size.
+func NewIovec(l int) *Iovec {
+	r := &Iovec{
+		cvec:   C.malloc(C.size_t(l) * C.size_t(iovecSize)),
+		length: l,
+	}
+	return r
+}
+
+// ByteSlicesToIovec takes a slice of byte slices and returns a new iovec that
+// maps the slice data to struct iovec entries.
+func ByteSlicesToIovec(data [][]byte) *Iovec {
+	iov := NewIovec(len(data))
+	for i := range data {
+		iov.Set(i, data[i])
+	}
+	return iov
+}
+
+// Pointer returns a StructIovecPtr that represents the C memory of the
+// underlying array.
+func (v *Iovec) Pointer() StructIovecPtr {
+	return StructIovecPtr(unsafe.Pointer(v.cvec))
+}
+
+// Len returns the number of entries in the Iovec.
+func (v *Iovec) Len() int {
+	return v.length
+}
+
+// Free the C memory in the Iovec.
+func (v *Iovec) Free() {
+	if v.cvec != nil {
+		C.free(v.cvec)
+		v.cvec = nil
+		v.length = 0
+	}
+}
+
+// Set will map the memory of the given byte slice to the iovec at the
+// specified position.
+func (v *Iovec) Set(i int, buf []byte) {
+	offset := uintptr(i) * iovecSize
+	iov := (*C.struct_iovec)(unsafe.Pointer(
+		uintptr(unsafe.Pointer(v.cvec)) + offset))
+	iov.iov_base = unsafe.Pointer(&buf[0])
+	iov.iov_len = C.size_t(len(buf))
+}
+
+func init() {
+	var iovec C.struct_iovec
+	iovecSize = unsafe.Sizeof(iovec)
+}


Test patch:
diff --git a/cephfs/file_test.go b/cephfs/file_test.go
index b85bb0c..517b989 100644
--- a/cephfs/file_test.go
+++ b/cephfs/file_test.go
@@ -17,7 +17,7 @@ func TestFileOpen(t *testing.T) {
 
 	// idempotent open for read and write
 	t.Run("create", func(t *testing.T) {
-		f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+		f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
 		assert.NoError(t, err)
 		assert.NotNil(t, f1)
 		err = f1.Close()
@@ -27,7 +27,7 @@ func TestFileOpen(t *testing.T) {
 
 	t.Run("errorMissing", func(t *testing.T) {
 		// try to open a file we know should not exist
-		f2, err := mount.Open(".nope", os.O_RDONLY, 0666)
+		f2, err := mount.Open(".nope", os.O_RDONLY, 0644)
 		assert.Error(t, err)
 		assert.Nil(t, f2)
 	})
@@ -35,7 +35,7 @@ func TestFileOpen(t *testing.T) {
 	t.Run("existsInMount", func(t *testing.T) {
 		useMount(t)
 
-		f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+		f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
 		assert.NoError(t, err)
 		assert.NotNil(t, f1)
 		err = f1.Close()
@@ -48,7 +48,7 @@ func TestFileOpen(t *testing.T) {
 	})
 
 	t.Run("idempotentClose", func(t *testing.T) {
-		f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+		f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
 		assert.NoError(t, err)
 		assert.NotNil(t, f1)
 		assert.NoError(t, f1.Close())
@@ -71,7 +71,7 @@ func TestFileOpen(t *testing.T) {
 
 	t.Run("openInvalidMount", func(t *testing.T) {
 		m := &MountInfo{}
-		_, err := m.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+		_, err := m.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
 		assert.Error(t, err)
 	})
 }
@@ -83,7 +83,7 @@ func TestFileReadWrite(t *testing.T) {
 
 	t.Run("writeAndRead", func(t *testing.T) {
 		// idempotent open for read and write
-		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
 		n, err := f1.Write([]byte("yello world!"))
 		assert.NoError(t, err)
 		assert.EqualValues(t, 12, n)
@@ -101,7 +101,7 @@ func TestFileReadWrite(t *testing.T) {
 
 	t.Run("openForWriteOnly", func(t *testing.T) {
 		buf := make([]byte, 32)
-		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
 		assert.NoError(t, err)
 		defer func() { assert.NoError(t, f1.Close()) }()
 		_, err = f1.Read(buf)
@@ -110,11 +110,11 @@ func TestFileReadWrite(t *testing.T) {
 
 	t.Run("openForReadOnly", func(t *testing.T) {
 		// "touch" the file
-		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
 		assert.NoError(t, err)
 		assert.NoError(t, f1.Close())
 
-		f1, err = mount.Open(fname, os.O_RDONLY, 0666)
+		f1, err = mount.Open(fname, os.O_RDONLY, 0644)
 		assert.NoError(t, err)
 		defer func() { assert.NoError(t, f1.Close()) }()
 		_, err = f1.Write([]byte("yo"))
@@ -140,7 +140,7 @@ func TestFileReadWriteAt(t *testing.T) {
 
 	t.Run("writeAtAndReadAt", func(t *testing.T) {
 		// idempotent open for read and write
-		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
 		n, err := f1.WriteAt([]byte("foo"), 0)
 		assert.NoError(t, err)
 		assert.EqualValues(t, 3, n)
@@ -172,7 +172,7 @@ func TestFileReadWriteAt(t *testing.T) {
 
 	t.Run("openForWriteOnly", func(t *testing.T) {
 		buf := make([]byte, 32)
-		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
 		assert.NoError(t, err)
 		defer func() { assert.NoError(t, f1.Close()) }()
 		_, err = f1.ReadAt(buf, 0)
@@ -181,11 +181,11 @@ func TestFileReadWriteAt(t *testing.T) {
 
 	t.Run("openForReadOnly", func(t *testing.T) {
 		// "touch" the file
-		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
 		assert.NoError(t, err)
 		assert.NoError(t, f1.Close())
 
-		f1, err = mount.Open(fname, os.O_RDONLY, 0666)
+		f1, err = mount.Open(fname, os.O_RDONLY, 0644)
 		assert.NoError(t, err)
 		defer func() { assert.NoError(t, f1.Close()) }()
 		_, err = f1.WriteAt([]byte("yo"), 0)
@@ -210,7 +210,7 @@ func TestFileInterfaces(t *testing.T) {
 	fname := "TestFileInterfaces.txt"
 
 	t.Run("ioWriter", func(t *testing.T) {
-		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
 		assert.NoError(t, err)
 		defer func() { assert.NoError(t, f1.Close()) }()
 		defer func() { assert.NoError(t, mount.Unlink(fname)) }()
@@ -221,13 +221,13 @@ func TestFileInterfaces(t *testing.T) {
 	})
 
 	t.Run("ioReader", func(t *testing.T) {
-		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
 		assert.NoError(t, err)
 		_, err = f1.Write([]byte("foo"))
 		assert.NoError(t, err)
 		assert.NoError(t, f1.Close())
 
-		f1, err = mount.Open(fname, os.O_RDONLY, 0666)
+		f1, err = mount.Open(fname, os.O_RDONLY, 0644)
 		assert.NoError(t, err)
 		defer func() { assert.NoError(t, f1.Close()) }()
 		defer func() { assert.NoError(t, mount.Unlink(fname)) }()
@@ -248,7 +248,7 @@ func TestFileSeek(t *testing.T) {
 	fname := "TestFileSeek.txt"
 
 	t.Run("validSeek", func(t *testing.T) {
-		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
 		assert.NoError(t, err)
 		defer func() { assert.NoError(t, f1.Close()) }()
 		defer func() { assert.NoError(t, mount.Unlink(fname)) }()
@@ -263,7 +263,7 @@ func TestFileSeek(t *testing.T) {
 	})
 
 	t.Run("invalidWhence", func(t *testing.T) {
-		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
 		assert.NoError(t, err)
 		defer func() { assert.NoError(t, f1.Close()) }()
 		defer func() { assert.NoError(t, mount.Unlink(fname)) }()
@@ -274,7 +274,7 @@ func TestFileSeek(t *testing.T) {
 	})
 
 	t.Run("invalidSeek", func(t *testing.T) {
-		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
 		assert.NoError(t, err)
 		defer func() { assert.NoError(t, f1.Close()) }()
 		defer func() { assert.NoError(t, mount.Unlink(fname)) }()
@@ -297,7 +297,7 @@ func TestMixedReadReadAt(t *testing.T) {
 	defer fsDisconnect(t, mount)
 	fname := "TestMixedReadReadAt.txt"
 
-	f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+	f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
 	assert.NoError(t, err)
 	_, err = f1.Write([]byte("abc def ghi wow!"))
 	assert.NoError(t, err)
@@ -386,7 +386,7 @@ func TestFchown(t *testing.T) {
 	mount := fsConnect(t)
 	defer fsDisconnect(t, mount)
 
-	f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0666)
+	f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0644)
 	assert.NoError(t, err)
 	assert.NotNil(t, f1)
 	defer func() {
@@ -480,7 +480,7 @@ func TestFallocate(t *testing.T) {
 	mount := fsConnect(t)
 	defer fsDisconnect(t, mount)
 	fname := "file1.txt"
-	f, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0666)
+	f, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0644)
 	assert.NoError(t, err)
 	assert.NotNil(t, f)
 	defer func() {
@@ -517,7 +517,7 @@ func TestFallocate(t *testing.T) {
 	t.Run("increaseSize", func(t *testing.T) {
 		useMount(t)
 		fname := "file2.txt"
-		f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0666)
+		f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0644)
 		assert.NoError(t, err)
 		assert.NotNil(t, f1)
 		defer func() {
@@ -550,7 +550,7 @@ func TestFallocate(t *testing.T) {
 	t.Run("allocateSpaceWithFlag", func(t *testing.T) {
 		useMount(t)
 		fname := "file3.txt"
-		f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0666)
+		f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0644)
 		assert.NoError(t, err)
 		assert.NotNil(t, f1)
 		defer func() {
@@ -580,7 +580,7 @@ func TestFallocate(t *testing.T) {
 	// De-allocate space - punch holes.
 	t.Run("punchActualHoles", func(t *testing.T) {
 		fname := "file4.txt"
-		f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0666)
+		f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0644)
 		assert.NoError(t, err)
 		assert.NotNil(t, f1)
 		defer func() {
@@ -628,7 +628,7 @@ func TestFlock(t *testing.T) {
 
 	t.Run("validateOperation", func(t *testing.T) {
 		fname := "Flockfile.txt"
-		f, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0666)
+		f, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0644)
 		assert.NoError(t, err)
 		assert.NotNil(t, f)
 		defer func() {
@@ -646,7 +646,7 @@ func TestFlock(t *testing.T) {
 			chris = 44
 		)
 		fname1 := "Flockfile1.txt"
-		f1, err := mount.Open(fname1, os.O_RDWR|os.O_CREATE, 0666)
+		f1, err := mount.Open(fname1, os.O_RDWR|os.O_CREATE, 0644)
 		assert.NoError(t, err)
 		assert.NotNil(t, f1)
 		defer func() {
@@ -722,7 +722,7 @@ func TestFsync(t *testing.T) {
 	// unfortunately there's not much to assert around the the behavior of
 	// fsync in these simple tests so we sort-of have to trust ceph on this :-)
 	t.Run("simpleFsync", func(t *testing.T) {
-		f, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0666)
+		f, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0644)
 		defer func() { assert.NoError(t, f.Close()) }()
 		assert.NoError(t, err)
 		_, err = f.Write([]byte("batman"))
@@ -731,7 +731,7 @@ func TestFsync(t *testing.T) {
 		assert.NoError(t, err)
 	})
 	t.Run("DataOnly", func(t *testing.T) {
-		f, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0666)
+		f, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0644)
 		defer func() { assert.NoError(t, f.Close()) }()
 		assert.NoError(t, err)
 		_, err = f.Write([]byte("superman"))
@@ -755,7 +755,7 @@ func TestSync(t *testing.T) {
 
 	// see fsync
 	t.Run("simple", func(t *testing.T) {
-		f, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0666)
+		f, err := mount.Open(fname, os.O_RDWR|os.O_CREATE, 0644)
 		defer func() { assert.NoError(t, f.Close()) }()
 		assert.NoError(t, err)
 		_, err = f.Write([]byte("question"))
@@ -769,3 +769,152 @@ func TestSync(t *testing.T) {
 		assert.Error(t, err)
 	})
 }
+
+func TestFilePreadvPwritev(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+
+	fname := "TestFilePreadvPwritev.txt"
+	defer mount.Unlink(fname)
+
+	t.Run("simple", func(t *testing.T) {
+		f, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, f.Close()) }()
+
+		b1 := []byte("foobarbaz")
+		b2 := []byte("alphabeta")
+		b3 := []byte("superawseomefuntime")
+		n, err := f.Pwritev([][]byte{b1, b2, b3}, 0)
+		assert.NoError(t, err)
+		assert.Equal(t, 37, n)
+
+		o := [][]byte{
+			make([]byte, 3),
+			make([]byte, 3),
+			make([]byte, 3),
+			make([]byte, 3),
+		}
+		n, err = f.Preadv(o, 0)
+		assert.NoError(t, err)
+		assert.Equal(t, 12, n)
+		assert.Equal(t, "foo", string(o[0]))
+		assert.Equal(t, "bar", string(o[1]))
+		assert.Equal(t, "baz", string(o[2]))
+		assert.Equal(t, "alp", string(o[3]))
+	})
+
+	t.Run("silly", func(t *testing.T) {
+		f, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, f.Close()) }()
+
+		b := []byte("foo")
+		x := make([][]byte, 8)
+		for i := range x {
+			x[i] = b
+		}
+		n, err := f.Pwritev(x, 0)
+		assert.NoError(t, err)
+		assert.Equal(t, 24, n)
+
+		for i := range x {
+			x[i] = make([]byte, 6)
+		}
+		n, err = f.Preadv(x, 1)
+		assert.NoError(t, err)
+		assert.Equal(t, 23, n)
+		assert.Equal(t, "oofoof", string(x[0]))
+		assert.Equal(t, "oofoof", string(x[1]))
+		assert.Equal(t, "oofoof", string(x[2]))
+		assert.Equal(t, "oofoo\x00", string(x[3]))
+	})
+
+	t.Run("readEOF", func(t *testing.T) {
+		f, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, f.Close()) }()
+
+		x := make([][]byte, 8)
+		for i := range x {
+			x[i] = make([]byte, 6)
+		}
+		n, err := f.Preadv(x, 16)
+		assert.Error(t, err)
+		assert.Equal(t, io.EOF, err)
+		assert.Equal(t, 0, n)
+		assert.Equal(t, make([]byte, 6), x[0])
+	})
+
+	t.Run("shortRead", func(t *testing.T) {
+		f, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, f.Close()) }()
+
+		b := []byte("Antidisestablishmentarianism\n")
+		n, err := f.Pwritev([][]byte{b}, 0)
+		assert.NoError(t, err)
+		assert.Equal(t, 29, n)
+
+		// this is an explicit short read test.
+		// some of the buffers in the vector will be left unfilled.
+		x := make([][]byte, 8)
+		for i := range x {
+			x[i] = make([]byte, 6)
+		}
+		n, err = f.Preadv(x, 0)
+		assert.NoError(t, err)
+		assert.Equal(t, 29, n)
+		assert.Equal(t, "Antidi", string(x[0]))
+		assert.Equal(t, "sestab", string(x[1]))
+		assert.Equal(t, "lishme", string(x[2]))
+		assert.Equal(t, "ntaria", string(x[3]))
+		assert.Equal(t, "nism\n\x00", string(x[4]))
+		assert.Equal(t, make([]byte, 6), x[5])
+		assert.Equal(t, make([]byte, 6), x[6])
+		assert.Equal(t, make([]byte, 6), x[7])
+	})
+
+	t.Run("openForWriteOnly", func(t *testing.T) {
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, f1.Close()) }()
+
+		x := make([][]byte, 8)
+		for i := range x {
+			x[i] = make([]byte, 6)
+		}
+		_, err = f1.Preadv(x, 0)
+		assert.Error(t, err)
+	})
+
+	t.Run("openForReadOnly", func(t *testing.T) {
+		// "touch" the file
+		f1, err := mount.Open(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+		assert.NoError(t, err)
+		assert.NoError(t, f1.Close())
+
+		f1, err = mount.Open(fname, os.O_RDONLY, 0644)
+		assert.NoError(t, err)
+		defer func() { assert.NoError(t, f1.Close()) }()
+
+		x := make([][]byte, 8)
+		for i := range x {
+			x[i] = []byte("robble")
+		}
+		_, err = f1.Pwritev(x, 0)
+		assert.Error(t, err)
+	})
+
+	t.Run("writeInvalidFile", func(t *testing.T) {
+		f := &File{}
+		_, err := f.Pwritev([][]byte{}, 0)
+		assert.Error(t, err)
+	})
+
+	t.Run("readInvalidFile", func(t *testing.T) {
+		f := &File{}
+		_, err := f.Preadv([][]byte{}, 0)
+		assert.Error(t, err)
+	})
+}
diff --git a/internal/cutil/iovec_test.go b/internal/cutil/iovec_test.go
new file mode 100644
index 0000000..9978877
--- /dev/null
+++ b/internal/cutil/iovec_test.go
@@ -0,0 +1,57 @@
+package cutil
+
+import (
+	"testing"
+	"unsafe"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIovec(t *testing.T) {
+	t.Run("newAndFree", func(t *testing.T) {
+		iov := NewIovec(3)
+		iov.Free()
+	})
+	t.Run("setBufs", func(t *testing.T) {
+		b1 := []byte("foo")
+		b2 := []byte("barbar")
+		b3 := []byte("bazbazbaz")
+		iov := NewIovec(3)
+		iov.Set(0, b1)
+		iov.Set(1, b2)
+		iov.Set(2, b3)
+		iov.Free()
+		// free also unsets internal values
+		assert.Equal(t, unsafe.Pointer(nil), iov.cvec)
+		assert.Equal(t, 0, iov.length)
+	})
+	t.Run("testGetters", func(t *testing.T) {
+		b1 := []byte("foo")
+		b2 := []byte("barbar")
+		b3 := []byte("bazbazbaz")
+		b4 := []byte("zonk")
+		iov := NewIovec(4)
+		defer iov.Free()
+		iov.Set(0, b1)
+		iov.Set(1, b2)
+		iov.Set(2, b3)
+		iov.Set(3, b4)
+
+		assert.NotNil(t, iov.Pointer())
+		assert.Equal(t, 4, iov.Len())
+	})
+}
+
+func TestByteSlicesToIovec(t *testing.T) {
+	d := [][]byte{
+		[]byte("ramekin"),
+		[]byte("shuffleboard"),
+		[]byte("tranche"),
+		[]byte("phycobilisomes"),
+	}
+	iov := ByteSlicesToIovec(d)
+	defer iov.Free()
+
+	assert.NotNil(t, iov.Pointer())
+	assert.Equal(t, 4, iov.Len())
+}