taildrop: document and cleanup the package (#9699)

Changes made:
* Unexport declarations specific to internal taildrop functionality.
* Document all exported functionality.
* Move TestRedactErr to the taildrop package.
* Rename and invert Handler.DirectFileDoFinalRename as AvoidFinalRename.

Updates tailscale/corp#14772

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
pull/9700/head
Joe Tsai 1 year ago committed by GitHub
parent e6aa7b815d
commit e4cb83b18b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -3545,11 +3545,11 @@ func (b *LocalBackend) initPeerAPIListener() {
ps := &peerAPIServer{ ps := &peerAPIServer{
b: b, b: b,
taildrop: &taildrop.Handler{ taildrop: &taildrop.Handler{
Logf: b.logf, Logf: b.logf,
Clock: b.clock, Clock: b.clock,
RootDir: fileRoot, Dir: fileRoot,
DirectFileMode: b.directFileRoot != "", DirectFileMode: b.directFileRoot != "",
DirectFileDoFinalRename: b.directFileDoFinalRename, AvoidFinalRename: !b.directFileDoFinalRename,
}, },
} }
if dm, ok := b.sys.DNSManager.GetOK(); ok { if dm, ok := b.sys.DNSManager.GetOK(); ok {

@ -5,7 +5,6 @@ package ipnlocal
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
@ -67,7 +66,7 @@ func bodyNotContains(sub string) check {
func fileHasSize(name string, size int) check { func fileHasSize(name string, size int) check {
return func(t *testing.T, e *peerAPITestEnv) { return func(t *testing.T, e *peerAPITestEnv) {
root := e.ph.ps.taildrop.RootDir root := e.ph.ps.taildrop.Dir
if root == "" { if root == "" {
t.Errorf("no rootdir; can't check whether %q has size %v", name, size) t.Errorf("no rootdir; can't check whether %q has size %v", name, size)
return return
@ -83,7 +82,7 @@ func fileHasSize(name string, size int) check {
func fileHasContents(name string, want string) check { func fileHasContents(name string, want string) check {
return func(t *testing.T, e *peerAPITestEnv) { return func(t *testing.T, e *peerAPITestEnv) {
root := e.ph.ps.taildrop.RootDir root := e.ph.ps.taildrop.Dir
if root == "" { if root == "" {
t.Errorf("no rootdir; can't check contents of %q", name) t.Errorf("no rootdir; can't check contents of %q", name)
return return
@ -498,7 +497,7 @@ func TestHandlePeerAPI(t *testing.T) {
Clock: &tstest.Clock{}, Clock: &tstest.Clock{},
} }
} }
e.ph.ps.taildrop.RootDir = rootDir e.ph.ps.taildrop.Dir = rootDir
} }
for _, req := range tt.reqs { for _, req := range tt.reqs {
e.rr = httptest.NewRecorder() e.rr = httptest.NewRecorder()
@ -538,9 +537,9 @@ func TestFileDeleteRace(t *testing.T) {
clock: &tstest.Clock{}, clock: &tstest.Clock{},
}, },
taildrop: &taildrop.Handler{ taildrop: &taildrop.Handler{
Logf: t.Logf, Logf: t.Logf,
Clock: &tstest.Clock{}, Clock: &tstest.Clock{},
RootDir: dir, Dir: dir,
}, },
} }
ph := &peerAPIHandler{ ph := &peerAPIHandler{
@ -635,67 +634,3 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
t.Errorf("unexpectedly IPv6 deny; wanted to be a DNS server") t.Errorf("unexpectedly IPv6 deny; wanted to be a DNS server")
} }
} }
func TestRedactErr(t *testing.T) {
testCases := []struct {
name string
err func() error
want string
}{
{
name: "PathError",
err: func() error {
return &os.PathError{
Op: "open",
Path: "/tmp/sensitive.txt",
Err: fs.ErrNotExist,
}
},
want: `open redacted.41360718: file does not exist`,
},
{
name: "LinkError",
err: func() error {
return &os.LinkError{
Op: "symlink",
Old: "/tmp/sensitive.txt",
New: "/tmp/othersensitive.txt",
Err: fs.ErrNotExist,
}
},
want: `symlink redacted.41360718 redacted.6bcf093a: file does not exist`,
},
{
name: "something else",
err: func() error { return errors.New("i am another error type") },
want: `i am another error type`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// For debugging
var i int
for err := tc.err(); err != nil; err = errors.Unwrap(err) {
t.Logf("%d: %T @ %p", i, err, err)
i++
}
t.Run("Root", func(t *testing.T) {
got := taildrop.RedactErr(tc.err()).Error()
if got != tc.want {
t.Errorf("err = %q; want %q", got, tc.want)
}
})
t.Run("Wrapped", func(t *testing.T) {
wrapped := fmt.Errorf("wrapped error: %w", tc.err())
want := "wrapped error: " + tc.want
got := taildrop.RedactErr(wrapped).Error()
if got != want {
t.Errorf("err = %q; want %q", got, want)
}
})
})
}
}

@ -19,10 +19,10 @@ import (
"tailscale.com/logtail/backoff" "tailscale.com/logtail/backoff"
) )
// HasFilesWaiting reports whether any files are buffered in the // HasFilesWaiting reports whether any files are buffered in [Handler.Dir].
// tailscaled daemon storage. // This always returns false when [Handler.DirectFileMode] is false.
func (s *Handler) HasFilesWaiting() bool { func (s *Handler) HasFilesWaiting() bool {
if s == nil || s.RootDir == "" || s.DirectFileMode { if s == nil || s.Dir == "" || s.DirectFileMode {
return false return false
} }
if s.knownEmpty.Load() { if s.knownEmpty.Load() {
@ -33,7 +33,7 @@ func (s *Handler) HasFilesWaiting() bool {
// keep this negative cache. // keep this negative cache.
return false return false
} }
f, err := os.Open(s.RootDir) f, err := os.Open(s.Dir)
if err != nil { if err != nil {
return false return false
} }
@ -42,7 +42,7 @@ func (s *Handler) HasFilesWaiting() bool {
des, err := f.ReadDir(10) des, err := f.ReadDir(10)
for _, de := range des { for _, de := range des {
name := de.Name() name := de.Name()
if strings.HasSuffix(name, PartialSuffix) { if strings.HasSuffix(name, partialSuffix) {
continue continue
} }
if name, ok := strings.CutSuffix(name, deletedSuffix); ok { // for Windows + tests if name, ok := strings.CutSuffix(name, deletedSuffix); ok { // for Windows + tests
@ -51,16 +51,16 @@ func (s *Handler) HasFilesWaiting() bool {
// as the OS may return "foo.jpg.deleted" before "foo.jpg" // as the OS may return "foo.jpg.deleted" before "foo.jpg"
// and we don't want to delete the ".deleted" file before // and we don't want to delete the ".deleted" file before
// enumerating to the "foo.jpg" file. // enumerating to the "foo.jpg" file.
defer tryDeleteAgain(filepath.Join(s.RootDir, name)) defer tryDeleteAgain(filepath.Join(s.Dir, name))
continue continue
} }
if de.Type().IsRegular() { if de.Type().IsRegular() {
_, err := os.Stat(filepath.Join(s.RootDir, name+deletedSuffix)) _, err := os.Stat(filepath.Join(s.Dir, name+deletedSuffix))
if os.IsNotExist(err) { if os.IsNotExist(err) {
return true return true
} }
if err == nil { if err == nil {
tryDeleteAgain(filepath.Join(s.RootDir, name)) tryDeleteAgain(filepath.Join(s.Dir, name))
continue continue
} }
} }
@ -76,22 +76,19 @@ func (s *Handler) HasFilesWaiting() bool {
} }
// WaitingFiles returns the list of files that have been sent by a // WaitingFiles returns the list of files that have been sent by a
// peer that are waiting in the buffered "pick up" directory owned by // peer that are waiting in [Handler.Dir].
// the Tailscale daemon. // This always returns nil when [Handler.DirectFileMode] is false.
//
// As a side effect, it also does any lazy deletion of files as
// required by Windows.
func (s *Handler) WaitingFiles() (ret []apitype.WaitingFile, err error) { func (s *Handler) WaitingFiles() (ret []apitype.WaitingFile, err error) {
if s == nil { if s == nil {
return nil, errNilHandler return nil, errNilHandler
} }
if s.RootDir == "" { if s.Dir == "" {
return nil, ErrNoTaildrop return nil, errNoTaildrop
} }
if s.DirectFileMode { if s.DirectFileMode {
return nil, nil return nil, nil
} }
f, err := os.Open(s.RootDir) f, err := os.Open(s.Dir)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -101,7 +98,7 @@ func (s *Handler) WaitingFiles() (ret []apitype.WaitingFile, err error) {
des, err := f.ReadDir(10) des, err := f.ReadDir(10)
for _, de := range des { for _, de := range des {
name := de.Name() name := de.Name()
if strings.HasSuffix(name, PartialSuffix) { if strings.HasSuffix(name, partialSuffix) {
continue continue
} }
if name, ok := strings.CutSuffix(name, deletedSuffix); ok { // for Windows + tests if name, ok := strings.CutSuffix(name, deletedSuffix); ok { // for Windows + tests
@ -143,7 +140,7 @@ func (s *Handler) WaitingFiles() (ret []apitype.WaitingFile, err error) {
// Maybe Windows is done virus scanning the file we tried // Maybe Windows is done virus scanning the file we tried
// to delete a long time ago and will let us delete it now. // to delete a long time ago and will let us delete it now.
for name := range deleted { for name := range deleted {
tryDeleteAgain(filepath.Join(s.RootDir, name)) tryDeleteAgain(filepath.Join(s.Dir, name))
} }
} }
sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name }) sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name })
@ -164,17 +161,19 @@ func tryDeleteAgain(fullPath string) {
} }
} }
// DeleteFile deletes a file of the given baseName from [Handler.Dir].
// This method is only allowed when [Handler.DirectFileMode] is false.
func (s *Handler) DeleteFile(baseName string) error { func (s *Handler) DeleteFile(baseName string) error {
if s == nil { if s == nil {
return errNilHandler return errNilHandler
} }
if s.RootDir == "" { if s.Dir == "" {
return ErrNoTaildrop return errNoTaildrop
} }
if s.DirectFileMode { if s.DirectFileMode {
return errors.New("deletes not allowed in direct mode") return errors.New("deletes not allowed in direct mode")
} }
path, ok := s.DiskPath(baseName) path, ok := s.diskPath(baseName)
if !ok { if !ok {
return errors.New("bad filename") return errors.New("bad filename")
} }
@ -184,7 +183,7 @@ func (s *Handler) DeleteFile(baseName string) error {
for { for {
err := os.Remove(path) err := os.Remove(path)
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
err = RedactErr(err) err = redactErr(err)
// Put a retry loop around deletes on Windows. Windows // Put a retry loop around deletes on Windows. Windows
// file descriptor closes are effectively asynchronous, // file descriptor closes are effectively asynchronous,
// as a bunch of hooks run on/after close, and we can't // as a bunch of hooks run on/after close, and we can't
@ -203,7 +202,7 @@ func (s *Handler) DeleteFile(baseName string) error {
bo.BackOff(context.Background(), err) bo.BackOff(context.Background(), err)
continue continue
} }
if err := TouchFile(path + deletedSuffix); err != nil { if err := touchFile(path + deletedSuffix); err != nil {
logf("peerapi: failed to leave deleted marker: %v", err) logf("peerapi: failed to leave deleted marker: %v", err)
} }
} }
@ -214,25 +213,27 @@ func (s *Handler) DeleteFile(baseName string) error {
} }
} }
func TouchFile(path string) error { func touchFile(path string) error {
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666) f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
if err != nil { if err != nil {
return RedactErr(err) return redactErr(err)
} }
return f.Close() return f.Close()
} }
// OpenFile opens a file of the given baseName from [Handler.Dir].
// This method is only allowed when [Handler.DirectFileMode] is false.
func (s *Handler) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) { func (s *Handler) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
if s == nil { if s == nil {
return nil, 0, errNilHandler return nil, 0, errNilHandler
} }
if s.RootDir == "" { if s.Dir == "" {
return nil, 0, ErrNoTaildrop return nil, 0, errNoTaildrop
} }
if s.DirectFileMode { if s.DirectFileMode {
return nil, 0, errors.New("opens not allowed in direct mode") return nil, 0, errors.New("opens not allowed in direct mode")
} }
path, ok := s.DiskPath(baseName) path, ok := s.diskPath(baseName)
if !ok { if !ok {
return nil, 0, errors.New("bad filename") return nil, 0, errors.New("bad filename")
} }
@ -242,12 +243,12 @@ func (s *Handler) OpenFile(baseName string) (rc io.ReadCloser, size int64, err e
} }
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
return nil, 0, RedactErr(err) return nil, 0, redactErr(err)
} }
fi, err := f.Stat() fi, err := f.Stat()
if err != nil { if err != nil {
f.Close() f.Close()
return nil, 0, RedactErr(err) return nil, 0, redactErr(err)
} }
return f, fi.Size(), nil return f, fi.Size(), nil
} }

@ -63,6 +63,8 @@ func (f *incomingFile) Write(p []byte) (n int, err error) {
} }
// HandlePut receives a file. // HandlePut receives a file.
// It handles an HTTP PUT request to the "/v0/put/{filename}" endpoint,
// where {filename} is a base filename.
// It returns the number of bytes received and whether it was received successfully. // It returns the number of bytes received and whether it was received successfully.
func (h *Handler) HandlePut(w http.ResponseWriter, r *http.Request) (finalSize int64, success bool) { func (h *Handler) HandlePut(w http.ResponseWriter, r *http.Request) (finalSize int64, success bool) {
if !envknob.CanTaildrop() { if !envknob.CanTaildrop() {
@ -73,8 +75,8 @@ func (h *Handler) HandlePut(w http.ResponseWriter, r *http.Request) (finalSize i
http.Error(w, "expected method PUT", http.StatusMethodNotAllowed) http.Error(w, "expected method PUT", http.StatusMethodNotAllowed)
return finalSize, success return finalSize, success
} }
if h == nil || h.RootDir == "" { if h == nil || h.Dir == "" {
http.Error(w, ErrNoTaildrop.Error(), http.StatusInternalServerError) http.Error(w, errNoTaildrop.Error(), http.StatusInternalServerError)
return finalSize, success return finalSize, success
} }
if distro.Get() == distro.Unraid && !h.DirectFileMode { if distro.Get() == distro.Unraid && !h.DirectFileMode {
@ -100,9 +102,9 @@ func (h *Handler) HandlePut(w http.ResponseWriter, r *http.Request) (finalSize i
http.Error(w, "bad path encoding", http.StatusBadRequest) http.Error(w, "bad path encoding", http.StatusBadRequest)
return finalSize, success return finalSize, success
} }
dstFile, ok := h.DiskPath(baseName) dstFile, ok := h.diskPath(baseName)
if !ok { if !ok {
http.Error(w, "bad filename", 400) http.Error(w, "bad filename", http.StatusBadRequest)
return finalSize, success return finalSize, success
} }
// TODO(bradfitz): prevent same filename being sent by two peers at once // TODO(bradfitz): prevent same filename being sent by two peers at once
@ -113,10 +115,10 @@ func (h *Handler) HandlePut(w http.ResponseWriter, r *http.Request) (finalSize i
return finalSize, success return finalSize, success
} }
partialFile := dstFile + PartialSuffix partialFile := dstFile + partialSuffix
f, err := os.Create(partialFile) f, err := os.Create(partialFile)
if err != nil { if err != nil {
h.Logf("put Create error: %v", RedactErr(err)) h.Logf("put Create error: %v", redactErr(err))
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return finalSize, success return finalSize, success
} }
@ -146,7 +148,7 @@ func (h *Handler) HandlePut(w http.ResponseWriter, r *http.Request) (finalSize i
defer h.incomingFiles.Delete(inFile) defer h.incomingFiles.Delete(inFile)
n, err := io.Copy(inFile, r.Body) n, err := io.Copy(inFile, r.Body)
if err != nil { if err != nil {
err = RedactErr(err) err = redactErr(err)
f.Close() f.Close()
h.Logf("put Copy error: %v", err) h.Logf("put Copy error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -154,18 +156,18 @@ func (h *Handler) HandlePut(w http.ResponseWriter, r *http.Request) (finalSize i
} }
finalSize = n finalSize = n
} }
if err := RedactErr(f.Close()); err != nil { if err := redactErr(f.Close()); err != nil {
h.Logf("put Close error: %v", err) h.Logf("put Close error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return finalSize, success return finalSize, success
} }
if h.DirectFileMode && !h.DirectFileDoFinalRename { if h.DirectFileMode && h.AvoidFinalRename {
if inFile != nil { // non-zero length; TODO: notify even for zero length if inFile != nil { // non-zero length; TODO: notify even for zero length
inFile.markAndNotifyDone() inFile.markAndNotifyDone()
} }
} else { } else {
if err := os.Rename(partialFile, dstFile); err != nil { if err := os.Rename(partialFile, dstFile); err != nil {
err = RedactErr(err) err = redactErr(err)
h.Logf("put final rename: %v", err) h.Logf("put final rename: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return finalSize, success return finalSize, success

@ -1,6 +1,12 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
// Package taildrop contains the implementation of the Taildrop
// functionality including sending and retrieving files.
// This package does not validate permissions, the caller should
// be responsible for ensuring correct authorization.
//
// For related documentation see: http://go/taildrop-how-does-it-work
package taildrop package taildrop
import ( import (
@ -22,30 +28,38 @@ import (
"tailscale.com/util/multierr" "tailscale.com/util/multierr"
) )
// Handler manages the state for receiving and managing taildropped files.
type Handler struct { type Handler struct {
Logf logger.Logf Logf logger.Logf
Clock tstime.Clock Clock tstime.Clock
RootDir string // empty means file receiving unavailable // Dir is the directory to store received files.
// This main either be the final location for the files
// DirectFileMode is whether we're writing files directly to a // or just a temporary staging directory (see DirectFileMode).
// download directory (as *.partial files), rather than making Dir string
// the frontend retrieve it over localapi HTTP and write it
// somewhere itself. This is used on the GUI macOS versions // DirectFileMode reports whether we are writing files
// and on Synology. // directly to a download directory, rather than writing them to
// In DirectFileMode, the peerapi doesn't do the final rename // a temporary staging directory.
// from "foo.jpg.partial" to "foo.jpg" unless //
// directFileDoFinalRename is set. // The following methods:
// - HasFilesWaiting
// - WaitingFiles
// - DeleteFile
// - OpenFile
// have no purpose in DirectFileMode.
// They are only used to check whether files are in the staging directory,
// copy them out, and then delete them.
DirectFileMode bool DirectFileMode bool
// DirectFileDoFinalRename is whether in directFileMode we // AvoidFinalRename specifies whether in DirectFileMode
// additionally move the *.direct file to its final name after // we should avoid renaming "foo.jpg.partial" to "foo.jpg" after reception.
// it's received. AvoidFinalRename bool
DirectFileDoFinalRename bool
// SendFileNotify is called periodically while a file is actively // SendFileNotify is called periodically while a file is actively
// receiving the contents for the file. There is a final call // receiving the contents for the file. There is a final call
// to the function when reception completes. // to the function when reception completes.
// It is not called if nil.
SendFileNotify func() SendFileNotify func()
knownEmpty atomic.Bool knownEmpty atomic.Bool
@ -55,13 +69,13 @@ type Handler struct {
var ( var (
errNilHandler = errors.New("handler unavailable; not listening") errNilHandler = errors.New("handler unavailable; not listening")
ErrNoTaildrop = errors.New("Taildrop disabled; no storage directory") errNoTaildrop = errors.New("Taildrop disabled; no storage directory")
) )
const ( const (
// PartialSuffix is the suffix appended to files while they're // partialSuffix is the suffix appended to files while they're
// still in the process of being transferred. // still in the process of being transferred.
PartialSuffix = ".partial" partialSuffix = ".partial"
// deletedSuffix is the suffix for a deleted marker file // deletedSuffix is the suffix for a deleted marker file
// that's placed next to a file (without the suffix) that we // that's placed next to a file (without the suffix) that we
@ -93,7 +107,7 @@ func validFilenameRune(r rune) bool {
return unicode.IsPrint(r) return unicode.IsPrint(r)
} }
func (s *Handler) DiskPath(baseName string) (fullPath string, ok bool) { func (s *Handler) diskPath(baseName string) (fullPath string, ok bool) {
if !utf8.ValidString(baseName) { if !utf8.ValidString(baseName) {
return "", false return "", false
} }
@ -108,7 +122,7 @@ func (s *Handler) DiskPath(baseName string) (fullPath string, ok bool) {
if clean != baseName || if clean != baseName ||
clean == "." || clean == ".." || clean == "." || clean == ".." ||
strings.HasSuffix(clean, deletedSuffix) || strings.HasSuffix(clean, deletedSuffix) ||
strings.HasSuffix(clean, PartialSuffix) { strings.HasSuffix(clean, partialSuffix) {
return "", false return "", false
} }
for _, r := range baseName { for _, r := range baseName {
@ -119,7 +133,7 @@ func (s *Handler) DiskPath(baseName string) (fullPath string, ok bool) {
if !filepath.IsLocal(baseName) { if !filepath.IsLocal(baseName) {
return "", false return "", false
} }
return filepath.Join(s.RootDir, baseName), true return filepath.Join(s.Dir, baseName), true
} }
func (s *Handler) IncomingFiles() []ipn.PartialFile { func (s *Handler) IncomingFiles() []ipn.PartialFile {
@ -166,7 +180,7 @@ func redactString(s string) string {
return string(b) return string(b)
} }
func RedactErr(root error) error { func redactErr(root error) error {
// redactStrings is a list of sensitive strings that were redacted. // redactStrings is a list of sensitive strings that were redacted.
// It is not sufficient to just snub out sensitive fields in Go errors // It is not sufficient to just snub out sensitive fields in Go errors
// since some wrapper errors like fmt.Errorf pre-cache the error string, // since some wrapper errors like fmt.Errorf pre-cache the error string,

@ -4,6 +4,9 @@
package taildrop package taildrop
import ( import (
"errors"
"fmt"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -13,7 +16,7 @@ import (
// Tests "foo.jpg.deleted" marks (for Windows). // Tests "foo.jpg.deleted" marks (for Windows).
func TestDeletedMarkers(t *testing.T) { func TestDeletedMarkers(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
h := &Handler{RootDir: dir} h := &Handler{Dir: dir}
nothingWaiting := func() { nothingWaiting := func() {
t.Helper() t.Helper()
@ -24,7 +27,7 @@ func TestDeletedMarkers(t *testing.T) {
} }
touch := func(base string) { touch := func(base string) {
t.Helper() t.Helper()
if err := TouchFile(filepath.Join(dir, base)); err != nil { if err := touchFile(filepath.Join(dir, base)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }
@ -86,3 +89,67 @@ func TestDeletedMarkers(t *testing.T) {
rc.Close() rc.Close()
} }
} }
func TestRedactErr(t *testing.T) {
testCases := []struct {
name string
err func() error
want string
}{
{
name: "PathError",
err: func() error {
return &os.PathError{
Op: "open",
Path: "/tmp/sensitive.txt",
Err: fs.ErrNotExist,
}
},
want: `open redacted.41360718: file does not exist`,
},
{
name: "LinkError",
err: func() error {
return &os.LinkError{
Op: "symlink",
Old: "/tmp/sensitive.txt",
New: "/tmp/othersensitive.txt",
Err: fs.ErrNotExist,
}
},
want: `symlink redacted.41360718 redacted.6bcf093a: file does not exist`,
},
{
name: "something else",
err: func() error { return errors.New("i am another error type") },
want: `i am another error type`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// For debugging
var i int
for err := tc.err(); err != nil; err = errors.Unwrap(err) {
t.Logf("%d: %T @ %p", i, err, err)
i++
}
t.Run("Root", func(t *testing.T) {
got := redactErr(tc.err()).Error()
if got != tc.want {
t.Errorf("err = %q; want %q", got, tc.want)
}
})
t.Run("Wrapped", func(t *testing.T) {
wrapped := fmt.Errorf("wrapped error: %w", tc.err())
want := "wrapped error: " + tc.want
got := redactErr(wrapped).Error()
if got != want {
t.Errorf("err = %q; want %q", got, want)
}
})
})
}
}

Loading…
Cancel
Save