html: limit buffering during tokenization.
This is optional. By default, buffering is unlimited.
Fixes golang/go#7053
R=bradfitz
CC=golang-codereviews
https://golang.org/cl/43190044
diff --git a/html/token.go b/html/token.go
index c43debb..6e3624c 100644
--- a/html/token.go
+++ b/html/token.go
@@ -6,6 +6,7 @@
import (
"bytes"
+ "errors"
"io"
"strconv"
"strings"
@@ -33,6 +34,9 @@
DoctypeToken
)
+// ErrBufferExceeded means that the buffering limit was exceeded.
+var ErrBufferExceeded = errors.New("max buffer exceeded")
+
// String returns a string representation of the TokenType.
func (t TokenType) String() string {
switch t {
@@ -142,6 +146,8 @@
// buf[raw.end:] is buffered input that will yield future tokens.
raw span
buf []byte
+ // maxBuf limits the data buffered in buf. A value of 0 means unlimited.
+ maxBuf int
// buf[data.start:data.end] holds the raw bytes of the current token's data:
// a text token's text, a tag token's tag name, etc.
data span
@@ -273,6 +279,10 @@
}
x := z.buf[z.raw.end]
z.raw.end++
+ if z.maxBuf > 0 && z.raw.end-z.raw.start >= z.maxBuf {
+ z.err = ErrBufferExceeded
+ return 0
+ }
return x
}
@@ -1167,6 +1177,12 @@
return t
}
+// SetMaxBuf sets a limit on the amount of data buffered during tokenization.
+// A value of 0 means unlimited.
+func (z *Tokenizer) SetMaxBuf(n int) {
+ z.maxBuf = n
+}
+
// NewTokenizer returns a new HTML Tokenizer for the given Reader.
// The input is assumed to be UTF-8 encoded.
func NewTokenizer(r io.Reader) *Tokenizer {
diff --git a/html/token_test.go b/html/token_test.go
index 7d54d89..1db43c2 100644
--- a/html/token_test.go
+++ b/html/token_test.go
@@ -469,6 +469,63 @@
}
}
+func TestMaxBuffer(t *testing.T) {
+ // Exceeding the maximum buffer size generates ErrBufferExceeded.
+ z := NewTokenizer(strings.NewReader("<" + strings.Repeat("t", 10)))
+ z.SetMaxBuf(5)
+ tt := z.Next()
+ if got, want := tt, ErrorToken; got != want {
+ t.Fatalf("token type: got: %v want: %v", got, want)
+ }
+ if got, want := z.Err(), ErrBufferExceeded; got != want {
+ t.Errorf("error type: got: %v want: %v", got, want)
+ }
+ if got, want := string(z.Raw()), "<tttt"; got != want {
+ t.Fatalf("buffered before overflow: got: %q want: %q", got, want)
+ }
+}
+
+func TestMaxBufferReconstruction(t *testing.T) {
+ // Exceeding the maximum buffer size at any point while tokenizing permits
+ // reconstructing the original input.
+tests:
+ for _, test := range tokenTests {
+ buffer:
+ for maxBuf := 1; ; maxBuf++ {
+ r := strings.NewReader(test.html)
+ z := NewTokenizer(r)
+ z.SetMaxBuf(maxBuf)
+ var tokenized bytes.Buffer
+ for {
+ tt := z.Next()
+ tokenized.Write(z.Raw())
+ if tt == ErrorToken {
+ if z.Err() == ErrBufferExceeded {
+ continue buffer
+ }
+ // EOF is expected, and indicates that we found the max maxBuf that
+ // generates ErrBufferExceeded, so continue to the next test.
+ if err := z.Err(); err != io.EOF {
+ t.Errorf("%s: unexpected error: %v", test.desc, err)
+ }
+ break
+ }
+ }
+ // Anything tokenizing along with input left in the reader.
+ assembled, err := ioutil.ReadAll(io.MultiReader(&tokenized, r))
+ if err != nil {
+ t.Errorf("%s: ReadAll: %v", test.desc, err)
+ continue tests
+ }
+ if got, want := string(assembled), test.html; got != want {
+ t.Errorf("%s: reassembled html:\n got: %q\nwant: %q", test.desc, got, want)
+ continue tests
+ }
+ break
+ } // buffer sizes
+ } // tests
+}
+
func TestPassthrough(t *testing.T) {
// Accumulating the raw output for each parse event should reconstruct the
// original input.