summaryrefslogtreecommitdiffhomepage
path: root/pkg/sentry/kernel/pipe
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/sentry/kernel/pipe')
-rw-r--r--pkg/sentry/kernel/pipe/BUILD54
-rw-r--r--pkg/sentry/kernel/pipe/node_test.go320
-rw-r--r--pkg/sentry/kernel/pipe/pipe_state_autogen.go151
-rw-r--r--pkg/sentry/kernel/pipe/pipe_test.go139
-rw-r--r--pkg/sentry/kernel/pipe/pipe_unsafe_state_autogen.go3
5 files changed, 154 insertions, 513 deletions
diff --git a/pkg/sentry/kernel/pipe/BUILD b/pkg/sentry/kernel/pipe/BUILD
deleted file mode 100644
index 449643118..000000000
--- a/pkg/sentry/kernel/pipe/BUILD
+++ /dev/null
@@ -1,54 +0,0 @@
-load("//tools:defs.bzl", "go_library", "go_test")
-
-package(licenses = ["notice"])
-
-go_library(
- name = "pipe",
- srcs = [
- "device.go",
- "node.go",
- "pipe.go",
- "pipe_unsafe.go",
- "pipe_util.go",
- "reader.go",
- "reader_writer.go",
- "vfs.go",
- "writer.go",
- ],
- visibility = ["//pkg/sentry:internal"],
- deps = [
- "//pkg/abi/linux",
- "//pkg/amutex",
- "//pkg/buffer",
- "//pkg/context",
- "//pkg/safemem",
- "//pkg/sentry/arch",
- "//pkg/sentry/device",
- "//pkg/sentry/fs",
- "//pkg/sentry/fs/fsutil",
- "//pkg/sentry/fs/lock",
- "//pkg/sentry/vfs",
- "//pkg/sync",
- "//pkg/syserror",
- "//pkg/usermem",
- "//pkg/waiter",
- ],
-)
-
-go_test(
- name = "pipe_test",
- size = "small",
- srcs = [
- "node_test.go",
- "pipe_test.go",
- ],
- library = ":pipe",
- deps = [
- "//pkg/context",
- "//pkg/sentry/contexttest",
- "//pkg/sentry/fs",
- "//pkg/syserror",
- "//pkg/usermem",
- "//pkg/waiter",
- ],
-)
diff --git a/pkg/sentry/kernel/pipe/node_test.go b/pkg/sentry/kernel/pipe/node_test.go
deleted file mode 100644
index ce0db5583..000000000
--- a/pkg/sentry/kernel/pipe/node_test.go
+++ /dev/null
@@ -1,320 +0,0 @@
-// Copyright 2018 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 pipe
-
-import (
- "testing"
- "time"
-
- "gvisor.dev/gvisor/pkg/context"
- "gvisor.dev/gvisor/pkg/sentry/contexttest"
- "gvisor.dev/gvisor/pkg/sentry/fs"
- "gvisor.dev/gvisor/pkg/syserror"
- "gvisor.dev/gvisor/pkg/usermem"
-)
-
-type sleeper struct {
- context.Context
- ch chan struct{}
-}
-
-func newSleeperContext(t *testing.T) context.Context {
- return &sleeper{
- Context: contexttest.Context(t),
- ch: make(chan struct{}),
- }
-}
-
-func (s *sleeper) SleepStart() <-chan struct{} {
- return s.ch
-}
-
-func (s *sleeper) SleepFinish(bool) {
-}
-
-func (s *sleeper) Cancel() {
- s.ch <- struct{}{}
-}
-
-func (s *sleeper) Interrupted() bool {
- return len(s.ch) != 0
-}
-
-type openResult struct {
- *fs.File
- error
-}
-
-var perms fs.FilePermissions = fs.FilePermissions{
- User: fs.PermMask{Read: true, Write: true},
-}
-
-func testOpenOrDie(ctx context.Context, t *testing.T, n fs.InodeOperations, flags fs.FileFlags, doneChan chan<- struct{}) (*fs.File, error) {
- inode := fs.NewMockInode(ctx, fs.NewMockMountSource(nil), fs.StableAttr{Type: fs.Pipe})
- d := fs.NewDirent(ctx, inode, "pipe")
- file, err := n.GetFile(ctx, d, flags)
- if err != nil {
- t.Fatalf("open with flags %+v failed: %v", flags, err)
- }
- if doneChan != nil {
- doneChan <- struct{}{}
- }
- return file, err
-}
-
-func testOpen(ctx context.Context, t *testing.T, n fs.InodeOperations, flags fs.FileFlags, resChan chan<- openResult) (*fs.File, error) {
- inode := fs.NewMockInode(ctx, fs.NewMockMountSource(nil), fs.StableAttr{Type: fs.Pipe})
- d := fs.NewDirent(ctx, inode, "pipe")
- file, err := n.GetFile(ctx, d, flags)
- if resChan != nil {
- resChan <- openResult{file, err}
- }
- return file, err
-}
-
-func newNamedPipe(t *testing.T) *Pipe {
- return NewPipe(true, DefaultPipeSize, usermem.PageSize)
-}
-
-func newAnonPipe(t *testing.T) *Pipe {
- return NewPipe(false, DefaultPipeSize, usermem.PageSize)
-}
-
-// assertRecvBlocks ensures that a recv attempt on c blocks for at least
-// blockDuration. This is useful for checking that a goroutine that is supposed
-// to be executing a blocking operation is actually blocking.
-func assertRecvBlocks(t *testing.T, c <-chan struct{}, blockDuration time.Duration, failMsg string) {
- select {
- case <-c:
- t.Fatalf(failMsg)
- case <-time.After(blockDuration):
- // Ok, blocked for the required duration.
- }
-}
-
-func TestReadOpenBlocksForWriteOpen(t *testing.T) {
- ctx := newSleeperContext(t)
- f := NewInodeOperations(ctx, perms, newNamedPipe(t))
-
- rDone := make(chan struct{})
- go testOpenOrDie(ctx, t, f, fs.FileFlags{Read: true}, rDone)
-
- // Verify that the open for read is blocking.
- assertRecvBlocks(t, rDone, time.Millisecond*100,
- "open for read not blocking with no writers")
-
- wDone := make(chan struct{})
- go testOpenOrDie(ctx, t, f, fs.FileFlags{Write: true}, wDone)
-
- <-wDone
- <-rDone
-}
-
-func TestWriteOpenBlocksForReadOpen(t *testing.T) {
- ctx := newSleeperContext(t)
- f := NewInodeOperations(ctx, perms, newNamedPipe(t))
-
- wDone := make(chan struct{})
- go testOpenOrDie(ctx, t, f, fs.FileFlags{Write: true}, wDone)
-
- // Verify that the open for write is blocking
- assertRecvBlocks(t, wDone, time.Millisecond*100,
- "open for write not blocking with no readers")
-
- rDone := make(chan struct{})
- go testOpenOrDie(ctx, t, f, fs.FileFlags{Read: true}, rDone)
-
- <-rDone
- <-wDone
-}
-
-func TestMultipleWriteOpenDoesntCountAsReadOpen(t *testing.T) {
- ctx := newSleeperContext(t)
- f := NewInodeOperations(ctx, perms, newNamedPipe(t))
-
- rDone1 := make(chan struct{})
- rDone2 := make(chan struct{})
- go testOpenOrDie(ctx, t, f, fs.FileFlags{Read: true}, rDone1)
- go testOpenOrDie(ctx, t, f, fs.FileFlags{Read: true}, rDone2)
-
- assertRecvBlocks(t, rDone1, time.Millisecond*100,
- "open for read didn't block with no writers")
- assertRecvBlocks(t, rDone2, time.Millisecond*100,
- "open for read didn't block with no writers")
-
- wDone := make(chan struct{})
- go testOpenOrDie(ctx, t, f, fs.FileFlags{Write: true}, wDone)
-
- <-wDone
- <-rDone2
- <-rDone1
-}
-
-func TestClosedReaderBlocksWriteOpen(t *testing.T) {
- ctx := newSleeperContext(t)
- f := NewInodeOperations(ctx, perms, newNamedPipe(t))
-
- rFile, _ := testOpenOrDie(ctx, t, f, fs.FileFlags{Read: true, NonBlocking: true}, nil)
- rFile.DecRef(ctx)
-
- wDone := make(chan struct{})
- // This open for write should block because the reader is now gone.
- go testOpenOrDie(ctx, t, f, fs.FileFlags{Write: true}, wDone)
- assertRecvBlocks(t, wDone, time.Millisecond*100,
- "open for write didn't block with no concurrent readers")
-
- // Open for read again. This should unblock the open for write.
- rDone := make(chan struct{})
- go testOpenOrDie(ctx, t, f, fs.FileFlags{Read: true}, rDone)
-
- <-rDone
- <-wDone
-}
-
-func TestReadWriteOpenNeverBlocks(t *testing.T) {
- ctx := newSleeperContext(t)
- f := NewInodeOperations(ctx, perms, newNamedPipe(t))
-
- rwDone := make(chan struct{})
- // Open for read-write never wait for a reader or writer, even if the
- // nonblocking flag is not set.
- go testOpenOrDie(ctx, t, f, fs.FileFlags{Read: true, Write: true, NonBlocking: false}, rwDone)
- <-rwDone
-}
-
-func TestReadWriteOpenUnblocksReadOpen(t *testing.T) {
- ctx := newSleeperContext(t)
- f := NewInodeOperations(ctx, perms, newNamedPipe(t))
-
- rDone := make(chan struct{})
- go testOpenOrDie(ctx, t, f, fs.FileFlags{Read: true}, rDone)
-
- rwDone := make(chan struct{})
- go testOpenOrDie(ctx, t, f, fs.FileFlags{Read: true, Write: true}, rwDone)
-
- <-rwDone
- <-rDone
-}
-
-func TestReadWriteOpenUnblocksWriteOpen(t *testing.T) {
- ctx := newSleeperContext(t)
- f := NewInodeOperations(ctx, perms, newNamedPipe(t))
-
- wDone := make(chan struct{})
- go testOpenOrDie(ctx, t, f, fs.FileFlags{Write: true}, wDone)
-
- rwDone := make(chan struct{})
- go testOpenOrDie(ctx, t, f, fs.FileFlags{Read: true, Write: true}, rwDone)
-
- <-rwDone
- <-wDone
-}
-
-func TestBlockedOpenIsCancellable(t *testing.T) {
- ctx := newSleeperContext(t)
- f := NewInodeOperations(ctx, perms, newNamedPipe(t))
-
- done := make(chan openResult)
- go testOpen(ctx, t, f, fs.FileFlags{Read: true}, done)
- select {
- case <-done:
- t.Fatalf("open for read didn't block with no writers")
- case <-time.After(time.Millisecond * 100):
- // Ok.
- }
-
- ctx.(*sleeper).Cancel()
- // If the cancel on the sleeper didn't work, the open for read would never
- // return.
- res := <-done
- if res.error != syserror.ErrInterrupted {
- t.Fatalf("Cancellation didn't cause GetFile to return fs.ErrInterrupted, got %v.",
- res.error)
- }
-}
-
-func TestNonblockingReadOpenFileNoWriters(t *testing.T) {
- ctx := newSleeperContext(t)
- f := NewInodeOperations(ctx, perms, newNamedPipe(t))
-
- if _, err := testOpen(ctx, t, f, fs.FileFlags{Read: true, NonBlocking: true}, nil); err != nil {
- t.Fatalf("Nonblocking open for read failed with error %v.", err)
- }
-}
-
-func TestNonblockingWriteOpenFileNoReaders(t *testing.T) {
- ctx := newSleeperContext(t)
- f := NewInodeOperations(ctx, perms, newNamedPipe(t))
-
- if _, err := testOpen(ctx, t, f, fs.FileFlags{Write: true, NonBlocking: true}, nil); err != syserror.ENXIO {
- t.Fatalf("Nonblocking open for write failed unexpected error %v.", err)
- }
-}
-
-func TestNonBlockingReadOpenWithWriter(t *testing.T) {
- ctx := newSleeperContext(t)
- f := NewInodeOperations(ctx, perms, newNamedPipe(t))
-
- wDone := make(chan struct{})
- go testOpenOrDie(ctx, t, f, fs.FileFlags{Write: true}, wDone)
-
- // Open for write blocks since there are no readers yet.
- assertRecvBlocks(t, wDone, time.Millisecond*100,
- "Open for write didn't block with no reader.")
-
- if _, err := testOpen(ctx, t, f, fs.FileFlags{Read: true, NonBlocking: true}, nil); err != nil {
- t.Fatalf("Nonblocking open for read failed with error %v.", err)
- }
-
- // Open for write should now be unblocked.
- <-wDone
-}
-
-func TestNonBlockingWriteOpenWithReader(t *testing.T) {
- ctx := newSleeperContext(t)
- f := NewInodeOperations(ctx, perms, newNamedPipe(t))
-
- rDone := make(chan struct{})
- go testOpenOrDie(ctx, t, f, fs.FileFlags{Read: true}, rDone)
-
- // Open for write blocked, since no reader yet.
- assertRecvBlocks(t, rDone, time.Millisecond*100,
- "Open for reader didn't block with no writer.")
-
- if _, err := testOpen(ctx, t, f, fs.FileFlags{Write: true, NonBlocking: true}, nil); err != nil {
- t.Fatalf("Nonblocking open for write failed with error %v.", err)
- }
-
- // Open for write should now be unblocked.
- <-rDone
-}
-
-func TestAnonReadOpen(t *testing.T) {
- ctx := newSleeperContext(t)
- f := NewInodeOperations(ctx, perms, newAnonPipe(t))
-
- if _, err := testOpen(ctx, t, f, fs.FileFlags{Read: true}, nil); err != nil {
- t.Fatalf("open anon pipe for read failed: %v", err)
- }
-}
-
-func TestAnonWriteOpen(t *testing.T) {
- ctx := newSleeperContext(t)
- f := NewInodeOperations(ctx, perms, newAnonPipe(t))
-
- if _, err := testOpen(ctx, t, f, fs.FileFlags{Write: true}, nil); err != nil {
- t.Fatalf("open anon pipe for write failed: %v", err)
- }
-}
diff --git a/pkg/sentry/kernel/pipe/pipe_state_autogen.go b/pkg/sentry/kernel/pipe/pipe_state_autogen.go
new file mode 100644
index 000000000..df55abce6
--- /dev/null
+++ b/pkg/sentry/kernel/pipe/pipe_state_autogen.go
@@ -0,0 +1,151 @@
+// automatically generated by stateify.
+
+package pipe
+
+import (
+ "gvisor.dev/gvisor/pkg/state"
+)
+
+func (x *inodeOperations) StateTypeName() string {
+ return "pkg/sentry/kernel/pipe.inodeOperations"
+}
+
+func (x *inodeOperations) StateFields() []string {
+ return []string{
+ "InodeSimpleAttributes",
+ "p",
+ }
+}
+
+func (x *inodeOperations) beforeSave() {}
+
+func (x *inodeOperations) StateSave(m state.Sink) {
+ x.beforeSave()
+ m.Save(0, &x.InodeSimpleAttributes)
+ m.Save(1, &x.p)
+}
+
+func (x *inodeOperations) afterLoad() {}
+
+func (x *inodeOperations) StateLoad(m state.Source) {
+ m.Load(0, &x.InodeSimpleAttributes)
+ m.Load(1, &x.p)
+}
+
+func (x *Pipe) StateTypeName() string {
+ return "pkg/sentry/kernel/pipe.Pipe"
+}
+
+func (x *Pipe) StateFields() []string {
+ return []string{
+ "isNamed",
+ "atomicIOBytes",
+ "readers",
+ "writers",
+ "view",
+ "max",
+ "hadWriter",
+ }
+}
+
+func (x *Pipe) beforeSave() {}
+
+func (x *Pipe) StateSave(m state.Sink) {
+ x.beforeSave()
+ m.Save(0, &x.isNamed)
+ m.Save(1, &x.atomicIOBytes)
+ m.Save(2, &x.readers)
+ m.Save(3, &x.writers)
+ m.Save(4, &x.view)
+ m.Save(5, &x.max)
+ m.Save(6, &x.hadWriter)
+}
+
+func (x *Pipe) afterLoad() {}
+
+func (x *Pipe) StateLoad(m state.Source) {
+ m.Load(0, &x.isNamed)
+ m.Load(1, &x.atomicIOBytes)
+ m.Load(2, &x.readers)
+ m.Load(3, &x.writers)
+ m.Load(4, &x.view)
+ m.Load(5, &x.max)
+ m.Load(6, &x.hadWriter)
+}
+
+func (x *Reader) StateTypeName() string {
+ return "pkg/sentry/kernel/pipe.Reader"
+}
+
+func (x *Reader) StateFields() []string {
+ return []string{
+ "ReaderWriter",
+ }
+}
+
+func (x *Reader) beforeSave() {}
+
+func (x *Reader) StateSave(m state.Sink) {
+ x.beforeSave()
+ m.Save(0, &x.ReaderWriter)
+}
+
+func (x *Reader) afterLoad() {}
+
+func (x *Reader) StateLoad(m state.Source) {
+ m.Load(0, &x.ReaderWriter)
+}
+
+func (x *ReaderWriter) StateTypeName() string {
+ return "pkg/sentry/kernel/pipe.ReaderWriter"
+}
+
+func (x *ReaderWriter) StateFields() []string {
+ return []string{
+ "Pipe",
+ }
+}
+
+func (x *ReaderWriter) beforeSave() {}
+
+func (x *ReaderWriter) StateSave(m state.Sink) {
+ x.beforeSave()
+ m.Save(0, &x.Pipe)
+}
+
+func (x *ReaderWriter) afterLoad() {}
+
+func (x *ReaderWriter) StateLoad(m state.Source) {
+ m.Load(0, &x.Pipe)
+}
+
+func (x *Writer) StateTypeName() string {
+ return "pkg/sentry/kernel/pipe.Writer"
+}
+
+func (x *Writer) StateFields() []string {
+ return []string{
+ "ReaderWriter",
+ }
+}
+
+func (x *Writer) beforeSave() {}
+
+func (x *Writer) StateSave(m state.Sink) {
+ x.beforeSave()
+ m.Save(0, &x.ReaderWriter)
+}
+
+func (x *Writer) afterLoad() {}
+
+func (x *Writer) StateLoad(m state.Source) {
+ m.Load(0, &x.ReaderWriter)
+}
+
+func init() {
+ state.Register((*inodeOperations)(nil))
+ state.Register((*Pipe)(nil))
+ state.Register((*Reader)(nil))
+ state.Register((*ReaderWriter)(nil))
+ state.Register((*Writer)(nil))
+}
diff --git a/pkg/sentry/kernel/pipe/pipe_test.go b/pkg/sentry/kernel/pipe/pipe_test.go
deleted file mode 100644
index fe97e9800..000000000
--- a/pkg/sentry/kernel/pipe/pipe_test.go
+++ /dev/null
@@ -1,139 +0,0 @@
-// Copyright 2018 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 pipe
-
-import (
- "bytes"
- "testing"
-
- "gvisor.dev/gvisor/pkg/sentry/contexttest"
- "gvisor.dev/gvisor/pkg/syserror"
- "gvisor.dev/gvisor/pkg/usermem"
- "gvisor.dev/gvisor/pkg/waiter"
-)
-
-func TestPipeRW(t *testing.T) {
- ctx := contexttest.Context(t)
- r, w := NewConnectedPipe(ctx, 65536, 4096)
- defer r.DecRef(ctx)
- defer w.DecRef(ctx)
-
- msg := []byte("here's some bytes")
- wantN := int64(len(msg))
- n, err := w.Writev(ctx, usermem.BytesIOSequence(msg))
- if n != wantN || err != nil {
- t.Fatalf("Writev: got (%d, %v), wanted (%d, nil)", n, err, wantN)
- }
-
- buf := make([]byte, len(msg))
- n, err = r.Readv(ctx, usermem.BytesIOSequence(buf))
- if n != wantN || err != nil || !bytes.Equal(buf, msg) {
- t.Fatalf("Readv: got (%d, %v) %q, wanted (%d, nil) %q", n, err, buf, wantN, msg)
- }
-}
-
-func TestPipeReadBlock(t *testing.T) {
- ctx := contexttest.Context(t)
- r, w := NewConnectedPipe(ctx, 65536, 4096)
- defer r.DecRef(ctx)
- defer w.DecRef(ctx)
-
- n, err := r.Readv(ctx, usermem.BytesIOSequence(make([]byte, 1)))
- if n != 0 || err != syserror.ErrWouldBlock {
- t.Fatalf("Readv: got (%d, %v), wanted (0, %v)", n, err, syserror.ErrWouldBlock)
- }
-}
-
-func TestPipeWriteBlock(t *testing.T) {
- const atomicIOBytes = 2
- const capacity = MinimumPipeSize
-
- ctx := contexttest.Context(t)
- r, w := NewConnectedPipe(ctx, capacity, atomicIOBytes)
- defer r.DecRef(ctx)
- defer w.DecRef(ctx)
-
- msg := make([]byte, capacity+1)
- n, err := w.Writev(ctx, usermem.BytesIOSequence(msg))
- if wantN, wantErr := int64(capacity), syserror.ErrWouldBlock; n != wantN || err != wantErr {
- t.Fatalf("Writev: got (%d, %v), wanted (%d, %v)", n, err, wantN, wantErr)
- }
-}
-
-func TestPipeWriteUntilEnd(t *testing.T) {
- const atomicIOBytes = 2
-
- ctx := contexttest.Context(t)
- r, w := NewConnectedPipe(ctx, atomicIOBytes, atomicIOBytes)
- defer r.DecRef(ctx)
- defer w.DecRef(ctx)
-
- msg := []byte("here's some bytes")
-
- wDone := make(chan struct{}, 0)
- rDone := make(chan struct{}, 0)
- defer func() {
- // Signal the reader to stop and wait until it does so.
- close(wDone)
- <-rDone
- }()
-
- go func() {
- defer close(rDone)
- // Read from r until done is closed.
- ctx := contexttest.Context(t)
- buf := make([]byte, len(msg)+1)
- dst := usermem.BytesIOSequence(buf)
- e, ch := waiter.NewChannelEntry(nil)
- r.EventRegister(&e, waiter.EventIn)
- defer r.EventUnregister(&e)
- for {
- n, err := r.Readv(ctx, dst)
- dst = dst.DropFirst64(n)
- if err == syserror.ErrWouldBlock {
- select {
- case <-ch:
- continue
- case <-wDone:
- // We expect to have 1 byte left in dst since len(buf) ==
- // len(msg)+1.
- if dst.NumBytes() != 1 || !bytes.Equal(buf[:len(msg)], msg) {
- t.Errorf("Reader: got %q (%d bytes remaining), wanted %q", buf, dst.NumBytes(), msg)
- }
- return
- }
- }
- if err != nil {
- t.Fatalf("Readv: got unexpected error %v", err)
- }
- }
- }()
-
- src := usermem.BytesIOSequence(msg)
- e, ch := waiter.NewChannelEntry(nil)
- w.EventRegister(&e, waiter.EventOut)
- defer w.EventUnregister(&e)
- for src.NumBytes() != 0 {
- n, err := w.Writev(ctx, src)
- src = src.DropFirst64(n)
- if err == syserror.ErrWouldBlock {
- <-ch
- continue
- }
- if err != nil {
- t.Fatalf("Writev: got (%d, %v)", n, err)
- }
- }
-}
diff --git a/pkg/sentry/kernel/pipe/pipe_unsafe_state_autogen.go b/pkg/sentry/kernel/pipe/pipe_unsafe_state_autogen.go
new file mode 100644
index 000000000..d3b40feb4
--- /dev/null
+++ b/pkg/sentry/kernel/pipe/pipe_unsafe_state_autogen.go
@@ -0,0 +1,3 @@
+// automatically generated by stateify.
+
+package pipe