github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/libdokan/dir.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 libdokan 6 7 import ( 8 "fmt" 9 "strings" 10 "sync" 11 12 "github.com/keybase/client/go/kbfs/data" 13 "github.com/keybase/client/go/kbfs/dokan" 14 "github.com/keybase/client/go/kbfs/idutil" 15 "github.com/keybase/client/go/kbfs/libfs" 16 "github.com/keybase/client/go/kbfs/libkbfs" 17 "github.com/keybase/client/go/kbfs/tlf" 18 "github.com/keybase/client/go/kbfs/tlfhandle" 19 "github.com/keybase/client/go/libkb" 20 "github.com/pkg/errors" 21 "golang.org/x/net/context" 22 ) 23 24 // HiddenFilePrefix is the prefix for files to be hidden. 25 const HiddenFilePrefix = `._` 26 27 // Folder represents KBFS top-level folders 28 type Folder struct { 29 fs *FS 30 list *FolderList 31 32 handleMu sync.RWMutex 33 h *tlfhandle.Handle 34 hPreferredName tlf.PreferredName 35 36 folderBranchMu sync.Mutex 37 folderBranch data.FolderBranch 38 39 // Protects the nodes map. 40 mu sync.Mutex 41 // Map KBFS nodes to FUSE nodes, to be able to handle multiple 42 // lookups and incoming change notifications. A node is present 43 // here if the kernel holds a reference to it. 44 // 45 // If we ever support hardlinks, this would need refcounts. 46 // 47 // Children must call folder.forgetChildLocked on receiving the 48 // FUSE Forget request. 49 nodes map[libkbfs.NodeID]dokan.File 50 51 // Protects the updateChan. 52 updateMu sync.Mutex 53 // updateChan is non-nil when the user disables updates via the 54 // file system. Sending a struct{}{} on this channel will unpause 55 // the updates. 56 updateChan chan<- struct{} 57 58 // noForget is turned on when the folder may not be forgotten 59 // because it has attached special file state with it. 60 noForget bool 61 } 62 63 func newFolder(fl *FolderList, h *tlfhandle.Handle, 64 hPreferredName tlf.PreferredName) *Folder { 65 f := &Folder{ 66 fs: fl.fs, 67 list: fl, 68 h: h, 69 hPreferredName: hPreferredName, 70 nodes: map[libkbfs.NodeID]dokan.File{}, 71 } 72 return f 73 } 74 75 func (f *Folder) name() tlf.CanonicalName { 76 f.handleMu.RLock() 77 defer f.handleMu.RUnlock() 78 return tlf.CanonicalName(f.hPreferredName) 79 } 80 81 func (f *Folder) setFolderBranch(folderBranch data.FolderBranch) error { 82 f.folderBranchMu.Lock() 83 defer f.folderBranchMu.Unlock() 84 85 // TODO unregister all at unmount 86 err := f.list.fs.config.Notifier().RegisterForChanges( 87 []data.FolderBranch{folderBranch}, f) 88 if err != nil { 89 return err 90 } 91 f.folderBranch = folderBranch 92 return nil 93 } 94 95 func (f *Folder) unsetFolderBranch(ctx context.Context) { 96 f.folderBranchMu.Lock() 97 defer f.folderBranchMu.Unlock() 98 if f.folderBranch == (data.FolderBranch{}) { 99 // Wasn't set. 100 return 101 } 102 103 err := f.list.fs.config.Notifier().UnregisterFromChanges([]data.FolderBranch{f.folderBranch}, f) 104 if err != nil { 105 f.fs.log.Info("cannot unregister change notifier for folder %q: %v", 106 f.name(), err) 107 } 108 f.folderBranch = data.FolderBranch{} 109 } 110 111 func (f *Folder) getFolderBranch() data.FolderBranch { 112 f.folderBranchMu.Lock() 113 defer f.folderBranchMu.Unlock() 114 return f.folderBranch 115 } 116 117 // forgetNode forgets a formerly active child with basename name. 118 func (f *Folder) forgetNode(ctx context.Context, node libkbfs.Node) { 119 f.mu.Lock() 120 defer f.mu.Unlock() 121 122 delete(f.nodes, node.GetID()) 123 if len(f.nodes) == 0 && !f.noForget { 124 f.unsetFolderBranch(ctx) 125 f.list.forgetFolder(string(f.name())) 126 } 127 } 128 129 func (f *Folder) reportErr(ctx context.Context, 130 mode libkbfs.ErrorModeType, err error) { 131 if err == nil { 132 f.fs.vlog.CLogf(ctx, libkb.VLog1, "Request complete") 133 return 134 } 135 136 f.fs.config.Reporter().ReportErr(ctx, f.name(), f.list.tlfType, mode, err) 137 // We just log the error as debug, rather than error, because it 138 // might just indicate an expected error such as an ENOENT. 139 // 140 // TODO: Classify errors and escalate the logging level of the 141 // important ones. 142 f.fs.log.CDebugf(ctx, err.Error()) 143 } 144 145 func (f *Folder) lockedAddNode(node libkbfs.Node, val dokan.File) { 146 f.mu.Lock() 147 f.nodes[node.GetID()] = val 148 f.mu.Unlock() 149 } 150 151 // LocalChange is called for changes originating within in this process. 152 func (f *Folder) LocalChange(ctx context.Context, node libkbfs.Node, write libkbfs.WriteRange) { 153 f.fs.queueNotification(func() {}) 154 } 155 156 // BatchChanges is called for changes originating anywhere, including 157 // other hosts. 158 func (f *Folder) BatchChanges( 159 ctx context.Context, changes []libkbfs.NodeChange, _ []libkbfs.NodeID) { 160 f.fs.queueNotification(func() {}) 161 } 162 163 // TlfHandleChange is called when the name of a folder changes. 164 // Note that newHandle may be nil. Then the handle in the folder is used. 165 // This is used on e.g. logout/login. 166 func (f *Folder) TlfHandleChange(ctx context.Context, 167 newHandle *tlfhandle.Handle) { 168 f.fs.log.CDebugf(ctx, "TlfHandleChange called on %q", 169 canonicalNameIfNotNil(newHandle)) 170 171 // Handle in the background because we shouldn't lock during 172 // the notification 173 f.fs.queueNotification(func() { 174 ctx := context.Background() 175 session, err := idutil.GetCurrentSessionIfPossible(ctx, f.fs.config.KBPKI(), f.list.tlfType == tlf.Public) 176 // Here we get an error, but there is little that can be done. 177 // session will be empty in the error case in which case we will default to the 178 // canonical format. 179 if err != nil { 180 f.fs.log.CDebugf(ctx, 181 "tlfHandleChange: GetCurrentUserInfoIfPossible failed: %v", err) 182 } 183 oldName, newName := func() (tlf.PreferredName, tlf.PreferredName) { 184 f.handleMu.Lock() 185 defer f.handleMu.Unlock() 186 oldName := f.hPreferredName 187 if newHandle != nil { 188 f.h = newHandle 189 } 190 f.hPreferredName = f.h.GetPreferredFormat(session.Name) 191 return oldName, f.hPreferredName 192 }() 193 194 if oldName != newName { 195 f.list.updateTlfName(ctx, string(oldName), string(newName)) 196 } 197 }) 198 } 199 200 func canonicalNameIfNotNil(h *tlfhandle.Handle) string { 201 if h == nil { 202 return "(nil)" 203 } 204 return string(h.GetCanonicalName()) 205 } 206 207 func (f *Folder) resolve(ctx context.Context) (*tlfhandle.Handle, error) { 208 if f.h.TlfID() == tlf.NullID { 209 // If the handle doesn't have a TLF ID yet, fetch it now. 210 handle, err := tlfhandle.ParseHandlePreferred( 211 ctx, f.fs.config.KBPKI(), f.fs.config.MDOps(), f.fs.config, 212 string(f.hPreferredName), f.h.Type()) 213 switch errors.Cause(err).(type) { 214 case nil: 215 f.TlfHandleChange(ctx, handle) 216 return handle, nil 217 case idutil.NoSuchNameError, idutil.BadTLFNameError, 218 tlf.NoSuchUserError, idutil.NoSuchUserError: 219 return nil, dokan.ErrObjectNameNotFound 220 default: 221 return nil, err 222 } 223 } 224 225 // In case there were any unresolved assertions, try them again on 226 // the first load. Otherwise, since we haven't subscribed to 227 // updates yet for this folder, we might have missed a name 228 // change. 229 handle, err := f.h.ResolveAgain( 230 ctx, f.fs.config.KBPKI(), f.fs.config.MDOps(), f.fs.config) 231 if err != nil { 232 return nil, err 233 } 234 eq, err := f.h.Equals(f.fs.config.Codec(), *handle) 235 if err != nil { 236 return nil, err 237 } 238 if !eq { 239 // Make sure the name changes in the folder and the folder list 240 f.TlfHandleChange(ctx, handle) 241 } 242 return handle, nil 243 } 244 245 // Dir represents KBFS subdirectories. 246 type Dir struct { 247 FSO 248 } 249 250 func newDir(folder *Folder, node libkbfs.Node, name string, parent libkbfs.Node) *Dir { 251 d := &Dir{FSO{ 252 name: name, 253 parent: parent, 254 folder: folder, 255 node: node, 256 }} 257 d.refcount.Increase() 258 return d 259 } 260 261 // GetFileInformation for dokan. 262 func (d *Dir) GetFileInformation(ctx context.Context, fi *dokan.FileInfo) (st *dokan.Stat, err error) { 263 d.folder.fs.logEnter(ctx, "Dir GetFileInformation") 264 defer func() { d.folder.reportErr(ctx, libkbfs.ReadMode, err) }() 265 266 return eiToStat(d.folder.fs.config.KBFSOps().Stat(ctx, d.node)) 267 } 268 269 // SetFileAttributes for Dokan. 270 func (d *Dir) SetFileAttributes(ctx context.Context, fi *dokan.FileInfo, fileAttributes dokan.FileAttribute) error { 271 d.folder.fs.logEnter(ctx, "Dir SetFileAttributes") 272 // TODO handle attributes for real. 273 return nil 274 } 275 276 // isNoSuchNameError checks for libkbfs.NoSuchNameError. 277 func isNoSuchNameError(err error) bool { 278 _, ok := err.(idutil.NoSuchNameError) 279 return ok 280 } 281 282 // lastStr returns last string in a string slice or "" if the slice is empty. 283 func lastStr(strs []string) string { 284 if len(strs) == 0 { 285 return "" 286 } 287 return strs[len(strs)-1] 288 } 289 290 // isSafeFolder returns whether a Folder is considered safe. 291 func isSafeFolder(ctx context.Context, f *Folder) bool { 292 return libkbfs.IsOnlyWriterInNonTeamTlf(ctx, f.list.fs.config.KBPKI(), f.h) 293 } 294 295 // open tries to open a file. 296 func (d *Dir) open(ctx context.Context, oc *openContext, path []string) (dokan.File, dokan.CreateStatus, error) { 297 d.folder.fs.vlog.CLogf(ctx, libkb.VLog1, "Dir openDir %s", path) 298 299 specialNode := handleTLFSpecialFile(lastStr(path), d.folder) 300 if specialNode != nil { 301 return oc.returnFileNoCleanup(specialNode) 302 } 303 304 origPath := path 305 rootDir := d 306 for len(path) > 0 { 307 // Handle upper case filenames from junctions etc 308 if c := lowerTranslateCandidate(oc, path[0]); c != "" { 309 var hit string 310 var nhits int 311 err := d.FindFiles(ctx, nil, c, func(ns *dokan.NamedStat) error { 312 if strings.ToLower(ns.Name) == c { 313 hit = ns.Name 314 nhits++ 315 } 316 return nil 317 }) 318 if err != nil { 319 return nil, 0, dokan.ErrObjectNameNotFound 320 } 321 if nhits != 1 { 322 return nil, 0, dokan.ErrObjectNameNotFound 323 } 324 path[0] = hit 325 } 326 327 leaf := len(path) == 1 328 329 // Check if this is a per-file metainformation file, if so 330 // return the corresponding SpecialReadFile. 331 if leaf && strings.HasPrefix(path[0], libfs.FileInfoPrefix) { 332 if err := oc.ReturningFileAllowed(); err != nil { 333 return nil, 0, err 334 } 335 name := path[0][len(libfs.FileInfoPrefix):] 336 kbfsName, err := libkb.DecodeWindowsNameForKbfs(name) 337 if err != nil { 338 return nil, 0, err 339 } 340 return NewFileInfoFile(d.folder.fs, d.node, kbfsName), 0, nil 341 } 342 343 kbfsName, err := libkb.DecodeWindowsNameForKbfs(path[0]) 344 if err != nil { 345 return nil, 0, err 346 } 347 newNode, de, err := d.folder.fs.config.KBFSOps().Lookup( 348 ctx, d.node, d.node.ChildName(kbfsName)) 349 350 // If we are in the final component, check if it is a creation. 351 if leaf { 352 notFound := isNoSuchNameError(err) 353 switch { 354 case notFound && oc.isCreateDirectory(): 355 return d.mkdir(ctx, oc, path[0]) 356 case notFound && oc.isCreation(): 357 return d.create(ctx, oc, path[0]) 358 case !notFound && oc.isExistingError(): 359 return nil, 0, dokan.ErrFileAlreadyExists 360 } 361 } 362 363 // Return errors from Lookup 364 if err != nil { 365 return nil, 0, err 366 } 367 368 // Refuse to execute files by checking FILE_EXECUTE (not exported by syscall) 369 // in TLFs considered unsafe. 370 if de.Type.IsFile() && oc.CreateData.DesiredAccess&0x20 != 0 && !isSafeFolder(ctx, d.folder) { 371 d.folder.fs.log.CErrorf(ctx, "Denying execution access to: %q", path[0]) 372 373 return nil, 0, dokan.ErrAccessDenied 374 } 375 376 if newNode != nil { 377 d.folder.mu.Lock() 378 f := d.folder.nodes[newNode.GetID()] 379 d.folder.mu.Unlock() 380 // Symlinks don't have stored nodes, so they are impossible here. 381 switch x := f.(type) { 382 default: 383 return nil, 0, fmt.Errorf("unhandled node type: %T", f) 384 case nil: 385 case *File: 386 if err := oc.ReturningFileAllowed(); err != nil { 387 return nil, 0, err 388 } 389 x.refcount.Increase() 390 return openFile(ctx, oc, path, x) 391 case *Dir: 392 d = x 393 path = path[1:] 394 continue 395 } 396 } 397 switch de.Type { 398 default: 399 return nil, 0, fmt.Errorf("unhandled entry type: %v", de.Type) 400 case data.File, data.Exec: 401 if err := oc.ReturningFileAllowed(); err != nil { 402 return nil, 0, err 403 } 404 child := newFile(d.folder, newNode, path[0], d.node) 405 f, _, err := openFile(ctx, oc, path, child) 406 if err == nil { 407 d.folder.lockedAddNode(newNode, child) 408 } 409 return f, dokan.ExistingFile, err 410 case data.Dir: 411 child := newDir(d.folder, newNode, path[0], d.node) 412 d.folder.lockedAddNode(newNode, child) 413 d = child 414 path = path[1:] 415 case data.Sym: 416 return openSymlink(ctx, oc, d, rootDir, origPath, path, de.SymPath) 417 } 418 } 419 if err := oc.ReturningDirAllowed(); err != nil { 420 return nil, 0, err 421 } 422 d.refcount.Increase() 423 return d, dokan.ExistingDir, nil 424 } 425 426 func openFile(ctx context.Context, oc *openContext, path []string, f *File) (dokan.File, dokan.CreateStatus, error) { 427 var err error 428 // Files only allowed as leafs... 429 if len(path) > 1 { 430 return nil, 0, dokan.ErrObjectNameNotFound 431 } 432 if oc.isTruncate() { 433 err = f.folder.fs.config.KBFSOps().Truncate(ctx, f.node, 0) 434 } 435 if err != nil { 436 return nil, 0, err 437 } 438 return f, dokan.ExistingFile, nil 439 } 440 441 func openSymlink(ctx context.Context, oc *openContext, parent *Dir, rootDir *Dir, origPath, path []string, target string) (dokan.File, dokan.CreateStatus, error) { 442 // TODO handle file/directory type flags here from CreateOptions. 443 if !oc.reduceRedirectionsLeft() { 444 return nil, 0, dokan.ErrObjectNameNotFound 445 } 446 // Take relevant prefix of original path. 447 origPath = origPath[:len(origPath)-len(path)] 448 if len(path) == 1 && oc.isOpenReparsePoint() { 449 // a Symlink is never included in Folder.nodes, as it doesn't 450 // have a libkbfs.Node to keep track of renames. 451 // Here we may get an error if the symlink destination does not exist. 452 // which is fine, treat such non-existing targets as symlinks to a file. 453 cst, err := resolveSymlinkIsDir(ctx, oc, rootDir, origPath, target) 454 parent.folder.fs.vlog.CLogf( 455 ctx, libkb.VLog1, "openSymlink leaf returned %v,%v => %v,%v", 456 origPath, target, cst, err) 457 return &Symlink{parent: parent, name: path[0], isTargetADirectory: cst.IsDir()}, cst, nil 458 } 459 // reference symlink, symbolic links always use '/' instead of '\'. 460 if target == "" || target[0] == '/' { 461 return nil, 0, dokan.ErrNotSupported 462 } 463 464 dst, err := resolveSymlinkPath(ctx, origPath, target) 465 parent.folder.fs.vlog.CLogf( 466 ctx, libkb.VLog1, "openSymlink resolve returned %v,%v => %v,%v", 467 origPath, target, dst, err) 468 if err != nil { 469 return nil, 0, err 470 } 471 dst = append(dst, path[1:]...) 472 return rootDir.open(ctx, oc, dst) 473 } 474 475 func getExclFromOpenContext(oc *openContext) libkbfs.Excl { 476 return libkbfs.Excl(oc.CreateDisposition == dokan.FileCreate) 477 } 478 479 func (d *Dir) create(ctx context.Context, oc *openContext, name string) (f dokan.File, cst dokan.CreateStatus, err error) { 480 if name, err = libkb.DecodeWindowsNameForKbfs(name); err != nil { 481 return nil, 0, err 482 } 483 namePPS := d.node.ChildName(name) 484 d.folder.fs.vlog.CLogf(ctx, libkb.VLog1, "Dir Create %s", namePPS) 485 defer func() { d.folder.reportErr(ctx, libkbfs.WriteMode, err) }() 486 487 isExec := false // Windows lacks executable modes. 488 excl := getExclFromOpenContext(oc) 489 newNode, _, err := d.folder.fs.config.KBFSOps().CreateFile( 490 ctx, d.node, namePPS, isExec, excl) 491 if err != nil { 492 return nil, 0, err 493 } 494 495 child := newFile(d.folder, newNode, name, d.node) 496 d.folder.lockedAddNode(newNode, child) 497 return child, dokan.NewFile, nil 498 } 499 500 func (d *Dir) mkdir(ctx context.Context, oc *openContext, name string) ( 501 f *Dir, cst dokan.CreateStatus, err error) { 502 if name, err = libkb.DecodeWindowsNameForKbfs(name); err != nil { 503 return nil, 0, err 504 } 505 namePPS := d.node.ChildName(name) 506 d.folder.fs.vlog.CLogf(ctx, libkb.VLog1, "Dir Mkdir %s", namePPS) 507 defer func() { d.folder.reportErr(ctx, libkbfs.WriteMode, err) }() 508 509 newNode, _, err := d.folder.fs.config.KBFSOps().CreateDir( 510 ctx, d.node, namePPS) 511 if err != nil { 512 return nil, 0, err 513 } 514 515 child := newDir(d.folder, newNode, name, d.node) 516 d.folder.lockedAddNode(newNode, child) 517 return child, dokan.NewDir, nil 518 } 519 520 // FindFiles does readdir for dokan. 521 func (d *Dir) FindFiles(ctx context.Context, fi *dokan.FileInfo, ignored string, callback func(*dokan.NamedStat) error) (err error) { 522 d.folder.fs.logEnter(ctx, "Dir FindFiles") 523 defer func() { d.folder.reportErr(ctx, libkbfs.ReadMode, err) }() 524 525 children, err := d.folder.fs.config.KBFSOps().GetDirChildren(ctx, d.node) 526 if err != nil { 527 return err 528 } 529 530 empty := true 531 var ns dokan.NamedStat 532 for pps, de := range children { 533 windowsName := libkb.EncodeKbfsNameForWindows(pps.Plaintext()) 534 empty = false 535 ns.Name = windowsName 536 // TODO perhaps resolve symlinks here? 537 fillStat(&ns.Stat, &de) 538 if strings.HasPrefix(windowsName, HiddenFilePrefix) { 539 addFileAttribute(&ns.Stat, dokan.FileAttributeHidden) 540 } 541 err = callback(&ns) 542 if err != nil { 543 return err 544 } 545 } 546 if empty { 547 return dokan.ErrObjectNameNotFound 548 } 549 return nil 550 } 551 552 // CanDeleteDirectory - return just nil 553 // TODO check for permissions here. 554 func (d *Dir) CanDeleteDirectory(ctx context.Context, fi *dokan.FileInfo) (err error) { 555 d.folder.fs.logEnterf(ctx, "Dir CanDeleteDirectory %q", d.name) 556 defer func() { d.folder.reportErr(ctx, libkbfs.WriteMode, err) }() 557 558 children, err := d.folder.fs.config.KBFSOps().GetDirChildren(ctx, d.node) 559 if err != nil { 560 return errToDokan(err) 561 } 562 if len(children) > 0 { 563 return dokan.ErrDirectoryNotEmpty 564 } 565 566 return nil 567 } 568 569 // Cleanup - forget references, perform deletions etc. 570 // If Cleanup is called with non-nil FileInfo that has IsDeleteOnClose() 571 // no libdokan locks should be held prior to the call. 572 func (d *Dir) Cleanup(ctx context.Context, fi *dokan.FileInfo) { 573 namePPS := d.node.ChildName(d.name) 574 var err error 575 if fi != nil { 576 d.folder.fs.logEnterf(ctx, "Dir Cleanup %s delete=%v", namePPS, 577 fi.IsDeleteOnClose()) 578 } else { 579 d.folder.fs.logEnterf(ctx, "Dir Cleanup %s", namePPS) 580 } 581 defer func() { d.folder.reportErr(ctx, libkbfs.WriteMode, err) }() 582 583 if fi != nil && fi.IsDeleteOnClose() && d.parent != nil { 584 // renameAndDeletionLock should be the first lock to be grabbed in libdokan. 585 d.folder.fs.renameAndDeletionLock.Lock() 586 defer d.folder.fs.renameAndDeletionLock.Unlock() 587 d.folder.fs.vlog.CLogf( 588 ctx, libkb.VLog1, "Removing (Delete) dir in cleanup %s", namePPS) 589 590 err = d.folder.fs.config.KBFSOps().RemoveDir(ctx, d.parent, namePPS) 591 } 592 593 if d.refcount.Decrease() { 594 d.folder.forgetNode(ctx, d.node) 595 } 596 } 597 598 func resolveSymlinkPath(ctx context.Context, origPath []string, targetPath string) ([]string, error) { 599 pathComponents := make([]string, len(origPath), len(origPath)+1) 600 copy(pathComponents, origPath) 601 602 for _, p := range strings.FieldsFunc(targetPath, isPathSeparator) { 603 switch p { 604 case ".": 605 case "..": 606 if len(pathComponents) == 0 { 607 return nil, dokan.ErrNotSupported 608 } 609 pathComponents = pathComponents[:len(pathComponents)-1] 610 default: 611 pathComponents = append(pathComponents, p) 612 } 613 } 614 return pathComponents, nil 615 } 616 617 func resolveSymlinkIsDir(ctx context.Context, oc *openContext, rootDir *Dir, origPath []string, targetPath string) (dokan.CreateStatus, error) { 618 dst, err := resolveSymlinkPath(ctx, origPath, targetPath) 619 if err != nil { 620 return dokan.NewFile, err 621 } 622 obj, cst, err := rootDir.open(ctx, oc, dst) 623 if err == nil { 624 obj.Cleanup(ctx, nil) 625 } 626 return cst, err 627 } 628 func isPathSeparator(r rune) bool { 629 return r == '/' || r == '\\' 630 } 631 632 func asDir(ctx context.Context, f dokan.File) *Dir { 633 switch x := f.(type) { 634 case *Dir: 635 return x 636 case *TLF: 637 branch := x.folder.getFolderBranch().Branch 638 filterErr := false 639 if branch != data.MasterBranch { 640 filterErr = true 641 } 642 d, _, _ := x.loadDirHelper( 643 ctx, "asDir", libkbfs.WriteMode, branch, filterErr) 644 return d 645 } 646 return nil 647 }