diff options
author | Zach Koopmans <zkoopmans@google.com> | 2021-03-29 13:28:32 -0700 |
---|---|---|
committer | gVisor bot <gvisor-bot@google.com> | 2021-03-29 13:30:21 -0700 |
commit | 8a2f7e716dcc62f04d2808e8ade34941c94fc956 (patch) | |
tree | b2195d5728dcbc4f4e59c23ad95d7486ef744371 /pkg/sentry/fs/fsutil | |
parent | b125afba416ebeba906ea595a44a55afe4729d64 (diff) |
[syserror] Split usermem package
Split usermem package to help remove syserror dependency in go_marshal.
New hostarch package contains code not dependent on syserror.
PiperOrigin-RevId: 365651233
Diffstat (limited to 'pkg/sentry/fs/fsutil')
-rw-r--r-- | pkg/sentry/fs/fsutil/BUILD | 2 | ||||
-rw-r--r-- | pkg/sentry/fs/fsutil/dirty_set.go | 4 | ||||
-rw-r--r-- | pkg/sentry/fs/fsutil/dirty_set_test.go | 12 | ||||
-rw-r--r-- | pkg/sentry/fs/fsutil/file_range_set.go | 8 | ||||
-rw-r--r-- | pkg/sentry/fs/fsutil/host_file_mapper.go | 6 | ||||
-rw-r--r-- | pkg/sentry/fs/fsutil/host_mappable.go | 13 | ||||
-rw-r--r-- | pkg/sentry/fs/fsutil/inode_cached.go | 21 | ||||
-rw-r--r-- | pkg/sentry/fs/fsutil/inode_cached_test.go | 51 |
8 files changed, 61 insertions, 56 deletions
diff --git a/pkg/sentry/fs/fsutil/BUILD b/pkg/sentry/fs/fsutil/BUILD index d388f0e92..6469cc3a9 100644 --- a/pkg/sentry/fs/fsutil/BUILD +++ b/pkg/sentry/fs/fsutil/BUILD @@ -76,6 +76,7 @@ go_library( deps = [ "//pkg/abi/linux", "//pkg/context", + "//pkg/hostarch", "//pkg/log", "//pkg/safemem", "//pkg/sentry/arch", @@ -105,6 +106,7 @@ go_test( library = ":fsutil", deps = [ "//pkg/context", + "//pkg/hostarch", "//pkg/safemem", "//pkg/sentry/contexttest", "//pkg/sentry/fs", diff --git a/pkg/sentry/fs/fsutil/dirty_set.go b/pkg/sentry/fs/fsutil/dirty_set.go index 2c9446c1d..38383e730 100644 --- a/pkg/sentry/fs/fsutil/dirty_set.go +++ b/pkg/sentry/fs/fsutil/dirty_set.go @@ -18,9 +18,9 @@ import ( "math" "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/hostarch" "gvisor.dev/gvisor/pkg/safemem" "gvisor.dev/gvisor/pkg/sentry/memmap" - "gvisor.dev/gvisor/pkg/usermem" ) // DirtySet maps offsets into a memmap.Mappable to DirtyInfo. It is used to @@ -215,7 +215,7 @@ func syncDirtyRange(ctx context.Context, mr memmap.MappableRange, cache *FileRan if max < wbr.Start { break } - ims, err := mem.MapInternal(cseg.FileRangeOf(wbr), usermem.Read) + ims, err := mem.MapInternal(cseg.FileRangeOf(wbr), hostarch.Read) if err != nil { return err } diff --git a/pkg/sentry/fs/fsutil/dirty_set_test.go b/pkg/sentry/fs/fsutil/dirty_set_test.go index e3579c23c..48448c97c 100644 --- a/pkg/sentry/fs/fsutil/dirty_set_test.go +++ b/pkg/sentry/fs/fsutil/dirty_set_test.go @@ -18,18 +18,18 @@ import ( "reflect" "testing" + "gvisor.dev/gvisor/pkg/hostarch" "gvisor.dev/gvisor/pkg/sentry/memmap" - "gvisor.dev/gvisor/pkg/usermem" ) func TestDirtySet(t *testing.T) { var set DirtySet - set.MarkDirty(memmap.MappableRange{0, 2 * usermem.PageSize}) - set.KeepDirty(memmap.MappableRange{usermem.PageSize, 2 * usermem.PageSize}) - set.MarkClean(memmap.MappableRange{0, 2 * usermem.PageSize}) + set.MarkDirty(memmap.MappableRange{0, 2 * hostarch.PageSize}) + set.KeepDirty(memmap.MappableRange{hostarch.PageSize, 2 * hostarch.PageSize}) + set.MarkClean(memmap.MappableRange{0, 2 * hostarch.PageSize}) want := &DirtySegmentDataSlices{ - Start: []uint64{usermem.PageSize}, - End: []uint64{2 * usermem.PageSize}, + Start: []uint64{hostarch.PageSize}, + End: []uint64{2 * hostarch.PageSize}, Values: []DirtyInfo{{Keep: true}}, } if got := set.ExportSortedSlices(); !reflect.DeepEqual(got, want) { diff --git a/pkg/sentry/fs/fsutil/file_range_set.go b/pkg/sentry/fs/fsutil/file_range_set.go index 1dc409d38..fdaceb1db 100644 --- a/pkg/sentry/fs/fsutil/file_range_set.go +++ b/pkg/sentry/fs/fsutil/file_range_set.go @@ -20,11 +20,11 @@ import ( "math" "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/hostarch" "gvisor.dev/gvisor/pkg/safemem" "gvisor.dev/gvisor/pkg/sentry/memmap" "gvisor.dev/gvisor/pkg/sentry/pgalloc" "gvisor.dev/gvisor/pkg/sentry/usage" - "gvisor.dev/gvisor/pkg/usermem" ) // FileRangeSet maps offsets into a memmap.Mappable to offsets into a @@ -130,7 +130,7 @@ func (frs *FileRangeSet) Fill(ctx context.Context, required, optional memmap.Map // MemoryFile.AllocateAndFill truncates down to a page // boundary, but FileRangeSet.Fill is supposed to // zero-fill to the end of the page in this case. - donepgaddr, ok := usermem.Addr(done).RoundUp() + donepgaddr, ok := hostarch.Addr(done).RoundUp() if donepg := uint64(donepgaddr); ok && donepg != done { dsts.DropFirst64(donepg - done) done = donepg @@ -184,7 +184,7 @@ func (frs *FileRangeSet) DropAll(mf *pgalloc.MemoryFile) { // bytes after the new EOF on the same page are zeroed, and pages after the new // EOF are freed. func (frs *FileRangeSet) Truncate(end uint64, mf *pgalloc.MemoryFile) { - pgendaddr, ok := usermem.Addr(end).RoundUp() + pgendaddr, ok := hostarch.Addr(end).RoundUp() if ok { pgend := uint64(pgendaddr) @@ -208,7 +208,7 @@ func (frs *FileRangeSet) Truncate(end uint64, mf *pgalloc.MemoryFile) { if seg.Ok() { fr := seg.FileRange() fr.Start += end - seg.Start() - ims, err := mf.MapInternal(fr, usermem.Write) + ims, err := mf.MapInternal(fr, hostarch.Write) if err != nil { // There's no good recourse from here. This means // that we can't keep cached memory consistent with diff --git a/pkg/sentry/fs/fsutil/host_file_mapper.go b/pkg/sentry/fs/fsutil/host_file_mapper.go index 54f7b7cdc..23528bf25 100644 --- a/pkg/sentry/fs/fsutil/host_file_mapper.go +++ b/pkg/sentry/fs/fsutil/host_file_mapper.go @@ -18,11 +18,11 @@ import ( "fmt" "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/hostarch" "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/pkg/safemem" "gvisor.dev/gvisor/pkg/sentry/memmap" "gvisor.dev/gvisor/pkg/sync" - "gvisor.dev/gvisor/pkg/usermem" ) // HostFileMapper caches mappings of an arbitrary host file descriptor. It is @@ -50,13 +50,13 @@ type HostFileMapper struct { } const ( - chunkShift = usermem.HugePageShift + chunkShift = hostarch.HugePageShift chunkSize = 1 << chunkShift chunkMask = chunkSize - 1 ) func pagesInChunk(mr memmap.MappableRange, chunkStart uint64) int32 { - return int32(mr.Intersect(memmap.MappableRange{chunkStart, chunkStart + chunkSize}).Length() / usermem.PageSize) + return int32(mr.Intersect(memmap.MappableRange{chunkStart, chunkStart + chunkSize}).Length() / hostarch.PageSize) } type mapping struct { diff --git a/pkg/sentry/fs/fsutil/host_mappable.go b/pkg/sentry/fs/fsutil/host_mappable.go index c15d8a946..e1e38b498 100644 --- a/pkg/sentry/fs/fsutil/host_mappable.go +++ b/pkg/sentry/fs/fsutil/host_mappable.go @@ -18,6 +18,7 @@ import ( "math" "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/hostarch" "gvisor.dev/gvisor/pkg/safemem" "gvisor.dev/gvisor/pkg/sentry/fs" "gvisor.dev/gvisor/pkg/sentry/memmap" @@ -59,7 +60,7 @@ func NewHostMappable(backingFile CachedFileObject) *HostMappable { } // AddMapping implements memmap.Mappable.AddMapping. -func (h *HostMappable) AddMapping(ctx context.Context, ms memmap.MappingSpace, ar usermem.AddrRange, offset uint64, writable bool) error { +func (h *HostMappable) AddMapping(ctx context.Context, ms memmap.MappingSpace, ar hostarch.AddrRange, offset uint64, writable bool) error { // Hot path. Avoid defers. h.mu.Lock() mapped := h.mappings.AddMapping(ms, ar, offset, writable) @@ -71,7 +72,7 @@ func (h *HostMappable) AddMapping(ctx context.Context, ms memmap.MappingSpace, a } // RemoveMapping implements memmap.Mappable.RemoveMapping. -func (h *HostMappable) RemoveMapping(ctx context.Context, ms memmap.MappingSpace, ar usermem.AddrRange, offset uint64, writable bool) { +func (h *HostMappable) RemoveMapping(ctx context.Context, ms memmap.MappingSpace, ar hostarch.AddrRange, offset uint64, writable bool) { // Hot path. Avoid defers. h.mu.Lock() unmapped := h.mappings.RemoveMapping(ms, ar, offset, writable) @@ -82,18 +83,18 @@ func (h *HostMappable) RemoveMapping(ctx context.Context, ms memmap.MappingSpace } // CopyMapping implements memmap.Mappable.CopyMapping. -func (h *HostMappable) CopyMapping(ctx context.Context, ms memmap.MappingSpace, srcAR, dstAR usermem.AddrRange, offset uint64, writable bool) error { +func (h *HostMappable) CopyMapping(ctx context.Context, ms memmap.MappingSpace, srcAR, dstAR hostarch.AddrRange, offset uint64, writable bool) error { return h.AddMapping(ctx, ms, dstAR, offset, writable) } // Translate implements memmap.Mappable.Translate. -func (h *HostMappable) Translate(ctx context.Context, required, optional memmap.MappableRange, at usermem.AccessType) ([]memmap.Translation, error) { +func (h *HostMappable) Translate(ctx context.Context, required, optional memmap.MappableRange, at hostarch.AccessType) ([]memmap.Translation, error) { return []memmap.Translation{ { Source: optional, File: h, Offset: optional.Start, - Perms: usermem.AnyAccess, + Perms: hostarch.AnyAccess, }, }, nil } @@ -124,7 +125,7 @@ func (h *HostMappable) NotifyChangeFD() error { } // MapInternal implements memmap.File.MapInternal. -func (h *HostMappable) MapInternal(fr memmap.FileRange, at usermem.AccessType) (safemem.BlockSeq, error) { +func (h *HostMappable) MapInternal(fr memmap.FileRange, at hostarch.AccessType) (safemem.BlockSeq, error) { return h.hostFileMapper.MapInternal(fr, h.backingFile.FD(), at.Write) } diff --git a/pkg/sentry/fs/fsutil/inode_cached.go b/pkg/sentry/fs/fsutil/inode_cached.go index 0ed7aafa5..7856b354b 100644 --- a/pkg/sentry/fs/fsutil/inode_cached.go +++ b/pkg/sentry/fs/fsutil/inode_cached.go @@ -19,6 +19,7 @@ import ( "io" "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/hostarch" "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/pkg/safemem" "gvisor.dev/gvisor/pkg/sentry/fs" @@ -622,7 +623,7 @@ func (rw *inodeReadWriter) ReadToBlocks(dsts safemem.BlockSeq) (uint64, error) { switch { case seg.Ok(): // Get internal mappings from the cache. - ims, err := mem.MapInternal(seg.FileRangeOf(seg.Range().Intersect(mr)), usermem.Read) + ims, err := mem.MapInternal(seg.FileRangeOf(seg.Range().Intersect(mr)), hostarch.Read) if err != nil { unlock() return done, err @@ -647,7 +648,7 @@ func (rw *inodeReadWriter) ReadToBlocks(dsts safemem.BlockSeq) (uint64, error) { // Read into the cache, then re-enter the loop to read from the // cache. reqMR := memmap.MappableRange{ - Start: uint64(usermem.Addr(gapMR.Start).RoundDown()), + Start: uint64(hostarch.Addr(gapMR.Start).RoundDown()), End: fs.OffsetPageEnd(int64(gapMR.End)), } optMR := gap.Range() @@ -729,7 +730,7 @@ func (rw *inodeReadWriter) WriteFromBlocks(srcs safemem.BlockSeq) (uint64, error case seg.Ok() && seg.Start() < mr.End: // Get internal mappings from the cache. segMR := seg.Range().Intersect(mr) - ims, err := mf.MapInternal(seg.FileRangeOf(segMR), usermem.Write) + ims, err := mf.MapInternal(seg.FileRangeOf(segMR), hostarch.Write) if err != nil { rw.maybeGrowFile() rw.c.dataMu.Unlock() @@ -786,7 +787,7 @@ func (c *CachingInodeOperations) useHostPageCache() bool { } // AddMapping implements memmap.Mappable.AddMapping. -func (c *CachingInodeOperations) AddMapping(ctx context.Context, ms memmap.MappingSpace, ar usermem.AddrRange, offset uint64, writable bool) error { +func (c *CachingInodeOperations) AddMapping(ctx context.Context, ms memmap.MappingSpace, ar hostarch.AddrRange, offset uint64, writable bool) error { // Hot path. Avoid defers. c.mapsMu.Lock() mapped := c.mappings.AddMapping(ms, ar, offset, writable) @@ -808,7 +809,7 @@ func (c *CachingInodeOperations) AddMapping(ctx context.Context, ms memmap.Mappi } // RemoveMapping implements memmap.Mappable.RemoveMapping. -func (c *CachingInodeOperations) RemoveMapping(ctx context.Context, ms memmap.MappingSpace, ar usermem.AddrRange, offset uint64, writable bool) { +func (c *CachingInodeOperations) RemoveMapping(ctx context.Context, ms memmap.MappingSpace, ar hostarch.AddrRange, offset uint64, writable bool) { // Hot path. Avoid defers. c.mapsMu.Lock() unmapped := c.mappings.RemoveMapping(ms, ar, offset, writable) @@ -836,12 +837,12 @@ func (c *CachingInodeOperations) RemoveMapping(ctx context.Context, ms memmap.Ma } // CopyMapping implements memmap.Mappable.CopyMapping. -func (c *CachingInodeOperations) CopyMapping(ctx context.Context, ms memmap.MappingSpace, srcAR, dstAR usermem.AddrRange, offset uint64, writable bool) error { +func (c *CachingInodeOperations) CopyMapping(ctx context.Context, ms memmap.MappingSpace, srcAR, dstAR hostarch.AddrRange, offset uint64, writable bool) error { return c.AddMapping(ctx, ms, dstAR, offset, writable) } // Translate implements memmap.Mappable.Translate. -func (c *CachingInodeOperations) Translate(ctx context.Context, required, optional memmap.MappableRange, at usermem.AccessType) ([]memmap.Translation, error) { +func (c *CachingInodeOperations) Translate(ctx context.Context, required, optional memmap.MappableRange, at hostarch.AccessType) ([]memmap.Translation, error) { // Hot path. Avoid defer. if c.useHostPageCache() { mr := optional @@ -853,7 +854,7 @@ func (c *CachingInodeOperations) Translate(ctx context.Context, required, option Source: mr, File: c, Offset: mr.Start, - Perms: usermem.AnyAccess, + Perms: hostarch.AnyAccess, }, }, nil } @@ -885,7 +886,7 @@ func (c *CachingInodeOperations) Translate(ctx context.Context, required, option segMR := seg.Range().Intersect(optional) // TODO(jamieliu): Make Translations writable even if writability is // not required if already kept-dirty by another writable translation. - perms := usermem.AccessType{ + perms := hostarch.AccessType{ Read: true, Execute: true, } @@ -1050,7 +1051,7 @@ func (c *CachingInodeOperations) DecRef(fr memmap.FileRange) { // MapInternal implements memmap.File.MapInternal. This is used when we // directly map an underlying host fd and CachingInodeOperations is used as the // memmap.File during translation. -func (c *CachingInodeOperations) MapInternal(fr memmap.FileRange, at usermem.AccessType) (safemem.BlockSeq, error) { +func (c *CachingInodeOperations) MapInternal(fr memmap.FileRange, at hostarch.AccessType) (safemem.BlockSeq, error) { return c.hostFileMapper.MapInternal(fr, c.backingFile.FD(), at.Write) } diff --git a/pkg/sentry/fs/fsutil/inode_cached_test.go b/pkg/sentry/fs/fsutil/inode_cached_test.go index 1547584c5..e107c3096 100644 --- a/pkg/sentry/fs/fsutil/inode_cached_test.go +++ b/pkg/sentry/fs/fsutil/inode_cached_test.go @@ -20,6 +20,7 @@ import ( "testing" "gvisor.dev/gvisor/pkg/context" + "gvisor.dev/gvisor/pkg/hostarch" "gvisor.dev/gvisor/pkg/safemem" "gvisor.dev/gvisor/pkg/sentry/contexttest" "gvisor.dev/gvisor/pkg/sentry/fs" @@ -249,7 +250,7 @@ func (f *sliceBackingFile) Allocate(ctx context.Context, offset int64, length in type noopMappingSpace struct{} // Invalidate implements memmap.MappingSpace.Invalidate. -func (noopMappingSpace) Invalidate(ar usermem.AddrRange, opts memmap.InvalidateOpts) { +func (noopMappingSpace) Invalidate(ar hostarch.AddrRange, opts memmap.InvalidateOpts) { } func anonInode(ctx context.Context) *fs.Inode { @@ -259,14 +260,14 @@ func anonInode(ctx context.Context) *fs.Inode { }, 0), }, fs.NewPseudoMountSource(ctx), fs.StableAttr{ Type: fs.Anonymous, - BlockSize: usermem.PageSize, + BlockSize: hostarch.PageSize, }) } func pagesOf(bs ...byte) []byte { - buf := make([]byte, 0, len(bs)*usermem.PageSize) + buf := make([]byte, 0, len(bs)*hostarch.PageSize) for _, b := range bs { - buf = append(buf, bytes.Repeat([]byte{b}, usermem.PageSize)...) + buf = append(buf, bytes.Repeat([]byte{b}, hostarch.PageSize)...) } return buf } @@ -292,28 +293,28 @@ func TestRead(t *testing.T) { // expects to only cache mapped pages), then call Translate to force it to // be cached. var ms noopMappingSpace - ar := usermem.AddrRange{usermem.PageSize, 2 * usermem.PageSize} - if err := iops.AddMapping(ctx, ms, ar, usermem.PageSize, true); err != nil { + ar := hostarch.AddrRange{hostarch.PageSize, 2 * hostarch.PageSize} + if err := iops.AddMapping(ctx, ms, ar, hostarch.PageSize, true); err != nil { t.Fatalf("AddMapping got %v, want nil", err) } - mr := memmap.MappableRange{usermem.PageSize, 2 * usermem.PageSize} - if _, err := iops.Translate(ctx, mr, mr, usermem.Read); err != nil { + mr := memmap.MappableRange{hostarch.PageSize, 2 * hostarch.PageSize} + if _, err := iops.Translate(ctx, mr, mr, hostarch.Read); err != nil { t.Fatalf("Translate got %v, want nil", err) } - if cached := iops.cache.Span(); cached != usermem.PageSize { - t.Errorf("SpanRange got %d, want %d", cached, usermem.PageSize) + if cached := iops.cache.Span(); cached != hostarch.PageSize { + t.Errorf("SpanRange got %d, want %d", cached, hostarch.PageSize) } // Try to read 4 pages. The first and third pages should be read directly // from the "file", the second page should be read from the cache, and only // 3 pages (the size of the file) should be readable. - rbuf := make([]byte, 4*usermem.PageSize) + rbuf := make([]byte, 4*hostarch.PageSize) dst := usermem.BytesIOSequence(rbuf) n, err := iops.Read(ctx, file, dst, 0) - if n != 3*usermem.PageSize || (err != nil && err != io.EOF) { - t.Fatalf("Read got (%d, %v), want (%d, nil or EOF)", n, err, 3*usermem.PageSize) + if n != 3*hostarch.PageSize || (err != nil && err != io.EOF) { + t.Fatalf("Read got (%d, %v), want (%d, nil or EOF)", n, err, 3*hostarch.PageSize) } - rbuf = rbuf[:3*usermem.PageSize] + rbuf = rbuf[:3*hostarch.PageSize] // Did we get the bytes we expect? if !bytes.Equal(rbuf, buf) { @@ -323,7 +324,7 @@ func TestRead(t *testing.T) { // Delete the memory mapping before iops.Release(). The cached page will // either be evicted by ctx's pgalloc.MemoryFile, or dropped by // iops.Release(). - iops.RemoveMapping(ctx, ms, ar, usermem.PageSize, true) + iops.RemoveMapping(ctx, ms, ar, hostarch.PageSize, true) } func TestWrite(t *testing.T) { @@ -348,25 +349,25 @@ func TestWrite(t *testing.T) { // CachingInodeOperations expects to only cache mapped pages), then call // Translate to force them to be cached. var ms noopMappingSpace - ar := usermem.AddrRange{usermem.PageSize, 3 * usermem.PageSize} - if err := iops.AddMapping(ctx, ms, ar, usermem.PageSize, true); err != nil { + ar := hostarch.AddrRange{hostarch.PageSize, 3 * hostarch.PageSize} + if err := iops.AddMapping(ctx, ms, ar, hostarch.PageSize, true); err != nil { t.Fatalf("AddMapping got %v, want nil", err) } - defer iops.RemoveMapping(ctx, ms, ar, usermem.PageSize, true) - mr := memmap.MappableRange{usermem.PageSize, 3 * usermem.PageSize} - if _, err := iops.Translate(ctx, mr, mr, usermem.Read); err != nil { + defer iops.RemoveMapping(ctx, ms, ar, hostarch.PageSize, true) + mr := memmap.MappableRange{hostarch.PageSize, 3 * hostarch.PageSize} + if _, err := iops.Translate(ctx, mr, mr, hostarch.Read); err != nil { t.Fatalf("Translate got %v, want nil", err) } - if cached := iops.cache.Span(); cached != 2*usermem.PageSize { - t.Errorf("SpanRange got %d, want %d", cached, 2*usermem.PageSize) + if cached := iops.cache.Span(); cached != 2*hostarch.PageSize { + t.Errorf("SpanRange got %d, want %d", cached, 2*hostarch.PageSize) } // Write to the first 2 pages. wbuf := pagesOf('e', 'f') src := usermem.BytesIOSequence(wbuf) n, err := iops.Write(ctx, src, 0) - if n != 2*usermem.PageSize || err != nil { - t.Fatalf("Write got (%d, %v), want (%d, nil)", n, err, 2*usermem.PageSize) + if n != 2*hostarch.PageSize || err != nil { + t.Fatalf("Write got (%d, %v), want (%d, nil)", n, err, 2*hostarch.PageSize) } // The first page should have been written directly, since it was not cached. @@ -382,7 +383,7 @@ func TestWrite(t *testing.T) { } // Now the second page should have been written as well. - copy(want[usermem.PageSize:], pagesOf('f')) + copy(want[hostarch.PageSize:], pagesOf('f')) if !bytes.Equal(buf, want) { t.Errorf("File contents are %v, want %v", buf, want) } |