From 8ad554e7301b90c72ed91a0e72f33f4218b31090 Mon Sep 17 00:00:00 2001 From: Dylan Cant Date: Mon, 13 Jan 2025 09:14:47 +0100 Subject: [PATCH] ConditionalMatch with multiple etags --- fs_local.go | 25 +++++++++++++------------ webdav.go | 37 ++++++++++++++++++++++++++++++++----- webdav_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 webdav_test.go diff --git a/fs_local.go b/fs_local.go index c2cfd46..ce3c363 100644 --- a/fs_local.go +++ b/fs_local.go @@ -126,19 +126,20 @@ func (fs LocalFileSystem) Create(ctx context.Context, name string, body io.ReadC etag = fi.ETag } - if opts.IfMatch.IsSet() { - if ok, err := opts.IfMatch.MatchETag(etag); err != nil { - return nil, false, NewHTTPError(http.StatusBadRequest, err) - } else if !ok { - return nil, false, NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("If-Match condition failed")) - } + if isSet, ok, err := opts.IfMatch.MatchETag(etag); !isSet { + // not set so continue + } else if err != nil { + return nil, false, NewHTTPError(http.StatusBadRequest, err) + } else if isSet && !ok { + return nil, false, NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("If-Match condition failed")) } - if opts.IfNoneMatch.IsSet() { - if ok, err := opts.IfNoneMatch.MatchETag(etag); err != nil { - return nil, false, NewHTTPError(http.StatusBadRequest, err) - } else if ok { - return nil, false, NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("If-None-Match condition failed")) - } + + if isSet, ok, err := opts.IfNoneMatch.MatchETag(etag); !isSet { + // not set so continue + } else if err != nil { + return nil, false, NewHTTPError(http.StatusBadRequest, err) + } else if isSet && ok { + return nil, false, NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("If-None-Match condition failed")) } wc, err := os.Create(p) diff --git a/webdav.go b/webdav.go index eb5c742..243c82c 100644 --- a/webdav.go +++ b/webdav.go @@ -4,6 +4,7 @@ package webdav import ( + "strings" "time" "github.com/emersion/go-webdav/internal" @@ -54,10 +55,36 @@ func (val ConditionalMatch) ETag() (string, error) { return string(e), nil } -func (val ConditionalMatch) MatchETag(etag string) (bool, error) { - if val.IsWildcard() { - return true, nil +// MatchETag checks if the ETag provided matches any of the ETags in the ConditionalMatch header value. +// +// Parameters: +// - etag: The ETag to match against. +// +// Returns: +// - isSet: Indicates if the ConditionalMatch has any ETags set, or is wildcard. +// - match: True if the etag matches any of the ETags in ConditionalMatch, false otherwise. +// - err: An error if there was a problem parsing one of the ETags. +// If an error occurs during parsing, match will be set to false, but isSet will be true. +// Callers should check for a non-nil error to ensure the match result is valid. +// +// The function returns early if no ETags are set (isSet is false) or if a wildcard (*) is used, +// in which case all ETags match. For multiple ETags, it checks each one until a match is found or all are checked. +func (val ConditionalMatch) MatchETag(etag string) (isSet bool, match bool, err error) { + if !val.IsSet() { + return false, false, nil + } else if val.IsWildcard() { + return true, true, nil + } + quoted_etags := strings.Split(string(val), ", ") + for _, quoted_etag := range quoted_etags { + var e internal.ETag + if err := e.UnmarshalText([]byte(quoted_etag)); err != nil { + // opinionated returning `false` on match so caller + // should definitely check for non-nil `err` + return true, false, err + } else if string(e) == etag { + return true, true, nil + } } - t, err := val.ETag() - return t == etag, err + return true, false, nil } diff --git a/webdav_test.go b/webdav_test.go new file mode 100644 index 0000000..4773aab --- /dev/null +++ b/webdav_test.go @@ -0,0 +1,46 @@ +package webdav + +import ( + "testing" +) + +func TestConditionalMatch(t *testing.T) { + // testing match and no match + val := ConditionalMatch("\"AAA\", \"BBB\", \"CCC\"") + if isSet, ok, err := val.MatchETag("AAA"); err != nil { + t.Fatal(err) + } else if !isSet { + t.Fatalf("Expected isSet true") + } else if !ok { + t.Fatalf("Expected ok true") + } + if isSet, ok, err := val.MatchETag("DDD"); err != nil { + t.Fatal(err) + } else if !isSet { + t.Fatalf("Expected isSet true") + } else if ok { + t.Fatalf("Expected ok false") + } + + // testing parse error + val = ConditionalMatch("\"AAA\", BBB, CCC") + if _, _, err := val.MatchETag("BBB"); err == nil { + t.Fatalf("Expected non-nil error") + } + + // testing WildCard + val = ConditionalMatch("*") + if isSet, ok, err := val.MatchETag("BBB"); err != nil { + t.Fatal(err) + } else if !isSet { + t.Fatalf("Expected isSet true") + } else if !ok { + t.Fatalf("Expected ok true") + } + + // testing isSet + val = ConditionalMatch("") + if isSet, _, _ := val.MatchETag("BBB"); isSet { + t.Fatalf("Expected isSet false") + } +}