diff --git a/bpf/doc.go b/bpf/doc.go index 04ec1c8..1ea566b 100644 --- a/bpf/doc.go +++ b/bpf/doc.go
@@ -49,6 +49,13 @@ functions. Currently, the only extensions supported by this package are the Linux packet filter extensions. +# Security Considerations + +The implementation of the BPF VM in this package is suitable for +testing BPF programs. It aims for consistency with other BPF VM +implementations, but divergence in behavior is not considered a +security issue. + # Examples This packet filter selects all ARP packets.
diff --git a/dns/dnsmessage/message_test.go b/dns/dnsmessage/message_test.go index 03f9b42..9e46c63 100644 --- a/dns/dnsmessage/message_test.go +++ b/dns/dnsmessage/message_test.go
@@ -1924,3 +1924,63 @@ t.Fatalf("msg[maxPtr:] = %v, want: %v", msg[maxPtr:], expect) } } + +func TestInvalidMessages(t *testing.T) { + for _, test := range []struct { + name string + in []byte + }{ + { + name: "invalid SVCB", + in: []byte{ + 0x12, 0x34, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x41, 0x00, 0x01, + 0x00, 0x00, 0x41, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x64, + 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x5d, + }, + }, + { + name: "SVCB out-of-order keys", + in: []byte{ + 0x12, 0x34, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x41, 0x00, 0x01, + 0x00, 0x00, 0x41, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0b, + 0x00, 0x01, 0x00, + 0x00, 0x02, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, + }, + }, + { + name: "SVCB duplicate keys", + in: []byte{ + 0x12, 0x34, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x41, 0x00, 0x01, + 0x00, 0x00, 0x41, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0b, + 0x00, 0x01, 0x00, + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, + }, + }, + } { + t.Run(test.name, func(t *testing.T) { + var p Parser + _, err := p.Start(test.in) + if err != nil { + return + } + _, err = p.AllQuestions() + if err != nil { + return + } + _, err = p.AllAnswers() + if err != nil { + return + } + t.Errorf("successfully parsed message: want error") + t.Errorf("message: {%x}", test.in) + }) + } +}
diff --git a/dns/dnsmessage/svcb.go b/dns/dnsmessage/svcb.go index 4840516..7729378 100644 --- a/dns/dnsmessage/svcb.go +++ b/dns/dnsmessage/svcb.go
@@ -168,9 +168,8 @@ if err != nil { return oldMsg, &nestedError{"SVCBResource.Target", err} } - var previousKey SVCParamKey for i, param := range r.Params { - if i > 0 && param.Key <= previousKey { + if i > 0 && param.Key <= r.Params[i-1].Key { return oldMsg, &nestedError{"SVCBResource.Params", errParamOutOfOrder} } if len(param.Value) > (1<<16)-1 { @@ -205,7 +204,7 @@ off = paramsOff var previousKey uint16 for off < bodyEnd { - var key, len uint16 + var key, size uint16 if key, off, err = unpackUint16(msg, off); err != nil { return SVCBResource{}, &nestedError{"Params key", err} } @@ -214,14 +213,15 @@ // consider the RR malformed if the SvcParamKeys are not in strictly increasing numeric order return SVCBResource{}, &nestedError{"Params", errParamOutOfOrder} } - if len, off, err = unpackUint16(msg, off); err != nil { + if size, off, err = unpackUint16(msg, off); err != nil { return SVCBResource{}, &nestedError{"Params value length", err} } - if off+int(len) > bodyEnd { + if off+int(size) > bodyEnd { return SVCBResource{}, errResourceLen } - totalValueLen += len - off += int(len) + previousKey = key + totalValueLen += size + off += int(size) n++ } if off != bodyEnd { @@ -236,20 +236,23 @@ off = paramsOff for i := 0; i < n; i++ { p := &r.Params[i] - var key, len uint16 + var key, size uint16 if key, off, err = unpackUint16(msg, off); err != nil { return SVCBResource{}, &nestedError{"param key", err} } p.Key = SVCParamKey(key) - if len, off, err = unpackUint16(msg, off); err != nil { + if size, off, err = unpackUint16(msg, off); err != nil { return SVCBResource{}, &nestedError{"param length", err} } - if copy(valuesBuf, msg[off:off+int(len)]) != int(len) { + if len(msg[off:]) < int(size) { return SVCBResource{}, &nestedError{"param value", errCalcLen} } - p.Value = valuesBuf[:len:len] - valuesBuf = valuesBuf[len:] - off += int(len) + if copy(valuesBuf, msg[off:][:int(size)]) != int(size) { + return SVCBResource{}, &nestedError{"param value", errCalcLen} + } + p.Value = valuesBuf[:size:size] + valuesBuf = valuesBuf[size:] + off += int(size) } return r, nil
diff --git a/dns/dnsmessage/svcb_test.go b/dns/dnsmessage/svcb_test.go index 74fcccd..64a1bfd 100644 --- a/dns/dnsmessage/svcb_test.go +++ b/dns/dnsmessage/svcb_test.go
@@ -366,28 +366,74 @@ testRecord(bytes, parsed) } -func TestSVCBPackLongValue(t *testing.T) { - b := NewBuilder(nil, Header{}) - b.StartQuestions() - b.StartAnswers() - - res := SVCBResource{ - Target: MustNewName("example.com."), - Params: []SVCParam{ - { - Key: SVCParamMandatory, - Value: make([]byte, math.MaxUint16+1), +func TestSVCBPackErrors(t *testing.T) { + for _, test := range []struct { + name string + r SVCBResource + }{ + { + name: "long value", + r: SVCBResource{ + Target: MustNewName("example.com."), + Params: []SVCParam{ + { + Key: SVCParamMandatory, + Value: make([]byte, math.MaxUint16+1), + }, + }, }, }, - } + { + name: "out-of-order keys", + r: SVCBResource{ + Target: MustNewName("example.com."), + Params: []SVCParam{ + { + Key: SVCParamPort, // 3 + Value: []byte("443"), + }, + { + Key: SVCParamALPN, // 1 + Value: []byte("h2"), + }, + }, + }, + }, + { + name: "duplicate keys", + r: SVCBResource{ + Target: MustNewName("example.com."), + Params: []SVCParam{ + { + Key: SVCParamALPN, + Value: []byte("h3"), + }, + { + Key: SVCParamALPN, + Value: []byte("h2"), + }, + }, + }, + }, + } { + t.Run(test.name, func(t *testing.T) { + b := NewBuilder(nil, Header{}) + b.StartQuestions() + b.StartAnswers() + if err := b.SVCBResource(ResourceHeader{ + Name: MustNewName("example.com."), + }, test.r); err == nil { + t.Errorf("b.SVCBResource() succeeded; want error") + } - err := b.SVCBResource(ResourceHeader{Name: MustNewName("example.com.")}, res) - if err == nil || err.Error() != "ResourceBody: SVCBResource.Params: value too long (>65535 bytes)" { - t.Fatalf(`b.SVCBResource() = %v; want = "ResourceBody: SVCBResource.Params: value too long (>65535 bytes)"`, err) - } - - err = b.HTTPSResource(ResourceHeader{Name: MustNewName("example.com.")}, HTTPSResource{res}) - if err == nil || err.Error() != "ResourceBody: SVCBResource.Params: value too long (>65535 bytes)" { - t.Fatalf(`b.HTTPSResource() = %v; want = "ResourceBody: SVCBResource.Params: value too long (>65535 bytes)"`, err) + b = NewBuilder(nil, Header{}) + b.StartQuestions() + b.StartAnswers() + if err := b.HTTPSResource(ResourceHeader{ + Name: MustNewName("example.com."), + }, HTTPSResource{test.r}); err == nil { + t.Errorf("b.HTTPSResource() succeeded; want error") + } + }) } }
diff --git a/go.mod b/go.mod index 42e19a7..bb33976 100644 --- a/go.mod +++ b/go.mod
@@ -3,8 +3,8 @@ go 1.25.0 require ( - golang.org/x/crypto v0.52.0 - golang.org/x/sys v0.45.0 - golang.org/x/term v0.43.0 - golang.org/x/text v0.37.0 + golang.org/x/crypto v0.54.0 + golang.org/x/sys v0.47.0 + golang.org/x/term v0.45.0 + golang.org/x/text v0.40.0 )
diff --git a/go.sum b/go.sum index 5a66008..b68816c 100644 --- a/go.sum +++ b/go.sum
@@ -1,8 +1,8 @@ -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
diff --git a/html/foreign.go b/html/foreign.go index e8515d8..65d01d1 100644 --- a/html/foreign.go +++ b/html/foreign.go
@@ -23,7 +23,7 @@ } switch a.Key { case "xlink:actuate", "xlink:arcrole", "xlink:href", "xlink:role", "xlink:show", - "xlink:title", "xlink:type", "xml:base", "xml:lang", "xml:space", "xmlns:xlink": + "xlink:title", "xlink:type", "xml:lang", "xml:space", "xmlns:xlink": j := strings.Index(a.Key, ":") aa[i].Namespace = a.Key[:j] aa[i].Key = a.Key[j+1:]
diff --git a/html/parse.go b/html/parse.go index b3d2a25..165b610 100644 --- a/html/parse.go +++ b/html/parse.go
@@ -63,7 +63,7 @@ // Stop tags for use in popUntil. These come from section 12.2.4.2. var ( defaultScopeStopTags = map[string][]a.Atom{ - "": {a.Applet, a.Caption, a.Html, a.Table, a.Td, a.Th, a.Marquee, a.Object, a.Template}, + "": {a.Applet, a.Caption, a.Html, a.Table, a.Td, a.Th, a.Marquee, a.Object, a.Template, a.Select}, "math": {a.AnnotationXml, a.Mi, a.Mn, a.Mo, a.Ms, a.Mtext}, "svg": {a.Desc, a.ForeignObject, a.Title}, } @@ -78,7 +78,6 @@ tableScope tableRowScope tableBodyScope - selectScope ) // popUntil pops the stack of open elements at the highest element whose tag @@ -133,10 +132,6 @@ if tagAtom == a.Html || tagAtom == a.Table || tagAtom == a.Template { return -1 } - case selectScope: - if tagAtom != a.Optgroup && tagAtom != a.Option { - return -1 - } default: panic(fmt.Sprintf("html: internal error: indexOfElementInScope unknown scope: %d", s)) } @@ -460,21 +455,6 @@ } switch n.DataAtom { - case a.Select: - if !last { - for ancestor, first := n, p.oe[0]; ancestor != first; { - ancestor = p.oe[p.oe.index(ancestor)-1] - switch ancestor.DataAtom { - case a.Template: - p.im = inSelectIM - return - case a.Table: - p.im = inSelectInTableIM - return - } - } - } - p.im = inSelectIM case a.Td, a.Th: // TODO: remove this divergence from the HTML5 spec. // @@ -1002,7 +982,10 @@ p.popUntil(buttonScope, a.P) p.addElement() case a.Button: - p.popUntil(defaultScope, a.Button) + if p.elementInScope(defaultScope, a.Button) { + p.generateImpliedEndTags() + p.popUntil(defaultScope, a.Button) + } p.reconstructActiveFormattingElements() p.addElement() p.framesetOK = false @@ -1040,7 +1023,18 @@ p.framesetOK = false p.im = inTableIM return true - case a.Area, a.Br, a.Embed, a.Img, a.Input, a.Keygen, a.Wbr: + case a.Area, a.Br, a.Embed, a.Img, a.Keygen, a.Wbr: + p.reconstructActiveFormattingElements() + p.addElement() + p.oe.pop() + p.acknowledgeSelfClosingTag() + p.framesetOK = false + case a.Input: + if p.fragment && p.context.DataAtom == a.Select { + // Ignore the token. + return true + } + p.popUntil(defaultScope, a.Select) p.reconstructActiveFormattingElements() p.addElement() p.oe.pop() @@ -1061,7 +1055,13 @@ p.oe.pop() p.acknowledgeSelfClosingTag() case a.Hr: - p.popUntil(buttonScope, a.P) + if p.elementInScope(buttonScope, a.P) { + p.generateImpliedEndTags("p") + p.popUntil(defaultScope, a.P) + } + if p.elementInScope(defaultScope, a.Select) { + p.generateImpliedEndTags() + } p.addElement() p.oe.pop() p.acknowledgeSelfClosingTag() @@ -1095,13 +1095,30 @@ // Don't let the tokenizer go into raw text mode when scripting is disabled. p.tokenizer.NextIsNotRawText() case a.Select: + if p.fragment && p.context.DataAtom == a.Select { + // Ignore the token. + return true + } else if p.popUntil(defaultScope, a.Select) { + return true + } p.reconstructActiveFormattingElements() p.addElement() p.framesetOK = false - p.im = inSelectIM return true - case a.Optgroup, a.Option: - if p.top().DataAtom == a.Option { + case a.Option: + if p.elementInScope(defaultScope, a.Select) { + p.generateImpliedEndTags("optgroup") + // If oe has option element in scope, parse error? + } else if p.top().DataAtom == a.Option { + p.oe.pop() + } + p.reconstructActiveFormattingElements() + p.addElement() + case a.Optgroup: + if p.elementInScope(defaultScope, a.Select) { + p.generateImpliedEndTags() + // If oe has option or optgroup element in scope, parse error? + } else if p.top().DataAtom == a.Option { p.oe.pop() } p.reconstructActiveFormattingElements() @@ -1149,7 +1166,12 @@ return false } return true - case a.Address, a.Article, a.Aside, a.Blockquote, a.Button, a.Center, a.Details, a.Dialog, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Listing, a.Main, a.Menu, a.Nav, a.Ol, a.Pre, a.Search, a.Section, a.Summary, a.Ul: + case a.Address, a.Article, a.Aside, a.Blockquote, a.Button, a.Center, a.Details, a.Dialog, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Listing, a.Main, a.Menu, a.Nav, a.Ol, a.Pre, a.Search, a.Section, a.Select, a.Summary, a.Ul: + if !p.elementInScope(defaultScope, p.tok.DataAtom) { + // Ignore the token. + return true + } + p.generateImpliedEndTags() p.popUntil(defaultScope, p.tok.DataAtom) case a.Form: if p.oe.contains(a.Template) { @@ -1488,17 +1510,6 @@ } p.addElement() p.form = p.oe.pop() - case a.Select: - p.reconstructActiveFormattingElements() - switch p.top().DataAtom { - case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr: - p.fosterParenting = true - } - p.addElement() - p.fosterParenting = false - p.framesetOK = false - p.im = inSelectInTableIM - return true } case EndTagToken: switch p.tok.DataAtom { @@ -1547,12 +1558,6 @@ p.clearActiveFormattingElements() p.im = inTableIM return false - case a.Select: - p.reconstructActiveFormattingElements() - p.addElement() - p.framesetOK = false - p.im = inSelectInTableIM - return true } case EndTagToken: switch p.tok.DataAtom { @@ -1762,12 +1767,6 @@ } // Ignore the token. return true - case a.Select: - p.reconstructActiveFormattingElements() - p.addElement() - p.framesetOK = false - p.im = inSelectInTableIM - return true } case EndTagToken: switch p.tok.DataAtom { @@ -1798,118 +1797,6 @@ return inBodyIM(p) } -// Section 12.2.6.4.16. -func inSelectIM(p *parser) bool { - switch p.tok.Type { - case TextToken: - p.addText(strings.Replace(p.tok.Data, "\x00", "", -1)) - case StartTagToken: - switch p.tok.DataAtom { - case a.Html: - return inBodyIM(p) - case a.Option: - if p.top().DataAtom == a.Option { - p.oe.pop() - } - p.addElement() - case a.Optgroup: - if p.top().DataAtom == a.Option { - p.oe.pop() - } - if p.top().DataAtom == a.Optgroup { - p.oe.pop() - } - p.addElement() - case a.Select: - if !p.popUntil(selectScope, a.Select) { - // Ignore the token. - return true - } - p.resetInsertionMode() - case a.Input, a.Keygen, a.Textarea: - if p.elementInScope(selectScope, a.Select) { - p.parseImpliedToken(EndTagToken, a.Select, a.Select.String()) - return false - } - // In order to properly ignore <textarea>, we need to change the tokenizer mode. - p.tokenizer.NextIsNotRawText() - // Ignore the token. - return true - case a.Script, a.Template: - return inHeadIM(p) - case a.Iframe, a.Noembed, a.Noframes, a.Noscript, a.Plaintext, a.Style, a.Title, a.Xmp: - // Don't let the tokenizer go into raw text mode when there are raw tags - // to be ignored. These tags should be ignored from the tokenizer - // properly. - p.tokenizer.NextIsNotRawText() - // Ignore the token. - return true - } - case EndTagToken: - switch p.tok.DataAtom { - case a.Option: - if p.top().DataAtom == a.Option { - p.oe.pop() - } - case a.Optgroup: - i := len(p.oe) - 1 - if p.oe[i].DataAtom == a.Option { - i-- - } - if p.oe[i].DataAtom == a.Optgroup { - p.oe = p.oe[:i] - } - case a.Select: - if !p.popUntil(selectScope, a.Select) { - // Ignore the token. - return true - } - p.resetInsertionMode() - case a.Template: - return inHeadIM(p) - } - case CommentToken: - p.addChild(&Node{ - Type: CommentNode, - Data: p.tok.Data, - }) - case DoctypeToken: - // Ignore the token. - return true - case ErrorToken: - return inBodyIM(p) - } - - return true -} - -// Section 12.2.6.4.17. -func inSelectInTableIM(p *parser) bool { - switch p.tok.Type { - case StartTagToken, EndTagToken: - switch p.tok.DataAtom { - case a.Caption, a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr, a.Td, a.Th: - if p.tok.Type == EndTagToken && !p.elementInScope(tableScope, p.tok.DataAtom) { - // Ignore the token. - return true - } - // This is like p.popUntil(selectScope, a.Select), but it also - // matches <math select>, not just <select>. Matching the MathML - // tag is arguably incorrect (conceptually), but it mimics what - // Chromium does. - for i := len(p.oe) - 1; i >= 0; i-- { - if n := p.oe[i]; n.DataAtom == a.Select { - p.oe = p.oe[:i] - break - } - } - p.resetInsertionMode() - return false - } - } - return inSelectIM(p) -} - // Section 12.2.6.4.18. func inTemplateIM(p *parser) bool { switch p.tok.Type { @@ -2174,7 +2061,7 @@ const whitespaceOrNUL = whitespace + "\x00" -// Section 12.2.6.5 +// Section 13.2.6.5 func parseForeignContent(p *parser) bool { switch p.tok.Type { case TextToken: @@ -2189,28 +2076,26 @@ Data: p.tok.Data, }) case StartTagToken: - if !p.fragment { - b := breakout[p.tok.Data] - if p.tok.DataAtom == a.Font { - loop: - for _, attr := range p.tok.Attr { - switch attr.Key { - case "color", "face", "size": - b = true - break loop - } + b := breakout[p.tok.Data] + if p.tok.DataAtom == a.Font { + loop: + for _, attr := range p.tok.Attr { + switch attr.Key { + case "color", "face", "size": + b = true + break loop } } - if b { - for i := len(p.oe) - 1; i >= 0; i-- { - n := p.oe[i] - if n.Namespace == "" || htmlIntegrationPoint(n) || mathMLTextIntegrationPoint(n) { - p.oe = p.oe[:i+1] - break - } + } + if b { + for i := len(p.oe) - 1; i >= 0; i-- { + n := p.oe[i] + if n.Namespace == "" || htmlIntegrationPoint(n) || mathMLTextIntegrationPoint(n) { + p.oe = p.oe[:i+1] + break } - return false } + return p.im(p) } current := p.adjustedCurrentNode() switch current.Namespace {
diff --git a/html/parse_test.go b/html/parse_test.go index cafa34d..8f2b49b 100644 --- a/html/parse_test.go +++ b/html/parse_test.go
@@ -152,9 +152,6 @@ } func (a sortedAttributes) Less(i, j int) bool { - if a[i].Namespace != a[j].Namespace { - return a[i].Namespace < a[j].Namespace - } return a[i].Key < a[j].Key } @@ -242,7 +239,7 @@ return b.String(), nil } -var testDataDirs = []string{"testdata/webkit/", "testdata/go/"} +var testDataDirs = []string{"testdata/html5lib-tests/tree-construction/", "testdata/go/"} func TestParser(t *testing.T) { for _, testDataDir := range testDataDirs { @@ -251,35 +248,33 @@ t.Fatal(err) } for _, tf := range testFiles { - t.Run(tf, func(t *testing.T) { - f, err := os.Open(tf) + f, err := os.Open(tf) + if err != nil { + t.Fatal(err) + } + defer f.Close() + r := bufio.NewReader(f) + + for i := 0; ; i++ { + ta, err := readParseTest(r) + if err == io.EOF { + break + } if err != nil { t.Fatal(err) } - defer f.Close() - r := bufio.NewReader(f) - - for i := 0; ; i++ { - ta, err := readParseTest(r) - if err == io.EOF { - break - } - if err != nil { - t.Fatal(err) - } - if parseTestBlacklist[ta.text] { - continue - } - - t.Run(fmt.Sprint(i), func(t *testing.T) { - err = testParseCase(ta.text, ta.want, ta.context, ParseOptionEnableScripting(ta.scripting)) - - if err != nil { - t.Errorf("%s test #%d %q, %s", tf, i, ta.text, err) - } - }) + if parseTestBlacklist[ta.text] { + continue } - }) + + testId := fmt.Sprintf("%s/%d", strings.ReplaceAll(tf, string(os.PathSeparator), "/"), i) + t.Run(testId, func(t *testing.T) { + err = testParseCase(ta.text, ta.want, ta.context, ParseOptionEnableScripting(ta.scripting)) + if err != nil { + t.Errorf("%s test #%d %q, %s", tf, i, ta.text, err) + } + }) + } } } } @@ -391,6 +386,11 @@ // See the a.Template TODO in inHeadIM. `<math><template><mo><template>`: true, `<template><svg><foo><template><foreignObject><div></template><div>`: true, + // We don't support "element insertion steps" + `<select><button><selectedcontent></button><option>X`: true, + `<select><button><selectedcontent></button><option>x<i>i<b>ib</i>b`: true, + `<select><button><selectedcontent></button><option>X<option>Y`: true, + `<select><button><selectedcontent></button><option>X<option selected>Y`: true, } // Some test input result in parse trees are not 'well-formed' despite @@ -451,6 +451,8 @@ `<!doctype html><svg><plaintext>a</plaintext>b`: true, // Due to fostering, parsing the rendered output produces a different tree. `<math><mtext><table><mglyph><style><img>`: true, + // Confusing plaintext behavior + `<!doctype html><table><select><plaintext>a<caption>b`: true, } func TestNodeConsistency(t *testing.T) {
diff --git a/html/testdata/go/raw_tags_to_be_ignored.dat b/html/testdata/go/raw_tags_to_be_ignored.dat deleted file mode 100644 index 50bac59..0000000 --- a/html/testdata/go/raw_tags_to_be_ignored.dat +++ /dev/null
@@ -1,97 +0,0 @@ -#data -<!doctype html><table><select><iframe>a<caption>b -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| "a" -| <table> -| <caption> -| "b" - -#data -<!doctype html><table><select><noembed>a<caption>b -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| "a" -| <table> -| <caption> -| "b" - -#data -<!doctype html><table><select><noframes>a<caption>b -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| "a" -| <table> -| <caption> -| "b" - -#data -<!doctype html><table><select><noscript>a<caption>b -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| "a" -| <table> -| <caption> -| "b" - -#data -<!doctype html><table><select><style>a<caption>b -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| "a" -| <table> -| <caption> -| "b" - -#data -<!doctype html><table><select><title>a<caption>b -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| "a" -| <table> -| <caption> -| "b" - -#data -<!doctype html><table><select><xmp>a<caption>b -#errors -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| "a" -| <table> -| <caption> -| "b"
diff --git a/html/testdata/html5lib-tests/tree-construction/README.md b/html/testdata/html5lib-tests/tree-construction/README.md new file mode 100644 index 0000000..4737a3a --- /dev/null +++ b/html/testdata/html5lib-tests/tree-construction/README.md
@@ -0,0 +1,108 @@ +Tree Construction Tests +======================= + +Each file containing tree construction tests consists of any number of +tests separated by two newlines (LF) and a single newline before the end +of the file. For instance: + + [TEST]LF + LF + [TEST]LF + LF + [TEST]LF + +Where [TEST] is the following format: + +Each test must begin with a string "\#data" followed by a newline (LF). +All subsequent lines until a line that says "\#errors" are the test data +and must be passed to the system being tested unchanged, except with the +final newline (on the last line) removed. + +Then there must be a line that says "\#errors". It must be followed by +one line per parse error that a conformant checker would return. It +doesn't matter what those lines are, although they can't be +"\#new-errors", "\#document-fragment", "\#document", "\#script-off", +"\#script-on", or empty, the only thing that matters is that there be +the right number of parse errors. + +Then there \*may\* be a line that says "\#new-errors", which works like +the "\#errors" section adding more errors to the expected number of +errors. + +Then there \*may\* be a line that says "\#document-fragment", which must +be followed by a newline (LF), followed by a string of characters that +indicates the context element, followed by a newline (LF). If the string +of characters starts with "svg ", the context element is in the SVG +namespace and the substring after "svg " is the local name. If the +string of characters starts with "math ", the context element is in the +MathML namespace and the substring after "math " is the local name. +Otherwise, the context element is in the HTML namespace and the string +is the local name. If this line is present the "\#data" must be parsed +using the HTML fragment parsing algorithm with the context element as +context. + +Then there \*may\* be a line that says "\#script-off" or +"\#script-on". If a line that says "\#script-off" is present, the +parser must set the scripting flag to disabled. If a line that says +"\#script-on" is present, it must set it to enabled. Otherwise, the +test should be run in both modes. + +Then there must be a line that says "\#document", which must be followed +by a dump of the tree of the parsed DOM. Each node must be represented +by a single line. Each line must start with "| ", followed by two spaces +per parent node that the node has before the root document node. + +- Element nodes must be represented by a "`<`" then the *tag name + string* "`>`", and all the attributes must be given, sorted + lexicographically by UTF-16 code unit according to their *attribute + name string*, on subsequent lines, as if they were children of the + element node. +- Attribute nodes must have the *attribute name string*, then an "=" + sign, then the attribute value in double quotes ("). +- Text nodes must be the string, in double quotes. Newlines aren't + escaped. +- Comments must be "`<`" then "`!-- `" then the data then "` -->`". +- DOCTYPEs must be "`<!DOCTYPE `" then the name then if either of the + system id or public id is non-empty a space, public id in + double-quotes, another space an the system id in double-quotes, and + then in any case "`>`". +- Processing instructions must be "`<?`", then the target, then a + space, then the data and then "`>`". (The HTML parser cannot emit + processing instructions, but scripts can, and the WebVTT to DOM + rules can emit them.) +- Template contents are represented by the string "content" with the + children below it. + +The *tag name string* is the local name prefixed by a namespace +designator. For the HTML namespace, the namespace designator is the +empty string, i.e. there's no prefix. For the SVG namespace, the +namespace designator is "svg ". For the MathML namespace, the namespace +designator is "math ". + +The *attribute name string* is the local name prefixed by a namespace +designator. For no namespace, the namespace designator is the empty +string, i.e. there's no prefix. For the XLink namespace, the namespace +designator is "xlink ". For the XML namespace, the namespace designator +is "xml ". For the XMLNS namespace, the namespace designator is "xmlns +". Note the difference between "xlink:href" which is an attribute in no +namespace with the local name "xlink:href" and "xlink href" which is an +attribute in the xlink namespace with the local name "href". + +If there is also a "\#document-fragment" the bit following "\#document" +must be a representation of the HTML fragment serialization for the +context element given by "\#document-fragment". + +For example: + + #data + <p>One<p>Two + #errors + 3: Missing document type declaration + #document + | <html> + | <head> + | <body> + | <p> + | "One" + | <p> + | "Two"
diff --git a/html/testdata/webkit/adoption01.dat b/html/testdata/html5lib-tests/tree-construction/adoption01.dat similarity index 100% rename from html/testdata/webkit/adoption01.dat rename to html/testdata/html5lib-tests/tree-construction/adoption01.dat
diff --git a/html/testdata/webkit/adoption02.dat b/html/testdata/html5lib-tests/tree-construction/adoption02.dat similarity index 100% rename from html/testdata/webkit/adoption02.dat rename to html/testdata/html5lib-tests/tree-construction/adoption02.dat
diff --git a/html/testdata/webkit/blocks.dat b/html/testdata/html5lib-tests/tree-construction/blocks.dat similarity index 91% rename from html/testdata/webkit/blocks.dat rename to html/testdata/html5lib-tests/tree-construction/blocks.dat index 5d3871e..a1a9c75 100644 --- a/html/testdata/webkit/blocks.dat +++ b/html/testdata/html5lib-tests/tree-construction/blocks.dat
@@ -2,7 +2,6 @@ <!doctype html><p>foo<address>bar<p>baz #errors (1,39): expected-closing-tag-but-got-eof -30: Unclosed element “address”. #document | <!DOCTYPE html> | <html> @@ -32,7 +31,6 @@ <!doctype html><p>foo<article>bar<p>baz #errors (1,39): expected-closing-tag-but-got-eof -30: Unclosed element “article”. #document | <!DOCTYPE html> | <html> @@ -62,7 +60,6 @@ <!doctype html><p>foo<aside>bar<p>baz #errors (1,37): expected-closing-tag-but-got-eof -28: Unclosed element “aside”. #document | <!DOCTYPE html> | <html> @@ -92,7 +89,6 @@ <!doctype html><p>foo<blockquote>bar<p>baz #errors (1,42): expected-closing-tag-but-got-eof -33: Unclosed element “blockquote”. #document | <!DOCTYPE html> | <html> @@ -122,7 +118,6 @@ <!doctype html><p>foo<center>bar<p>baz #errors (1,38): expected-closing-tag-but-got-eof -29: Unclosed element “center”. #document | <!DOCTYPE html> | <html> @@ -152,7 +147,6 @@ <!doctype html><p>foo<details>bar<p>baz #errors (1,39): expected-closing-tag-but-got-eof -30: Unclosed element “details”. #document | <!DOCTYPE html> | <html> @@ -182,7 +176,6 @@ <!doctype html><p>foo<dialog>bar<p>baz #errors (1,38): expected-closing-tag-but-got-eof -29: Unclosed element “dialog”. #document | <!DOCTYPE html> | <html> @@ -212,7 +205,6 @@ <!doctype html><p>foo<dir>bar<p>baz #errors (1,35): expected-closing-tag-but-got-eof -26: Unclosed element “dir”. #document | <!DOCTYPE html> | <html> @@ -242,7 +234,6 @@ <!doctype html><p>foo<div>bar<p>baz #errors (1,35): expected-closing-tag-but-got-eof -26: Unclosed element “div”. #document | <!DOCTYPE html> | <html> @@ -272,7 +263,6 @@ <!doctype html><p>foo<dl>bar<p>baz #errors (1,34): expected-closing-tag-but-got-eof -25: Unclosed element “dl”. #document | <!DOCTYPE html> | <html> @@ -302,7 +292,6 @@ <!doctype html><p>foo<fieldset>bar<p>baz #errors (1,40): expected-closing-tag-but-got-eof -31: Unclosed element “fieldset”. #document | <!DOCTYPE html> | <html> @@ -332,7 +321,6 @@ <!doctype html><p>foo<figcaption>bar<p>baz #errors (1,42): expected-closing-tag-but-got-eof -33: Unclosed element “figcaption”. #document | <!DOCTYPE html> | <html> @@ -362,7 +350,6 @@ <!doctype html><p>foo<figure>bar<p>baz #errors (1,38): expected-closing-tag-but-got-eof -29: Unclosed element “figure”. #document | <!DOCTYPE html> | <html> @@ -392,7 +379,6 @@ <!doctype html><p>foo<footer>bar<p>baz #errors (1,38): expected-closing-tag-but-got-eof -29: Unclosed element “footer”. #document | <!DOCTYPE html> | <html> @@ -422,7 +408,6 @@ <!doctype html><p>foo<header>bar<p>baz #errors (1,38): expected-closing-tag-but-got-eof -29: Unclosed element “header”. #document | <!DOCTYPE html> | <html> @@ -452,7 +437,6 @@ <!doctype html><p>foo<hgroup>bar<p>baz #errors (1,38): expected-closing-tag-but-got-eof -29: Unclosed element “hgroup”. #document | <!DOCTYPE html> | <html> @@ -482,7 +466,6 @@ <!doctype html><p>foo<listing>bar<p>baz #errors (1,39): expected-closing-tag-but-got-eof -30: Unclosed element “listing”. #document | <!DOCTYPE html> | <html> @@ -512,7 +495,6 @@ <!doctype html><p>foo<menu>bar<p>baz #errors (1,36): expected-closing-tag-but-got-eof -27: Unclosed element “menu”. #document | <!DOCTYPE html> | <html> @@ -542,7 +524,6 @@ <!doctype html><p>foo<nav>bar<p>baz #errors (1,35): expected-closing-tag-but-got-eof -26: Unclosed element “nav”. #document | <!DOCTYPE html> | <html> @@ -572,7 +553,6 @@ <!doctype html><p>foo<ol>bar<p>baz #errors (1,34): expected-closing-tag-but-got-eof -25: Unclosed element “ol”. #document | <!DOCTYPE html> | <html> @@ -602,7 +582,6 @@ <!doctype html><p>foo<pre>bar<p>baz #errors (1,35): expected-closing-tag-but-got-eof -26: Unclosed element “pre”. #document | <!DOCTYPE html> | <html> @@ -632,7 +611,6 @@ <!doctype html><p>foo<section>bar<p>baz #errors (1,39): expected-closing-tag-but-got-eof -30: Unclosed element “section”. #document | <!DOCTYPE html> | <html> @@ -662,7 +640,6 @@ <!doctype html><p>foo<summary>bar<p>baz #errors (1,39): expected-closing-tag-but-got-eof -30: Unclosed element “summary”. #document | <!DOCTYPE html> | <html> @@ -692,7 +669,6 @@ <!doctype html><p>foo<ul>bar<p>baz #errors (1,34): expected-closing-tag-but-got-eof -25: Unclosed element “ul”. #document | <!DOCTYPE html> | <html>
diff --git a/html/testdata/webkit/comments01.dat b/html/testdata/html5lib-tests/tree-construction/comments01.dat similarity index 89% rename from html/testdata/webkit/comments01.dat rename to html/testdata/html5lib-tests/tree-construction/comments01.dat index fa79c2b..4b9ff95 100644 --- a/html/testdata/webkit/comments01.dat +++ b/html/testdata/html5lib-tests/tree-construction/comments01.dat
@@ -29,8 +29,9 @@ FOO<!-- BAR --! >BAZ #errors (1,3): expected-doctype-but-got-chars +(1:21) eof-in-comment #new-errors -(1:20) eof-in-comment +(1:21) eof-in-comment #document | <html> | <head> @@ -43,8 +44,9 @@ >BAZ #errors (1,3): expected-doctype-but-got-chars +(2:5) eof-in-comment #new-errors -(1:20) eof-in-comment +(2:5) eof-in-comment #document | <html> | <head> @@ -57,7 +59,6 @@ FOO<!-- BAR -- >BAZ #errors (1,3): expected-doctype-but-got-chars -(1,15): unexpected-char-in-comment (1,21): eof-in-comment #new-errors (1:22) eof-in-comment @@ -72,8 +73,6 @@ FOO<!-- BAR -- <QUX> -- MUX -->BAZ #errors (1,3): expected-doctype-but-got-chars -(1,15): unexpected-char-in-comment -(1,24): unexpected-char-in-comment #document | <html> | <head> @@ -86,8 +85,6 @@ FOO<!-- BAR -- <QUX> -- MUX --!>BAZ #errors (1,3): expected-doctype-but-got-chars -(1,15): unexpected-char-in-comment -(1,24): unexpected-char-in-comment (1,31): unexpected-bang-after-double-dash-in-comment #new-errors (1:32) incorrectly-closed-comment @@ -103,9 +100,6 @@ FOO<!-- BAR -- <QUX> -- MUX -- >BAZ #errors (1,3): expected-doctype-but-got-chars -(1,15): unexpected-char-in-comment -(1,24): unexpected-char-in-comment -(1,31): unexpected-char-in-comment (1,35): eof-in-comment #new-errors (1:36) eof-in-comment @@ -202,7 +196,6 @@ FOO<!----->BAZ #errors (1,3): expected-doctype-but-got-chars -(1,10): unexpected-dash-after-double-dash-in-comment #document | <html> | <head>
diff --git a/html/testdata/webkit/doctype01.dat b/html/testdata/html5lib-tests/tree-construction/doctype01.dat similarity index 98% rename from html/testdata/webkit/doctype01.dat rename to html/testdata/html5lib-tests/tree-construction/doctype01.dat index c845bec..9efdaf7 100644 --- a/html/testdata/webkit/doctype01.dat +++ b/html/testdata/html5lib-tests/tree-construction/doctype01.dat
@@ -34,7 +34,6 @@ #data <!DOCTYPE>Hello #errors -(1,9): need-space-after-doctype (1,10): expected-doctype-name-but-got-right-bracket (1,10): unknown-doctype #new-errors @@ -337,6 +336,7 @@ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">Hello #errors +(2,43): unknown-doctype #document | <!DOCTYPE html "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> | <html> @@ -421,6 +421,7 @@ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd"> #errors (1,50): unexpected-char-in-doctype +(1,89): unknown-doctype #new-errors (1:50) missing-whitespace-between-doctype-public-and-system-identifiers #document @@ -433,6 +434,7 @@ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"'http://www.w3.org/TR/html4/strict.dtd'> #errors (1,50): unexpected-char-in-doctype +(1,89): unknown-doctype #new-errors (1:50) missing-whitespace-between-doctype-public-and-system-identifiers #document @@ -446,6 +448,7 @@ #errors (1,21): unexpected-char-in-doctype (1,49): unexpected-char-in-doctype +(1,88): unknown-doctype #new-errors (1:22) missing-whitespace-after-doctype-public-keyword (1:49) missing-whitespace-between-doctype-public-and-system-identifiers @@ -460,6 +463,7 @@ #errors (1,21): unexpected-char-in-doctype (1,49): unexpected-char-in-doctype +(1,88): unknown-doctype #new-errors (1:22) missing-whitespace-after-doctype-public-keyword (1:49) missing-whitespace-between-doctype-public-and-system-identifiers
diff --git a/html/testdata/webkit/domjs-unsafe.dat b/html/testdata/html5lib-tests/tree-construction/domjs-unsafe.dat similarity index 100% rename from html/testdata/webkit/domjs-unsafe.dat rename to html/testdata/html5lib-tests/tree-construction/domjs-unsafe.dat Binary files differ
diff --git a/html/testdata/webkit/entities01.dat b/html/testdata/html5lib-tests/tree-construction/entities01.dat similarity index 100% rename from html/testdata/webkit/entities01.dat rename to html/testdata/html5lib-tests/tree-construction/entities01.dat
diff --git a/html/testdata/webkit/entities02.dat b/html/testdata/html5lib-tests/tree-construction/entities02.dat similarity index 97% rename from html/testdata/webkit/entities02.dat rename to html/testdata/html5lib-tests/tree-construction/entities02.dat index 0c6e898..74965a3 100644 --- a/html/testdata/webkit/entities02.dat +++ b/html/testdata/html5lib-tests/tree-construction/entities02.dat
@@ -45,7 +45,6 @@ #data <div bar="ZZ>=YY"></div> #errors -(1,15): named-entity-without-semicolon (1,20): expected-doctype-but-got-start-tag #document | <html> @@ -204,7 +203,6 @@ #data <div bar="ZZ£=23"></div> #errors -(1,18): named-entity-without-semicolon (1,23): expected-doctype-but-got-start-tag #document | <html> @@ -299,6 +297,8 @@ #data <div>ZZÆ=</div> #errors +(1,5): expected-doctype-but-got-start-tag +(1:14) missing-semicolon-after-character-reference #new-errors (1:14) missing-semicolon-after-character-reference #document
diff --git a/html/testdata/webkit/foreign-fragment.dat b/html/testdata/html5lib-tests/tree-construction/foreign-fragment.dat similarity index 81% rename from html/testdata/webkit/foreign-fragment.dat rename to html/testdata/html5lib-tests/tree-construction/foreign-fragment.dat index c81ae81..e562c6b 100644 --- a/html/testdata/webkit/foreign-fragment.dat +++ b/html/testdata/html5lib-tests/tree-construction/foreign-fragment.dat
@@ -3,11 +3,10 @@ #errors 6: HTML start tag “nobr” in a foreign namespace context. 7: End of file seen and there were open elements. -6: Unclosed element “nobr”. #document-fragment svg path #document -| <svg nobr> +| <nobr> | "X" #data @@ -17,7 +16,7 @@ #document-fragment svg path #document -| <svg font> +| <font> | color="" | "X" @@ -35,7 +34,6 @@ #errors 10: End tag “path” did not match the name of the current open element (“g”). 11: End of file seen and there were open elements. -3: Unclosed element “g”. #document-fragment svg path #document @@ -173,7 +171,6 @@ #errors 51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag. 52: End of file seen and there were open elements. -51: Unclosed element “ms”. #new-errors (1:44-1:49) non-void-html-element-start-tag-with-trailing-solidus #document-fragment @@ -216,7 +213,6 @@ #errors 51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag. 52: End of file seen and there were open elements. -51: Unclosed element “mn”. #new-errors (1:44-1:49) non-void-html-element-start-tag-with-trailing-solidus #document-fragment @@ -259,7 +255,6 @@ #errors 51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag. 52: End of file seen and there were open elements. -51: Unclosed element “mo”. #new-errors (1:44-1:49) non-void-html-element-start-tag-with-trailing-solidus #document-fragment @@ -302,7 +297,6 @@ #errors 51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag. 52: End of file seen and there were open elements. -51: Unclosed element “mi”. #new-errors (1:44-1:49) non-void-html-element-start-tag-with-trailing-solidus #document-fragment @@ -345,7 +339,6 @@ #errors 51: Self-closing syntax (“/>”) used on a non-void HTML element. Ignoring the slash and treating as a start tag. 52: End of file seen and there were open elements. -51: Unclosed element “mtext”. #new-errors (1:44-1:52) non-void-html-element-start-tag-with-trailing-solidus #document-fragment @@ -390,7 +383,7 @@ #document-fragment math annotation-xml #document -| <math div> +| <div> #data <figure></figure> @@ -407,7 +400,7 @@ #document-fragment math math #document -| <math div> +| <div> #data <figure></figure> @@ -461,12 +454,11 @@ <div><h1>X</h1></div> #errors 5: HTML start tag “div” in a foreign namespace context. -9: HTML start tag “h1” in a foreign namespace context. #document-fragment svg svg #document -| <svg div> -| <svg h1> +| <div> +| <h1> | "X" #data @@ -476,7 +468,7 @@ #document-fragment svg svg #document -| <svg div> +| <div> #data <div></div> @@ -487,14 +479,6 @@ | <div> #data -<figure></figure> -#errors -#document-fragment -svg desc -#document -| <figure> - -#data <plaintext><foo> #errors (1,16): expected-closing-tag-but-got-eof @@ -557,3 +541,105 @@ svg desc #document | "X" + +#data +<svg><p> +#errors +8: HTML start tag “p” in a foreign namespace context. +#document-fragment +div +#document +| <svg svg> +| <p> + +#data +<p> +#errors +3: HTML start tag “p” in a foreign namespace context. +#document-fragment +svg svg +#document +| <p> + +#data +<svg></p><foo> +#errors +9: HTML end tag “p” in a foreign namespace context. +(1:6) Unexpected </p> from in body insertion mode +(1:15) Unexpected EOF +#document-fragment +div +#document +| <svg svg> +| <p> +| <foo> + +#data +<svg></br><foo> +#errors +10: HTML end tag “br” in a foreign namespace context. +(1:6) Unexpected </br> from in body insertion mode +(1:16) Unexpected EOF +#document-fragment +div +#document +| <svg svg> +| <br> +| <foo> + +#data +</p><foo> +#errors +4: HTML end tag “p” in a foreign namespace context. +(1:1) Unexpected </p> from in body insertion mode +(1:10) Unexpected EOF +#document-fragment +svg svg +#document +| <p> +| <svg foo> + +#data +</br><foo> +#errors +5: HTML end tag “br” in a foreign namespace context. +(1:1) Unexpected </br> from in body insertion mode +(1:11) Unexpected EOF +#document-fragment +svg svg +#document +| <br> +| <svg foo> + +#data +<body><foo> +#errors +6: HTML start tag “body” in a foreign namespace context. +(1:1) Unexpected <body> from in body insertion mode +(1:12) Unexpected EOF +#document-fragment +svg svg +#document +| <svg foo> + +#data +<p><foo> +#errors +3: HTML start tag “p” in a foreign namespace context. +(1:9) Unexpected EOF +#document-fragment +svg svg +#document +| <p> +| <foo> + +#data +<p></p><foo> +#errors +3: HTML start tag “p” in a foreign namespace context. +(1:13) Unexpected EOF +#document-fragment +svg svg +#document +| <p> +| <svg foo>
diff --git a/html/testdata/webkit/html5test-com.dat b/html/testdata/html5lib-tests/tree-construction/html5test-com.dat similarity index 98% rename from html/testdata/webkit/html5test-com.dat rename to html/testdata/html5lib-tests/tree-construction/html5test-com.dat index f738010..48d0bf9 100644 --- a/html/testdata/webkit/html5test-com.dat +++ b/html/testdata/html5lib-tests/tree-construction/html5test-com.dat
@@ -142,7 +142,6 @@ #data <!--foo--bar--> #errors -(1,10): unexpected-char-in-comment (1,15): expected-doctype-but-got-eof #document | <!-- foo--bar -->
diff --git a/html/testdata/webkit/inbody01.dat b/html/testdata/html5lib-tests/tree-construction/inbody01.dat similarity index 100% rename from html/testdata/webkit/inbody01.dat rename to html/testdata/html5lib-tests/tree-construction/inbody01.dat
diff --git a/html/testdata/webkit/isindex.dat b/html/testdata/html5lib-tests/tree-construction/isindex.dat similarity index 100% rename from html/testdata/webkit/isindex.dat rename to html/testdata/html5lib-tests/tree-construction/isindex.dat
diff --git a/html/testdata/webkit/main-element.dat b/html/testdata/html5lib-tests/tree-construction/main-element.dat similarity index 100% rename from html/testdata/webkit/main-element.dat rename to html/testdata/html5lib-tests/tree-construction/main-element.dat
diff --git a/html/testdata/html5lib-tests/tree-construction/math.dat b/html/testdata/html5lib-tests/tree-construction/math.dat new file mode 100644 index 0000000..d6a8ae5 --- /dev/null +++ b/html/testdata/html5lib-tests/tree-construction/math.dat
@@ -0,0 +1,104 @@ +#data +<math><tr><td><mo><tr> +#errors +(1,22): unexpected-start-tag +(1,23): expected-closing-tag-but-got-eof +#document-fragment +td +#document +| <math math> +| <math tr> +| <math td> +| <math mo> + +#data +<math><tr><td><mo><tr> +#errors +(1,6): foster-parenting-start-tag +(1,22): expected-tr-in-table-scope +(1,23): expected-closing-tag-but-got-eof +#document-fragment +tr +#document +| <math math> +| <math tr> +| <math td> +| <math mo> + +#data +<math><thead><mo><tbody> +#errors +(1,6): foster-parenting-start-tag +(1,24): expected-table-part-in-table-scope +(1,25): expected-closing-tag-but-got-eof +#document-fragment +thead +#document +| <math math> +| <math thead> +| <math mo> + +#data +<math><tfoot><mo><tbody> +#errors +(1,6): foster-parenting-start-tag +(1,24): expected-table-part-in-table-scope +(1,25): expected-closing-tag-but-got-eof +#document-fragment +tfoot +#document +| <math math> +| <math tfoot> +| <math mo> + +#data +<math><tbody><mo><tfoot> +#errors +(1,6): foster-parenting-start-tag +(1,24): expected-table-part-in-table-scope +(1,25): expected-closing-tag-but-got-eof +#document-fragment +tbody +#document +| <math math> +| <math tbody> +| <math mo> + +#data +<math><tbody><mo></table> +#errors +(1,6): foster-parenting-start-tag +(1,25): unexpected-end-tag-in-math +(1,26): expected-closing-tag-but-got-eof +#document-fragment +tbody +#document +| <math math> +| <math tbody> +| <math mo> + +#data +<math><thead><mo></table> +#errors +(1,6): foster-parenting-start-tag +(1,25): unexpected-end-tag-in-math +(1,26): expected-closing-tag-but-got-eof +#document-fragment +tbody +#document +| <math math> +| <math thead> +| <math mo> + +#data +<math><tfoot><mo></table> +#errors +(1,6): foster-parenting-start-tag +(1,25): unexpected-end-tag-in-math +(1,26): expected-closing-tag-but-got-eof +#document-fragment +tbody +#document +| <math math> +| <math tfoot> +| <math mo>
diff --git a/html/testdata/webkit/menuitem-element.dat b/html/testdata/html5lib-tests/tree-construction/menuitem-element.dat similarity index 85% rename from html/testdata/webkit/menuitem-element.dat rename to html/testdata/html5lib-tests/tree-construction/menuitem-element.dat index 43aa0c6..f7c8e2c 100644 --- a/html/testdata/webkit/menuitem-element.dat +++ b/html/testdata/html5lib-tests/tree-construction/menuitem-element.dat
@@ -3,7 +3,6 @@ #errors 10: Start tag seen without seeing a doctype first. Expected “<!DOCTYPE html>”. 10: End of file seen and there were open elements. -10: Unclosed element “menuitem”. #document | <html> | <head> @@ -24,7 +23,6 @@ <!DOCTYPE html><body><menuitem>A #errors 32: End of file seen and there were open elements. -31: Unclosed element “menuitem”. #document | <!DOCTYPE html> | <html> @@ -37,8 +35,6 @@ <!DOCTYPE html><body><menuitem>A<menuitem>B #errors 43: End of file seen and there were open elements. -42: Unclosed element “menuitem”. -31: Unclosed element “menuitem”. #document | <!DOCTYPE html> | <html> @@ -53,7 +49,6 @@ <!DOCTYPE html><body><menuitem>A<menu>B</menu> #errors 46: End of file seen and there were open elements. -31: Unclosed element “menuitem”. #document | <!DOCTYPE html> | <html> @@ -68,7 +63,6 @@ <!DOCTYPE html><body><menuitem>A<hr>B #errors 37: End of file seen and there were open elements. -31: Unclosed element “menuitem”. #document | <!DOCTYPE html> | <html> @@ -83,7 +77,6 @@ <!DOCTYPE html><li><menuitem><li> #errors 33: End tag “li” implied, but there were open elements. -29: Unclosed element “menuitem”. #document | <!DOCTYPE html> | <html> @@ -98,7 +91,6 @@ #errors 39: Stray end tag “menuitem”. 40: End of file seen and there were open elements. -25: Unclosed element “menuitem”. #document | <!DOCTYPE html> | <html> @@ -112,9 +104,7 @@ <!DOCTYPE html><p><b></p><menuitem> #errors 25: End tag “p” seen, but there were open elements. -21: Unclosed element “b”. 35: End of file seen and there were open elements. -35: Unclosed element “menuitem”. #document | <!DOCTYPE html> | <html> @@ -129,7 +119,6 @@ <!DOCTYPE html><menuitem><asdf></menuitem>x #errors 42: End tag “menuitem” seen, but there were open elements. -31: Unclosed element “asdf”. #document | <!DOCTYPE html> | <html> @@ -172,19 +161,19 @@ #data <!DOCTYPE html><select><menuitem></select> #errors -33: Stray start tag “menuitem”. +1:34: ERROR: End tag 'select' isn't allowed here. Currently open tags: html, body, select, menuitem. #document | <!DOCTYPE html> | <html> | <head> | <body> | <select> +| <menuitem> #data <!DOCTYPE html><option><menuitem> #errors 33: End of file seen and there were open elements. -33: Unclosed element “menuitem”. #document | <!DOCTYPE html> | <html> @@ -197,7 +186,6 @@ <!DOCTYPE html><menuitem><option> #errors 33: End of file seen and there were open elements. -25: Unclosed element “menuitem”. #document | <!DOCTYPE html> | <html> @@ -210,7 +198,6 @@ <!DOCTYPE html><menuitem></body> #errors 32: End tag for “body” seen, but there were unclosed elements. -25: Unclosed element “menuitem”. #document | <!DOCTYPE html> | <html> @@ -222,7 +209,6 @@ <!DOCTYPE html><menuitem></html> #errors 32: End tag for “html” seen, but there were unclosed elements. -25: Unclosed element “menuitem”. #document | <!DOCTYPE html> | <html> @@ -234,7 +220,6 @@ <!DOCTYPE html><menuitem><p> #errors 28: End of file seen and there were open elements. -25: Unclosed element “menuitem”. #document | <!DOCTYPE html> | <html> @@ -247,7 +232,6 @@ <!DOCTYPE html><menuitem><li> #errors 29: End of file seen and there were open elements. -25: Unclosed element “menuitem”. #document | <!DOCTYPE html> | <html>
diff --git a/html/testdata/html5lib-tests/tree-construction/namespace-sensitivity.dat b/html/testdata/html5lib-tests/tree-construction/namespace-sensitivity.dat new file mode 100644 index 0000000..050dca7 --- /dev/null +++ b/html/testdata/html5lib-tests/tree-construction/namespace-sensitivity.dat
@@ -0,0 +1,22 @@ +#data +<body><table><tr><td><svg><td><foreignObject><span></td>Foo +#errors +(1,6): expected-doctype-but-got-start-tag +(1,56): unexpected-end-tag +(1,60): foster-parenting-character +(1,60): foster-parenting-character +(1,60): foster-parenting-character +(1,60): expected-closing-tag-but-got-eof +#document +| <html> +| <head> +| <body> +| "Foo" +| <table> +| <tbody> +| <tr> +| <td> +| <svg svg> +| <svg td> +| <svg foreignObject> +| <span>
diff --git a/html/testdata/webkit/noscript01.dat b/html/testdata/html5lib-tests/tree-construction/noscript01.dat similarity index 100% rename from html/testdata/webkit/noscript01.dat rename to html/testdata/html5lib-tests/tree-construction/noscript01.dat
diff --git a/html/testdata/webkit/pending-spec-changes-plain-text-unsafe.dat b/html/testdata/html5lib-tests/tree-construction/pending-spec-changes-plain-text-unsafe.dat similarity index 100% rename from html/testdata/webkit/pending-spec-changes-plain-text-unsafe.dat rename to html/testdata/html5lib-tests/tree-construction/pending-spec-changes-plain-text-unsafe.dat Binary files differ
diff --git a/html/testdata/webkit/pending-spec-changes.dat b/html/testdata/html5lib-tests/tree-construction/pending-spec-changes.dat similarity index 100% rename from html/testdata/webkit/pending-spec-changes.dat rename to html/testdata/html5lib-tests/tree-construction/pending-spec-changes.dat
diff --git a/html/testdata/webkit/plain-text-unsafe.dat b/html/testdata/html5lib-tests/tree-construction/plain-text-unsafe.dat similarity index 98% rename from html/testdata/webkit/plain-text-unsafe.dat rename to html/testdata/html5lib-tests/tree-construction/plain-text-unsafe.dat index dfb5cb6..e904eff 100644 --- a/html/testdata/webkit/plain-text-unsafe.dat +++ b/html/testdata/html5lib-tests/tree-construction/plain-text-unsafe.dat Binary files differ
diff --git a/html/testdata/html5lib-tests/tree-construction/quirks01.dat b/html/testdata/html5lib-tests/tree-construction/quirks01.dat new file mode 100644 index 0000000..bc58de5 --- /dev/null +++ b/html/testdata/html5lib-tests/tree-construction/quirks01.dat
@@ -0,0 +1,53 @@ +#data +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" +"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd"><p><table> +#errors +(2,54): unknown-doctype +(2,64): eof-in-table +#document +| <!DOCTYPE html "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd"> +| <html> +| <head> +| <body> +| <p> +| <table> + +#data +<!DOCTYPE html SYSTEM "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd"><p><table> +#errors +(1,83): unknown-doctype +(1,93): eof-in-table +#document +| <!DOCTYPE html "" "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd"> +| <html> +| <head> +| <body> +| <p> +| <table> + +#data +<!DOCTYPE html PUBLIC "html"><p><table> +#errors +(1,30): unknown-doctype +(1,39): eof-in-table +#document +| <!DOCTYPE html "html" ""> +| <html> +| <head> +| <body> +| <p> +| <table> + +#data +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN" + "http://www.w3.org/TR/html4/strict.dtd"><p><table> +#errors +(2,43): unknown-doctype +(2,53): eof-in-table +#document +| <!DOCTYPE html "-//W3C//DTD HTML 3.2//EN" "http://www.w3.org/TR/html4/strict.dtd"> +| <html> +| <head> +| <body> +| <p> +| <table>
diff --git a/html/testdata/webkit/ruby.dat b/html/testdata/html5lib-tests/tree-construction/ruby.dat similarity index 98% rename from html/testdata/webkit/ruby.dat rename to html/testdata/html5lib-tests/tree-construction/ruby.dat index 696782f..f4e5e4e 100644 --- a/html/testdata/webkit/ruby.dat +++ b/html/testdata/html5lib-tests/tree-construction/ruby.dat
@@ -203,6 +203,7 @@ <html><ruby>a<rtc>b<span></ruby></html> #errors (1,6): expected-doctype-but-got-start-tag +(1,32): unexpected-end-tag #document | <html> | <head>
diff --git a/html/testdata/webkit/scriptdata01.dat b/html/testdata/html5lib-tests/tree-construction/scriptdata01.dat similarity index 96% rename from html/testdata/webkit/scriptdata01.dat rename to html/testdata/html5lib-tests/tree-construction/scriptdata01.dat index e570858..6abcb65 100644 --- a/html/testdata/webkit/scriptdata01.dat +++ b/html/testdata/html5lib-tests/tree-construction/scriptdata01.dat
@@ -173,19 +173,6 @@ | "BAR" #data -FOO<script>'<!-->'</script>BAR -#errors -(1,3): expected-doctype-but-got-chars -#document -| <html> -| <head> -| <body> -| "FOO" -| <script> -| "'<!-->'" -| "BAR" - -#data FOO<script>'<!-- potato'</script>BAR #errors (1,3): expected-doctype-but-got-chars
diff --git a/html/testdata/webkit/scripted/adoption01.dat b/html/testdata/html5lib-tests/tree-construction/scripted/adoption01.dat similarity index 100% rename from html/testdata/webkit/scripted/adoption01.dat rename to html/testdata/html5lib-tests/tree-construction/scripted/adoption01.dat
diff --git a/html/testdata/webkit/scripted/ark.dat b/html/testdata/html5lib-tests/tree-construction/scripted/ark.dat similarity index 100% rename from html/testdata/webkit/scripted/ark.dat rename to html/testdata/html5lib-tests/tree-construction/scripted/ark.dat
diff --git a/html/testdata/webkit/scripted/webkit01.dat b/html/testdata/html5lib-tests/tree-construction/scripted/webkit01.dat similarity index 100% rename from html/testdata/webkit/scripted/webkit01.dat rename to html/testdata/html5lib-tests/tree-construction/scripted/webkit01.dat
diff --git a/html/testdata/html5lib-tests/tree-construction/search-element.dat b/html/testdata/html5lib-tests/tree-construction/search-element.dat new file mode 100644 index 0000000..2866d7e --- /dev/null +++ b/html/testdata/html5lib-tests/tree-construction/search-element.dat
@@ -0,0 +1,46 @@ +#data +<!doctype html><p>foo<search>bar<p>baz +#errors +(1,38): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| "foo" +| <search> +| "bar" +| <p> +| "baz" + +#data +<!doctype html><search><p>foo</search>bar +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <search> +| <p> +| "foo" +| "bar" + +#data +<!DOCTYPE html>xxx<svg><x><g><a><search><b> +#errors + * (1,44) unexpected HTML-like start tag token in foreign content + * (1,44) unexpected end of file +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| "xxx" +| <svg svg> +| <svg x> +| <svg g> +| <svg a> +| <svg search> +| <b>
diff --git a/html/testdata/webkit/svg.dat b/html/testdata/html5lib-tests/tree-construction/svg.dat similarity index 62% rename from html/testdata/webkit/svg.dat rename to html/testdata/html5lib-tests/tree-construction/svg.dat index 8e9a2bb..a452e7a 100644 --- a/html/testdata/webkit/svg.dat +++ b/html/testdata/html5lib-tests/tree-construction/svg.dat
@@ -1,6 +1,8 @@ #data <svg><tr><td><title><tr> #errors +(1:21) Unexpected <tr> tag +(1:25) Unexpected EOF #document-fragment td #document @@ -12,6 +14,9 @@ #data <svg><tr><td><title><tr> #errors +(1:1) Unexpected <svg> tag +(1:21) Unexpected <tr> tag +(1:25) Unexpected EOF #document-fragment tr #document @@ -23,6 +28,9 @@ #data <svg><thead><title><tbody> #errors +(1:1) Unexpected <svg> tag +(1:20) Unexpected <tbody> tag +(1:27) Unexpected EOF #document-fragment thead #document @@ -33,6 +41,9 @@ #data <svg><tfoot><title><tbody> #errors +(1:1) Unexpected <svg> tag +(1:20) Unexpected <tbody> tag +(1:27) Unexpected EOF #document-fragment tfoot #document @@ -43,6 +54,9 @@ #data <svg><tbody><title><tfoot> #errors +(1:1) Unexpected <svg> tag +(1:20) Unexpected <tfoot> tag +(1:27) Unexpected EOF #document-fragment tbody #document @@ -53,6 +67,9 @@ #data <svg><tbody><title></table> #errors +(1:1) Unexpected <svg> tag +(1:20) Unexpected </table> tag +(1:28) Unexpected EOF #document-fragment tbody #document @@ -63,6 +80,9 @@ #data <svg><thead><title></table> #errors +(1:1) Unexpected <svg> tag +(1:20) Unexpected </table> tag +(1:28) Unexpected EOF #document-fragment tbody #document @@ -73,6 +93,9 @@ #data <svg><tfoot><title></table> #errors +(1:1) Unexpected <svg> tag +(1:20) Unexpected </table> tag +(1:28) Unexpected EOF #document-fragment tbody #document
diff --git a/html/testdata/webkit/tables01.dat b/html/testdata/html5lib-tests/tree-construction/tables01.dat similarity index 69% rename from html/testdata/webkit/tables01.dat rename to html/testdata/html5lib-tests/tree-construction/tables01.dat index f0caaa3..4079888 100644 --- a/html/testdata/webkit/tables01.dat +++ b/html/testdata/html5lib-tests/tree-construction/tables01.dat
@@ -100,8 +100,11 @@ #data <table><select><option>3</select></table> #errors -(1,7): expected-doctype-but-got-start-tag -(1,15): unexpected-start-tag-implies-table-voodoo +1:1: ERROR: Expected a doctype token +1:8: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table. +1:16: ERROR: Start tag 'option' isn't allowed here. Currently open tags: html, body, table, select. +1:24: ERROR: Character tokens aren't legal here +1:25: ERROR: End tag 'select' isn't allowed here. Currently open tags: html, body, table, select, option. #document | <html> | <head> @@ -114,12 +117,11 @@ #data <table><select><table></table></select></table> #errors -(1,7): expected-doctype-but-got-start-tag -(1,15): unexpected-start-tag-implies-table-voodoo -(1,22): unexpected-table-element-start-tag-in-select-in-table -(1,22): unexpected-start-tag-implies-end-tag -(1,39): unexpected-end-tag -(1,47): unexpected-end-tag +1:1: ERROR: Expected a doctype token +1:8: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table. +1:16: ERROR: Start tag 'table' isn't allowed here. Currently open tags: html, body, table, select. +1:31: ERROR: End tag 'select' isn't allowed here. Currently open tags: html, body. +1:40: ERROR: End tag 'table' isn't allowed here. Currently open tags: html, body. #document | <html> | <head> @@ -131,9 +133,8 @@ #data <table><select></table> #errors -(1,7): expected-doctype-but-got-start-tag -(1,15): unexpected-start-tag-implies-table-voodoo -(1,23): unexpected-table-element-end-tag-in-select-in-table +1:1: ERROR: Expected a doctype token +1:8: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table. #document | <html> | <head> @@ -144,9 +145,10 @@ #data <table><select><option>A<tr><td>B</td></tr></table> #errors -(1,7): expected-doctype-but-got-start-tag -(1,15): unexpected-start-tag-implies-table-voodoo -(1,28): unexpected-table-element-start-tag-in-select-in-table +1:1: ERROR: Expected a doctype token +1:8: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table. +1:16: ERROR: Start tag 'option' isn't allowed here. Currently open tags: html, body, table, select. +1:24: ERROR: Character tokens aren't legal here #document | <html> | <head> @@ -284,3 +286,38 @@ | <svg svg> | <svg desc> | <td> + +#data +<div><table><svg><foreignObject><select><table><s> +#errors +1:1: Expected a doctype token +1:13: 'svg' tag isn't allowed here. Currently open tags: html, body, div, table. +1:33: 'select' tag isn't allowed here. Currently open tags: html, body, div, table, svg, foreignobject. +1:41: 'table' tag isn't allowed here. Currently open tags: html, body, div, table, svg, foreignobject, select. +1:48: 's' tag isn't allowed here. Currently open tags: html, body, div, table. +1:51: Premature end of file. Currently open tags: html, body, div, table, s. +#document +| <html> +| <head> +| <body> +| <div> +| <svg svg> +| <svg foreignObject> +| <select> +| <table> +| <s> +| <table> + +#data +<table>a<!doctype html> +#errors +(1,1): expected-doctype-but-got-start-tag +(1,8): illegal-character-token +(1,9): illegal-doctype +(1,24): expected-closing-tag-but-got-eof +#document +| <html> +| <head> +| <body> +| "a" +| <table>
diff --git a/html/testdata/webkit/template.dat b/html/testdata/html5lib-tests/tree-construction/template.dat similarity index 93% rename from html/testdata/webkit/template.dat rename to html/testdata/html5lib-tests/tree-construction/template.dat index b38d4f5..45fb507 100644 --- a/html/testdata/webkit/template.dat +++ b/html/testdata/html5lib-tests/tree-construction/template.dat
@@ -868,21 +868,6 @@ | <td> #data -<body><template><template><tr></tr></template><td></td></template> -#errors -no doctype -#document -| <html> -| <head> -| <body> -| <template> -| content -| <template> -| content -| <tr> -| <td> - -#data <body><table><colgroup><template><col></col></template></colgroup></table></body> #errors no doctype @@ -1089,7 +1074,11 @@ <body><template><col>Hello #errors no doctype -unexpected text +(1,27): foster-parenting-character +(1,27): foster-parenting-character +(1,27): foster-parenting-character +(1,27): foster-parenting-character +(1,27): foster-parenting-character eof in template #document | <html> @@ -1103,7 +1092,7 @@ <body><template><i><menu>Foo</i> #errors no doctype -mising /menu +missing /menu eof in template #document | <html> @@ -1569,6 +1558,19 @@ | <body> #data +<html><head></head><template></template><head> +#errors +no doctype +template-after-head +head-after-head +#document +| <html> +| <head> +| <template> +| content +| <body> + +#data <!DOCTYPE HTML><dummy><table><template><table><template><table><script> #errors eof script @@ -1593,6 +1595,11 @@ #data <template><a><table><a> #errors +(1,10): expected-doctype-but-got-start-tag +(1,23): foster-parenting-start-tag +(1,23): unexpected-start-tag +(1,23): formatting-element-not-in-scope +(1,24): eof-in-template #document | <html> | <head> @@ -1602,3 +1609,65 @@ | <a> | <table> | <body> + +#data +<template><form><input name="q"></form><div>second</div></template> +#errors +#document-fragment +template +#document +| <template> +| content +| <form> +| <input> +| name="q" +| <div> +| "second" + +#data +<!DOCTYPE HTML><template><tr><td>cell</td></tr></template> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <template> +| content +| <tr> +| <td> +| "cell" +| <body> + +#data +<!DOCTYPE HTML><template> <tr> <td>cell</td> </tr> </template> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <template> +| content +| " " +| <tr> +| " " +| <td> +| "cell" +| " " +| " " +| <body> + +#data +<!DOCTYPE HTML><template><tr><td>cell</td></tr>a</template> +#errors +(1,59): foster-parenting-character +#document +| <!DOCTYPE html> +| <html> +| <head> +| <template> +| content +| <tr> +| <td> +| "cell" +| "a" +| <body>
diff --git a/html/testdata/webkit/tests1.dat b/html/testdata/html5lib-tests/tree-construction/tests1.dat similarity index 97% rename from html/testdata/webkit/tests1.dat rename to html/testdata/html5lib-tests/tree-construction/tests1.dat index 1c36c1b..15d496e 100644 --- a/html/testdata/webkit/tests1.dat +++ b/html/testdata/html5lib-tests/tree-construction/tests1.dat
@@ -355,19 +355,20 @@ #data <select><b><option><select><option></b></select>X #errors -(1,8): expected-doctype-but-got-start-tag -(1,11): unexpected-start-tag-in-select -(1,27): unexpected-select-in-select -(1,39): unexpected-end-tag -(1,48): unexpected-end-tag +1:1: ERROR: Expected a doctype token +1:20: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, b, option. +1:36: ERROR: End tag 'b' isn't allowed here. Currently open tags: html, body, b, select, option. +1:50: ERROR: Premature end of file. Currently open tags: html, body, b. #document | <html> | <head> | <body> | <select> +| <b> +| <option> +| <b> | <option> -| <option> -| "X" +| "X" #data <a><table><td><a><table></table><a></tr><a></table><b>X</b>C<a>Y @@ -425,7 +426,6 @@ #data <!-----><font><div>hello<table>excite!<b>me!<th><i>please!</tr><!--X--> #errors -(1,7): unexpected-dash-after-double-dash-in-comment (1,14): expected-doctype-but-got-start-tag (1,41): unexpected-start-tag-implies-table-voodoo (1,48): foster-parenting-character-in-table @@ -1435,24 +1435,6 @@ | <p> #data -<b><table><td><i></table> -#errors -(1,3): expected-doctype-but-got-start-tag -(1,14): unexpected-cell-in-table-body -(1,25): unexpected-cell-end-tag -(1,25): expected-closing-tag-but-got-eof -#document -| <html> -| <head> -| <body> -| <b> -| <table> -| <tbody> -| <tr> -| <td> -| <i> - -#data <b><table><td></b><i></table> #errors (1,3): expected-doctype-but-got-start-tag @@ -1549,33 +1531,21 @@ | <p> #data -<p><hr></p> -#errors -(1,3): expected-doctype-but-got-start-tag -(1,11): unexpected-end-tag -#document -| <html> -| <head> -| <body> -| <p> -| <hr> -| <p> - -#data <select><b><option><select><option></b></select> #errors -(1,8): expected-doctype-but-got-start-tag -(1,11): unexpected-start-tag-in-select -(1,27): unexpected-select-in-select -(1,39): unexpected-end-tag -(1,48): unexpected-end-tag +1:1: ERROR: Expected a doctype token +1:20: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, b, option. +1:36: ERROR: End tag 'b' isn't allowed here. Currently open tags: html, body, b, select, option. +1:49: ERROR: Premature end of file. Currently open tags: html, body, b. #document | <html> | <head> | <body> | <select> +| <b> +| <option> +| <b> | <option> -| <option> #data <html><head><title></title><body></body></html>
diff --git a/html/testdata/webkit/tests10.dat b/html/testdata/html5lib-tests/tree-construction/tests10.dat similarity index 93% rename from html/testdata/webkit/tests10.dat rename to html/testdata/html5lib-tests/tree-construction/tests10.dat index f84e2d5..3b311e4 100644 --- a/html/testdata/webkit/tests10.dat +++ b/html/testdata/html5lib-tests/tree-construction/tests10.dat
@@ -35,20 +35,17 @@ #data <!DOCTYPE html><body><select><svg></svg></select> #errors -(1,34) unexpected-start-tag-in-select -(1,40) unexpected-end-tag-in-select #document | <!DOCTYPE html> | <html> | <head> | <body> | <select> +| <svg svg> #data <!DOCTYPE html><body><select><option><svg></svg></option></select> #errors -(1,42) unexpected-start-tag-in-select -(1,48) unexpected-end-tag-in-select #document | <!DOCTYPE html> | <html> @@ -56,6 +53,7 @@ | <body> | <select> | <option> +| <svg svg> #data <!DOCTYPE html><body><table><svg></svg></table> @@ -261,13 +259,8 @@ #data <!DOCTYPE html><body><table><tr><td><select><svg><g>foo</g><g>bar</g><p>baz</table><p>quux #errors -(1,49) unexpected-start-tag-in-select -(1,52) unexpected-start-tag-in-select -(1,59) unexpected-end-tag-in-select -(1,62) unexpected-start-tag-in-select -(1,69) unexpected-end-tag-in-select -(1,72) unexpected-start-tag-in-select -(1,83) unexpected-table-element-end-tag-in-select-in-table +1:70: ERROR: Start tag 'p' isn't allowed here. Currently open tags: html, body, table, tbody, tr, td, select, svg. +1:76: ERROR: End tag 'table' isn't allowed here. Currently open tags: html, body, table, tbody, tr, td, select. #document | <!DOCTYPE html> | <html> @@ -278,28 +271,39 @@ | <tr> | <td> | <select> -| "foobarbaz" +| <svg svg> +| <svg g> +| "foo" +| <svg g> +| "bar" +| <p> +| "baz" | <p> | "quux" #data <!DOCTYPE html><body><table><select><svg><g>foo</g><g>bar</g><p>baz</table><p>quux #errors -(1,36) unexpected-start-tag-implies-table-voodoo -(1,41) unexpected-start-tag-in-select -(1,44) unexpected-start-tag-in-select -(1,51) unexpected-end-tag-in-select -(1,54) unexpected-start-tag-in-select -(1,61) unexpected-end-tag-in-select -(1,64) unexpected-start-tag-in-select -(1,75) unexpected-table-element-end-tag-in-select-in-table +1:29: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table. +1:37: ERROR: Start tag 'svg' isn't allowed here. Currently open tags: html, body, table, select. +1:62: ERROR: Start tag 'p' isn't allowed here. Currently open tags: html, body, table, select, svg. +1:62: ERROR: Start tag 'p' isn't allowed here. Currently open tags: html, body, table, select. +1:65: ERROR: Character tokens aren't legal here +1:66: ERROR: Character tokens aren't legal here +1:67: ERROR: Character tokens aren't legal here #document | <!DOCTYPE html> | <html> | <head> | <body> | <select> -| "foobarbaz" +| <svg svg> +| <svg g> +| "foo" +| <svg g> +| "bar" +| <p> +| "baz" | <table> | <p> | "quux"
diff --git a/html/testdata/webkit/tests11.dat b/html/testdata/html5lib-tests/tree-construction/tests11.dat similarity index 100% rename from html/testdata/webkit/tests11.dat rename to html/testdata/html5lib-tests/tree-construction/tests11.dat
diff --git a/html/testdata/webkit/tests12.dat b/html/testdata/html5lib-tests/tree-construction/tests12.dat similarity index 100% rename from html/testdata/webkit/tests12.dat rename to html/testdata/html5lib-tests/tree-construction/tests12.dat
diff --git a/html/testdata/webkit/tests14.dat b/html/testdata/html5lib-tests/tree-construction/tests14.dat similarity index 100% rename from html/testdata/webkit/tests14.dat rename to html/testdata/html5lib-tests/tree-construction/tests14.dat
diff --git a/html/testdata/webkit/tests15.dat b/html/testdata/html5lib-tests/tree-construction/tests15.dat similarity index 100% rename from html/testdata/webkit/tests15.dat rename to html/testdata/html5lib-tests/tree-construction/tests15.dat
diff --git a/html/testdata/webkit/tests16.dat b/html/testdata/html5lib-tests/tree-construction/tests16.dat similarity index 99% rename from html/testdata/webkit/tests16.dat rename to html/testdata/html5lib-tests/tree-construction/tests16.dat index cea7340..05f34c1 100644 --- a/html/testdata/webkit/tests16.dat +++ b/html/testdata/html5lib-tests/tree-construction/tests16.dat
@@ -221,7 +221,6 @@ <!doctype html><script><! #errors (1,25): expected-script-data-but-got-eof -(1,25): expected-named-closing-tag-but-got-eof #document | <!DOCTYPE html> | <html> @@ -1525,7 +1524,6 @@ #errors (1,8): expected-doctype-but-got-start-tag (1,10): expected-script-data-but-got-eof -(1,10): expected-named-closing-tag-but-got-eof #document | <html> | <head>
diff --git a/html/testdata/webkit/tests17.dat b/html/testdata/html5lib-tests/tree-construction/tests17.dat similarity index 90% rename from html/testdata/webkit/tests17.dat rename to html/testdata/html5lib-tests/tree-construction/tests17.dat index e49bcf0..1c472d1 100644 --- a/html/testdata/webkit/tests17.dat +++ b/html/testdata/html5lib-tests/tree-construction/tests17.dat
@@ -1,9 +1,8 @@ #data <!doctype html><table><tbody><select><tr> #errors -(1,37): unexpected-start-tag-implies-table-voodoo -(1,41): unexpected-table-element-start-tag-in-select-in-table -(1,41): eof-in-table +(1,30): unexpected-start-tag +(1,42): premature-eof #document | <!DOCTYPE html> | <html> @@ -17,9 +16,8 @@ #data <!doctype html><table><tr><select><td> #errors -(1,34): unexpected-start-tag-implies-table-voodoo -(1,38): unexpected-table-element-start-tag-in-select-in-table -(1,38): expected-closing-tag-but-got-eof +(1,27): unexpected-start-tag +(1,39): premature-eof #document | <!DOCTYPE html> | <html>
diff --git a/html/testdata/webkit/tests18.dat b/html/testdata/html5lib-tests/tree-construction/tests18.dat similarity index 71% rename from html/testdata/webkit/tests18.dat rename to html/testdata/html5lib-tests/tree-construction/tests18.dat index 05363b3..a3bb69a 100644 --- a/html/testdata/webkit/tests18.dat +++ b/html/testdata/html5lib-tests/tree-construction/tests18.dat
@@ -3,7 +3,6 @@ #errors 11: Start tag seen without seeing a doctype first. Expected “<!DOCTYPE html>”. 23: End of file seen and there were open elements. -11: Unclosed element “plaintext”. #document | <html> | <head> @@ -27,7 +26,6 @@ <!doctype html><html><plaintext></plaintext> #errors 44: End of file seen and there were open elements. -32: Unclosed element “plaintext”. #document | <!DOCTYPE html> | <html> @@ -40,7 +38,6 @@ <!doctype html><head><plaintext></plaintext> #errors 44: End of file seen and there were open elements. -32: Unclosed element “plaintext”. #document | <!DOCTYPE html> | <html> @@ -54,7 +51,6 @@ #errors 42: Bad start tag in “plaintext” in “head”. 54: End of file seen and there were open elements. -42: Unclosed element “plaintext”. #script-off #document | <!DOCTYPE html> @@ -69,7 +65,6 @@ <!doctype html></head><plaintext></plaintext> #errors 45: End of file seen and there were open elements. -33: Unclosed element “plaintext”. #document | <!DOCTYPE html> | <html> @@ -82,7 +77,6 @@ <!doctype html><body><plaintext></plaintext> #errors 44: End of file seen and there were open elements. -32: Unclosed element “plaintext”. #document | <!DOCTYPE html> | <html> @@ -95,8 +89,19 @@ <!doctype html><table><plaintext></plaintext> #errors (1,33): foster-parenting-start-tag -(1,45): foster-parenting-character -(1,45): eof-in-table +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): eof-in-table #document | <!DOCTYPE html> | <html> @@ -110,8 +115,19 @@ <!doctype html><table><tbody><plaintext></plaintext> #errors (1,40): foster-parenting-start-tag -(1,41): foster-parenting-character -(1,52): eof-in-table +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): eof-in-table #document | <!DOCTYPE html> | <html> @@ -126,8 +142,19 @@ <!doctype html><table><tbody><tr><plaintext></plaintext> #errors (1,44): foster-parenting-start-tag -(1,56): foster-parenting-character -(1,56): eof-in-table +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): eof-in-table #document | <!DOCTYPE html> | <html> @@ -173,11 +200,20 @@ #data <!doctype html><table><colgroup><plaintext></plaintext> #errors -43: Start tag “plaintext” seen in “table”. -55: Misplaced non-space characters inside a table. +(1,43): foster-parenting-start-tag +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character 55: End of file seen and there were open elements. -43: Unclosed element “plaintext”. -22: Unclosed element “table”. #document | <!DOCTYPE html> | <html> @@ -191,44 +227,47 @@ #data <!doctype html><select><plaintext></plaintext>X #errors -34: Stray start tag “plaintext”. -46: Stray end tag “plaintext”. -47: End of file seen and there were open elements. -23: Unclosed element “select”. +1:48: ERROR: Premature end of file. Currently open tags: html, body, select, plaintext. #document | <!DOCTYPE html> | <html> | <head> | <body> | <select> -| "X" +| <plaintext> +| "</plaintext>X" #data <!doctype html><table><select><plaintext>a<caption>b #errors -30: Start tag “select” seen in “table”. -41: Stray start tag “plaintext”. -51: “caption” start tag with “select” open. -52: End of file seen and there were open elements. -51: Unclosed element “caption”. -22: Unclosed element “table”. +1:23: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table. +1:31: ERROR: Start tag 'plaintext' isn't allowed here. Currently open tags: html, body, table, select. +1:42: ERROR: Character tokens aren't legal here +1:43: ERROR: Character tokens aren't legal here +1:44: ERROR: Character tokens aren't legal here +1:45: ERROR: Character tokens aren't legal here +1:46: ERROR: Character tokens aren't legal here +1:47: ERROR: Character tokens aren't legal here +1:48: ERROR: Character tokens aren't legal here +1:49: ERROR: Character tokens aren't legal here +1:50: ERROR: Character tokens aren't legal here +1:51: ERROR: Character tokens aren't legal here +1:52: ERROR: Character tokens aren't legal here +1:53: ERROR: Premature end of file. Currently open tags: html, body, table, select, plaintext. #document | <!DOCTYPE html> | <html> | <head> | <body> | <select> -| "a" +| <plaintext> +| "a<caption>b" | <table> -| <caption> -| "b" #data <!doctype html><template><plaintext>a</template>b #errors 49: End of file seen and there were open elements. -36: Unclosed element “plaintext”. -25: Unclosed element “template”. #document | <!DOCTYPE html> | <html> @@ -244,7 +283,6 @@ #errors 39: Stray start tag “plaintext”. 51: End of file seen and there were open elements. -39: Unclosed element “plaintext”. #document | <!DOCTYPE html> | <html> @@ -259,7 +297,6 @@ 36: Stray start tag “plaintext”. 48: Stray end tag “plaintext”. 48: End of file seen and there were open elements. -25: Unclosed element “frameset”. #document | <!DOCTYPE html> | <html> @@ -282,7 +319,6 @@ #errors 46: Stray start tag “plaintext”. 58: End of file seen and there were open elements. -46: Unclosed element “plaintext”. #document | <!DOCTYPE html> | <html> @@ -306,7 +342,6 @@ <!doctype html><svg><plaintext>a</plaintext>b #errors 45: End of file seen and there were open elements. -20: Unclosed element “svg”. #document | <!DOCTYPE html> | <html> @@ -321,9 +356,6 @@ <!doctype html><svg><title><plaintext>a</plaintext>b #errors 52: End of file seen and there were open elements. -38: Unclosed element “plaintext”. -27: Unclosed element “title”. -20: Unclosed element “svg”. #document | <!DOCTYPE html> | <html> @@ -422,8 +454,11 @@ #data <!doctype html><table><select><script></style></script>abc #errors -(1,30): unexpected-start-tag-implies-table-voodoo -(1,58): eof-in-select +1:23: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table. +1:56: ERROR: Character tokens aren't legal here +1:57: ERROR: Character tokens aren't legal here +1:58: ERROR: Character tokens aren't legal here +1:59: ERROR: Premature end of file. Currently open tags: html, body, table, select. #document | <!DOCTYPE html> | <html> @@ -438,8 +473,11 @@ #data <!doctype html><table><tr><select><script></style></script>abc #errors -(1,34): unexpected-start-tag-implies-table-voodoo -(1,62): eof-in-select +1:27: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table, tbody, tr. +1:60: ERROR: Character tokens aren't legal here +1:61: ERROR: Character tokens aren't legal here +1:62: ERROR: Character tokens aren't legal here +1:63: ERROR: Premature end of file. Currently open tags: html, body, table, tbody, tr, select. #document | <!DOCTYPE html> | <html>
diff --git a/html/testdata/webkit/tests19.dat b/html/testdata/html5lib-tests/tree-construction/tests19.dat similarity index 94% rename from html/testdata/webkit/tests19.dat rename to html/testdata/html5lib-tests/tree-construction/tests19.dat index a189777..20cdeab 100644 --- a/html/testdata/webkit/tests19.dat +++ b/html/testdata/html5lib-tests/tree-construction/tests19.dat
@@ -388,19 +388,6 @@ | <option> #data -<!doctype html><select><option></optgroup> -#errors -(1,42): unexpected-end-tag-in-select -(1,42): eof-in-select -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <select> -| <option> - -#data <!doctype html><dd><optgroup><dd> #errors #document @@ -1015,7 +1002,6 @@ <!doctype html><p><math></p>a #errors (1,28): unexpected-end-tag -(1,28): unexpected-end-tag #document | <!DOCTYPE html> | <html> @@ -1237,48 +1223,6 @@ | <table> #data -<!doctype html><table><i>a<b>b<div>c<a>d</i>e</b>f -#errors -(1,25): foster-parenting-start-tag -(1,26): foster-parenting-character -(1,29): foster-parenting-start-tag -(1,30): foster-parenting-character -(1,35): foster-parenting-start-tag -(1,36): foster-parenting-character -(1,39): foster-parenting-start-tag -(1,40): foster-parenting-character -(1,44): foster-parenting-end-tag -(1,44): adoption-agency-1.3 -(1,44): adoption-agency-1.3 -(1,45): foster-parenting-character -(1,49): foster-parenting-end-tag -(1,44): adoption-agency-1.3 -(1,44): adoption-agency-1.3 -(1,50): foster-parenting-character -(1,50): eof-in-table -#document -| <!DOCTYPE html> -| <html> -| <head> -| <body> -| <i> -| "a" -| <b> -| "b" -| <b> -| <div> -| <b> -| <i> -| "c" -| <a> -| "d" -| <a> -| "e" -| <a> -| "f" -| <table> - -#data <!doctype html><table><i>a<div>b<tr>c<b>d</i>e #errors (1,25): foster-parenting-start-tag
diff --git a/html/testdata/webkit/tests2.dat b/html/testdata/html5lib-tests/tree-construction/tests2.dat similarity index 97% rename from html/testdata/webkit/tests2.dat rename to html/testdata/html5lib-tests/tree-construction/tests2.dat index b44fec4..9232edd 100644 --- a/html/testdata/webkit/tests2.dat +++ b/html/testdata/html5lib-tests/tree-construction/tests2.dat
@@ -500,7 +500,7 @@ #data <!DOCTYPE html><select><optgroup><option></optgroup><option><select><option> #errors -(1,68): unexpected-select-in-select +1:61: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, option. #document | <!DOCTYPE html> | <html> @@ -585,6 +585,16 @@ | <body> #data +<!DOCTYPE html> <!DOCTYPE html> +#errors +Line: 1 Col: 31 Unexpected DOCTYPE. Ignored. +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> + +#data test test #errors
diff --git a/html/testdata/webkit/tests20.dat b/html/testdata/html5lib-tests/tree-construction/tests20.dat similarity index 70% rename from html/testdata/webkit/tests20.dat rename to html/testdata/html5lib-tests/tree-construction/tests20.dat index afdae74..80c57d1 100644 --- a/html/testdata/webkit/tests20.dat +++ b/html/testdata/html5lib-tests/tree-construction/tests20.dat
@@ -26,6 +26,32 @@ | <address> #data +<!doctype html><p><button><article> +#errors +(1,36): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <article> + +#data +<!doctype html><p><button><aside> +#errors +(1,34): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <aside> + +#data <!doctype html><p><button><blockquote> #errors (1,38): expected-closing-tag-but-got-eof @@ -39,6 +65,175 @@ | <blockquote> #data +<!doctype html><p><button><center> +#errors +(1,35): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <center> + +#data +<!doctype html><p><button><details> +#errors +(1,36): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <details> + +#data +<!doctype html><p><button><dialog> +#errors +(1,35): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <dialog> + +#data +<!doctype html><p><button><dir> +#errors +(1,32): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <dir> + +#data +<!doctype html><p><button><div> +#errors +(1,32): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <div> + +#data +<!doctype html><p><button><dl> +#errors +(1,31): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <dl> + +#data +<!doctype html><p><button><fieldset> +#errors +(1,37): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <fieldset> + +#data +<!doctype html><p><button><figcaption> +#errors +(1,39): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <figcaption> + +#data +<!doctype html><p><button><figure> +#errors +(1,35): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <figure> + +#data +<!doctype html><p><button><footer> +#errors +(1,35): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <footer> + +#data +<!doctype html><p><button><header> +#errors +(1,35): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <header> + +#data +<!doctype html><p><button><hgroup> +#errors +(1,35): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <hgroup> + +#data +<!doctype html><p><button><main> +#errors +(1,33): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <main> + +#data <!doctype html><p><button><menu> #errors (1,32): expected-closing-tag-but-got-eof @@ -52,6 +247,32 @@ | <menu> #data +<!doctype html><p><button><nav> +#errors +(1,32): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <nav> + +#data +<!doctype html><p><button><ol> +#errors +(1,31): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <ol> + +#data <!doctype html><p><button><p> #errors (1,29): expected-closing-tag-but-got-eof @@ -65,6 +286,45 @@ | <p> #data +<!doctype html><p><button><search> +#errors +(1,35): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <search> + +#data +<!doctype html><p><button><section> +#errors +(1,36): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <section> + +#data +<!doctype html><p><button><summary> +#errors +(1,36): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <button> +| <summary> + +#data <!doctype html><p><button><ul> #errors (1,30): expected-closing-tag-but-got-eof @@ -249,17 +509,16 @@ | <p> #data -<!doctype html><address><button></address>a +<!doctype html><button><p></button>x #errors -(1,42): end-tag-too-early #document | <!DOCTYPE html> | <html> | <head> | <body> -| <address> -| <button> -| "a" +| <button> +| <p> +| "x" #data <!doctype html><address><button></address>a @@ -557,6 +816,7 @@ <math><annotation-xml></svg>x #errors (1,6): expected-doctype-but-got-start-tag +(1,28): unexpected-end-tag-in-math (1,28): unexpected-end-tag (1,29): expected-closing-tag-but-got-eof #document
diff --git a/html/testdata/webkit/tests21.dat b/html/testdata/html5lib-tests/tree-construction/tests21.dat similarity index 92% rename from html/testdata/webkit/tests21.dat rename to html/testdata/html5lib-tests/tree-construction/tests21.dat index 1e2af7c..a926b13 100644 --- a/html/testdata/webkit/tests21.dat +++ b/html/testdata/html5lib-tests/tree-construction/tests21.dat
@@ -41,20 +41,7 @@ <svg><![CDATA[foo #errors (1,5): expected-doctype-but-got-start-tag -(1,17): expected-closing-tag-but-got-eof -#new-errors (1:18) eof-in-cdata -#document -| <html> -| <head> -| <body> -| <svg svg> -| "foo" - -#data -<svg><![CDATA[foo -#errors -(1,5): expected-doctype-but-got-start-tag (1,17): expected-closing-tag-but-got-eof #new-errors (1:18) eof-in-cdata @@ -69,6 +56,7 @@ <svg><![CDATA[ #errors (1,5): expected-doctype-but-got-start-tag +(1:15) eof-in-cdata (1,14): expected-closing-tag-but-got-eof #new-errors (1:15) eof-in-cdata @@ -102,21 +90,10 @@ | "]] >" #data -<svg><![CDATA[]] >]]> -#errors -(1,5): expected-doctype-but-got-start-tag -(1,21): expected-closing-tag-but-got-eof -#document -| <html> -| <head> -| <body> -| <svg svg> -| "]] >" - -#data <svg><![CDATA[]] #errors (1,5): expected-doctype-but-got-start-tag +(1:17) eof-in-cdata (1,16): expected-closing-tag-but-got-eof #new-errors (1:17) eof-in-cdata @@ -131,6 +108,7 @@ <svg><![CDATA[] #errors (1,5): expected-doctype-but-got-start-tag +(1:16) eof-in-cdata (1,15): expected-closing-tag-but-got-eof #new-errors (1:16) eof-in-cdata @@ -145,6 +123,7 @@ <svg><![CDATA[]>a #errors (1,5): expected-doctype-but-got-start-tag +(1:16) eof-in-cdata (1,17): expected-closing-tag-but-got-eof #new-errors (1:18) eof-in-cdata @@ -236,6 +215,7 @@ <svg><![CDATA[<svg>a #errors (1,5): expected-doctype-but-got-start-tag +(1:21) eof-in-cdata (1,20): expected-closing-tag-but-got-eof #new-errors (1:21) eof-in-cdata @@ -250,6 +230,7 @@ <svg><![CDATA[</svg>a #errors (1,5): expected-doctype-but-got-start-tag +(1:22) eof-in-cdata (1,21): expected-closing-tag-but-got-eof #new-errors (1:22) eof-in-cdata
diff --git a/html/testdata/webkit/tests22.dat b/html/testdata/html5lib-tests/tree-construction/tests22.dat similarity index 100% rename from html/testdata/webkit/tests22.dat rename to html/testdata/html5lib-tests/tree-construction/tests22.dat
diff --git a/html/testdata/webkit/tests23.dat b/html/testdata/html5lib-tests/tree-construction/tests23.dat similarity index 100% rename from html/testdata/webkit/tests23.dat rename to html/testdata/html5lib-tests/tree-construction/tests23.dat
diff --git a/html/testdata/webkit/tests24.dat b/html/testdata/html5lib-tests/tree-construction/tests24.dat similarity index 100% rename from html/testdata/webkit/tests24.dat rename to html/testdata/html5lib-tests/tree-construction/tests24.dat
diff --git a/html/testdata/webkit/tests25.dat b/html/testdata/html5lib-tests/tree-construction/tests25.dat similarity index 100% rename from html/testdata/webkit/tests25.dat rename to html/testdata/html5lib-tests/tree-construction/tests25.dat
diff --git a/html/testdata/webkit/tests26.dat b/html/testdata/html5lib-tests/tree-construction/tests26.dat similarity index 87% rename from html/testdata/webkit/tests26.dat rename to html/testdata/html5lib-tests/tree-construction/tests26.dat index de453b9..1ba2be2 100644 --- a/html/testdata/webkit/tests26.dat +++ b/html/testdata/html5lib-tests/tree-construction/tests26.dat
@@ -391,3 +391,63 @@ | <button> | <p> | <button> + +#data +<svg></p><foo> +#errors +(1:1) Missing doctype +9: HTML end tag “p” in a foreign namespace context. +(1:6) Unexpected </p> from in body insertion mode +(1:16) Unexpected EOF +#document +| <html> +| <head> +| <body> +| <svg svg> +| <p> +| <foo> + +#data +<svg></br><foo> +#errors +(1:1) Missing doctype +10: HTML end tag “br” in a foreign namespace context. +(1:6) Unexpected </br> from in body insertion mode +(1:16) Unexpected EOF +#document +| <html> +| <head> +| <body> +| <svg svg> +| <br> +| <foo> + +#data +<math></p><foo> +#errors +(1:1) Missing doctype +10: HTML end tag “p” in a foreign namespace context. +(1:7) Unexpected </p> from in body insertion mode +(1:16) Unexpected EOF +#document +| <html> +| <head> +| <body> +| <math math> +| <p> +| <foo> + +#data +<math></br><foo> +#errors +(1:1) Missing doctype +11: HTML end tag “br” in a foreign namespace context. +(1:7) Unexpected </br> from in body insertion mode +(1:17) Unexpected EOF +#document +| <html> +| <head> +| <body> +| <math math> +| <br> +| <foo>
diff --git a/html/testdata/webkit/tests3.dat b/html/testdata/html5lib-tests/tree-construction/tests3.dat similarity index 100% rename from html/testdata/webkit/tests3.dat rename to html/testdata/html5lib-tests/tree-construction/tests3.dat
diff --git a/html/testdata/webkit/tests4.dat b/html/testdata/html5lib-tests/tree-construction/tests4.dat similarity index 79% rename from html/testdata/webkit/tests4.dat rename to html/testdata/html5lib-tests/tree-construction/tests4.dat index 0a6174c..4f0cf70 100644 --- a/html/testdata/webkit/tests4.dat +++ b/html/testdata/html5lib-tests/tree-construction/tests4.dat
@@ -56,3 +56,19 @@ #document | <title> | "setting head's innerHTML" + +#data +direct <title> content +#errors +#document-fragment +title +#document +| "direct <title> content" + +#data +<!-- inside </script> --> +#errors +#document-fragment +script +#document +| "<!-- inside </script> -->"
diff --git a/html/testdata/webkit/tests5.dat b/html/testdata/html5lib-tests/tree-construction/tests5.dat similarity index 100% rename from html/testdata/webkit/tests5.dat rename to html/testdata/html5lib-tests/tree-construction/tests5.dat
diff --git a/html/testdata/webkit/tests6.dat b/html/testdata/html5lib-tests/tree-construction/tests6.dat similarity index 99% rename from html/testdata/webkit/tests6.dat rename to html/testdata/html5lib-tests/tree-construction/tests6.dat index f399123..8c36dd3 100644 --- a/html/testdata/webkit/tests6.dat +++ b/html/testdata/html5lib-tests/tree-construction/tests6.dat
@@ -48,7 +48,6 @@ #data <!doctype> #errors -(1,9): need-space-after-doctype (1,10): expected-doctype-name-but-got-right-bracket (1,10): unknown-doctype #new-errors @@ -604,6 +603,7 @@ #data <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"><html></html> #errors +(1,50): doctype-has-public-identifier #document | <!DOCTYPE html "-//W3C//DTD HTML 4.01//EN" ""> | <html>
diff --git a/html/testdata/webkit/tests7.dat b/html/testdata/html5lib-tests/tree-construction/tests7.dat similarity index 88% rename from html/testdata/webkit/tests7.dat rename to html/testdata/html5lib-tests/tree-construction/tests7.dat index 395dc72..aa7e16b 100644 --- a/html/testdata/webkit/tests7.dat +++ b/html/testdata/html5lib-tests/tree-construction/tests7.dat
@@ -47,6 +47,42 @@ | <body> #data +<!doctype html></head><base>X +#errors +(1,28): unexpected-start-tag-out-of-my-head +#document +| <!DOCTYPE html> +| <html> +| <head> +| <base> +| <body> +| "X" + +#data +<!doctype html></head><basefont>X +#errors +(1,32): unexpected-start-tag-out-of-my-head +#document +| <!DOCTYPE html> +| <html> +| <head> +| <basefont> +| <body> +| "X" + +#data +<!doctype html></head><bgsound>X +#errors +(1,31): unexpected-start-tag-out-of-my-head +#document +| <!DOCTYPE html> +| <html> +| <head> +| <bgsound> +| <body> +| "X" + +#data <!doctype html><table><meta></table> #errors (1,28): unexpected-start-tag-implies-table-voodoo @@ -164,7 +200,7 @@ #data <!doctype html><select><input>X #errors -(1,30): unexpected-input-in-select +1:32: ERROR: Premature end of file. Currently open tags: html, body, select. #document | <!DOCTYPE html> | <html> @@ -177,7 +213,7 @@ #data <!doctype html><select><select>X #errors -(1,31): unexpected-select-in-select +1:24: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select. #document | <!DOCTYPE html> | <html> @@ -391,7 +427,6 @@ (1,1): expected-doctype-but-got-chars (1,13): foster-parenting-character (1,14): foster-parenting-character -(1,20): foster-parenting-character (1,25): unexpected-end-tag (1,25): unexpected-end-tag-in-special-element (1,26): foster-parenting-character @@ -408,11 +443,11 @@ #data <select><keygen> #errors -(1,8): expected-doctype-but-got-start-tag -(1,16): unexpected-input-in-select +1:1: ERROR: Expected a doctype token +1:17: ERROR: Premature end of file. Currently open tags: html, body, select. #document | <html> | <head> | <body> | <select> -| <keygen> +| <keygen>
diff --git a/html/testdata/webkit/tests8.dat b/html/testdata/html5lib-tests/tree-construction/tests8.dat similarity index 95% rename from html/testdata/webkit/tests8.dat rename to html/testdata/html5lib-tests/tree-construction/tests8.dat index ba2e63d..d532801 100644 --- a/html/testdata/webkit/tests8.dat +++ b/html/testdata/html5lib-tests/tree-construction/tests8.dat
@@ -90,6 +90,9 @@ #data <table><li><li></table> #errors +(1,7): expected-doctype-but-got-start-tag +(1,11): foster-parenting-start-tag +(1,15): foster-parenting-start-tag #document | <html> | <head>
diff --git a/html/testdata/webkit/tests9.dat b/html/testdata/html5lib-tests/tree-construction/tests9.dat similarity index 86% rename from html/testdata/webkit/tests9.dat rename to html/testdata/html5lib-tests/tree-construction/tests9.dat index f8d04b2..1456324 100644 --- a/html/testdata/webkit/tests9.dat +++ b/html/testdata/html5lib-tests/tree-construction/tests9.dat
@@ -48,20 +48,17 @@ #data <!DOCTYPE html><body><select><math></math></select> #errors -(1,35) unexpected-start-tag-in-select -(1,42) unexpected-end-tag-in-select #document | <!DOCTYPE html> | <html> | <head> | <body> | <select> +| <math math> #data <!DOCTYPE html><body><select><option><math></math></option></select> #errors -(1,43) unexpected-start-tag-in-select -(1,50) unexpected-end-tag-in-select #document | <!DOCTYPE html> | <html> @@ -69,6 +66,7 @@ | <body> | <select> | <option> +| <math math> #data <!DOCTYPE html><body><table><math></math></table> @@ -301,13 +299,8 @@ #data <!DOCTYPE html><body><table><tr><td><select><math><mi>foo</mi><mi>bar</mi><p>baz</table><p>quux #errors -(1,50) unexpected-start-tag-in-select -(1,54) unexpected-start-tag-in-select -(1,62) unexpected-end-tag-in-select -(1,66) unexpected-start-tag-in-select -(1,74) unexpected-end-tag-in-select -(1,77) unexpected-start-tag-in-select -(1,88) unexpected-table-element-end-tag-in-select-in-table +1:75: ERROR: Start tag 'p' isn't allowed here. Currently open tags: html, body, table, tbody, tr, td, select, math. +1:81: ERROR: End tag 'table' isn't allowed here. Currently open tags: html, body, table, tbody, tr, td, select. #document | <!DOCTYPE html> | <html> @@ -318,28 +311,45 @@ | <tr> | <td> | <select> -| "foobarbaz" +| <math math> +| <math mi> +| "foo" +| <math mi> +| "bar" +| <p> +| "baz" | <p> | "quux" #data <!DOCTYPE html><body><table><select><math><mi>foo</mi><mi>bar</mi><p>baz</table><p>quux #errors -(1,36) unexpected-start-tag-implies-table-voodoo -(1,42) unexpected-start-tag-in-select -(1,46) unexpected-start-tag-in-select -(1,54) unexpected-end-tag-in-select -(1,58) unexpected-start-tag-in-select -(1,66) unexpected-end-tag-in-select -(1,69) unexpected-start-tag-in-select -(1,80) unexpected-table-element-end-tag-in-select-in-table +1:29: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, table. +1:37: ERROR: Start tag 'math' isn't allowed here. Currently open tags: html, body, table, select. +1:47: ERROR: Character tokens aren't legal here +1:48: ERROR: Character tokens aren't legal here +1:49: ERROR: Character tokens aren't legal here +1:59: ERROR: Character tokens aren't legal here +1:60: ERROR: Character tokens aren't legal here +1:61: ERROR: Character tokens aren't legal here +1:67: ERROR: Start tag 'p' isn't allowed here. Currently open tags: html, body, table, select, math. +1:67: ERROR: Start tag 'p' isn't allowed here. Currently open tags: html, body, table, select. +1:70: ERROR: Character tokens aren't legal here +1:71: ERROR: Character tokens aren't legal here +1:72: ERROR: Character tokens aren't legal here #document | <!DOCTYPE html> | <html> | <head> | <body> | <select> -| "foobarbaz" +| <math math> +| <math mi> +| "foo" +| <math mi> +| "bar" +| <p> +| "baz" | <table> | <p> | "quux"
diff --git a/html/testdata/webkit/tests_innerHTML_1.dat b/html/testdata/html5lib-tests/tree-construction/tests_innerHTML_1.dat similarity index 93% rename from html/testdata/webkit/tests_innerHTML_1.dat rename to html/testdata/html5lib-tests/tree-construction/tests_innerHTML_1.dat index 54f4368..09e0456 100644 --- a/html/testdata/webkit/tests_innerHTML_1.dat +++ b/html/testdata/html5lib-tests/tree-construction/tests_innerHTML_1.dat
@@ -111,16 +111,6 @@ | <a> #data -<a> -#errors -(1,3): unexpected-start-tag-implies-table-voodoo -(1,3): eof-in-table -#document-fragment -table -#document -| <a> - -#data <a><caption>a #errors (1,3): unexpected-start-tag-implies-table-voodoo @@ -503,30 +493,6 @@ | <td> #data -<a><td> -#errors -(1,3): unexpected-start-tag-implies-table-voodoo -(1,7): unexpected-cell-in-table-body -#document-fragment -tbody -#document -| <a> -| <tr> -| <td> - -#data -<a><td> -#errors -(1,3): unexpected-start-tag-implies-table-voodoo -(1,7): unexpected-cell-in-table-body -#document-fragment -tbody -#document -| <a> -| <tr> -| <td> - -#data <td><table><tbody><a><tr> #errors (1,4): unexpected-cell-in-table-body @@ -649,16 +615,6 @@ | <td> #data -<td><table></table><td> -#errors -#document-fragment -tr -#document -| <td> -| <table> -| <td> - -#data <caption><a> #errors (1,9): XXX-undefined-error @@ -834,7 +790,7 @@ #data <input><option> #errors -(1,7): unexpected-input-in-select +(1,7): XXX-undefined-error #document-fragment select #document @@ -843,20 +799,21 @@ #data <keygen><option> #errors -(1,8): unexpected-input-in-select #document-fragment select #document +| <keygen> | <option> #data <textarea><option> #errors -(1,10): unexpected-input-in-select +1:19: ERROR: Premature end of file. Currently open tags: html, textarea. #document-fragment select #document -| <option> +| <textarea> +| "<option>" #data </html><!--abc-->
diff --git a/html/testdata/webkit/tricky01.dat b/html/testdata/html5lib-tests/tree-construction/tricky01.dat similarity index 100% rename from html/testdata/webkit/tricky01.dat rename to html/testdata/html5lib-tests/tree-construction/tricky01.dat
diff --git a/html/testdata/webkit/webkit01.dat b/html/testdata/html5lib-tests/tree-construction/webkit01.dat similarity index 87% rename from html/testdata/webkit/webkit01.dat rename to html/testdata/html5lib-tests/tree-construction/webkit01.dat index 2127cfe..fc7f5c8 100644 --- a/html/testdata/webkit/webkit01.dat +++ b/html/testdata/html5lib-tests/tree-construction/webkit01.dat
@@ -360,6 +360,32 @@ | <!-- Again --> #data +<html><body></body> + <!-- Hi there --></html> +#errors +no-doctype +#document +| <html> +| <head> +| <body> +| " + " +| <!-- Hi there --> + +#data +<html><body></body></html> + <!-- Hi there --> +#errors +no-doctype +#document +| <html> +| <head> +| <body> +| " + " +| <!-- Hi there --> + +#data <html><body><ruby><div><rp>xx</rp></div></ruby></body></html> #errors (1,6): expected-doctype-but-got-start-tag @@ -411,11 +437,11 @@ #data <select><option>A<select><option>B<select><option>C<select><option>D<select><option>E<select><option>F<select><option>G<select> #errors -(1,8): expected-doctype-but-got-start-tag -(1,25): unexpected-select-in-select -(1,59): unexpected-select-in-select -(1,93): unexpected-select-in-select -(1,127): unexpected-select-in-select +1:1: ERROR: Expected a doctype token +1:18: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, option. +1:52: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, option. +1:86: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, option. +1:120: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, option. #document | <html> | <head> @@ -502,12 +528,11 @@ #data <kbd><table></kbd><col><select><tr> #errors -(1,5): expected-doctype-but-got-start-tag -(1,18): unexpected-end-tag-implies-table-voodoo -(1,18): unexpected-end-tag -(1,31): unexpected-start-tag-implies-table-voodoo -(1,35): unexpected-table-element-start-tag-in-select-in-table -(1,35): eof-in-table +1:1: ERROR: Expected a doctype token +1:13: ERROR: End tag 'kbd' isn't allowed here. Currently open tags: html, body, kbd, table. +1:13: ERROR: End tag 'kbd' isn't allowed here. Currently open tags: html, body, kbd, table. +1:24: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, kbd, table. +1:36: ERROR: Premature end of file. Currently open tags: html, body, kbd, table, tbody, tr. #document | <html> | <head> @@ -523,12 +548,11 @@ #data <kbd><table></kbd><col><select><tr></table><div> #errors -(1,5): expected-doctype-but-got-start-tag -(1,18): unexpected-end-tag-implies-table-voodoo -(1,18): unexpected-end-tag -(1,31): unexpected-start-tag-implies-table-voodoo -(1,35): unexpected-table-element-start-tag-in-select-in-table -(1,48): expected-closing-tag-but-got-eof +1:1: ERROR: Expected a doctype token +1:13: ERROR: End tag 'kbd' isn't allowed here. Currently open tags: html, body, kbd, table. +1:13: ERROR: End tag 'kbd' isn't allowed here. Currently open tags: html, body, kbd, table. +1:24: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, kbd, table. +1:49: ERROR: Premature end of file. Currently open tags: html, body, kbd, div. #document | <html> | <head> @@ -687,6 +711,10 @@ #data <table><tr><td><svg><desc><td></desc><circle> #errors +(1,7): expected-doctype-but-got-start-tag +(1,30): unexpected-start-tag +(1,37): unexpected-end-tag +(1,22): expected-closing-tag-but-got-eof #document | <html> | <head>
diff --git a/html/testdata/html5lib-tests/tree-construction/webkit02.dat b/html/testdata/html5lib-tests/tree-construction/webkit02.dat new file mode 100644 index 0000000..47a3c57 --- /dev/null +++ b/html/testdata/html5lib-tests/tree-construction/webkit02.dat
@@ -0,0 +1,775 @@ +#data +<foo bar=qux/> +#errors +(1,14): expected-doctype-but-got-start-tag +(1,14): expected-closing-tag-but-got-eof +#document +| <html> +| <head> +| <body> +| <foo> +| bar="qux/" + +#data +<p id="status"><noscript><strong>A</strong></noscript><span>B</span></p> +#errors +(1,15): expected-doctype-but-got-start-tag +#script-on +#document +| <html> +| <head> +| <body> +| <p> +| id="status" +| <noscript> +| "<strong>A</strong>" +| <span> +| "B" + +#data +<p id="status"><noscript><strong>A</strong></noscript><span>B</span></p> +#errors +(1,15): expected-doctype-but-got-start-tag +#script-off +#document +| <html> +| <head> +| <body> +| <p> +| id="status" +| <noscript> +| <strong> +| "A" +| <span> +| "B" + +#data +<div><sarcasm><div></div></sarcasm></div> +#errors +(1,5): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <body> +| <div> +| <sarcasm> +| <div> + +#data +<html><body><img src="" border="0" alt="><div>A</div></body></html> +#errors +(1,6): expected-doctype-but-got-start-tag +(1,67): eof-in-attribute-value-double-quote +#new-errors +(1:68) eof-in-tag +#document +| <html> +| <head> +| <body> + +#data +<table><td></tbody>A +#errors +(1,7): expected-doctype-but-got-start-tag +(1,11): unexpected-cell-in-table-body +(1,20): foster-parenting-character +(1,20): eof-in-table +#document +| <html> +| <head> +| <body> +| "A" +| <table> +| <tbody> +| <tr> +| <td> + +#data +<table><td></thead>A +#errors +(1,7): expected-doctype-but-got-start-tag +(1,11): unexpected-cell-in-table-body +(1,19): XXX-undefined-error +(1,20): expected-closing-tag-but-got-eof +#document +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| <td> +| "A" + +#data +<table><td></tfoot>A +#errors +(1,7): expected-doctype-but-got-start-tag +(1,11): unexpected-cell-in-table-body +(1,19): XXX-undefined-error +(1,20): expected-closing-tag-but-got-eof +#document +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| <td> +| "A" + +#data +<table><thead><td></tbody>A +#errors +(1,7): expected-doctype-but-got-start-tag +(1,18): unexpected-cell-in-table-body +(1,26): XXX-undefined-error +(1,27): expected-closing-tag-but-got-eof +#document +| <html> +| <head> +| <body> +| <table> +| <thead> +| <tr> +| <td> +| "A" + +#data +<legend>test</legend> +#errors +(1,7): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <body> +| <legend> +| "test" + +#data +<table><input> +#errors +(1,7): expected-doctype-but-got-start-tag +(1,14): foster-parenting-start-tag +(1,15): expected-closing-tag-but-got-eof +#document +| <html> +| <head> +| <body> +| <input> +| <table> + +#data +<b><em><dcell><postfield><postfield><postfield><postfield><missing_glyph><missing_glyph><missing_glyph><missing_glyph><hkern><aside></b></em> +#errors +unexpected-b-end-tag +unexpected-em-end-tag +eof-in-aside +#document-fragment +div +#document +| <b> +| <em> +| <dcell> +| <postfield> +| <postfield> +| <postfield> +| <postfield> +| <missing_glyph> +| <missing_glyph> +| <missing_glyph> +| <missing_glyph> +| <hkern> +| <aside> +| <b> + +#data +<b><em><foo><foo><aside></b> +#errors +(1,3): expected-doctype-but-got-start-tag +(1,28): adoption-agency-9 +(1,29): expected-closing-tag-but-got-eof +#document +| <html> +| <head> +| <body> +| <b> +| <em> +| <foo> +| <foo> +| <em> +| <aside> +| <b> + +#data +<b><em><foo><foo><aside></b></em> +#errors +(1,3): expected-doctype-but-got-start-tag +(1,28): adoption-agency-9 +(1,33): adoption-agency-9 +(1,34): expected-closing-tag-but-got-eof +#document +| <html> +| <head> +| <body> +| <b> +| <em> +| <foo> +| <foo> +| <em> +| <aside> +| <em> +| <b> + +#data +<b><em><foo><foo><foo><aside></b> +#errors +(1,3): expected-doctype-but-got-start-tag +(1,33): adoption-agency-9 +(1,34): expected-closing-tag-but-got-eof +#document +| <html> +| <head> +| <body> +| <b> +| <em> +| <foo> +| <foo> +| <foo> +| <aside> +| <b> + +#data +<b><em><foo><foo><foo><aside></b></em> +#errors +(1,3): expected-doctype-but-got-start-tag +(1,33): adoption-agency-9 +(1,38): adoption-agency-9 +(1,39): expected-closing-tag-but-got-eof +#document +| <html> +| <head> +| <body> +| <b> +| <em> +| <foo> +| <foo> +| <foo> +| <aside> +| <b> + +#data +<b><em><foo><foo><foo><foo><foo><foo><foo><foo><foo><foo><aside></b></em> +#errors +(1,68): adoption-agency-9 +(1,73): adoption-agency-9 +(1,74): expected-closing-tag-but-got-eof +#document-fragment +div +#document +| <b> +| <em> +| <foo> +| <foo> +| <foo> +| <foo> +| <foo> +| <foo> +| <foo> +| <foo> +| <foo> +| <foo> +| <aside> +| <b> + +#data +<b><em><foo><foob><foob><foob><foob><fooc><fooc><fooc><fooc><food><aside></b></em> +#errors +(1,77): adoption-agency-9 +(1,82): adoption-agency-9 +(1,83): expected-closing-tag-but-got-eof +#document-fragment +div +#document +| <b> +| <em> +| <foo> +| <foob> +| <foob> +| <foob> +| <foob> +| <fooc> +| <fooc> +| <fooc> +| <fooc> +| <food> +| <aside> +| <b> + +#data +<option><XH<optgroup></optgroup> +#errors +1:22: ERROR: End tag 'optgroup' isn't allowed here. Currently open tags: html, option, xh<optgroup. +1:33: ERROR: Premature end of file. Currently open tags: html, option, xh<optgroup. +#document-fragment +select +#document +| <option> +| <xh<optgroup> + +#data +<svg><foreignObject><div>foo</div><plaintext></foreignObject></svg><div>bar</div> +#errors +(1,5): expected-doctype-but-got-start-tag +(1,82): expected-closing-tag-but-got-eof +#document +| <html> +| <head> +| <body> +| <svg svg> +| <svg foreignObject> +| <div> +| "foo" +| <plaintext> +| "</foreignObject></svg><div>bar</div>" + +#data +<svg><foreignObject></foreignObject><title></svg>foo +#errors +(1,5): expected-doctype-but-got-start-tag +(1,49): expected-one-end-tag-but-got-another +#document +| <html> +| <head> +| <body> +| <svg svg> +| <svg foreignObject> +| <svg title> +| "foo" + +#data +</foreignObject><plaintext><div>foo</div> +#errors +(1,16): expected-doctype-but-got-end-tag +(1,16): unexpected-end-tag-before-html +(1,42): expected-closing-tag-but-got-eof +#document +| <html> +| <head> +| <body> +| <plaintext> +| "<div>foo</div>" + +#data +<svg xml:base xml:lang xml:space xml:baaah definitionurl> +#errors +no-doctype +eof-in-svg +#document +| <html> +| <head> +| <body> +| <svg svg> +| definitionurl="" +| xml lang="" +| xml space="" +| xml:baaah="" +| xml:base="" + +#data +<math definitionurl xlink:title xlink:show> +#errors +no-doctype +eof-in-math +#document +| <html> +| <head> +| <body> +| <math math> +| definitionURL="" +| xlink show="" +| xlink title="" + +#data +<math DEFINITIONURL> +#errors +no-doctype +eof-in-math +#document +| <html> +| <head> +| <body> +| <math math> +| definitionURL="" + +#data +<select><hr> +#errors +1:1: ERROR: Expected a doctype token +1:13: ERROR: Premature end of file. Currently open tags: html, body, select. +#document +| <html> +| <head> +| <body> +| <select> +| <hr> + +#data +<select><option><hr> +#errors +1:1: ERROR: Expected a doctype token +1:21: ERROR: Premature end of file. Currently open tags: html, body, select. +#document +| <html> +| <head> +| <body> +| <select> +| <option> +| <hr> + +#data +<select><optgroup><option><hr> +#errors +1:1: ERROR: Expected a doctype token +1:31: ERROR: Premature end of file. Currently open tags: html, body, select. +#document +| <html> +| <head> +| <body> +| <select> +| <optgroup> +| <option> +| <hr> + +#data +<select><optgroup><hr> +#errors +1:1: ERROR: Expected a doctype token +1:23: ERROR: Premature end of file. Currently open tags: html, body, select, optgroup. +#document +| <html> +| <head> +| <body> +| <select> +| <optgroup> +| <hr> + +#data +<select><option><optgroup><hr> +#errors +1:1: ERROR: Expected a doctype token +1:31: ERROR: Premature end of file. Currently open tags: html, body, select, optgroup. +#document +| <html> +| <head> +| <body> +| <select> +| <option> +| <optgroup> +| <hr> + +#data +<table><tr><td><select><hr> +#errors +1:1: ERROR: Expected a doctype token +1:28: ERROR: Premature end of file. Currently open tags: html, body, table, tbody, tr, td, select. +#document +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| <td> +| <select> +| <hr> + +#data +<table><tr><td><select><option><hr> +#errors +1:1: ERROR: Expected a doctype token +1:36: ERROR: Premature end of file. Currently open tags: html, body, table, tbody, tr, td, select. +#document +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| <td> +| <select> +| <option> +| <hr> + +#data +<table><tr><td><select><optgroup><option><hr> +#errors +1:1: ERROR: Expected a doctype token +1:46: ERROR: Premature end of file. Currently open tags: html, body, table, tbody, tr, td, select, optgroup. +#document +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| <td> +| <select> +| <optgroup> +| <option> +| <hr> + +#data +<table><tr><td><select><optgroup><hr> +#errors +1:1: ERROR: Expected a doctype token +1:38: ERROR: Premature end of file. Currently open tags: html, body, table, tbody, tr, td, select, optgroup. +#document +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| <td> +| <select> +| <optgroup> +| <hr> + +#data +<table><tr><td><select><option><optgroup><hr> +#errors +1:1: ERROR: Expected a doctype token +1:38: ERROR: Premature end of file. Currently open tags: html, body, table, tbody, tr, td, select, optgroup. +#document +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| <td> +| <select> +| <option> +| <optgroup> +| <hr> + +#data +<select><div><i></div><option>option +#errors +1:1: ERROR: Expected a doctype token +1:17: ERROR: End tag 'div' isn't allowed here. Currently open tags: html, body, select, div, i. +1:37: ERROR: Premature end of file. Currently open tags: html, body, select, option, i. +#document +| <html> +| <head> +| <body> +| <select> +| <div> +| <i> +| <i> +| <option> +| "option" + +#data +<div><i></div><option>option +#errors +1:1: ERROR: Expected a doctype token +1:9: ERROR: End tag 'div' isn't allowed here. Currently open tags: html, body, div, i. +1:29: ERROR: Premature end of file. Currently open tags: html, body, i, option. +#document +| <html> +| <head> +| <body> +| <div> +| <i> +| <i> +| <option> +| "option" + +#data +<select><div>div 1</div><button>button</button><div>div 2</div><datalist><option>option</option></datalist><div>div 3</div></select> +#errors +1:1: ERROR: Expected a doctype token +#document +| <html> +| <head> +| <body> +| <select> +| <div> +| "div 1" +| <button> +| "button" +| <div> +| "div 2" +| <datalist> +| <option> +| "option" +| <div> +| "div 3" + +#data +<select><button>button</select> +#errors +1:1: ERROR: Expected a doctype token +1:23: ERROR: End tag 'select' isn't allowed here. Currently open tags: html, body, select, button. +#document +| <html> +| <head> +| <body> +| <select> +| <button> +| "button" + +#data +<select><datalist>datalist</select> +#errors +1:1: ERROR: Expected a doctype token +1:27: ERROR: End tag 'select' isn't allowed here. Currently open tags: html, body, select, datalist. +#document +| <html> +| <head> +| <body> +| <select> +| <datalist> +| "datalist" + +#data +<select><button><select></select></button></select> +#errors +1:1: ERROR: Expected a doctype token +1:17: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, button. +1:25: ERROR: End tag 'select' isn't allowed here. Currently open tags: html, body. +1:34: ERROR: End tag 'button' isn't allowed here. Currently open tags: html, body. +1:43: ERROR: End tag 'select' isn't allowed here. Currently open tags: html, body. +#document +| <html> +| <head> +| <body> +| <select> +| <button> + +#data +<select><button><div><select></select> +#errors +1:1: ERROR: Expected a doctype token +1:22: ERROR: Start tag 'select' isn't allowed here. Currently open tags: html, body, select, button, div. +1:30: ERROR: End tag 'select' isn't allowed here. Currently open tags: html, body. +#document +| <html> +| <head> +| <body> +| <select> +| <button> +| <div> + +#data +<select><div><option><img>option</option></div></select> +#errors +1:1: ERROR: Expected a doctype token +#document +| <html> +| <head> +| <body> +| <select> +| <div> +| <option> +| <img> +| "option" + +#data +<select><input> +#errors +1:1: ERROR: Expected a doctype token +1:16: ERROR: Premature end of file. Currently open tags: html, body, select. +#document +| <html> +| <head> +| <body> +| <select> +| <input> + +#data +<select><button><selectedcontent></button><option>X +#errors +#document +| <html> +| <head> +| <body> +| <select> +| <button> +| <selectedcontent> +| "X" +| <option> +| "X" + +#data +<select><button><selectedcontent></button><option>x<i>i<b>ib</i>b +#errors +#document +| <html> +| <head> +| <body> +| <select> +| <button> +| <selectedcontent> +| "x" +| <i> +| "i" +| <b> +| "ib" +| <b> +| "b" +| <option> +| "x" +| <i> +| "i" +| <b> +| "ib" +| <b> +| "b" + +#data +<select><button><selectedcontent></button><option>X<option>Y +#errors +#document +| <html> +| <head> +| <body> +| <select> +| <button> +| <selectedcontent> +| "X" +| <option> +| "X" +| <option> +| "Y" + +#data +<select><button><selectedcontent></button><option>X<option selected>Y +#errors +#document +| <html> +| <head> +| <body> +| <select> +| <button> +| <selectedcontent> +| "Y" +| <option> +| "X" +| <option> +| selected="" +| "Y" + +#data +<font><select><option>a</option></font></select> +#errors +#document +| <html> +| <head> +| <body> +| <font> +| <select> +| <option> +| "a"
diff --git a/html/testdata/webkit/README b/html/testdata/webkit/README deleted file mode 100644 index 9b4c2d8..0000000 --- a/html/testdata/webkit/README +++ /dev/null
@@ -1,28 +0,0 @@ -The *.dat files in this directory are copied from The WebKit Open Source -Project, specifically $WEBKITROOT/LayoutTests/html5lib/resources. -WebKit is licensed under a BSD style license. -http://webkit.org/coding/bsd-license.html says: - -Copyright (C) 2009 Apple Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, -this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, -this list of conditions and the following disclaimer in the documentation -and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND ANY -EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -
diff --git a/html/testdata/webkit/math.dat b/html/testdata/webkit/math.dat deleted file mode 100644 index ae9cd7c..0000000 --- a/html/testdata/webkit/math.dat +++ /dev/null
@@ -1,81 +0,0 @@ -#data -<math><tr><td><mo><tr> -#errors -#document-fragment -td -#document -| <math math> -| <math tr> -| <math td> -| <math mo> - -#data -<math><tr><td><mo><tr> -#errors -#document-fragment -tr -#document -| <math math> -| <math tr> -| <math td> -| <math mo> - -#data -<math><thead><mo><tbody> -#errors -#document-fragment -thead -#document -| <math math> -| <math thead> -| <math mo> - -#data -<math><tfoot><mo><tbody> -#errors -#document-fragment -tfoot -#document -| <math math> -| <math tfoot> -| <math mo> - -#data -<math><tbody><mo><tfoot> -#errors -#document-fragment -tbody -#document -| <math math> -| <math tbody> -| <math mo> - -#data -<math><tbody><mo></table> -#errors -#document-fragment -tbody -#document -| <math math> -| <math tbody> -| <math mo> - -#data -<math><thead><mo></table> -#errors -#document-fragment -tbody -#document -| <math math> -| <math thead> -| <math mo> - -#data -<math><tfoot><mo></table> -#errors -#document-fragment -tbody -#document -| <math math> -| <math tfoot> -| <math mo>
diff --git a/html/testdata/webkit/namespace-sensitivity.dat b/html/testdata/webkit/namespace-sensitivity.dat deleted file mode 100644 index ca35c0e..0000000 --- a/html/testdata/webkit/namespace-sensitivity.dat +++ /dev/null
@@ -1,16 +0,0 @@ -#data -<body><table><tr><td><svg><td><foreignObject><span></td>Foo -#errors -#document -| <html> -| <head> -| <body> -| "Foo" -| <table> -| <tbody> -| <tr> -| <td> -| <svg svg> -| <svg td> -| <svg foreignObject> -| <span>
diff --git a/html/testdata/webkit/webkit02.dat b/html/testdata/webkit/webkit02.dat deleted file mode 100644 index 791991d..0000000 --- a/html/testdata/webkit/webkit02.dat +++ /dev/null
@@ -1,303 +0,0 @@ -#data -<foo bar=qux/> -#errors -(1,14): expected-doctype-but-got-start-tag -(1,14): expected-closing-tag-but-got-eof -#document -| <html> -| <head> -| <body> -| <foo> -| bar="qux/" - -#data -<p id="status"><noscript><strong>A</strong></noscript><span>B</span></p> -#errors -(1,15): expected-doctype-but-got-start-tag -#script-on -#document -| <html> -| <head> -| <body> -| <p> -| id="status" -| <noscript> -| "<strong>A</strong>" -| <span> -| "B" - -#data -<p id="status"><noscript><strong>A</strong></noscript><span>B</span></p> -#errors -(1,15): expected-doctype-but-got-start-tag -#script-off -#document -| <html> -| <head> -| <body> -| <p> -| id="status" -| <noscript> -| <strong> -| "A" -| <span> -| "B" - -#data -<div><sarcasm><div></div></sarcasm></div> -#errors -(1,5): expected-doctype-but-got-start-tag -#document -| <html> -| <head> -| <body> -| <div> -| <sarcasm> -| <div> - -#data -<html><body><img src="" border="0" alt="><div>A</div></body></html> -#errors -(1,6): expected-doctype-but-got-start-tag -(1,67): eof-in-attribute-value-double-quote -#new-errors -(1:68) eof-in-tag -#document -| <html> -| <head> -| <body> - -#data -<table><td></tbody>A -#errors -(1,7): expected-doctype-but-got-start-tag -(1,11): unexpected-cell-in-table-body -(1,20): foster-parenting-character -(1,20): eof-in-table -#document -| <html> -| <head> -| <body> -| "A" -| <table> -| <tbody> -| <tr> -| <td> - -#data -<table><td></thead>A -#errors -(1,7): expected-doctype-but-got-start-tag -(1,11): unexpected-cell-in-table-body -(1,19): XXX-undefined-error -(1,20): expected-closing-tag-but-got-eof -#document -| <html> -| <head> -| <body> -| <table> -| <tbody> -| <tr> -| <td> -| "A" - -#data -<table><td></tfoot>A -#errors -(1,7): expected-doctype-but-got-start-tag -(1,11): unexpected-cell-in-table-body -(1,19): XXX-undefined-error -(1,20): expected-closing-tag-but-got-eof -#document -| <html> -| <head> -| <body> -| <table> -| <tbody> -| <tr> -| <td> -| "A" - -#data -<table><thead><td></tbody>A -#errors -(1,7): expected-doctype-but-got-start-tag -(1,18): unexpected-cell-in-table-body -(1,26): XXX-undefined-error -(1,27): expected-closing-tag-but-got-eof -#document -| <html> -| <head> -| <body> -| <table> -| <thead> -| <tr> -| <td> -| "A" - -#data -<legend>test</legend> -#errors -#document -| <html> -| <head> -| <body> -| <legend> -| "test" - -#data -<table><input> -#errors -#document -| <html> -| <head> -| <body> -| <input> -| <table> - -#data -<b><em><foo><foo><aside></b> -#errors -#document -| <html> -| <head> -| <body> -| <b> -| <em> -| <foo> -| <foo> -| <em> -| <aside> -| <b> - -#data -<b><em><foo><foo><aside></b></em> -#errors -#document -| <html> -| <head> -| <body> -| <b> -| <em> -| <foo> -| <foo> -| <em> -| <aside> -| <em> -| <b> - -#data -<b><em><foo><foo><foo><aside></b> -#errors -#document -| <html> -| <head> -| <body> -| <b> -| <em> -| <foo> -| <foo> -| <foo> -| <aside> -| <b> - -#data -<b><em><foo><foo><foo><aside></b></em> -#errors -#document -| <html> -| <head> -| <body> -| <b> -| <em> -| <foo> -| <foo> -| <foo> -| <aside> -| <b> - -#data -<b><em><foo><foo><foo><foo><foo><foo><foo><foo><foo><foo><aside></b></em> -#errors -#document-fragment -div -#document -| <b> -| <em> -| <foo> -| <foo> -| <foo> -| <foo> -| <foo> -| <foo> -| <foo> -| <foo> -| <foo> -| <foo> -| <aside> -| <b> - -#data -<b><em><foo><foob><foob><foob><foob><fooc><fooc><fooc><fooc><food><aside></b></em> -#errors -#document-fragment -div -#document -| <b> -| <em> -| <foo> -| <foob> -| <foob> -| <foob> -| <foob> -| <fooc> -| <fooc> -| <fooc> -| <fooc> -| <food> -| <aside> -| <b> - -#data -<option><XH<optgroup></optgroup> -#errors -#document-fragment -select -#document -| <option> - -#data -<svg><foreignObject><div>foo</div><plaintext></foreignObject></svg><div>bar</div> -#errors -#document -| <html> -| <head> -| <body> -| <svg svg> -| <svg foreignObject> -| <div> -| "foo" -| <plaintext> -| "</foreignObject></svg><div>bar</div>" - -#data -<svg><foreignObject></foreignObject><title></svg>foo -#errors -#document -| <html> -| <head> -| <body> -| <svg svg> -| <svg foreignObject> -| <svg title> -| "foo" - -#data -</foreignObject><plaintext><div>foo</div> -#errors -#document -| <html> -| <head> -| <body> -| <plaintext> -| "<div>foo</div>"
diff --git a/html/token.go b/html/token.go index 09b0e7e..83fcf96 100644 --- a/html/token.go +++ b/html/token.go
@@ -1300,9 +1300,22 @@ attrNames: make(map[string]bool), } if contextTag != "" { + // Per the "Parsing HTML Fragments" portion of the spec: + // For performance reasons, an implementation that does not report errors + // and that uses the actual state machine described in this specification + // directly could use the PLAINTEXT state instead of the RAWTEXT and script + // data states where those are mentioned in the list above. Except for + // rules regarding parse errors, they are equivalent, since there is no + // appropriate end tag token in the fragment case, yet they involve far + // fewer state transitions. + // + // As such we just set everything to plaintext, which makes some complex parsing + // cases somewhat simpler. switch s := strings.ToLower(contextTag); s { - case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "title", "textarea", "xmp": + case "title", "textarea": z.rawTag = s + case "style", "xmp", "iframe", "noembed", "noframes", "script", "noscript", "plaintext": + z.rawTag = "plaintext" } } return z
diff --git a/http2/export_wrap_test.go b/http2/export_wrap_test.go index 7cb8a56..63fa3fc 100644 --- a/http2/export_wrap_test.go +++ b/http2/export_wrap_test.go
@@ -9,8 +9,7 @@ import "net/http" func (t *Transport) TestTransport() *http.Transport { - t.init() - return t.t1 + return t.init() } func (s *Server) TestSetNewConnFunc(f func(*ServerConn)) {
diff --git a/http2/transport_configure_test.go b/http2/transport_configure_test.go new file mode 100644 index 0000000..83993c6 --- /dev/null +++ b/http2/transport_configure_test.go
@@ -0,0 +1,63 @@ +// Copyright 2026 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 http2_test + +import ( + "crypto/tls" + "io" + "net/http" + "net/http/httptest" + "testing" + + . "golang.org/x/net/http2" +) + +// TestConfigureTransportEnablesHTTP2 verifies that ConfigureTransport and +// ConfigureTransports enable HTTP/2 on a transport with a custom TLSClientConfig. +// net/http does not auto-enable HTTP/2 for such a transport, so without +// ConfigureTransport doing so the request is sent over HTTP/1 to an HTTP/2 +// server. The test is intentionally not build-tagged: the behavior must hold for +// the legacy implementation (transport.go) and the go1.27 net/http wrapper +// (transport_wrap.go) alike. +func TestConfigureTransportEnablesHTTP2(t *testing.T) { + for _, tc := range []struct { + name string + configure func(*http.Transport) error + }{ + {"ConfigureTransport", ConfigureTransport}, + {"ConfigureTransports", func(t1 *http.Transport) error { + _, err := ConfigureTransports(t1) + return err + }}, + } { + t.Run(tc.name, func(t *testing.T) { + ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, r.Proto) + })) + if err := ConfigureServer(ts.Config, &Server{}); err != nil { + t.Fatal(err) + } + ts.TLS = ts.Config.TLSConfig + ts.StartTLS() + defer ts.Close() + + // A custom TLSClientConfig disables net/http's automatic HTTP/2; + // ConfigureTransport is responsible for re-enabling it. + tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} + if err := tc.configure(tr); err != nil { + t.Fatal(err) + } + res, err := (&http.Client{Transport: tr}).Get(ts.URL) + if err != nil { + t.Fatalf("Get: %v", err) + } + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + if res.ProtoMajor != 2 { + t.Errorf("request negotiated %s (server saw %q); want HTTP/2", res.Proto, body) + } + }) + } +}
diff --git a/http2/transport_wrap.go b/http2/transport_wrap.go index d25d99b..534e77a 100644 --- a/http2/transport_wrap.go +++ b/http2/transport_wrap.go
@@ -22,8 +22,8 @@ ) func configureTransport(t1 *http.Transport) error { - // ConfigureTransport is a no-op: The http.Transport already supports HTTP/2. - return nil + _, err := configureTransports(t1) + return err } func configureTransports(t1 *http.Transport) (*Transport, error) { @@ -31,6 +31,17 @@ // linked to the http.Transport's. tr2 := &Transport{} tr2.configure(t1) + // Enable HTTP/2 on the transport, as the pre-wrapping implementation did: + // net/http does not auto-enable it for a transport with a custom + // TLSClientConfig or dialer. + if t1.TLSClientConfig == nil { + t1.TLSClientConfig = &tls.Config{} + } + if t1.Protocols == nil { + t1.Protocols = new(http.Protocols) + t1.Protocols.SetHTTP1(true) + } + t1.Protocols.SetHTTP2(true) return tr2, nil } @@ -44,7 +55,7 @@ // Registered is called by net/http.Transport.RegisterProtocol, // to let us know that it understands the registration mechanism we're using. func (t transportConfig) Registered(t1 *http.Transport) { - t.t.t1 = t1 + t.t.lazyt1 = t1 } func (t transportConfig) DisableCompression() bool { @@ -134,29 +145,30 @@ type transportInternal struct { initOnce sync.Once - t1 *http.Transport + lazyt1 *http.Transport } -func (t *Transport) init() { +func (t *Transport) init() *http.Transport { t.initOnce.Do(func() { - if t.t1 != nil { + if t.lazyt1 != nil { return } t1 := &http.Transport{} t.configure(t1) }) + return t.lazyt1 } func (t *Transport) configure(t1 *http.Transport) { t1.RegisterProtocol("http/2", transportConfig{t}) - // tr2.t1 is set by transportConfig.Registered. - if t.t1 != t1 { + // tr2.lazyt1 is set by transportConfig.Registered. + if t.lazyt1 != t1 { panic("http2: net/http does not support this version of x/net/http2") } } func (t *Transport) roundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) { - t.init() + t1 := t.init() if req.URL.Scheme == "http" && !t.AllowHTTP { return nil, errors.New("http2: unencrypted HTTP/2 not enabled") @@ -177,22 +189,23 @@ ctx := context.WithValue(req.Context(), http2TransportContextKey{}, t) req = req.WithContext(ctx) - return t.t1.RoundTrip(req) + return t1.RoundTrip(req) } func (t *Transport) closeIdleConnections() { - t.init() - t.t1.CloseIdleConnections() + t1 := t.init() + t1.CloseIdleConnections() } func (t *Transport) newUserClientConn(c net.Conn) (*ClientConn, error) { + t1 := t.init() // http.Transport's NewClientConn doesn't provide a supported way to create // a connection from a net.Conn. (This might be useful to add in the future?) // We're going to craftily sneak one in via the context key, with the // scheme of "http/2" telling NewClientConn to look for it. ctx := context.WithValue(context.Background(), netConnContextKey{}, c) - nhcc, err := t.t1.NewClientConn(ctx, "http/2", "") + nhcc, err := t1.NewClientConn(ctx, "http/2", "") if err != nil { return nil, err }
diff --git a/idna/idna.go b/idna/idna.go index 2276712..e2f28fe 100644 --- a/idna/idna.go +++ b/idna/idna.go
@@ -400,7 +400,11 @@ // Spec says keep the old label. continue } - if unicode16 && err == nil && len(u) > 0 && isASCII(u) { + if err == nil && len(u) > 0 && isASCII(u) { + // UTS 43 pre-revision 33 doesn't classify a xn-- label + // which contains only ASCII characters as an error, + // but that's a specification bug and a security issue. + // Always return an error in this case. err = punyError(enc) } isBidi = isBidi || bidirule.DirectionString(u) != bidi.LeftToRight
diff --git a/idna/idna_test.go b/idna/idna_test.go index 0b067ca..2d58bff 100644 --- a/idna/idna_test.go +++ b/idna/idna_test.go
@@ -104,5 +104,15 @@ } } -// TODO(nigeltao): test errors, once we've specified when ToASCII and ToUnicode -// return errors. +func TestIDNAErrors(t *testing.T) { + for _, tc := range []string{ + "xn--example-.com", + } { + if got, err := ToASCII(tc); err == nil { + t.Errorf("ToASCII(%q) = %q, want error", tc, got) + } + if got, err := ToUnicode(tc); err == nil { + t.Errorf("ToUnicode(%q) = %q, want error", tc, got) + } + } +}
diff --git a/internal/http3/body.go b/internal/http3/body.go index 169417c..e025f5d 100644 --- a/internal/http3/body.go +++ b/internal/http3/body.go
@@ -108,8 +108,8 @@ w.st.writeVarint(int64(len(encTrailer))) w.st.Write(encTrailer) } - if w.st != nil && w.st.stream != nil { - w.st.stream.CloseWrite() + if w.st != nil { + w.st.CloseWrite() } return nil } @@ -127,9 +127,12 @@ send100Continue func() // A map where the key represents the trailer header names we expect. If // there is a HEADERS frame after reading DATA frames to EOF, the value of - // the headers will be written here, provided that the name of the header - // exists in the map already. - trailer http.Header + // the headers will be written here. Keys in the map are assumed to be + // canonicalized. + // If filterTrailer is true, headers that are not already in the map will + // be ignored; otherwise, all headers will be added to the map. + trailer http.Header + filterTrailer bool } func (r *bodyReader) Read(p []byte) (n int, err error) { @@ -188,8 +191,16 @@ } var dec qpackDecoder if err := dec.decode(r.st, func(_ indexType, name, value string) error { + if r.trailer == nil { + return nil + } + if !validWireHeaderFieldName(name) || !httpguts.ValidHeaderFieldValue(value) { + return nil + } name = textproto.CanonicalMIMEHeaderKey(textproto.TrimString(name)) - if _, ok := r.trailer[name]; ok { + if !r.filterTrailer { + r.trailer.Add(name, value) + } else if _, ok := r.trailer[name]; ok { r.trailer.Add(name, value) } return nil @@ -222,9 +233,11 @@ func (r *bodyReader) Close() error { // Unlike the HTTP/1 and HTTP/2 body readers (at the time of this comment being written), // calling Close concurrently with Read will interrupt the read. - r.st.stream.CloseRead() + r.st.CloseRead() // Make sure that any data that has already been written to bodyReader // cannot be read after it has been closed. + r.mu.Lock() + defer r.mu.Unlock() r.err = net.ErrClosed r.remain = 0 return nil
diff --git a/internal/http3/body_test.go b/internal/http3/body_test.go index ae93014..e10d062 100644 --- a/internal/http3/body_test.go +++ b/internal/http3/body_test.go
@@ -224,7 +224,7 @@ case receiveData: t.Logf("receive DATA frame content: size=%v", step.size) for range step.size { - st.stream.stream.WriteByte(byte(bytesSent)) + st.WriteByte(byte(bytesSent)) bytesSent++ } st.Flush() @@ -239,7 +239,7 @@ st.Flush() case receiveEOF: t.Logf("receive EOF on request stream") - st.stream.stream.CloseWrite() + st.CloseWrite() case wantBody: t.Logf("read %v bytes from response body", step.size) want := make([]byte, step.size)
diff --git a/internal/http3/conn.go b/internal/http3/conn.go index 6a3c962..a90464e 100644 --- a/internal/http3/conn.go +++ b/internal/http3/conn.go
@@ -99,14 +99,14 @@ case *connectionError: h.abort(err) case nil: - st.stream.CloseRead() - st.stream.CloseWrite() + st.CloseRead() + st.CloseWrite() case *streamError: - st.stream.CloseRead() - st.stream.Reset(uint64(err.code)) + st.CloseRead() + st.Reset(uint64(err.code)) default: - st.stream.CloseRead() - st.stream.Reset(uint64(errH3InternalError)) + st.CloseRead() + st.Reset(uint64(errH3InternalError)) } }
diff --git a/internal/http3/conn_test.go b/internal/http3/conn_test.go index 68c665e..91eb404 100644 --- a/internal/http3/conn_test.go +++ b/internal/http3/conn_test.go
@@ -132,7 +132,7 @@ t.Fatal(err) } st := newTestQUICStream(tc.t, newStream(qs)) - st.stream.stream.Close() + st.Close() tc.wantClosed("after peer creates and closes uni stream", errH3StreamCreationError) })
diff --git a/internal/http3/gzip.go b/internal/http3/gzip.go new file mode 100644 index 0000000..8854f2b --- /dev/null +++ b/internal/http3/gzip.go
@@ -0,0 +1,125 @@ +// Copyright 2026 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 http3 + +import ( + "compress/flate" + "compress/gzip" + "errors" + "io" + "io/fs" + "sync" +) + +var errConcurrentReadOnResBody = errors.New("http3: concurrent read on response body") + +// gzipReader wraps a response body so it can lazily +// get gzip.Reader from the pool on the first call to Read. +// After Close is called it puts gzip.Reader to the pool immediately +// if there is no Read in progress or later when Read completes. +type gzipReader struct { + body io.ReadCloser // underlying Response.Body + mu sync.Mutex // guards zr and zerr + zr *gzip.Reader // stores gzip reader from the pool between reads + zerr error // sticky gzip reader init error or sentinel value to detect concurrent read and read after close +} + +type eofReader struct{} + +func (eofReader) Read([]byte) (int, error) { return 0, io.EOF } +func (eofReader) ReadByte() (byte, error) { return 0, io.EOF } + +var gzipPool = sync.Pool{New: func() any { return new(gzip.Reader) }} + +// gzipPoolGet gets a gzip.Reader from the pool and resets it to read from r. +func gzipPoolGet(r io.Reader) (*gzip.Reader, error) { + zr := gzipPool.Get().(*gzip.Reader) + if err := zr.Reset(r); err != nil { + gzipPoolPut(zr) + return nil, err + } + return zr, nil +} + +// gzipPoolPut puts a gzip.Reader back into the pool. +func gzipPoolPut(zr *gzip.Reader) { + // Reset will allocate bufio.Reader if we pass it anything + // other than a flate.Reader, so ensure that it's getting one. + var r flate.Reader = eofReader{} + zr.Reset(r) + gzipPool.Put(zr) +} + +// acquire returns a gzip.Reader for reading response body. +// The reader must be released after use. +func (gz *gzipReader) acquire() (*gzip.Reader, error) { + gz.mu.Lock() + defer gz.mu.Unlock() + if gz.zerr != nil { + return nil, gz.zerr + } + if gz.zr == nil { + // gzipPoolGet might block indefinitely since it reads the gzip header. + // Therefore, drop mu temporarily when using gzipPoolGet. + // We set zerr to errConcurrentReadOnResBody to prevent concurrent read + // even when mu is temporarily dropped. + gz.zerr = errConcurrentReadOnResBody + gz.mu.Unlock() + zr, err := gzipPoolGet(gz.body) + gz.mu.Lock() + // Guard against Close being called while gzipPoolGet is running. + if gz.zerr != errConcurrentReadOnResBody { + if zr != nil { + gzipPoolPut(zr) + } + return nil, gz.zerr + } + gz.zr, gz.zerr = zr, err + if gz.zerr != nil { + return nil, gz.zerr + } + } + ret := gz.zr + gz.zr, gz.zerr = nil, errConcurrentReadOnResBody + return ret, nil +} + +// release returns the gzip.Reader to the pool if Close was called during Read. +func (gz *gzipReader) release(zr *gzip.Reader) { + gz.mu.Lock() + defer gz.mu.Unlock() + if gz.zerr == errConcurrentReadOnResBody { + gz.zr, gz.zerr = zr, nil + } else { // fs.ErrClosed + gzipPoolPut(zr) + } +} + +// close returns the gzip.Reader to the pool immediately or +// signals release to do so after Read completes. +func (gz *gzipReader) close() { + gz.mu.Lock() + defer gz.mu.Unlock() + if gz.zerr == nil && gz.zr != nil { + gzipPoolPut(gz.zr) + gz.zr = nil + } + gz.zerr = fs.ErrClosed +} + +func (gz *gzipReader) Read(p []byte) (n int, err error) { + zr, err := gz.acquire() + if err != nil { + return 0, err + } + defer gz.release(zr) + + return zr.Read(p) +} + +func (gz *gzipReader) Close() error { + gz.close() + + return gz.body.Close() +}
diff --git a/internal/http3/qpack.go b/internal/http3/qpack.go index e400d83..423b5aa 100644 --- a/internal/http3/qpack.go +++ b/internal/http3/qpack.go
@@ -5,8 +5,10 @@ package http3 import ( + "encoding/binary" "errors" "io" + "math" "golang.org/x/net/http/httpguts" "golang.org/x/net/http2/hpack" @@ -225,25 +227,19 @@ // readPrefixedIntWithByte reads an RFC 7541 prefixed integer from st. // The first byte has already been read from the stream. -func (st *stream) readPrefixedIntWithByte(firstByte byte, prefixLen uint8) (v int64, err error) { +func (st *stream) readPrefixedIntWithByte(firstByte byte, prefixLen uint8) (int64, error) { prefixMask := (byte(1) << prefixLen) - 1 - v = int64(firstByte & prefixMask) - if v != int64(prefixMask) { - return v, nil + if v := firstByte & prefixMask; v != prefixMask { + return int64(v), nil } - m := 0 - for { - b, err := st.ReadByte() - if err != nil { - return 0, errQPACKDecompressionFailed - } - v += int64(b&127) << m - m += 7 - if b&128 == 0 { - break - } + v, err := binary.ReadUvarint(st) + if err != nil { + return 0, errQPACKDecompressionFailed } - return v, err + if v > math.MaxInt64-uint64(prefixMask) { + return 0, errQPACKDecompressionFailed + } + return int64(v + uint64(prefixMask)), nil } // appendPrefixedInt appends an RFC 7541 prefixed integer to b. @@ -258,11 +254,7 @@ } b = append(b, firstByte|byte(prefixMask)) u -= prefixMask - for u >= 128 { - b = append(b, 0x80|byte(u&0x7f)) - u >>= 7 - } - return append(b, byte(u)) + return binary.AppendUvarint(b, u) } // String literal encoding from RFC 7541, section 5.2 @@ -291,6 +283,9 @@ if err != nil { return "", errQPACKDecompressionFailed } + if st.lim >= 0 && size > st.lim { + return "", errQPACKDecompressionFailed + } hbit := byte(1) << prefixLen isHuffman := firstByte&hbit != 0
diff --git a/internal/http3/qpack_decode_test.go b/internal/http3/qpack_decode_test.go index 59e67b0..34afc8d 100644 --- a/internal/http3/qpack_decode_test.go +++ b/internal/http3/qpack_decode_test.go
@@ -173,6 +173,12 @@ }, { name: "too high static table index", enc: unhex("0000ff23ff24"), + }, { + name: "prefixed string length overflow", + enc: unhex("000027ffffffffffffffff7f"), + }, { + name: "prefixed static index overflow", + enc: unhex("0000ffffffffffffffffff7f"), }} { synctestSubtest(t, test.name, func(t *testing.T) { st1, st2 := newStreamPair(t)
diff --git a/internal/http3/qpack_static.go b/internal/http3/qpack_static.go index 6c0b51c..395554c 100644 --- a/internal/http3/qpack_static.go +++ b/internal/http3/qpack_static.go
@@ -13,7 +13,7 @@ // staticTableEntry returns the static table entry with the given index. func staticTableEntry(index int64) (tableEntry, error) { - if index >= int64(len(staticTableEntries)) { + if index < 0 || index >= int64(len(staticTableEntries)) { return tableEntry{}, errQPACKDecompressionFailed } return staticTableEntries[index], nil
diff --git a/internal/http3/roundtrip.go b/internal/http3/roundtrip.go index edf5f13..c5aab72 100644 --- a/internal/http3/roundtrip.go +++ b/internal/http3/roundtrip.go
@@ -11,6 +11,7 @@ "net/http/httptrace" "net/textproto" "strconv" + "strings" "sync" "golang.org/x/net/http/httpguts" @@ -41,15 +42,21 @@ func (rt *roundTripState) abort(err error) error { rt.errOnce.Do(func() { rt.err = err + + rt.cc.mu.Lock() + rt.cc.active-- + rt.cc.mu.Unlock() + rt.cc.maybeCallStateHook() + switch e := err.(type) { case *connectionError: rt.cc.abort(e) case *streamError: - rt.st.stream.CloseRead() - rt.st.stream.Reset(uint64(e.code)) + rt.st.CloseRead() + rt.st.Reset(uint64(e.code)) default: - rt.st.stream.CloseRead() - rt.st.stream.Reset(uint64(errH3NoError)) + rt.st.CloseRead() + rt.st.Reset(uint64(errH3NoError)) } }) return rt.err @@ -88,9 +95,20 @@ // RoundTrip sends a request on the connection. func (cc *clientConn) RoundTrip(req *http.Request) (_ *http.Response, err error) { + cc.mu.Lock() + if cc.reserved > 0 { + cc.reserved-- + } + cc.active++ + cc.mu.Unlock() + // Each request gets its own QUIC stream. st, err := newConnStream(req.Context(), cc.qconn, streamTypeRequest) if err != nil { + cc.mu.Lock() + cc.active-- + cc.mu.Unlock() + cc.maybeCallStateHook() return nil, err } rt := &roundTripState{ @@ -112,6 +130,7 @@ st.stream.SetReadContext(req.Context()) st.stream.SetWriteContext(req.Context()) + addedGzip := httpcommon.IsRequestGzip(req.Method, req.Header, cc.tr.tr1.DisableCompression) headers := cc.enc.encode(func(yield func(itype indexType, name, value string)) { _, err = httpcommon.EncodeHeaders(req.Context(), httpcommon.EncodeHeadersParam{ Request: httpcommon.Request{ @@ -122,7 +141,7 @@ Trailer: req.Trailer, ActualContentLength: actualContentLength(req), }, - AddGzipHeader: false, // TODO: add when appropriate + AddGzipHeader: addedGzip, PeerMaxHeaderListSize: 0, DefaultUserAgent: "Go-http-client/3", }, func(name, value string) { @@ -215,7 +234,13 @@ Trailer: trailer, Body: (*transportResponseBody)(rt), } - // TODO: Automatic Content-Type: gzip decoding. + if addedGzip && strings.EqualFold(h.Get("Content-Encoding"), "gzip") { + resp.Body = &gzipReader{body: resp.Body} + h.Del("Content-Encoding") + h.Del("Content-Length") + resp.ContentLength = -1 + resp.Uncompressed = true + } return resp, nil case frameTypePushPromise: if err := cc.handlePushPromise(st); err != nil { @@ -290,7 +315,7 @@ rt.closeReqBody() // Close the request stream, since we're done with the request. // Reset closes the sending half of the stream. - rt.st.stream.Reset(uint64(errH3NoError)) + rt.st.Reset(uint64(errH3NoError)) // respBody.Close is responsible for closing the receiving half. err := rt.respBody.Close() if err == nil {
diff --git a/internal/http3/roundtrip_test.go b/internal/http3/roundtrip_test.go index 6c8b997..2ca08ae 100644 --- a/internal/http3/roundtrip_test.go +++ b/internal/http3/roundtrip_test.go
@@ -6,6 +6,7 @@ import ( "bytes" + "compress/gzip" "errors" "io" "net/http" @@ -13,6 +14,7 @@ "net/textproto" "reflect" "slices" + "strconv" "strings" "testing" "testing/synctest" @@ -29,7 +31,7 @@ req.Header["User-Agent"] = nil rt := tc.roundTrip(req) st := tc.wantStream(streamTypeRequest) - st.wantHeaders(http.Header{ + st.wantSomeHeaders(http.Header{ ":authority": []string{"example.tld"}, ":method": []string{"GET"}, ":path": []string{"/"}, @@ -427,7 +429,7 @@ ":status": []string{"200"}, }) st.writeData(serverBody) - st.stream.stream.CloseWrite() + st.CloseWrite() // Client receives the response from server. rt.wantStatus(200) @@ -476,7 +478,7 @@ st.wantIdle("client does not send its body without getting status 100") serverBody := []byte("server's body") st.writeData(serverBody) - st.stream.stream.CloseWrite() + st.CloseWrite() rt.wantStatus(200) rt.wantBody(serverBody) @@ -516,7 +518,7 @@ st.writeHeaders(http.Header{ ":status": {"200"}, }) - st.stream.stream.CloseWrite() + st.CloseWrite() rt.wantStatus(200) st.wantClosed("request is complete") @@ -557,13 +559,15 @@ req, _ = http.NewRequest("POST", "https://example.tld/", io.MultiReader( testReader{readFunc: func(_ []byte) (int, error) { req.Trailer["Client-Trailer-A"] = []string{"valuea"} - req.Trailer["Undeclared-Trailer"] = []string{"undeclared"} // Should be ignored. + // Transport should not send undeclared trailer. + req.Trailer["Undeclared-Trailer"] = []string{"undeclared"} return 0, io.EOF }}, strings.NewReader("a body"), testReader{readFunc: func(_ []byte) (int, error) { req.Trailer["Client-Trailer-B"] = []string{"valueb"} - req.Trailer["Undeclared-Trailer"] = []string{"undeclared"} // Should be ignored. + // Transport should not send undeclared trailer. + req.Trailer["Undeclared-Trailer"] = []string{"undeclared"} return 0, io.EOF }}, )) @@ -592,12 +596,14 @@ req, _ = http.NewRequest("POST", "https://example.tld/", io.MultiReader( testReader{readFunc: func(_ []byte) (int, error) { req.Trailer["Client-Trailer-A"] = []string{"valuea"} - req.Trailer["Undeclared-Trailer"] = []string{"undeclared"} // Should be ignored. + // Transport should not send undeclared trailer. + req.Trailer["Undeclared-Trailer"] = []string{"undeclared"} return 0, io.EOF }}, testReader{readFunc: func(_ []byte) (int, error) { req.Trailer["Client-Trailer-B"] = []string{"valueb"} - req.Trailer["Undeclared-Trailer"] = []string{"undeclared"} // Should be ignored. + // Transport should not send undeclared trailer. + req.Trailer["Undeclared-Trailer"] = []string{"undeclared"} return 0, io.EOF }}, )) @@ -637,7 +643,7 @@ "server-trailer-a": {"valuea"}, // Note that Server-Trailer-B is skipped. "server-trailer-c": {"valuec"}, - "undeclared-trailer": {"undeclared"}, // Should be ignored. + "undeclared-trailer": {"undeclared"}, }) rt.wantStatus(200) @@ -655,6 +661,8 @@ "Server-Trailer-A": {"valuea"}, "Server-Trailer-B": nil, "Server-Trailer-C": {"valuec"}, + // Transport should accept undeclared trailers. + "Undeclared-Trailer": {"undeclared"}, }) st.wantClosed("request is complete") }) @@ -680,7 +688,7 @@ "server-trailer-a": {"valuea"}, // Note that Server-Trailer-B is skipped. "server-trailer-c": {"valuec"}, - "undeclared-trailer": {"undeclared"}, // Should be ignored. + "undeclared-trailer": {"undeclared"}, }) rt.wantStatus(200) @@ -698,6 +706,8 @@ "Server-Trailer-A": {"valuea"}, "Server-Trailer-B": nil, "Server-Trailer-C": {"valuec"}, + // Transport should accept undeclared trailers. + "Undeclared-Trailer": {"undeclared"}, }) st.wantClosed("request is complete") }) @@ -750,10 +760,257 @@ }) body := []byte("some body") st.writeData(body) - st.stream.stream.CloseWrite() + st.CloseWrite() rt.wantStatus(200) rt.wantBody(body) st.wantClosed("request is complete") }) } + +func TestRoundTripGzipEnabled(t *testing.T) { + tests := []struct { + name string + explicit bool + }{ + { + name: "transparent", + explicit: false, + }, + { + name: "explicit", + explicit: true, + }, + } + for _, tt := range tests { + synctestSubtest(t, tt.name, func(t *testing.T) { + tc := newTestClientConn(t) + tc.greet() + + req, _ := http.NewRequest("GET", "https://example.tld/", nil) + if tt.explicit { + req.Header.Set("Accept-Encoding", "gzip") + } + rt := tc.roundTrip(req) + st := tc.wantStream(streamTypeRequest) + + // Verify that client sends Accept-Encoding: gzip. + st.wantSomeHeaders(http.Header{ + "Accept-Encoding": []string{"gzip"}, + }) + + // Server responds with gzip. + var buf bytes.Buffer + gw := gzip.NewWriter(&buf) + gw.Write([]byte("hello world")) + gw.Close() + st.writeHeaders(http.Header{ + ":status": []string{"200"}, + "content-encoding": []string{"gzip"}, + "content-length": []string{strconv.Itoa(buf.Len())}, + }) + st.writeData(buf.Bytes()) + st.CloseWrite() + + rt.wantStatus(200) + + if tt.explicit { + // When user explicitly sets gzip, the server response should + // be given as is. + rt.wantBody(buf.Bytes()) + resp, err := rt.result() + if err != nil { + t.Fatal(err) + } + if resp.Header.Get("Content-Encoding") != "gzip" { + t.Errorf("Content-Encoding = %q, want gzip", resp.Header.Get("Content-Encoding")) + } + if resp.Header.Get("Content-Length") != strconv.Itoa(buf.Len()) { + t.Errorf("Content-Length = %q, want %d", resp.Header.Get("Content-Length"), buf.Len()) + } + if resp.ContentLength != int64(buf.Len()) { + t.Errorf("ContentLength = %d, want %d", resp.ContentLength, buf.Len()) + } + if resp.Uncompressed { + t.Errorf("Uncompressed = true, want false") + } + } else { + // When gzip is transparently set, we automatically decode the + // response body, and make sure stale information about the + // gzip content length and encoding are updated. + rt.wantBody([]byte("hello world")) + resp, err := rt.result() + if err != nil { + t.Fatal(err) + } + if resp.Header.Get("Content-Encoding") != "" { + t.Errorf("Content-Encoding = %q, want empty", resp.Header.Get("Content-Encoding")) + } + if resp.Header.Get("Content-Length") != "" { + t.Errorf("Content-Length = %q, want empty", resp.Header.Get("Content-Length")) + } + if resp.ContentLength != -1 { + t.Errorf("ContentLength = %d, want -1", resp.ContentLength) + } + if !resp.Uncompressed { + t.Errorf("Uncompressed = false, want true") + } + } + }) + } +} + +func TestRoundTripGzipDisabled(t *testing.T) { + tests := []struct { + name string + setup func(tc *testClientConn, req *http.Request, wantHeaders http.Header) + }{ + { + name: "explicitly disabled", + setup: func(tc *testClientConn, req *http.Request, wantHeaders http.Header) { + tc.tr.tr1.DisableCompression = true + }, + }, + { + name: "HEAD request", + setup: func(tc *testClientConn, req *http.Request, wantHeaders http.Header) { + req.Method = "HEAD" + wantHeaders.Set(":method", "HEAD") + }, + }, + { + name: "contains Range header", + setup: func(tc *testClientConn, req *http.Request, wantHeaders http.Header) { + req.Header.Set("Range", "bytes=0-10") + wantHeaders.Set("Range", "bytes=0-10") + }, + }, + { + name: "contains Accept-Encoding-identity header", + setup: func(tc *testClientConn, req *http.Request, wantHeaders http.Header) { + req.Header.Set("Accept-Encoding", "identity") + wantHeaders.Set("Accept-Encoding", "identity") + }, + }, + } + for _, tt := range tests { + synctestSubtest(t, tt.name, func(t *testing.T) { + tc := newTestClientConn(t) + req, _ := http.NewRequest("GET", "https://example.tld/", nil) + wantHeaders := http.Header{ + ":authority": []string{"example.tld"}, + ":method": []string{"GET"}, + ":path": []string{"/"}, + ":scheme": []string{"https"}, + "User-Agent": []string{"Go-http-client/3"}, + } + tt.setup(tc, req, wantHeaders) + tc.greet() + + rt := tc.roundTrip(req) + st := tc.wantStream(streamTypeRequest) + + // Verify that client does not send Accept-Encoding: gzip. + st.wantHeaders(wantHeaders) + + st.writeHeaders(http.Header{ + ":status": []string{"200"}, + }) + rt.wantStatus(200) + }) + } +} + +func TestRoundTripGzipWithTrailers(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + tc := newTestClientConn(t) + tc.greet() + + req, _ := http.NewRequest("GET", "https://example.tld/", nil) + rt := tc.roundTrip(req) + st := tc.wantStream(streamTypeRequest) + + // Verify that client sends Accept-Encoding: gzip. + st.wantSomeHeaders(http.Header{ + "Accept-Encoding": []string{"gzip"}, + }) + + // Server responds with gzip and trailer declaration. + var buf bytes.Buffer + gw := gzip.NewWriter(&buf) + gw.Write([]byte("hello world")) + gw.Close() + st.writeHeaders(http.Header{ + ":status": []string{"200"}, + "content-encoding": []string{"gzip"}, + "trailer": []string{"Server-Trailer-A"}, + }) + st.writeData(buf.Bytes()) + st.writeHeaders(http.Header{ + "server-trailer-a": {"valuea"}, + }) + st.CloseWrite() + + rt.wantStatus(200) + rt.wantTrailers(http.Header{ + "Server-Trailer-A": nil, + }) + rt.wantBody([]byte("hello world")) + rt.wantTrailers(http.Header{ + "Server-Trailer-A": {"valuea"}, + }) + st.wantClosed("request is complete") + }) +} + +func TestRoundTripGzipConcurrentCloseAndRead(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + tc := newTestClientConn(t) + tc.greet() + + req, _ := http.NewRequest("GET", "https://example.tld/", nil) + rt := tc.roundTrip(req) + st := tc.wantStream(streamTypeRequest) + + // Verify that client sends Accept-Encoding: gzip. + st.wantSomeHeaders(http.Header{ + "Accept-Encoding": []string{"gzip"}, + }) + + // Server responds with gzip. + st.writeHeaders(http.Header{ + ":status": []string{"200"}, + "content-encoding": []string{"gzip"}, + }) + rt.wantStatus(200) + + resp, err := rt.result() + if err != nil { + t.Fatal(err) + } + + // Read from the response body in a goroutine while it is empty. + // This will block indefinitely. + readErrCh := make(chan error, 1) + go func() { + var p [10]byte + _, err := resp.Body.Read(p[:]) + readErrCh <- err + }() + synctest.Wait() + + if err := resp.Body.Close(); err != nil { + t.Fatalf("Body.Close() = %v", err) + } + synctest.Wait() + + select { + case err := <-readErrCh: + if err == nil { + t.Error("Read returned nil error, want error") + } + default: + t.Error("Read did not unblock on Close") + } + }) +}
diff --git a/internal/http3/server.go b/internal/http3/server.go index 14b510f..999b3f1 100644 --- a/internal/http3/server.go +++ b/internal/http3/server.go
@@ -7,10 +7,13 @@ import ( "context" "crypto/tls" + "errors" "fmt" "io" "maps" "net/http" + "net/textproto" + "os" "slices" "strconv" "strings" @@ -503,9 +506,10 @@ } if contentLength != 0 || len(reqInfo.Trailer) != 0 { body = &bodyReader{ - st: st, - remain: contentLength, - trailer: reqInfo.Trailer, + st: st, + remain: contentLength, + trailer: reqInfo.Trailer, + filterTrailer: true, } } else { body = http.NoBody @@ -578,11 +582,18 @@ return true } +// trailerPrefix is a magic prefix for [responseWriter.Header] map keys that, +// if present, signals that the map entry is actually for the response +// trailers, and not the response headers. See [net/http.TrailerPrefix] for +// details. +const trailerPrefix = "Trailer:" + type responseWriter struct { st *stream bw *bodyWriter mu sync.Mutex headers http.Header + snapHeaders http.Header // Snapshot of headers at WriteHeader time trailer http.Header bb bodyBuffer wroteHeader bool // Non-1xx header has been (logically) written. @@ -607,6 +618,12 @@ delete(rw.trailer, name) } } + for name, vals := range rw.headers { + if name, found := strings.CutPrefix(name, trailerPrefix); found { + name = textproto.CanonicalMIMEHeaderKey(textproto.TrimString(name)) + rw.trailer[name] = vals + } + } if len(rw.trailer) > 0 { rw.bw.trailer = rw.trailer } @@ -623,18 +640,18 @@ if !responseCanHaveBody(rw.statusCode) { rw.cannotHaveBody = true } - // If there is any Trailer declared in headers, save them so we know which - // trailers have been pre-declared. Also, write back the extracted value, - // which is canonicalized, to rw.Header for consistency. - if _, ok := rw.headers["Trailer"]; ok { - extractTrailerFromHeader(rw.headers, rw.trailer) - rw.headers.Set("Trailer", strings.Join(slices.Sorted(maps.Keys(rw.trailer)), ", ")) + // If there is any Trailer declared, save them so we know which trailers + // have been pre-declared. Also, write back the extracted value, which is + // canonicalized, for consistency. + if _, ok := rw.snapHeaders["Trailer"]; ok { + extractTrailerFromHeader(rw.snapHeaders, rw.trailer) + rw.snapHeaders.Set("Trailer", strings.Join(slices.Sorted(maps.Keys(rw.trailer)), ", ")) } - rw.bb.inferHeader(rw.headers, rw.statusCode) + rw.bb.inferHeader(rw.snapHeaders, rw.statusCode) encHeaders := rw.bw.enc.encode(func(f func(itype indexType, name, value string)) { f(mayIndex, ":status", strconv.Itoa(rw.statusCode)) - for name, values := range rw.headers { + for name, values := range rw.snapHeaders { if !httpguts.ValidHeaderFieldName(name) { continue } @@ -727,6 +744,7 @@ // buffered. rw.statusCodeSet = true rw.statusCode = statusCode + rw.snapHeaders = rw.headers.Clone() if n, err := strconv.Atoi(rw.Header().Get("Content-Length")); err == nil { rw.bodyLenLeft = n } else { @@ -796,7 +814,22 @@ return initialBLen, nil } -func (rw *responseWriter) Flush() { +func (rw *responseWriter) SetReadDeadline(deadline time.Time) error { + rw.st.readDeadline.set(deadline) + return nil +} + +func (rw *responseWriter) SetWriteDeadline(deadline time.Time) error { + rw.st.writeDeadline.set(deadline) + return nil +} + +func (rw *responseWriter) EnableFullDuplex() error { + return nil +} + +func (rw *responseWriter) Flush() { rw.FlushError() } +func (rw *responseWriter) FlushError() error { // Calling Flush implicitly calls WriteHeader(200) if WriteHeader has not // been called before. rw.WriteHeader(http.StatusOK) @@ -804,21 +837,31 @@ defer rw.mu.Unlock() rw.writeHeaderLockedOnce() if !rw.cannotHaveBody { - rw.bw.Write(rw.bb) + _, err := rw.bw.Write(rw.bb) rw.bb.discard() + if err != nil { + return err + } } - rw.st.Flush() + return rw.st.Flush() } func (rw *responseWriter) close() error { - rw.Flush() + retErr := rw.FlushError() rw.mu.Lock() defer rw.mu.Unlock() + rw.prepareTrailerForWriteLocked() - if err := rw.bw.Close(); err != nil { - return err + if err := rw.bw.Close(); retErr == nil { + retErr = err } - return rw.st.stream.Close() + + if errors.Is(rw.st.writeDeadline.err(), os.ErrDeadlineExceeded) { + rw.st.Reset(uint64(errH3RequestCancelled)) + } else if err := rw.st.Close(); retErr == nil { + retErr = err + } + return retErr } // defaultBodyBufferCap is the default number of bytes of body that we are
diff --git a/internal/http3/server_test.go b/internal/http3/server_test.go index 5210d6d..4908471 100644 --- a/internal/http3/server_test.go +++ b/internal/http3/server_test.go
@@ -12,6 +12,7 @@ "net/http" "net/netip" "net/url" + "os" "reflect" "slices" "strconv" @@ -93,6 +94,28 @@ }) } +func TestServerHeaderSnapshot(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Test-Header", "original") + w.WriteHeader(200) + w.Header().Set("X-Test-Header", "modified") + w.Write([]byte("body")) + })) + tc := ts.connect() + tc.greet() + + reqStream := tc.newStream(streamTypeRequest) + reqStream.writeHeaders(requestHeader(nil)) + reqStream.wantSomeHeaders(http.Header{ + ":status": {"200"}, + "X-Test-Header": {"original"}, + }) + reqStream.wantData([]byte("body")) + reqStream.wantClosed("request is complete") + }) +} + func TestServerHeaderInvalid(t *testing.T) { tests := []struct { name string @@ -406,7 +429,7 @@ reqStream.writeHeaders(requestHeader(nil)) bodyContent := []byte("some body content that should be echoed") reqStream.writeData(bodyContent) - reqStream.stream.stream.CloseWrite() + reqStream.CloseWrite() reqStream.wantSomeHeaders(http.Header{":status": {"200"}}) // Small multiple calls to Write will be coalesced into one DATA frame. reqStream.wantData(append([]byte("/"), bodyContent...)) @@ -649,7 +672,7 @@ reqStream.wantSomeHeaders(http.Header{":status": {"100"}}) body := []byte("body that will be echoed back if we get status 100") reqStream.writeData(body) - reqStream.stream.stream.CloseWrite() + reqStream.CloseWrite() // Receive the server's response after sending the body. reqStream.wantSomeHeaders(http.Header{":status": {"200"}}) @@ -703,7 +726,7 @@ // read the request body without hanging, which would normally cause an // HTTP 100 to be sent. reqStream.writeData([]byte("some body")) - reqStream.stream.stream.CloseWrite() + reqStream.CloseWrite() // Verify that no HTTP 100 was sent. reqStream.wantSomeHeaders(http.Header{":status": {"200"}}) @@ -727,7 +750,7 @@ // client closes the write direction of the stream. reqStream := tc.newStream(streamTypeRequest) reqStream.writeHeaders(requestHeader(nil)) - reqStream.stream.stream.CloseWrite() + reqStream.CloseWrite() reqStream.wantSomeHeaders(http.Header{":status": {"200"}}) reqStream.wantData(serverBody) reqStream.wantClosed("request is complete") @@ -776,9 +799,10 @@ })) reqStream.writeData(body) reqStream.writeHeaders(http.Header{ - "Client-Trailer-A": {"valuea"}, - "Client-Trailer-B": {"valueb"}, - "Undeclared-Trailer": {"undeclared"}, // Undeclared trailer should be ignored. + "Client-Trailer-A": {"valuea"}, + "Client-Trailer-B": {"valueb"}, + // Server should not accept undeclared trailers. + "Undeclared-Trailer": {"undeclared"}, }) reqStream.wantHeaders(nil) reqStream.wantClosed("request is complete") @@ -816,9 +840,10 @@ "content-length": {"0"}, })) reqStream.writeHeaders(http.Header{ - "Client-Trailer-A": {"valuea"}, - "Client-Trailer-B": {"valueb"}, - "Undeclared-Trailer": {"undeclared"}, // Undeclared trailer should be ignored. + "Client-Trailer-A": {"valuea"}, + "Client-Trailer-B": {"valueb"}, + // Server should not accept undeclared trailers. + "Undeclared-Trailer": {"undeclared"}, }) reqStream.wantHeaders(nil) reqStream.wantClosed("request is complete") @@ -836,7 +861,10 @@ w.Header().Set("server-trailer-a", "valuea") // Trailer header will be canonicalized. w.Header().Set("Server-Trailer-C", "valuec") // skipping B + // Server should not send undeclared trailers, unless it has the + // magic "Trailer:" prefix. w.Header().Set("Server-Trailer-Not-Declared", "should be omitted") + w.Header().Set("Trailer:Undeclared-Trailer-Exception", "should be sent") })) tc := ts.connect() tc.greet() @@ -849,8 +877,9 @@ }) reqStream.wantData(body) reqStream.wantSomeHeaders(http.Header{ - "Server-Trailer-A": {"valuea"}, - "Server-Trailer-C": {"valuec"}, + "Server-Trailer-A": {"valuea"}, + "Server-Trailer-C": {"valuec"}, + "Undeclared-Trailer-Exception": {"should be sent"}, }) reqStream.wantClosed("request is complete") }) @@ -866,7 +895,10 @@ w.Header().Set("server-trailer-a", "valuea") // Trailer header will be canonicalized. w.Header().Set("Server-Trailer-C", "valuec") // skipping B + // Server should not send undeclared trailers without "Trailer:" + // prefix. w.Header().Set("Server-Trailer-Not-Declared", "should be omitted") + w.Header().Set("Trailer:undeclared-trailer-exception", "should be sent") })) tc := ts.connect() tc.greet() @@ -878,8 +910,9 @@ "Trailer": {"Server-Trailer-A, Server-Trailer-B, Server-Trailer-C"}, }) reqStream.wantSomeHeaders(http.Header{ - "Server-Trailer-A": {"valuea"}, - "Server-Trailer-C": {"valuec"}, + "Server-Trailer-A": {"valuea"}, + "Server-Trailer-C": {"valuec"}, + "Undeclared-Trailer-Exception": {"should be sent"}, }) reqStream.wantClosed("request is complete") }) @@ -1202,6 +1235,220 @@ }) } +func TestServerPastWriteDeadline(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctl := http.NewResponseController(w) + io.WriteString(w, "one") + if err := ctl.Flush(); err != nil { + t.Errorf("Flush() = %v, want nil", err) + } + time.Sleep(time.Second) // T+1. + // Set past write deadline. Write should fail. + if err := ctl.SetWriteDeadline(time.Now().Add(-10 * time.Second)); err != nil { + t.Errorf("SetWriteDeadline() = %v, want nil", err) + } + var err error + _, err = io.WriteString(w, "x") + if err == nil { + err = ctl.Flush() + } + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Errorf("got write err %v, want %v", err, os.ErrDeadlineExceeded) + } + + // Extending the write deadline after it's exceeded should have no effect (sticky). + if err := ctl.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil { + t.Errorf("SetWriteDeadline() = %v, want nil", err) + } + _, err = io.WriteString(w, "x") + if err == nil { + err = ctl.Flush() + } + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Errorf("got write err %v (after extend), want %v", err, os.ErrDeadlineExceeded) + } + })) + tc := ts.connect() + tc.greet() + + reqStream := tc.newStream(streamTypeRequest) + reqStream.writeHeaders(requestHeader(nil)) + reqStream.wantSomeHeaders(http.Header{":status": {"200"}}) + reqStream.wantData([]byte("one")) + time.Sleep(2 * time.Second) // T+2. + synctest.Wait() + reqStream.wantError(quic.StreamErrorCode(errH3RequestCancelled)) + }) +} + +func TestServerFutureWriteDeadline(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctl := http.NewResponseController(w) + io.WriteString(w, "one") + if err := ctl.Flush(); err != nil { + t.Errorf("Flush() = %v, want nil", err) + } + + // Set future deadline at T+1. Write should succeed. + if err := ctl.SetWriteDeadline(time.Now().Add(time.Second)); err != nil { + t.Errorf("SetWriteDeadline() = %v, want nil", err) + } + io.WriteString(w, "two") + if err := ctl.Flush(); err != nil { + t.Errorf("Flush() = %v, want nil", err) + } + + // Extend deadline to T+3, before it expires. + if err := ctl.SetWriteDeadline(time.Now().Add(3 * time.Second)); err != nil { + t.Errorf("SetWriteDeadline() = %v, want nil", err) + } + // Sleep till T+2. Write should succeed since the deadline is T+3. + time.Sleep(2 * time.Second) + io.WriteString(w, "three") + if err := ctl.Flush(); err != nil { + t.Errorf("Flush() = %v, want nil", err) + } + + // Sleep till T+4. Write should fail since deadline is T+3. + time.Sleep(2 * time.Second) + var err error + _, err = io.WriteString(w, "x") + if err == nil { + err = ctl.Flush() + } + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Errorf("got write err %v, want %v", err, os.ErrDeadlineExceeded) + } + + // Extending the write deadline after it's exceeded should have no effect (sticky). + if err := ctl.SetWriteDeadline(time.Time{}); err != nil { + t.Errorf("SetWriteDeadline() = %v, want nil", err) + } + _, err = io.WriteString(w, "x") + if err == nil { + err = ctl.Flush() + } + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Errorf("got write err %v (after extend), want %v", err, os.ErrDeadlineExceeded) + } + })) + tc := ts.connect() + tc.greet() + + reqStream := tc.newStream(streamTypeRequest) + reqStream.writeHeaders(requestHeader(nil)) + reqStream.wantSomeHeaders(http.Header{":status": {"200"}}) + reqStream.wantData([]byte("one")) + reqStream.wantData([]byte("two")) + time.Sleep(3 * time.Second) // T+3. After "three" is written. + reqStream.wantData([]byte("three")) + time.Sleep(3 * time.Second) // T+6. After server exceeds deadline. + reqStream.wantError(quic.StreamErrorCode(errH3RequestCancelled)) + }) +} + +func TestServerPastReadDeadline(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctl := http.NewResponseController(w) + b := make([]byte, 3) + if _, err := io.ReadFull(r.Body, b); err != nil || string(b) != "one" { + t.Errorf("Read() got (%q, %v), want (%q, nil)", b, err, "one") + } + // Set past read deadline. Read should fail. + if err := ctl.SetReadDeadline(time.Now().Add(-10 * time.Second)); err != nil { + t.Errorf("SetReadDeadline() = %v, want nil", err) + } + _, err := io.ReadAll(r.Body) + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Errorf("got read err %v, want %v", err, os.ErrDeadlineExceeded) + } + + // Extending the read deadline after it's exceeded should have no effect (sticky). + if err := ctl.SetReadDeadline(time.Now().Add(10 * time.Second)); err != nil { + t.Errorf("SetReadDeadline() = %v, want nil", err) + } + _, err = io.ReadAll(r.Body) + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Errorf("got read err %v (after extend), want %v", err, os.ErrDeadlineExceeded) + } + })) + tc := ts.connect() + tc.greet() + + reqStream := tc.newStream(streamTypeRequest) + reqStream.writeHeaders(requestHeader(nil)) + reqStream.writeData([]byte("one")) + synctest.Wait() + }) +} + +func TestServerFutureReadDeadline(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctl := http.NewResponseController(w) + b := make([]byte, 3) + if _, err := io.ReadFull(r.Body, b); err != nil || string(b) != "one" { + t.Errorf("Read() got (%q, %v), want (%q, nil)", b, err, "one") + } + + // Set future deadline at T+2s. Read should succeed. + if err := ctl.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil { + t.Errorf("SetReadDeadline() = %v, want nil", err) + } + b2 := make([]byte, 3) + if _, err := io.ReadFull(r.Body, b2); err != nil || string(b2) != "two" { + t.Errorf("Read() got (%q, %v), want (%q, nil)", b2, err, "two") + } + + // Extend deadline to T+5s, before it expires. + if err := ctl.SetReadDeadline(time.Now().Add(4 * time.Second)); err != nil { + t.Errorf("SetReadDeadline() = %v, want nil", err) + } + // Sleep till T+3. Read should succeed since the deadline is T+5. + time.Sleep(2 * time.Second) + b3 := make([]byte, 5) + if _, err := io.ReadFull(r.Body, b3); err != nil || string(b3) != "three" { + t.Errorf("Read() got (%q, %v), want (%q, nil)", b3, err, "three") + } + + // Sleep till T+6. Read should fail since deadline has passed. + time.Sleep(3 * time.Second) + _, err := io.ReadAll(r.Body) + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Errorf("got read err %v, want %v", err, os.ErrDeadlineExceeded) + } + + // Extending the read deadline after it's exceeded should have no effect (sticky). + if err := ctl.SetReadDeadline(time.Time{}); err != nil { + t.Errorf("SetReadDeadline() = %v, want nil", err) + } + _, err = io.ReadAll(r.Body) + if !errors.Is(err, os.ErrDeadlineExceeded) { + t.Errorf("got read err %v (after extend), want %v", err, os.ErrDeadlineExceeded) + } + })) + tc := ts.connect() + tc.greet() + + reqStream := tc.newStream(streamTypeRequest) + reqStream.writeHeaders(requestHeader(nil)) + reqStream.writeData([]byte("one")) + + time.Sleep(time.Second) + reqStream.writeData([]byte("two")) // T+1. + synctest.Wait() + + time.Sleep(time.Second) + reqStream.writeData([]byte("three")) // T+2. + synctest.Wait() + + time.Sleep(4 * time.Second) // Advance to T+6 for server handler to complete. + }) +} + type testServer struct { t testing.TB s *server
diff --git a/internal/http3/stream.go b/internal/http3/stream.go index 93294d4..3529983 100644 --- a/internal/http3/stream.go +++ b/internal/http3/stream.go
@@ -7,6 +7,9 @@ import ( "context" "io" + "os" + "sync" + "time" "golang.org/x/net/quic" ) @@ -21,6 +24,9 @@ // results in an error. // -1 indicates no limit. lim int64 + + readDeadline deadline + writeDeadline deadline } // newConnStream creates a new stream on a connection. @@ -42,10 +48,7 @@ if err != nil { return nil, err } - st := &stream{ - stream: qs, - lim: -1, // no limit - } + st := newStream(qs) if stype != streamTypeRequest { // Unidirectional stream header. st.writeVarint(int64(stype)) @@ -54,9 +57,121 @@ } func newStream(qs *quic.Stream) *stream { - return &stream{ + readCtx, readCancel := context.WithCancelCause(context.Background()) + writeCtx, writeCancel := context.WithCancelCause(context.Background()) + st := &stream{ stream: qs, lim: -1, // no limit + readDeadline: deadline{ + ctx: readCtx, + cancel: readCancel, + }, + writeDeadline: deadline{ + ctx: writeCtx, + cancel: writeCancel, + }, + } + qs.SetReadContext(readCtx) + qs.SetWriteContext(writeCtx) + return st +} + +func (st *stream) Close() error { + st.readDeadline.stop() + st.writeDeadline.stop() + return st.stream.Close() +} + +func (st *stream) CloseRead() { + st.readDeadline.stop() + st.stream.CloseRead() +} + +func (st *stream) CloseWrite() { + st.writeDeadline.stop() + st.stream.CloseWrite() +} + +func (st *stream) Reset(code uint64) { + st.readDeadline.stop() + st.writeDeadline.stop() + st.stream.Reset(code) +} + +// deadline manages ctx, and cancels it when timer expires, with +// [os.ErrDeadlineExceeded] as the cause. If the deadline is manually stopped +// before timer expires, the context will be canceled with [context.Canceled] +// as the cause. Once a deadline is exceeded, its timer can no longer be +// extended. +// Practically, this lets the http3 package support time-based deadlines by +// utilizing the quic package's support for context-based deadlines. +type deadline struct { + ctx context.Context + cancel context.CancelCauseFunc + + mu sync.Mutex // Guards below. + timer *time.Timer +} + +// stopTimerLocked stops the deadline timer and sets it to nil. +// The caller must hold d.mu. +func (d *deadline) stopTimerLocked() { + if d.timer != nil { + d.timer.Stop() + d.timer = nil + } +} + +// stop stops the deadline timer and cancels the context with +// [context.Canceled] as the cause. +func (d *deadline) stop() { + d.mu.Lock() + d.stopTimerLocked() + d.mu.Unlock() + d.cancel(context.Canceled) +} + +// err returns the deadline's context cancelation cause, if any. +func (d *deadline) err() error { + return context.Cause(d.ctx) +} + +// errOf returns the deadline's context cancelation cause if the given err is +// non-nil. This can be used to check whether an error value returned by I/O +// operations at the QUIC layer is non-nil because the deadline has expired. +func (d *deadline) errOf(err error) error { + if dErr := d.err(); err != nil && dErr != nil { + return dErr + } + return err +} + +// set configures a new deadline using the given deadlineTime. +// Once deadline is exceeded, it remains in the expired (sticky) state, and +// subsequent attempts to extend or reset the deadline are ignored. +func (d *deadline) set(deadlineTime time.Time) { + d.mu.Lock() + defer d.mu.Unlock() + + if d.ctx.Err() != nil { // Already expired, sticky error. + return + } + if deadlineTime.IsZero() { + d.stopTimerLocked() + return + } + dur := time.Until(deadlineTime) + if dur <= 0 { + d.stopTimerLocked() + d.cancel(os.ErrDeadlineExceeded) + return + } + if d.timer == nil { + d.timer = time.AfterFunc(dur, func() { + d.cancel(os.ErrDeadlineExceeded) + }) + } else { + d.timer.Reset(dur) } } @@ -110,21 +225,36 @@ // ReadByte reads one byte from the stream. func (st *stream) ReadByte() (b byte, err error) { + // Check the deadline before doing I/O operations on the QUIC layer. We do + // this because the QUIC layer implements a fast path for I/O operations, + // allowing Read & Write to succeed depending on the state of buffer, even + // if its context has been canceled. By always checking the deadline here, + // we make it so that I/O operations fail as soon as its relevant deadline + // has been exceeded. + if err := st.readDeadline.err(); err != nil { + return 0, err + } if err := st.recordBytesRead(1); err != nil { return 0, err } b, err = st.stream.ReadByte() - if err != nil { - if err == io.EOF && st.lim < 0 { - return 0, io.EOF - } + if err == io.EOF && st.lim >= 0 { return 0, errH3FrameError } - return b, nil + return b, st.readDeadline.errOf(err) } // Read reads from the stream. func (st *stream) Read(b []byte) (int, error) { + // Check the deadline before doing I/O operations on the QUIC layer. We do + // this because the QUIC layer implements a fast path for I/O operations, + // allowing Read & Write to succeed depending on the state of buffer, even + // if its context has been canceled. By always checking the deadline here, + // we make it so that I/O operations fail as soon as its relevant deadline + // has been exceeded. + if err := st.readDeadline.err(); err != nil { + return 0, err + } n, err := st.stream.Read(b) if e2 := st.recordBytesRead(n); e2 != nil { return 0, e2 @@ -141,10 +271,7 @@ return n, io.EOF } } - if err != nil { - return 0, errH3FrameError - } - return n, nil + return n, st.readDeadline.errOf(err) } // discardUnknownFrame discards an unknown frame. @@ -173,7 +300,7 @@ func (st *stream) discardFrame() error { // TODO: Consider adding a *quic.Stream method to discard some amount of data. for range st.lim { - _, err := st.stream.ReadByte() + _, err := st.ReadByte() if err != nil { return &streamError{errH3FrameError, err.Error()} } @@ -183,29 +310,66 @@ } // Write writes to the stream. -func (st *stream) Write(b []byte) (int, error) { return st.stream.Write(b) } +func (st *stream) Write(b []byte) (int, error) { + // Check the deadline before doing I/O operations on the QUIC layer. We do + // this because the QUIC layer implements a fast path for I/O operations, + // allowing Read & Write to succeed depending on the state of buffer, even + // if its context has been canceled. By always checking the deadline here, + // we make it so that I/O operations fail as soon as its relevant deadline + // has been exceeded. + if err := st.writeDeadline.err(); err != nil { + return 0, err + } + n, err := st.stream.Write(b) + return n, st.writeDeadline.errOf(err) +} // Flush commits data written to the stream. -func (st *stream) Flush() error { return st.stream.Flush() } +func (st *stream) Flush() error { + // Check the deadline before doing I/O operations on the QUIC layer. We do + // this because the QUIC layer implements a fast path for I/O operations, + // allowing Read & Write to succeed depending on the state of buffer, even + // if its context has been canceled. By always checking the deadline here, + // we make it so that I/O operations fail as soon as its relevant deadline + // has been exceeded. + if err := st.writeDeadline.err(); err != nil { + return err + } + return st.writeDeadline.errOf(st.stream.Flush()) +} + +// WriteByte writes one byte to the stream. +func (st *stream) WriteByte(c byte) error { + // Check the deadline before doing I/O operations on the QUIC layer. We do + // this because the QUIC layer implements a fast path for I/O operations, + // allowing Read & Write to succeed depending on the state of buffer, even + // if its context has been canceled. By always checking the deadline here, + // we make it so that I/O operations fail as soon as its relevant deadline + // has been exceeded. + if err := st.writeDeadline.err(); err != nil { + return err + } + return st.writeDeadline.errOf(st.stream.WriteByte(c)) +} // readVarint reads a QUIC variable-length integer from the stream. func (st *stream) readVarint() (v int64, err error) { - b, err := st.stream.ReadByte() + b, err := st.ReadByte() if err != nil { return 0, err } v = int64(b & 0x3f) n := 1 << (b >> 6) for i := 1; i < n; i++ { - b, err := st.stream.ReadByte() + b, err := st.ReadByte() if err != nil { - return 0, errH3FrameError + if err == io.EOF { + return 0, errH3FrameError + } + return 0, err } v = (v << 8) | int64(b) } - if err := st.recordBytesRead(n); err != nil { - return 0, err - } return v, nil } @@ -219,24 +383,24 @@ func (st *stream) writeVarint(v int64) { switch { case v <= (1<<6)-1: - st.stream.WriteByte(byte(v)) + st.WriteByte(byte(v)) case v <= (1<<14)-1: - st.stream.WriteByte((1 << 6) | byte(v>>8)) - st.stream.WriteByte(byte(v)) + st.WriteByte((1 << 6) | byte(v>>8)) + st.WriteByte(byte(v)) case v <= (1<<30)-1: - st.stream.WriteByte((2 << 6) | byte(v>>24)) - st.stream.WriteByte(byte(v >> 16)) - st.stream.WriteByte(byte(v >> 8)) - st.stream.WriteByte(byte(v)) + st.WriteByte((2 << 6) | byte(v>>24)) + st.WriteByte(byte(v >> 16)) + st.WriteByte(byte(v >> 8)) + st.WriteByte(byte(v)) case v <= (1<<62)-1: - st.stream.WriteByte((3 << 6) | byte(v>>56)) - st.stream.WriteByte(byte(v >> 48)) - st.stream.WriteByte(byte(v >> 40)) - st.stream.WriteByte(byte(v >> 32)) - st.stream.WriteByte(byte(v >> 24)) - st.stream.WriteByte(byte(v >> 16)) - st.stream.WriteByte(byte(v >> 8)) - st.stream.WriteByte(byte(v)) + st.WriteByte((3 << 6) | byte(v>>56)) + st.WriteByte(byte(v >> 48)) + st.WriteByte(byte(v >> 40)) + st.WriteByte(byte(v >> 32)) + st.WriteByte(byte(v >> 24)) + st.WriteByte(byte(v >> 16)) + st.WriteByte(byte(v >> 8)) + st.WriteByte(byte(v)) default: panic("varint too large") }
diff --git a/internal/http3/stream_test.go b/internal/http3/stream_test.go index e08e4c5..0ea7fb3 100644 --- a/internal/http3/stream_test.go +++ b/internal/http3/stream_test.go
@@ -193,7 +193,7 @@ if err := st1.Flush(); err != nil { t.Fatal(err) } - st1.stream.CloseWrite() + st1.CloseWrite() if _, err := st2.readFrameHeader(); err == nil { t.Fatalf("%v/%v bytes of frame available: st.readFrameHeader() succeeded; want error", i, len(frame)) @@ -206,7 +206,7 @@ st1.writeVarint(1) // type st1.writeVarint(100) // size st1.Write(make([]byte, 50)) // data - st1.stream.CloseWrite() + st1.CloseWrite() if _, err := st2.readFrameHeader(); err != nil { t.Fatalf("st.readFrameHeader() = %v", err) } @@ -219,7 +219,7 @@ st1, st2 := newStreamPair(t) st1.writeVarint(1) // type st1.writeVarint(100) // size - st1.stream.CloseWrite() + st1.CloseWrite() if _, err := st2.readFrameHeader(); err != nil { t.Fatalf("st.readFrameHeader() = %v", err) } @@ -242,7 +242,7 @@ } st1.Write(data) // data - st1.stream.CloseWrite() // end stream + st1.CloseWrite() // end stream got := make([]byte, len(data)+1) if n, err := st2.Read(got); err != nil || n != len(data) || !bytes.Equal(got[:n], data) { t.Fatalf("st.Read() = %v, %v (data=%x); want %v, nil (data=%x)", n, err, got[:n], len(data), data) @@ -297,7 +297,7 @@ st1.writeVarint(typ) // type st1.writeVarint(int64(len(data))) // size st1.Write(data) // data - st1.stream.CloseWrite() + st1.CloseWrite() if got, err := st2.readFrameHeader(); err != nil || got != typ { t.Fatalf("st.readFrameHeader() = %v, %v; want %v, nil", got, err, typ)
diff --git a/internal/http3/transport.go b/internal/http3/transport.go index 52d9f7c..16daa24 100644 --- a/internal/http3/transport.go +++ b/internal/http3/transport.go
@@ -6,7 +6,9 @@ import ( "context" + "errors" "fmt" + "math" "net/http" "net/url" "sync" @@ -24,6 +26,7 @@ type transport struct { // config is the QUIC configuration used for client connections. config *quic.Config + tr1 *http.Transport listenQUIC func(addr string, config *quic.Config) (*quic.Endpoint, error) @@ -51,8 +54,8 @@ panic("netHTTPTransport.RoundTrip should never be called") } -func (t netHTTPTransport) DialClientConn(ctx context.Context, addr string, _ *url.URL, _ func()) (http.RoundTripper, error) { - return t.transport.dial(ctx, addr) +func (t netHTTPTransport) DialClientConn(ctx context.Context, addr string, _ *url.URL, stateHook func()) (http.RoundTripper, error) { + return t.transport.dial(ctx, addr, stateHook) } type TransportOpts struct { @@ -85,6 +88,7 @@ tr3 := &transport{ // initConfig will clone the tr.TLSClientConfig. config: initConfig(opts.QUICConfig), + tr1: tr, listenQUIC: opts.ListenQUIC, activeConns: make(map[*clientConn]struct{}), } @@ -138,7 +142,7 @@ } // dial creates a new HTTP/3 client connection. -func (tr *transport) dial(ctx context.Context, target string) (*clientConn, error) { +func (tr *transport) dial(ctx context.Context, target string, stateHook func()) (*clientConn, error) { tr.incInFlightDials() defer tr.decInFlightDials() @@ -149,7 +153,7 @@ if err != nil { return nil, err } - return tr.newClientConn(ctx, qconn) + return tr.newClientConn(ctx, qconn, stateHook) } // CloseIdleConnections is called by net/http.Transport.CloseIdleConnections @@ -172,11 +176,20 @@ // // Multiple goroutines may invoke methods on a clientConn simultaneously. type clientConn struct { + tr *transport + qconn *quic.Conn genericConn enc qpackEncoder dec qpackDecoder + + // Guarded by genericConn.mu + reserved int + active int + closed bool + + stateHook func() } func (tr *transport) registerConn(cc *clientConn) { @@ -191,9 +204,11 @@ delete(tr.activeConns, cc) } -func (tr *transport) newClientConn(ctx context.Context, qconn *quic.Conn) (*clientConn, error) { +func (tr *transport) newClientConn(ctx context.Context, qconn *quic.Conn, stateHook func()) (*clientConn, error) { cc := &clientConn{ - qconn: qconn, + tr: tr, + qconn: qconn, + stateHook: stateHook, } tr.registerConn(cc) cc.enc.init() @@ -209,12 +224,15 @@ go func() { cc.acceptStreams(qconn, cc) + cc.mu.Lock() + cc.closed = true + cc.mu.Unlock() tr.unregisterConn(cc) + cc.maybeCallStateHook() }() return cc, nil } -// TODO: implement the rest of net/http.ClientConn methods beyond Close. func (cc *clientConn) Close() error { // We need to use Close rather than Abort on the QUIC connection. // Otherwise, when a net/http.Transport.CloseIdleConnections is called, it @@ -226,22 +244,63 @@ } func (cc *clientConn) Err() error { + cc.mu.Lock() + defer cc.mu.Unlock() + if cc.closed { + return errors.New("connection closed") + } return nil } func (cc *clientConn) Reserve() error { + cc.mu.Lock() + defer cc.mu.Unlock() + if cc.closed { + return errors.New("connection closed") + } + cc.reserved++ return nil } func (cc *clientConn) Release() { + cc.mu.Lock() + defer cc.mu.Unlock() + // This is consistent with RoundTrip: both Release and RoundTrip will + // consume a reservation iff one exists. + if cc.reserved > 0 { + cc.reserved-- + } } func (cc *clientConn) Available() int { - return 0 + cc.mu.Lock() + defer cc.mu.Unlock() + if cc.closed { + return 0 + } + // The general recommendation for HTTP/3 is to reuse the same connection + // for multiple requests rather than creating new connections. As of now, + // we don't have a good understanding of when one might want to create + // multiple HTTP/3 connections to the same server. + // Therefore, for ClientConn API, let HTTP/3 connections have no limit. + // Starting a new RoundTrip when we are at the connection limit will just + // block until a new max stream limit is received. + return math.MaxInt } func (cc *clientConn) InFlight() int { - return 0 + cc.mu.Lock() + defer cc.mu.Unlock() + if cc.closed { + return 0 + } + return cc.reserved + cc.active +} + +func (cc *clientConn) maybeCallStateHook() { + if cc.stateHook != nil { + cc.stateHook() + } } func (cc *clientConn) handleControlStream(st *stream) error {
diff --git a/internal/http3/transport_test.go b/internal/http3/transport_test.go index 6d532f9..7d35525 100644 --- a/internal/http3/transport_test.go +++ b/internal/http3/transport_test.go
@@ -11,6 +11,7 @@ "fmt" "io" "maps" + "math" "net/http" "reflect" "slices" @@ -34,6 +35,111 @@ }) } +func TestClientConnMethods(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + var called bool + hook := func() { + if called { + t.Error("state hook was unexpectedly called") + } + called = true + } + verifyHookWasCalled := func() { + if !called { + t.Error("state hook was unexpectedly not called") + } + called = false + } + tc := newTestClientConnWithHook(t, hook) + tc.greet() + + // Initial state after establishing connection. + if err := tc.cc.Err(); err != nil { + t.Errorf("cc.Err() = %v, want nil", err) + } + if avail := tc.cc.Available(); avail != math.MaxInt { + t.Errorf("cc.Available() = %v, want %v", avail, math.MaxInt) + } + if inFlight := tc.cc.InFlight(); inFlight != 0 { + t.Errorf("cc.InFlight() = %v, want 0", inFlight) + } + + // Release, with Reserve before and without. + for _, reserveBefore := range []bool{true, false} { + if reserveBefore { + if err := tc.cc.Reserve(); err != nil { + t.Fatalf("cc.Reserve() failed: %v", err) + } + if avail := tc.cc.Available(); avail != math.MaxInt { + t.Errorf("after Reserve, cc.Available() = %v, want %v", avail, math.MaxInt) + } + if inFlight := tc.cc.InFlight(); inFlight != 1 { + t.Errorf("after Reserve, cc.InFlight() = %v, want 1", inFlight) + } + } + tc.cc.Release() + if avail := tc.cc.Available(); avail != math.MaxInt { + t.Errorf("after Release, cc.Available() = %v, want %v", avail, math.MaxInt) + } + if inFlight := tc.cc.InFlight(); inFlight != 0 { + t.Errorf("after Release, cc.InFlight() = %v, want 0", inFlight) + } + } + + // RoundTrip, with Reserve before and without. + for _, reserveBefore := range []bool{true, false} { + if reserveBefore { + if err := tc.cc.Reserve(); err != nil { + t.Fatalf("cc.Reserve() failed: %v", err) + } + if avail := tc.cc.Available(); avail != math.MaxInt { + t.Errorf("after Reserve, cc.Available() = %v, want %v", avail, math.MaxInt) + } + if inFlight := tc.cc.InFlight(); inFlight != 1 { + t.Errorf("after Reserve, cc.InFlight() = %v, want 1", inFlight) + } + } + req, _ := http.NewRequest("GET", "https://example.com/", nil) + rt := tc.roundTrip(req) + if avail := tc.cc.Available(); avail != math.MaxInt { + t.Errorf("after RoundTrip, cc.Available() = %v, want %v", avail, math.MaxInt) + } + st := tc.wantStream(streamTypeRequest) + st.wantHeaders(nil) + st.writeHeaders(http.Header{":status": []string{"200"}}) + resp := rt.response() + if resp.StatusCode != 200 { + t.Errorf("resp.StatusCode = %v, want 200", resp.StatusCode) + } + if inFlight := tc.cc.InFlight(); inFlight != 1 { // InFlight should decrement only after the body is closed. + t.Errorf("before body close, cc.InFlight() = %v, want 1", inFlight) + } + resp.Body.Close() + if inFlight := tc.cc.InFlight(); inFlight != 0 { + t.Errorf("after body close, cc.InFlight() = %v, want 0", inFlight) + } + verifyHookWasCalled() + } + + // Connection closure. + if err := tc.cc.Reserve(); err != nil { + t.Fatalf("cc.Reserve() failed: %v", err) + } + tc.cc.Close() + synctest.Wait() + verifyHookWasCalled() + if err := tc.cc.Err(); err == nil { + t.Error("after connection is closed, cc.Err() = nil, want err") + } + if avail := tc.cc.Available(); avail != 0 { + t.Errorf("after connection is closed, cc.Available() = %v, want 0", avail) + } + if inFlight := tc.cc.InFlight(); inFlight != 0 { + t.Errorf("after connection is closed, cc.InFlight() = %v, want 0", inFlight) + } + }) +} + // A testQUICConn wraps a *quic.Conn and provides methods for inspecting it. type testQUICConn struct { t testing.TB @@ -353,7 +459,7 @@ func (ts *testQUICStream) wantError(want quic.StreamErrorCode) { ts.t.Helper() synctest.Wait() - _, err := ts.stream.stream.ReadByte() + _, err := ts.ReadByte() if err == nil { ts.t.Fatalf("successfully read from stream; want stream error code %v", want) } @@ -440,17 +546,18 @@ control *testQUICStream } -func newTestClientConn(t testing.TB) *testClientConn { +func newTestClientConnWithHook(t testing.TB, stateHook func()) *testClientConn { e1, e2 := newQUICEndpointPair(t) tr := &transport{ endpoint: e1, config: &quic.Config{ TLSConfig: testTLSConfig, }, + tr1: new(http.Transport), activeConns: make(map[*clientConn]struct{}), } - cc, err := tr.dial(t.Context(), e2.LocalAddr().String()) + cc, err := tr.dial(t.Context(), e2.LocalAddr().String(), stateHook) if err != nil { t.Fatal(err) } @@ -471,6 +578,10 @@ return tc } +func newTestClientConn(t testing.TB) *testClientConn { + return newTestClientConnWithHook(t, nil) +} + // greet performs initial connection handshaking with the client. func (tc *testClientConn) greet() { // Client creates a control stream.
diff --git a/webdav/file.go b/webdav/file.go index 15196f9..b6f5edd 100644 --- a/webdav/file.go +++ b/webdav/file.go
@@ -61,6 +61,10 @@ // Dir does not prevent traversal of symbolic links within its directory tree, // including links that reference locations outside of the tree. // +// Dir does not prevent filesystem modifications which change the target +// of relative symbolic links. For example, moving a directory containing +// a symbolic link to "../target" may cause the link to point to a new target. +// // While the FileSystem.OpenFile method takes '/'-separated paths, a Dir's // string value is a filename on the native file system, not a URL, so it is // separated by filepath.Separator, which isn't necessarily '/'.
diff --git a/webdav/webdav.go b/webdav/webdav.go index aecc47a..6ba4f6c 100644 --- a/webdav/webdav.go +++ b/webdav/webdav.go
@@ -14,7 +14,8 @@ // // - Remote code execution. // - Access of a file outside of the restricted tree -// defined by [Dir]. +// defined by [Dir] (but see the Dir documentation for +// limitations). // // Other misbahaviors (crashes, excessive resource consumption, // lack of isolation between clients, etc.) will be handled
diff --git a/xsrftoken/xsrf.go b/xsrftoken/xsrf.go index e808e6d..5d588f3 100644 --- a/xsrftoken/xsrf.go +++ b/xsrftoken/xsrf.go
@@ -20,9 +20,11 @@ // It is exported so clients may set cookie timeouts that match generated tokens. const Timeout = 24 * time.Hour -// clean sanitizes a string for inclusion in a token by replacing all ":" with "::". +// clean sanitizes a string for inclusion in a token by replacing _ with __ and : with _c. func clean(s string) string { - return strings.Replace(s, `:`, `::`, -1) + s = strings.ReplaceAll(s, `_`, `__`) + s = strings.ReplaceAll(s, `:`, `_c`) + return s } // Generate returns a URL-safe secure XSRF token that expires in 24 hours.
diff --git a/xsrftoken/xsrf_test.go b/xsrftoken/xsrf_test.go index 60ff84a..14978dc 100644 --- a/xsrftoken/xsrf_test.go +++ b/xsrftoken/xsrf_test.go
@@ -65,11 +65,16 @@ generateTokenAtTime("key", ":foo:", "wah", now), generateTokenAtTime("key", "::foo::", "wah", now), }, + { + "End Colon and Start Colon", + generateTokenAtTime("key", "foo:", "wah", now), + generateTokenAtTime("key", "foo", ":wah", now), + }, } for _, st := range separatorTests { if st.token1 == st.token2 { - t.Errorf("%v: Expected generated tokens to be different", st.name) + t.Errorf("%v: Expected generated tokens (%q) to be different", st.name, st.token1) } } }