diff --git a/webdav/file.go b/webdav/file.go new file mode 100644 index 00000000..a2953ae4 --- /dev/null +++ b/webdav/file.go @@ -0,0 +1,28 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package webdav + +import ( + "io" + "net/http" + "os" +) + +// TODO: comment that paths are always "/"-separated, even for Windows servers. + +type FileSystem interface { + Mkdir(path string, perm os.FileMode) error + OpenFile(path string, flag int, perm os.FileMode) (File, error) + RemoveAll(path string) error + Stat(path string) (os.FileInfo, error) +} + +type File interface { + http.File + io.Writer +} + +// TODO: a MemFS implementation. +// TODO: a RealFS implementation, backed by the real, OS-provided file system. diff --git a/webdav/if.go b/webdav/if.go index e4d46706..416e81cd 100644 --- a/webdav/if.go +++ b/webdav/if.go @@ -16,18 +16,10 @@ type ifHeader struct { lists []ifList } -// ifList is a conjunction (AND) of ifConditions, and an optional resource tag. +// ifList is a conjunction (AND) of Conditions, and an optional resource tag. type ifList struct { resourceTag string - conditions []ifCondition -} - -// ifCondition can match a WebDAV resource, based on a stateToken or ETag. -// Exactly one of stateToken and entityTag should be non-empty. -type ifCondition struct { - not bool - stateToken string - entityTag string + conditions []Condition } // parseIfHeader parses the "If: foo bar" HTTP header. The httpHeader string @@ -110,19 +102,19 @@ func parseList(s string) (l ifList, remaining string, ok bool) { } } -func parseCondition(s string) (c ifCondition, remaining string, ok bool) { +func parseCondition(s string) (c Condition, remaining string, ok bool) { tokenType, tokenStr, s := lex(s) if tokenType == notTokenType { - c.not = true + c.Not = true tokenType, tokenStr, s = lex(s) } switch tokenType { case strTokenType, angleTokenType: - c.stateToken = tokenStr + c.Token = tokenStr case squareTokenType: - c.entityTag = tokenStr + c.ETag = tokenStr default: - return ifCondition{}, "", false + return Condition{}, "", false } return c, s, true } diff --git a/webdav/if_test.go b/webdav/if_test.go index 684e8b32..aad61a40 100644 --- a/webdav/if_test.go +++ b/webdav/if_test.go @@ -38,7 +38,7 @@ func TestParseIfHeader(t *testing.T) { ``, ifHeader{}, }, { - "bad: no list after resource #1", + "bad: no list after resource #2", ` (a)`, ifHeader{}, }, { @@ -66,12 +66,12 @@ func TestParseIfHeader(t *testing.T) { `(Not Not a)`, ifHeader{}, }, { - "good: one list with a stateToken", + "good: one list with a Token", `(a)`, ifHeader{ lists: []ifList{{ - conditions: []ifCondition{{ - stateToken: `a`, + conditions: []Condition{{ + Token: `a`, }}, }}, }, @@ -80,8 +80,8 @@ func TestParseIfHeader(t *testing.T) { `([a])`, ifHeader{ lists: []ifList{{ - conditions: []ifCondition{{ - entityTag: `a`, + conditions: []Condition{{ + ETag: `a`, }}, }}, }, @@ -90,15 +90,15 @@ func TestParseIfHeader(t *testing.T) { `(Not a Not b Not [d])`, ifHeader{ lists: []ifList{{ - conditions: []ifCondition{{ - not: true, - stateToken: `a`, + conditions: []Condition{{ + Not: true, + Token: `a`, }, { - not: true, - stateToken: `b`, + Not: true, + Token: `b`, }, { - not: true, - entityTag: `d`, + Not: true, + ETag: `d`, }}, }}, }, @@ -107,12 +107,12 @@ func TestParseIfHeader(t *testing.T) { `(a) (b)`, ifHeader{ lists: []ifList{{ - conditions: []ifCondition{{ - stateToken: `a`, + conditions: []Condition{{ + Token: `a`, }}, }, { - conditions: []ifCondition{{ - stateToken: `b`, + conditions: []Condition{{ + Token: `b`, }}, }}, }, @@ -121,14 +121,14 @@ func TestParseIfHeader(t *testing.T) { `(Not a) (Not b)`, ifHeader{ lists: []ifList{{ - conditions: []ifCondition{{ - not: true, - stateToken: `a`, + conditions: []Condition{{ + Not: true, + Token: `a`, }}, }, { - conditions: []ifCondition{{ - not: true, - stateToken: `b`, + conditions: []Condition{{ + Not: true, + Token: `b`, }}, }}, }, @@ -139,8 +139,8 @@ func TestParseIfHeader(t *testing.T) { ifHeader{ lists: []ifList{{ resourceTag: `http://www.example.com/users/f/fielding/index.html`, - conditions: []ifCondition{{ - stateToken: `urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6`, + conditions: []Condition{{ + Token: `urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6`, }}, }}, }, @@ -149,8 +149,8 @@ func TestParseIfHeader(t *testing.T) { `()`, ifHeader{ lists: []ifList{{ - conditions: []ifCondition{{ - stateToken: `urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf`, + conditions: []Condition{{ + Token: `urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf`, }}, }}, }, @@ -161,8 +161,8 @@ func TestParseIfHeader(t *testing.T) { ifHeader{ lists: []ifList{{ resourceTag: `http://example.com/locked/`, - conditions: []ifCondition{{ - stateToken: `urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf`, + conditions: []Condition{{ + Token: `urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf`, }}, }}, }, @@ -173,8 +173,8 @@ func TestParseIfHeader(t *testing.T) { ifHeader{ lists: []ifList{{ resourceTag: `http://example.com/locked/member`, - conditions: []ifCondition{{ - stateToken: `urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf`, + conditions: []Condition{{ + Token: `urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf`, }}, }}, }, @@ -184,12 +184,12 @@ func TestParseIfHeader(t *testing.T) { ()`, ifHeader{ lists: []ifList{{ - conditions: []ifCondition{{ - stateToken: `urn:uuid:fe184f2e-6eec-41d0-c765-01adc56e6bb4`, + conditions: []Condition{{ + Token: `urn:uuid:fe184f2e-6eec-41d0-c765-01adc56e6bb4`, }}, }, { - conditions: []ifCondition{{ - stateToken: `urn:uuid:e454f3f3-acdc-452a-56c7-00a5c91e4b77`, + conditions: []Condition{{ + Token: `urn:uuid:e454f3f3-acdc-452a-56c7-00a5c91e4b77`, }}, }}, }, @@ -198,8 +198,8 @@ func TestParseIfHeader(t *testing.T) { `()`, ifHeader{ lists: []ifList{{ - conditions: []ifCondition{{ - stateToken: `urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4`, + conditions: []Condition{{ + Token: `urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4`, }}, }}, }, @@ -210,14 +210,14 @@ func TestParseIfHeader(t *testing.T) { (["I am another ETag"])`, ifHeader{ lists: []ifList{{ - conditions: []ifCondition{{ - stateToken: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`, + conditions: []Condition{{ + Token: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`, }, { - entityTag: `"I am an ETag"`, + ETag: `"I am an ETag"`, }}, }, { - conditions: []ifCondition{{ - entityTag: `"I am another ETag"`, + conditions: []Condition{{ + ETag: `"I am another ETag"`, }}, }}, }, @@ -227,11 +227,11 @@ func TestParseIfHeader(t *testing.T) { )`, ifHeader{ lists: []ifList{{ - conditions: []ifCondition{{ - not: true, - stateToken: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`, + conditions: []Condition{{ + Not: true, + Token: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`, }, { - stateToken: `urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092`, + Token: `urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092`, }}, }}, }, @@ -241,13 +241,13 @@ func TestParseIfHeader(t *testing.T) { (Not )`, ifHeader{ lists: []ifList{{ - conditions: []ifCondition{{ - stateToken: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`, + conditions: []Condition{{ + Token: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`, }}, }, { - conditions: []ifCondition{{ - not: true, - stateToken: `DAV:no-lock`, + conditions: []Condition{{ + Not: true, + Token: `DAV:no-lock`, }}, }}, }, @@ -259,15 +259,15 @@ func TestParseIfHeader(t *testing.T) { ifHeader{ lists: []ifList{{ resourceTag: `/resource1`, - conditions: []ifCondition{{ - stateToken: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`, + conditions: []Condition{{ + Token: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`, }, { - entityTag: `W/"A weak ETag"`, + ETag: `W/"A weak ETag"`, }}, }, { resourceTag: `/resource1`, - conditions: []ifCondition{{ - entityTag: `"strong ETag"`, + conditions: []Condition{{ + ETag: `"strong ETag"`, }}, }}, }, @@ -278,8 +278,8 @@ func TestParseIfHeader(t *testing.T) { ifHeader{ lists: []ifList{{ resourceTag: `http://www.example.com/specs/`, - conditions: []ifCondition{{ - stateToken: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`, + conditions: []Condition{{ + Token: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`, }}, }}, }, @@ -289,8 +289,8 @@ func TestParseIfHeader(t *testing.T) { ifHeader{ lists: []ifList{{ resourceTag: `/specs/rfc2518.doc`, - conditions: []ifCondition{{ - entityTag: `"4217"`, + conditions: []Condition{{ + ETag: `"4217"`, }}, }}, }, @@ -300,9 +300,9 @@ func TestParseIfHeader(t *testing.T) { ifHeader{ lists: []ifList{{ resourceTag: `/specs/rfc2518.doc`, - conditions: []ifCondition{{ - not: true, - entityTag: `"4217"`, + conditions: []Condition{{ + Not: true, + ETag: `"4217"`, }}, }}, }, diff --git a/webdav/lock.go b/webdav/lock.go new file mode 100644 index 00000000..6538ded7 --- /dev/null +++ b/webdav/lock.go @@ -0,0 +1,44 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package webdav + +import ( + "errors" + "io" + "time" +) + +var ( + ErrConfirmationFailed = errors.New("webdav: confirmation failed") + ErrForbidden = errors.New("webdav: forbidden") + ErrNoSuchLock = errors.New("webdav: no such lock") +) + +// Condition can match a WebDAV resource, based on a token or ETag. +// Exactly one of Token and ETag should be non-empty. +type Condition struct { + Not bool + Token string + ETag string +} + +type LockSystem interface { + // TODO: comment that the conditions should be ANDed together. + Confirm(path string, conditions ...Condition) (c io.Closer, err error) + // TODO: comment that token should be an absolute URI as defined by RFC 3986, + // Section 4.3. In particular, it should not contain whitespace. + Create(path string, now time.Time, ld LockDetails) (token string, c io.Closer, err error) + Refresh(token string, now time.Time, duration time.Duration) (ld LockDetails, c io.Closer, err error) + Unlock(token string) error +} + +type LockDetails struct { + Depth int // Negative means infinite depth. + Duration time.Duration // Negative means unlimited duration. + OwnerXML string // Verbatim XML. + Path string +} + +// TODO: a MemLS implementation. diff --git a/webdav/webdav.go b/webdav/webdav.go new file mode 100644 index 00000000..7ffb8597 --- /dev/null +++ b/webdav/webdav.go @@ -0,0 +1,295 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package webdav etc etc TODO. +package webdav + +// TODO: ETag, properties. +// TODO: figure out what/when is responsible for path cleaning: no "../../etc/passwd"s. + +import ( + "errors" + "io" + "net/http" + "os" + "time" +) + +// TODO: define the PropSystem interface. +type PropSystem interface{} + +type Handler struct { + // FileSystem is the virtual file system. + FileSystem FileSystem + // LockSystem is the lock management system. + LockSystem LockSystem + // PropSystem is an optional property management system. If non-nil, TODO. + PropSystem PropSystem + // Logger is an optional error logger. If non-nil, it will be called + // whenever handling a http.Request results in an error. + Logger func(*http.Request, error) +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + status, err := http.StatusBadRequest, error(nil) + if h.FileSystem == nil { + status, err = http.StatusInternalServerError, errNoFileSystem + } else if h.LockSystem == nil { + status, err = http.StatusInternalServerError, errNoLockSystem + } else { + // TODO: COPY, MOVE, PROPFIND, PROPPATCH methods. Also, OPTIONS?? + switch r.Method { + case "GET", "HEAD", "POST": + status, err = h.handleGetHeadPost(w, r) + case "DELETE": + status, err = h.handleDelete(w, r) + case "PUT": + status, err = h.handlePut(w, r) + case "MKCOL": + status, err = h.handleMkcol(w, r) + case "LOCK": + status, err = h.handleLock(w, r) + case "UNLOCK": + status, err = h.handleUnlock(w, r) + } + } + + if status != 0 { + w.WriteHeader(status) + if status != http.StatusNoContent { + w.Write([]byte(StatusText(status))) + } + } + if h.Logger != nil && err != nil { + h.Logger(r, err) + } +} + +func (h *Handler) confirmLocks(r *http.Request) (closer io.Closer, status int, err error) { + ih, ok := parseIfHeader(r.Header.Get("If")) + if !ok { + return nil, http.StatusBadRequest, errInvalidIfHeader + } + // ih is a disjunction (OR) of ifLists, so any ifList will do. + for _, l := range ih.lists { + path := l.resourceTag + if path == "" { + path = r.URL.Path + } + closer, err = h.LockSystem.Confirm(path, l.conditions...) + if err == ErrConfirmationFailed { + continue + } + if err != nil { + return nil, http.StatusInternalServerError, err + } + return closer, 0, nil + } + return nil, http.StatusPreconditionFailed, errLocked +} + +func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (status int, err error) { + // TODO: check locks for read-only access?? + f, err := h.FileSystem.OpenFile(r.URL.Path, os.O_RDONLY, 0) + if err != nil { + return http.StatusNotFound, err + } + defer f.Close() + fi, err := f.Stat() + if err != nil { + return http.StatusNotFound, err + } + http.ServeContent(w, r, r.URL.Path, fi.ModTime(), f) + return 0, nil +} + +func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status int, err error) { + closer, status, err := h.confirmLocks(r) + if err != nil { + return status, err + } + defer closer.Close() + + if err := h.FileSystem.RemoveAll(r.URL.Path); err != nil { + // TODO: MultiStatus. + return http.StatusMethodNotAllowed, err + } + return http.StatusNoContent, nil +} + +func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int, err error) { + closer, status, err := h.confirmLocks(r) + if err != nil { + return status, err + } + defer closer.Close() + + f, err := h.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return http.StatusNotFound, err + } + defer f.Close() + if _, err := io.Copy(f, r.Body); err != nil { + return http.StatusMethodNotAllowed, err + } + return http.StatusCreated, nil +} + +func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status int, err error) { + closer, status, err := h.confirmLocks(r) + if err != nil { + return status, err + } + defer closer.Close() + + if err := h.FileSystem.Mkdir(r.URL.Path, 0777); err != nil { + if os.IsNotExist(err) { + return http.StatusConflict, err + } + return http.StatusMethodNotAllowed, err + } + return http.StatusCreated, nil +} + +func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus int, retErr error) { + duration, err := parseTimeout(r.Header.Get("Timeout")) + if err != nil { + return http.StatusBadRequest, err + } + li, status, err := readLockInfo(r.Body) + if err != nil { + return status, err + } + + token, ld := "", LockDetails{} + if li == (lockInfo{}) { + // An empty lockInfo means to refresh the lock. + ih, ok := parseIfHeader(r.Header.Get("If")) + if !ok { + return http.StatusBadRequest, errInvalidIfHeader + } + if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 { + token = ih.lists[0].conditions[0].Token + } + if token == "" { + return http.StatusBadRequest, errInvalidLockToken + } + var closer io.Closer + ld, closer, err = h.LockSystem.Refresh(token, time.Now(), duration) + if err != nil { + if err == ErrNoSuchLock { + return http.StatusPreconditionFailed, err + } + return http.StatusInternalServerError, err + } + defer closer.Close() + + } else { + depth, err := parseDepth(r.Header.Get("Depth")) + if err != nil { + return http.StatusBadRequest, err + } + ld = LockDetails{ + Depth: depth, + Duration: duration, + OwnerXML: li.Owner.InnerXML, + Path: r.URL.Path, + } + var closer io.Closer + token, closer, err = h.LockSystem.Create(r.URL.Path, time.Now(), ld) + if err != nil { + return http.StatusInternalServerError, err + } + defer func() { + if retErr != nil { + h.LockSystem.Unlock(token) + } + }() + defer closer.Close() + + // Create the resource if it didn't previously exist. + if _, err := h.FileSystem.Stat(r.URL.Path); err != nil { + f, err := h.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + // TODO: detect missing intermediate dirs and return http.StatusConflict? + return http.StatusInternalServerError, err + } + f.Close() + w.WriteHeader(http.StatusCreated) + // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the + // Lock-Token value is a Coded-URL. We add angle brackets. + w.Header().Set("Lock-Token", "<"+token+">") + } + } + + w.Header().Set("Content-Type", "application/xml; charset=utf-8") + writeLockInfo(w, token, ld) + return 0, nil +} + +func (h *Handler) handleUnlock(w http.ResponseWriter, r *http.Request) (status int, err error) { + // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the + // Lock-Token value is a Coded-URL. We strip its angle brackets. + t := r.Header.Get("Lock-Token") + if len(t) < 2 || t[0] != '<' || t[len(t)-1] != '>' { + return http.StatusBadRequest, errInvalidLockToken + } + t = t[1 : len(t)-1] + + switch err = h.LockSystem.Unlock(t); err { + case nil: + return http.StatusNoContent, err + case ErrForbidden: + return http.StatusForbidden, err + case ErrNoSuchLock: + return http.StatusConflict, err + default: + return http.StatusInternalServerError, err + } +} + +func parseDepth(s string) (int, error) { + // TODO: implement. + return -1, nil +} + +func parseTimeout(s string) (time.Duration, error) { + // TODO: implement. + return 1 * time.Second, nil +} + +// http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11 +const ( + StatusMulti = 207 + StatusUnprocessableEntity = 422 + StatusLocked = 423 + StatusFailedDependency = 424 + StatusInsufficientStorage = 507 +) + +func StatusText(code int) string { + switch code { + case StatusMulti: + return "Multi-Status" + case StatusUnprocessableEntity: + return "Unprocessable Entity" + case StatusLocked: + return "Locked" + case StatusFailedDependency: + return "Failed Dependency" + case StatusInsufficientStorage: + return "Insufficient Storage" + } + return http.StatusText(code) +} + +var ( + errInvalidIfHeader = errors.New("webdav: invalid If header") + errInvalidLockInfo = errors.New("webdav: invalid lock info") + errInvalidLockToken = errors.New("webdav: invalid lock token") + errLocked = errors.New("webdav: locked") + errNoFileSystem = errors.New("webdav: no file system") + errNoLockSystem = errors.New("webdav: no lock system") + errUnsupportedLockInfo = errors.New("webdav: unsupported lock info") +) diff --git a/webdav/xml.go b/webdav/xml.go new file mode 100644 index 00000000..5939373c --- /dev/null +++ b/webdav/xml.go @@ -0,0 +1,96 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package webdav + +// The XML encoding is covered by Section 14. +// http://www.webdav.org/specs/rfc4918.html#xml.element.definitions + +import ( + "bytes" + "encoding/xml" + "fmt" + "io" + "net/http" + "strconv" + "time" +) + +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_lockinfo +type lockInfo struct { + XMLName xml.Name `xml:"lockinfo"` + Exclusive *struct{} `xml:"lockscope>exclusive"` + Shared *struct{} `xml:"lockscope>shared"` + Write *struct{} `xml:"locktype>write"` + Owner owner `xml:"owner"` +} + +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_owner +type owner struct { + InnerXML string `xml:",innerxml"` +} + +func readLockInfo(r io.Reader) (li lockInfo, status int, err error) { + c := &countingReader{r: r} + if err = xml.NewDecoder(c).Decode(&li); err != nil { + if err == io.EOF { + if c.n == 0 { + // An empty body means to refresh the lock. + // http://www.webdav.org/specs/rfc4918.html#refreshing-locks + return lockInfo{}, 0, nil + } + err = errInvalidLockInfo + } + return lockInfo{}, http.StatusBadRequest, err + } + // We only support exclusive (non-shared) write locks. In practice, these are + // the only types of locks that seem to matter. + if li.Exclusive == nil || li.Shared != nil || li.Write == nil { + return lockInfo{}, http.StatusNotImplemented, errUnsupportedLockInfo + } + return li, 0, nil +} + +type countingReader struct { + n int + r io.Reader +} + +func (c *countingReader) Read(p []byte) (int, error) { + n, err := c.r.Read(p) + c.n += n + return n, err +} + +func writeLockInfo(w io.Writer, token string, ld LockDetails) (int, error) { + depth := "infinity" + if d := ld.Depth; d >= 0 { + depth = strconv.Itoa(d) + } + timeout := ld.Duration / time.Second + return fmt.Fprintf(w, "\n"+ + "\n"+ + " \n"+ + " \n"+ + " %s\n"+ + " %s\n"+ + " Second-%d\n"+ + " %s\n"+ + " %s\n"+ + "", + depth, ld.OwnerXML, timeout, escape(token), escape(ld.Path), + ) +} + +func escape(s string) string { + for i := 0; i < len(s); i++ { + switch s[i] { + case '"', '&', '\'', '<', '>': + b := bytes.NewBuffer(nil) + xml.EscapeText(b, []byte(s)) + return b.String() + } + } + return s +} diff --git a/webdav/xml_test.go b/webdav/xml_test.go new file mode 100644 index 00000000..26149a22 --- /dev/null +++ b/webdav/xml_test.go @@ -0,0 +1,129 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package webdav + +import ( + "encoding/xml" + "net/http" + "reflect" + "strings" + "testing" +) + +func TestParseLockInfo(t *testing.T) { + // The "section x.y.z" test cases come from section x.y.z of the spec at + // http://www.webdav.org/specs/rfc4918.html + testCases := []struct { + desc string + input string + wantLI lockInfo + wantStatus int + }{{ + "bad: junk", + "xxx", + lockInfo{}, + http.StatusBadRequest, + }, { + "bad: invalid owner XML", + "" + + "\n" + + " \n" + + " \n" + + " \n" + + " no end tag \n" + + " \n" + + "", + lockInfo{}, + http.StatusBadRequest, + }, { + "bad: invalid UTF-8", + "" + + "\n" + + " \n" + + " \n" + + " \n" + + " \xff \n" + + " \n" + + "", + lockInfo{}, + http.StatusBadRequest, + }, { + "bad: unfinished XML #1", + "" + + "\n" + + " \n" + + " \n", + lockInfo{}, + http.StatusBadRequest, + }, { + "bad: unfinished XML #2", + "" + + "\n" + + " \n" + + " \n" + + " \n", + lockInfo{}, + http.StatusBadRequest, + }, { + "good: empty", + "", + lockInfo{}, + 0, + }, { + "good: plain-text owner", + "" + + "\n" + + " \n" + + " \n" + + " gopher\n" + + "", + lockInfo{ + XMLName: xml.Name{Space: "DAV:", Local: "lockinfo"}, + Exclusive: new(struct{}), + Write: new(struct{}), + Owner: owner{ + InnerXML: "gopher", + }, + }, + 0, + }, { + "section 9.10.7", + "" + + "\n" + + " \n" + + " \n" + + " \n" + + " http://example.org/~ejw/contact.html\n" + + " \n" + + "", + lockInfo{ + XMLName: xml.Name{Space: "DAV:", Local: "lockinfo"}, + Exclusive: new(struct{}), + Write: new(struct{}), + Owner: owner{ + InnerXML: "\n http://example.org/~ejw/contact.html\n ", + }, + }, + 0, + }} + + for _, tc := range testCases { + li, status, err := readLockInfo(strings.NewReader(tc.input)) + if tc.wantStatus != 0 { + if err == nil { + t.Errorf("%s: got nil error, want non-nil", tc.desc) + continue + } + } else if err != nil { + t.Errorf("%s: %v", tc.desc, err) + continue + } + if !reflect.DeepEqual(li, tc.wantLI) || status != tc.wantStatus { + t.Errorf("%s:\ngot lockInfo=%v, status=%v\nwant lockInfo=%v, status=%v", + tc.desc, li, status, tc.wantLI, tc.wantStatus) + continue + } + } +}