webdav: implement parseTimeout.

Change-Id: Ieb3d550d06adc7457ed6870b0dd5fbbfede7c488
Reviewed-on: https://go-review.googlesource.com/3074
Reviewed-by: Dave Cheney <dave@cheney.net>
diff --git a/webdav/lock.go b/webdav/lock.go
index 2ecefc4..d31dec1 100644
--- a/webdav/lock.go
+++ b/webdav/lock.go
@@ -287,3 +287,33 @@
 	// held is whether this node's lock is actively held by a Confirm call.
 	held bool
 }
+
+const infiniteTimeout = -1
+
+// parseTimeout parses the Timeout HTTP header, as per section 10.7. If s is
+// empty, an infiniteTimeout is returned.
+func parseTimeout(s string) (time.Duration, error) {
+	if s == "" {
+		return infiniteTimeout, nil
+	}
+	if i := strings.IndexByte(s, ','); i >= 0 {
+		s = s[:i]
+	}
+	s = strings.TrimSpace(s)
+	if s == "Infinite" {
+		return infiniteTimeout, nil
+	}
+	const pre = "Second-"
+	if !strings.HasPrefix(s, pre) {
+		return 0, errInvalidTimeout
+	}
+	s = s[len(pre):]
+	if s == "" || s[0] < '0' || '9' < s[0] {
+		return 0, errInvalidTimeout
+	}
+	n, err := strconv.ParseInt(s, 10, 64)
+	if err != nil || 1<<32-1 < n {
+		return 0, errInvalidTimeout
+	}
+	return time.Duration(n) * time.Second, nil
+}
diff --git a/webdav/lock_test.go b/webdav/lock_test.go
index 0ae5308..9ee7865 100644
--- a/webdav/lock_test.go
+++ b/webdav/lock_test.go
@@ -251,3 +251,100 @@
 	}
 	return nil
 }
+
+func TestParseTimeout(t *testing.T) {
+	testCases := []struct {
+		s       string
+		want    time.Duration
+		wantErr error
+	}{{
+		"",
+		infiniteTimeout,
+		nil,
+	}, {
+		"Infinite",
+		infiniteTimeout,
+		nil,
+	}, {
+		"Infinitesimal",
+		0,
+		errInvalidTimeout,
+	}, {
+		"infinite",
+		0,
+		errInvalidTimeout,
+	}, {
+		"Second-0",
+		0 * time.Second,
+		nil,
+	}, {
+		"Second-123",
+		123 * time.Second,
+		nil,
+	}, {
+		"  Second-456    ",
+		456 * time.Second,
+		nil,
+	}, {
+		"Second-4100000000",
+		4100000000 * time.Second,
+		nil,
+	}, {
+		"junk",
+		0,
+		errInvalidTimeout,
+	}, {
+		"Second-",
+		0,
+		errInvalidTimeout,
+	}, {
+		"Second--1",
+		0,
+		errInvalidTimeout,
+	}, {
+		"Second--123",
+		0,
+		errInvalidTimeout,
+	}, {
+		"Second-+123",
+		0,
+		errInvalidTimeout,
+	}, {
+		"Second-0x123",
+		0,
+		errInvalidTimeout,
+	}, {
+		"second-123",
+		0,
+		errInvalidTimeout,
+	}, {
+		"Second-4294967295",
+		4294967295 * time.Second,
+		nil,
+	}, {
+		// Section 10.7 says that "The timeout value for TimeType "Second"
+		// must not be greater than 2^32-1."
+		"Second-4294967296",
+		0,
+		errInvalidTimeout,
+	}, {
+		// This test case comes from section 9.10.9 of the spec. It says,
+		//
+		// "In this request, the client has specified that it desires an
+		// infinite-length lock, if available, otherwise a timeout of 4.1
+		// billion seconds, if available."
+		//
+		// The Go WebDAV package always supports infinite length locks,
+		// and ignores the fallback after the comma.
+		"Infinite, Second-4100000000",
+		infiniteTimeout,
+		nil,
+	}}
+
+	for _, tc := range testCases {
+		got, gotErr := parseTimeout(tc.s)
+		if got != tc.want || gotErr != tc.wantErr {
+			t.Errorf("parsing %q:\ngot  %v, %v\nwant %v, %v", tc.s, got, gotErr, tc.want, tc.wantErr)
+		}
+	}
+}
diff --git a/webdav/webdav.go b/webdav/webdav.go
index 6ca037b..d596a39 100644
--- a/webdav/webdav.go
+++ b/webdav/webdav.go
@@ -323,11 +323,6 @@
 	return invalidDepth
 }
 
-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
@@ -360,6 +355,7 @@
 	errInvalidLockInfo     = errors.New("webdav: invalid lock info")
 	errInvalidLockToken    = errors.New("webdav: invalid lock token")
 	errInvalidPropfind     = errors.New("webdav: invalid propfind")
+	errInvalidTimeout      = errors.New("webdav: invalid timeout")
 	errNoFileSystem        = errors.New("webdav: no file system")
 	errNoLockSystem        = errors.New("webdav: no lock system")
 	errNotADirectory       = errors.New("webdav: not a directory")