go mod tidy and go mod vendor

This commit is contained in:
JBD
2026-01-10 12:04:16 -08:00
parent 18a4569ad4
commit 503e63c540
100 changed files with 71103 additions and 6051 deletions
+4 -5
View File
@@ -1,8 +1,7 @@
module github.com/rakyll/hey module github.com/rakyll/hey
require ( require golang.org/x/net v0.48.0
golang.org/x/net v0.0.0-20191009170851-d66e71096ffb
golang.org/x/text v0.3.2 // indirect
)
go 1.13 require golang.org/x/text v0.33.0 // indirect
go 1.24.0
+4 -8
View File
@@ -1,8 +1,4 @@
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.0.0-20191009170851-d66e71096ffb h1:TR699M2v0qoKTOHxeLgp6zPqaQNs74f01a/ob9W0qko= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.0.0-20191009170851-d66e71096ffb/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-3
View File
@@ -1,3 +0,0 @@
# This source code refers to The Go Authors for copyright purposes.
# The master list of authors is in the main Go distribution,
# visible at http://tip.golang.org/AUTHORS.
-3
View File
@@ -1,3 +0,0 @@
# This source code was written by the Go contributors.
# The master list of contributors is in the main Go distribution,
# visible at http://tip.golang.org/CONTRIBUTORS.
+2 -2
View File
@@ -1,4 +1,4 @@
Copyright (c) 2009 The Go Authors. All rights reserved. Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are modification, are permitted provided that the following conditions are
@@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
copyright notice, this list of conditions and the following disclaimer copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the in the documentation and/or other materials provided with the
distribution. distribution.
* Neither the name of Google Inc. nor the names of its * Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from contributors may be used to endorse or promote products derived from
this software without specific prior written permission. this software without specific prior written permission.
+39 -38
View File
@@ -12,7 +12,7 @@ import (
"golang.org/x/net/idna" "golang.org/x/net/idna"
) )
var isTokenTable = [127]bool{ var isTokenTable = [256]bool{
'!': true, '!': true,
'#': true, '#': true,
'$': true, '$': true,
@@ -93,12 +93,7 @@ var isTokenTable = [127]bool{
} }
func IsTokenRune(r rune) bool { func IsTokenRune(r rune) bool {
i := int(r) return r < utf8.RuneSelf && isTokenTable[byte(r)]
return i < len(isTokenTable) && isTokenTable[i]
}
func isNotToken(r rune) bool {
return !IsTokenRune(r)
} }
// HeaderValuesContainsToken reports whether any string in values // HeaderValuesContainsToken reports whether any string in values
@@ -137,11 +132,13 @@ func trimOWS(x string) string {
// contains token amongst its comma-separated tokens, ASCII // contains token amongst its comma-separated tokens, ASCII
// case-insensitively. // case-insensitively.
func headerValueContainsToken(v string, token string) bool { func headerValueContainsToken(v string, token string) bool {
v = trimOWS(v) for comma := strings.IndexByte(v, ','); comma != -1; comma = strings.IndexByte(v, ',') {
if comma := strings.IndexByte(v, ','); comma != -1 { if tokenEqual(trimOWS(v[:comma]), token) {
return tokenEqual(trimOWS(v[:comma]), token) || headerValueContainsToken(v[comma+1:], token) return true
}
v = v[comma+1:]
} }
return tokenEqual(v, token) return tokenEqual(trimOWS(v), token)
} }
// lowerASCII returns the ASCII lowercase version of b. // lowerASCII returns the ASCII lowercase version of b.
@@ -171,13 +168,15 @@ func tokenEqual(t1, t2 string) bool {
// isLWS reports whether b is linear white space, according // isLWS reports whether b is linear white space, according
// to http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2 // to http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
// LWS = [CRLF] 1*( SP | HT ) //
// LWS = [CRLF] 1*( SP | HT )
func isLWS(b byte) bool { return b == ' ' || b == '\t' } func isLWS(b byte) bool { return b == ' ' || b == '\t' }
// isCTL reports whether b is a control byte, according // isCTL reports whether b is a control byte, according
// to http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2 // to http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
// CTL = <any US-ASCII control character //
// (octets 0 - 31) and DEL (127)> // CTL = <any US-ASCII control character
// (octets 0 - 31) and DEL (127)>
func isCTL(b byte) bool { func isCTL(b byte) bool {
const del = 0x7f // a CTL const del = 0x7f // a CTL
return b < ' ' || b == del return b < ' ' || b == del
@@ -187,18 +186,19 @@ func isCTL(b byte) bool {
// HTTP/2 imposes the additional restriction that uppercase ASCII // HTTP/2 imposes the additional restriction that uppercase ASCII
// letters are not allowed. // letters are not allowed.
// //
// RFC 7230 says: // RFC 7230 says:
// header-field = field-name ":" OWS field-value OWS //
// field-name = token // header-field = field-name ":" OWS field-value OWS
// token = 1*tchar // field-name = token
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / // token = 1*tchar
// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
func ValidHeaderFieldName(v string) bool { func ValidHeaderFieldName(v string) bool {
if len(v) == 0 { if len(v) == 0 {
return false return false
} }
for _, r := range v { for i := 0; i < len(v); i++ {
if !IsTokenRune(r) { if !isTokenTable[v[i]] {
return false return false
} }
} }
@@ -265,27 +265,28 @@ var validHostByte = [256]bool{
// ValidHeaderFieldValue reports whether v is a valid "field-value" according to // ValidHeaderFieldValue reports whether v is a valid "field-value" according to
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 : // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 :
// //
// message-header = field-name ":" [ field-value ] // message-header = field-name ":" [ field-value ]
// field-value = *( field-content | LWS ) // field-value = *( field-content | LWS )
// field-content = <the OCTETs making up the field-value // field-content = <the OCTETs making up the field-value
// and consisting of either *TEXT or combinations // and consisting of either *TEXT or combinations
// of token, separators, and quoted-string> // of token, separators, and quoted-string>
// //
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2 : // http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2 :
// //
// TEXT = <any OCTET except CTLs, // TEXT = <any OCTET except CTLs,
// but including LWS> // but including LWS>
// LWS = [CRLF] 1*( SP | HT ) // LWS = [CRLF] 1*( SP | HT )
// CTL = <any US-ASCII control character // CTL = <any US-ASCII control character
// (octets 0 - 31) and DEL (127)> // (octets 0 - 31) and DEL (127)>
// //
// RFC 7230 says: // RFC 7230 says:
// field-value = *( field-content / obs-fold ) //
// obj-fold = N/A to http2, and deprecated // field-value = *( field-content / obs-fold )
// field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] // obj-fold = N/A to http2, and deprecated
// field-vchar = VCHAR / obs-text // field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
// obs-text = %x80-FF // field-vchar = VCHAR / obs-text
// VCHAR = "any visible [USASCII] character" // obs-text = %x80-FF
// VCHAR = "any visible [USASCII] character"
// //
// http2 further says: "Similarly, HTTP/2 allows header field values // http2 further says: "Similarly, HTTP/2 allows header field values
// that are not valid. While most of the values that can be encoded // that are not valid. While most of the values that can be encoded
-51
View File
@@ -1,51 +0,0 @@
#
# This Dockerfile builds a recent curl with HTTP/2 client support, using
# a recent nghttp2 build.
#
# See the Makefile for how to tag it. If Docker and that image is found, the
# Go tests use this curl binary for integration tests.
#
FROM ubuntu:trusty
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y git-core build-essential wget
RUN apt-get install -y --no-install-recommends \
autotools-dev libtool pkg-config zlib1g-dev \
libcunit1-dev libssl-dev libxml2-dev libevent-dev \
automake autoconf
# The list of packages nghttp2 recommends for h2load:
RUN apt-get install -y --no-install-recommends make binutils \
autoconf automake autotools-dev \
libtool pkg-config zlib1g-dev libcunit1-dev libssl-dev libxml2-dev \
libev-dev libevent-dev libjansson-dev libjemalloc-dev \
cython python3.4-dev python-setuptools
# Note: setting NGHTTP2_VER before the git clone, so an old git clone isn't cached:
ENV NGHTTP2_VER 895da9a
RUN cd /root && git clone https://github.com/tatsuhiro-t/nghttp2.git
WORKDIR /root/nghttp2
RUN git reset --hard $NGHTTP2_VER
RUN autoreconf -i
RUN automake
RUN autoconf
RUN ./configure
RUN make
RUN make install
WORKDIR /root
RUN wget http://curl.haxx.se/download/curl-7.45.0.tar.gz
RUN tar -zxvf curl-7.45.0.tar.gz
WORKDIR /root/curl-7.45.0
RUN ./configure --with-ssl --with-nghttp2=/usr/local
RUN make
RUN make install
RUN ldconfig
CMD ["-h"]
ENTRYPOINT ["/usr/local/bin/curl"]
-3
View File
@@ -1,3 +0,0 @@
curlimage:
docker build -t gohttp2/curl .
-20
View File
@@ -1,20 +0,0 @@
This is a work-in-progress HTTP/2 implementation for Go.
It will eventually live in the Go standard library and won't require
any changes to your code to use. It will just be automatic.
Status:
* The server support is pretty good. A few things are missing
but are being worked on.
* The client work has just started but shares a lot of code
is coming along much quicker.
Docs are at https://godoc.org/golang.org/x/net/http2
Demo test server at https://http2.golang.org/
Help & bug reports welcome!
Contributing: https://golang.org/doc/contribute.html
Bugs: https://golang.org/issue/new?title=x/net/http2:+
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2021 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
import "strings"
// The HTTP protocols are defined in terms of ASCII, not Unicode. This file
// contains helper functions which may use Unicode-aware functions which would
// otherwise be unsafe and could introduce vulnerabilities if used improperly.
// asciiEqualFold is strings.EqualFold, ASCII only. It reports whether s and t
// are equal, ASCII-case-insensitively.
func asciiEqualFold(s, t string) bool {
if len(s) != len(t) {
return false
}
for i := 0; i < len(s); i++ {
if lower(s[i]) != lower(t[i]) {
return false
}
}
return true
}
// lower returns the ASCII lowercase version of b.
func lower(b byte) byte {
if 'A' <= b && b <= 'Z' {
return b + ('a' - 'A')
}
return b
}
// isASCIIPrint returns whether s is ASCII and printable according to
// https://tools.ietf.org/html/rfc20#section-4.2.
func isASCIIPrint(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] < ' ' || s[i] > '~' {
return false
}
}
return true
}
// asciiToLower returns the lowercase version of s if s is ASCII and printable,
// and whether or not it was.
func asciiToLower(s string) (lower string, ok bool) {
if !isASCIIPrint(s) {
return "", false
}
return strings.ToLower(s), true
}
+83 -54
View File
@@ -7,13 +7,21 @@
package http2 package http2
import ( import (
"crypto/tls" "context"
"errors"
"net"
"net/http" "net/http"
"sync" "sync"
) )
// ClientConnPool manages a pool of HTTP/2 client connections. // ClientConnPool manages a pool of HTTP/2 client connections.
type ClientConnPool interface { type ClientConnPool interface {
// GetClientConn returns a specific HTTP/2 connection (usually
// a TLS-TCP connection) to an HTTP/2 server. On success, the
// returned ClientConn accounts for the upcoming RoundTrip
// call, so the caller should not omit it. If the caller needs
// to, ClientConn.RoundTrip can be called with a bogus
// new(http.Request) to release the stream reservation.
GetClientConn(req *http.Request, addr string) (*ClientConn, error) GetClientConn(req *http.Request, addr string) (*ClientConn, error)
MarkDead(*ClientConn) MarkDead(*ClientConn)
} }
@@ -40,7 +48,7 @@ type clientConnPool struct {
conns map[string][]*ClientConn // key is host:port conns map[string][]*ClientConn // key is host:port
dialing map[string]*dialCall // currently in-flight dials dialing map[string]*dialCall // currently in-flight dials
keys map[*ClientConn][]string keys map[*ClientConn][]string
addConnCalls map[string]*addConnCall // in-flight addConnIfNeede calls addConnCalls map[string]*addConnCall // in-flight addConnIfNeeded calls
} }
func (p *clientConnPool) GetClientConn(req *http.Request, addr string) (*ClientConn, error) { func (p *clientConnPool) GetClientConn(req *http.Request, addr string) (*ClientConn, error) {
@@ -52,87 +60,85 @@ const (
noDialOnMiss = false noDialOnMiss = false
) )
// shouldTraceGetConn reports whether getClientConn should call any
// ClientTrace.GetConn hook associated with the http.Request.
//
// This complexity is needed to avoid double calls of the GetConn hook
// during the back-and-forth between net/http and x/net/http2 (when the
// net/http.Transport is upgraded to also speak http2), as well as support
// the case where x/net/http2 is being used directly.
func (p *clientConnPool) shouldTraceGetConn(st clientConnIdleState) bool {
// If our Transport wasn't made via ConfigureTransport, always
// trace the GetConn hook if provided, because that means the
// http2 package is being used directly and it's the one
// dialing, as opposed to net/http.
if _, ok := p.t.ConnPool.(noDialClientConnPool); !ok {
return true
}
// Otherwise, only use the GetConn hook if this connection has
// been used previously for other requests. For fresh
// connections, the net/http package does the dialing.
return !st.freshConn
}
func (p *clientConnPool) getClientConn(req *http.Request, addr string, dialOnMiss bool) (*ClientConn, error) { func (p *clientConnPool) getClientConn(req *http.Request, addr string, dialOnMiss bool) (*ClientConn, error) {
// TODO(dneil): Dial a new connection when t.DisableKeepAlives is set?
if isConnectionCloseRequest(req) && dialOnMiss { if isConnectionCloseRequest(req) && dialOnMiss {
// It gets its own connection. // It gets its own connection.
traceGetConn(req, addr) traceGetConn(req, addr)
const singleUse = true const singleUse = true
cc, err := p.t.dialClientConn(addr, singleUse) cc, err := p.t.dialClientConn(req.Context(), addr, singleUse)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return cc, nil return cc, nil
} }
p.mu.Lock() for {
for _, cc := range p.conns[addr] { p.mu.Lock()
if st := cc.idleState(); st.canTakeNewRequest { for _, cc := range p.conns[addr] {
if p.shouldTraceGetConn(st) { if cc.ReserveNewRequest() {
traceGetConn(req, addr) // When a connection is presented to us by the net/http package,
// the GetConn hook has already been called.
// Don't call it a second time here.
if !cc.getConnCalled {
traceGetConn(req, addr)
}
cc.getConnCalled = false
p.mu.Unlock()
return cc, nil
} }
}
if !dialOnMiss {
p.mu.Unlock() p.mu.Unlock()
return nil, ErrNoCachedConn
}
traceGetConn(req, addr)
call := p.getStartDialLocked(req.Context(), addr)
p.mu.Unlock()
<-call.done
if shouldRetryDial(call, req) {
continue
}
cc, err := call.res, call.err
if err != nil {
return nil, err
}
if cc.ReserveNewRequest() {
return cc, nil return cc, nil
} }
} }
if !dialOnMiss {
p.mu.Unlock()
return nil, ErrNoCachedConn
}
traceGetConn(req, addr)
call := p.getStartDialLocked(addr)
p.mu.Unlock()
<-call.done
return call.res, call.err
} }
// dialCall is an in-flight Transport dial call to a host. // dialCall is an in-flight Transport dial call to a host.
type dialCall struct { type dialCall struct {
p *clientConnPool _ incomparable
p *clientConnPool
// the context associated with the request
// that created this dialCall
ctx context.Context
done chan struct{} // closed when done done chan struct{} // closed when done
res *ClientConn // valid after done is closed res *ClientConn // valid after done is closed
err error // valid after done is closed err error // valid after done is closed
} }
// requires p.mu is held. // requires p.mu is held.
func (p *clientConnPool) getStartDialLocked(addr string) *dialCall { func (p *clientConnPool) getStartDialLocked(ctx context.Context, addr string) *dialCall {
if call, ok := p.dialing[addr]; ok { if call, ok := p.dialing[addr]; ok {
// A dial is already in-flight. Don't start another. // A dial is already in-flight. Don't start another.
return call return call
} }
call := &dialCall{p: p, done: make(chan struct{})} call := &dialCall{p: p, done: make(chan struct{}), ctx: ctx}
if p.dialing == nil { if p.dialing == nil {
p.dialing = make(map[string]*dialCall) p.dialing = make(map[string]*dialCall)
} }
p.dialing[addr] = call p.dialing[addr] = call
go call.dial(addr) go call.dial(call.ctx, addr)
return call return call
} }
// run in its own goroutine. // run in its own goroutine.
func (c *dialCall) dial(addr string) { func (c *dialCall) dial(ctx context.Context, addr string) {
const singleUse = false // shared conn const singleUse = false // shared conn
c.res, c.err = c.p.t.dialClientConn(addr, singleUse) c.res, c.err = c.p.t.dialClientConn(ctx, addr, singleUse)
close(c.done)
c.p.mu.Lock() c.p.mu.Lock()
delete(c.p.dialing, addr) delete(c.p.dialing, addr)
@@ -140,6 +146,8 @@ func (c *dialCall) dial(addr string) {
c.p.addConnLocked(addr, c.res) c.p.addConnLocked(addr, c.res)
} }
c.p.mu.Unlock() c.p.mu.Unlock()
close(c.done)
} }
// addConnIfNeeded makes a NewClientConn out of c if a connection for key doesn't // addConnIfNeeded makes a NewClientConn out of c if a connection for key doesn't
@@ -150,7 +158,7 @@ func (c *dialCall) dial(addr string) {
// This code decides which ones live or die. // This code decides which ones live or die.
// The return value used is whether c was used. // The return value used is whether c was used.
// c is never closed. // c is never closed.
func (p *clientConnPool) addConnIfNeeded(key string, t *Transport, c *tls.Conn) (used bool, err error) { func (p *clientConnPool) addConnIfNeeded(key string, t *Transport, c net.Conn) (used bool, err error) {
p.mu.Lock() p.mu.Lock()
for _, cc := range p.conns[key] { for _, cc := range p.conns[key] {
if cc.CanTakeNewRequest() { if cc.CanTakeNewRequest() {
@@ -180,19 +188,21 @@ func (p *clientConnPool) addConnIfNeeded(key string, t *Transport, c *tls.Conn)
} }
type addConnCall struct { type addConnCall struct {
_ incomparable
p *clientConnPool p *clientConnPool
done chan struct{} // closed when done done chan struct{} // closed when done
err error err error
} }
func (c *addConnCall) run(t *Transport, key string, tc *tls.Conn) { func (c *addConnCall) run(t *Transport, key string, nc net.Conn) {
cc, err := t.NewClientConn(tc) cc, err := t.NewClientConn(nc)
p := c.p p := c.p
p.mu.Lock() p.mu.Lock()
if err != nil { if err != nil {
c.err = err c.err = err
} else { } else {
cc.getConnCalled = true // already called by the net/http package
p.addConnLocked(key, cc) p.addConnLocked(key, cc)
} }
delete(p.addConnCalls, key) delete(p.addConnCalls, key)
@@ -200,12 +210,6 @@ func (c *addConnCall) run(t *Transport, key string, tc *tls.Conn) {
close(c.done) close(c.done)
} }
func (p *clientConnPool) addConn(key string, cc *ClientConn) {
p.mu.Lock()
p.addConnLocked(key, cc)
p.mu.Unlock()
}
// p.mu must be held // p.mu must be held
func (p *clientConnPool) addConnLocked(key string, cc *ClientConn) { func (p *clientConnPool) addConnLocked(key string, cc *ClientConn) {
for _, v := range p.conns[key] { for _, v := range p.conns[key] {
@@ -280,3 +284,28 @@ type noDialClientConnPool struct{ *clientConnPool }
func (p noDialClientConnPool) GetClientConn(req *http.Request, addr string) (*ClientConn, error) { func (p noDialClientConnPool) GetClientConn(req *http.Request, addr string) (*ClientConn, error) {
return p.getClientConn(req, addr, noDialOnMiss) return p.getClientConn(req, addr, noDialOnMiss)
} }
// shouldRetryDial reports whether the current request should
// retry dialing after the call finished unsuccessfully, for example
// if the dial was canceled because of a context cancellation or
// deadline expiry.
func shouldRetryDial(call *dialCall, req *http.Request) bool {
if call.err == nil {
// No error, no need to retry
return false
}
if call.ctx == req.Context() {
// If the call has the same context as the request, the dial
// should not be retried, since any cancellation will have come
// from this request.
return false
}
if !errors.Is(call.err, context.Canceled) && !errors.Is(call.err, context.DeadlineExceeded) {
// If the call error is not because of a context cancellation or a deadline expiry,
// the dial should not be retried.
return false
}
// Only retry if the error is a context cancellation error or deadline expiry
// and the context associated with the call was canceled or expired.
return call.ctx.Err() != nil
}
+169
View File
@@ -0,0 +1,169 @@
// Copyright 2024 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
import (
"math"
"net/http"
"time"
)
// http2Config is a package-internal version of net/http.HTTP2Config.
//
// http.HTTP2Config was added in Go 1.24.
// When running with a version of net/http that includes HTTP2Config,
// we merge the configuration with the fields in Transport or Server
// to produce an http2Config.
//
// Zero valued fields in http2Config are interpreted as in the
// net/http.HTTPConfig documentation.
//
// Precedence order for reconciling configurations is:
//
// - Use the net/http.{Server,Transport}.HTTP2Config value, when non-zero.
// - Otherwise use the http2.{Server.Transport} value.
// - If the resulting value is zero or out of range, use a default.
type http2Config struct {
MaxConcurrentStreams uint32
StrictMaxConcurrentRequests bool
MaxDecoderHeaderTableSize uint32
MaxEncoderHeaderTableSize uint32
MaxReadFrameSize uint32
MaxUploadBufferPerConnection int32
MaxUploadBufferPerStream int32
SendPingTimeout time.Duration
PingTimeout time.Duration
WriteByteTimeout time.Duration
PermitProhibitedCipherSuites bool
CountError func(errType string)
}
// configFromServer merges configuration settings from
// net/http.Server.HTTP2Config and http2.Server.
func configFromServer(h1 *http.Server, h2 *Server) http2Config {
conf := http2Config{
MaxConcurrentStreams: h2.MaxConcurrentStreams,
MaxEncoderHeaderTableSize: h2.MaxEncoderHeaderTableSize,
MaxDecoderHeaderTableSize: h2.MaxDecoderHeaderTableSize,
MaxReadFrameSize: h2.MaxReadFrameSize,
MaxUploadBufferPerConnection: h2.MaxUploadBufferPerConnection,
MaxUploadBufferPerStream: h2.MaxUploadBufferPerStream,
SendPingTimeout: h2.ReadIdleTimeout,
PingTimeout: h2.PingTimeout,
WriteByteTimeout: h2.WriteByteTimeout,
PermitProhibitedCipherSuites: h2.PermitProhibitedCipherSuites,
CountError: h2.CountError,
}
fillNetHTTPConfig(&conf, h1.HTTP2)
setConfigDefaults(&conf, true)
return conf
}
// configFromTransport merges configuration settings from h2 and h2.t1.HTTP2
// (the net/http Transport).
func configFromTransport(h2 *Transport) http2Config {
conf := http2Config{
StrictMaxConcurrentRequests: h2.StrictMaxConcurrentStreams,
MaxEncoderHeaderTableSize: h2.MaxEncoderHeaderTableSize,
MaxDecoderHeaderTableSize: h2.MaxDecoderHeaderTableSize,
MaxReadFrameSize: h2.MaxReadFrameSize,
SendPingTimeout: h2.ReadIdleTimeout,
PingTimeout: h2.PingTimeout,
WriteByteTimeout: h2.WriteByteTimeout,
}
// Unlike most config fields, where out-of-range values revert to the default,
// Transport.MaxReadFrameSize clips.
if conf.MaxReadFrameSize < minMaxFrameSize {
conf.MaxReadFrameSize = minMaxFrameSize
} else if conf.MaxReadFrameSize > maxFrameSize {
conf.MaxReadFrameSize = maxFrameSize
}
if h2.t1 != nil {
fillNetHTTPConfig(&conf, h2.t1.HTTP2)
}
setConfigDefaults(&conf, false)
return conf
}
func setDefault[T ~int | ~int32 | ~uint32 | ~int64](v *T, minval, maxval, defval T) {
if *v < minval || *v > maxval {
*v = defval
}
}
func setConfigDefaults(conf *http2Config, server bool) {
setDefault(&conf.MaxConcurrentStreams, 1, math.MaxUint32, defaultMaxStreams)
setDefault(&conf.MaxEncoderHeaderTableSize, 1, math.MaxUint32, initialHeaderTableSize)
setDefault(&conf.MaxDecoderHeaderTableSize, 1, math.MaxUint32, initialHeaderTableSize)
if server {
setDefault(&conf.MaxUploadBufferPerConnection, initialWindowSize, math.MaxInt32, 1<<20)
} else {
setDefault(&conf.MaxUploadBufferPerConnection, initialWindowSize, math.MaxInt32, transportDefaultConnFlow)
}
if server {
setDefault(&conf.MaxUploadBufferPerStream, 1, math.MaxInt32, 1<<20)
} else {
setDefault(&conf.MaxUploadBufferPerStream, 1, math.MaxInt32, transportDefaultStreamFlow)
}
setDefault(&conf.MaxReadFrameSize, minMaxFrameSize, maxFrameSize, defaultMaxReadFrameSize)
setDefault(&conf.PingTimeout, 1, math.MaxInt64, 15*time.Second)
}
// adjustHTTP1MaxHeaderSize converts a limit in bytes on the size of an HTTP/1 header
// to an HTTP/2 MAX_HEADER_LIST_SIZE value.
func adjustHTTP1MaxHeaderSize(n int64) int64 {
// http2's count is in a slightly different unit and includes 32 bytes per pair.
// So, take the net/http.Server value and pad it up a bit, assuming 10 headers.
const perFieldOverhead = 32 // per http2 spec
const typicalHeaders = 10 // conservative
return n + typicalHeaders*perFieldOverhead
}
func fillNetHTTPConfig(conf *http2Config, h2 *http.HTTP2Config) {
if h2 == nil {
return
}
if h2.MaxConcurrentStreams != 0 {
conf.MaxConcurrentStreams = uint32(h2.MaxConcurrentStreams)
}
if http2ConfigStrictMaxConcurrentRequests(h2) {
conf.StrictMaxConcurrentRequests = true
}
if h2.MaxEncoderHeaderTableSize != 0 {
conf.MaxEncoderHeaderTableSize = uint32(h2.MaxEncoderHeaderTableSize)
}
if h2.MaxDecoderHeaderTableSize != 0 {
conf.MaxDecoderHeaderTableSize = uint32(h2.MaxDecoderHeaderTableSize)
}
if h2.MaxConcurrentStreams != 0 {
conf.MaxConcurrentStreams = uint32(h2.MaxConcurrentStreams)
}
if h2.MaxReadFrameSize != 0 {
conf.MaxReadFrameSize = uint32(h2.MaxReadFrameSize)
}
if h2.MaxReceiveBufferPerConnection != 0 {
conf.MaxUploadBufferPerConnection = int32(h2.MaxReceiveBufferPerConnection)
}
if h2.MaxReceiveBufferPerStream != 0 {
conf.MaxUploadBufferPerStream = int32(h2.MaxReceiveBufferPerStream)
}
if h2.SendPingTimeout != 0 {
conf.SendPingTimeout = h2.SendPingTimeout
}
if h2.PingTimeout != 0 {
conf.PingTimeout = h2.PingTimeout
}
if h2.WriteByteTimeout != 0 {
conf.WriteByteTimeout = h2.WriteByteTimeout
}
if h2.PermitProhibitedCipherSuites {
conf.PermitProhibitedCipherSuites = true
}
if h2.CountError != nil {
conf.CountError = h2.CountError
}
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2025 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.
//go:build !go1.26
package http2
import (
"net/http"
)
func http2ConfigStrictMaxConcurrentRequests(h2 *http.HTTP2Config) bool {
return false
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2025 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.
//go:build go1.26
package http2
import (
"net/http"
)
func http2ConfigStrictMaxConcurrentRequests(h2 *http.HTTP2Config) bool {
return h2.StrictMaxConcurrentRequests
}
-82
View File
@@ -1,82 +0,0 @@
// Copyright 2015 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.
// +build go1.6
package http2
import (
"crypto/tls"
"fmt"
"net/http"
)
func configureTransport(t1 *http.Transport) (*Transport, error) {
connPool := new(clientConnPool)
t2 := &Transport{
ConnPool: noDialClientConnPool{connPool},
t1: t1,
}
connPool.t = t2
if err := registerHTTPSProtocol(t1, noDialH2RoundTripper{t2}); err != nil {
return nil, err
}
if t1.TLSClientConfig == nil {
t1.TLSClientConfig = new(tls.Config)
}
if !strSliceContains(t1.TLSClientConfig.NextProtos, "h2") {
t1.TLSClientConfig.NextProtos = append([]string{"h2"}, t1.TLSClientConfig.NextProtos...)
}
if !strSliceContains(t1.TLSClientConfig.NextProtos, "http/1.1") {
t1.TLSClientConfig.NextProtos = append(t1.TLSClientConfig.NextProtos, "http/1.1")
}
upgradeFn := func(authority string, c *tls.Conn) http.RoundTripper {
addr := authorityAddr("https", authority)
if used, err := connPool.addConnIfNeeded(addr, t2, c); err != nil {
go c.Close()
return erringRoundTripper{err}
} else if !used {
// Turns out we don't need this c.
// For example, two goroutines made requests to the same host
// at the same time, both kicking off TCP dials. (since protocol
// was unknown)
go c.Close()
}
return t2
}
if m := t1.TLSNextProto; len(m) == 0 {
t1.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{
"h2": upgradeFn,
}
} else {
m["h2"] = upgradeFn
}
return t2, nil
}
// registerHTTPSProtocol calls Transport.RegisterProtocol but
// converting panics into errors.
func registerHTTPSProtocol(t *http.Transport, rt noDialH2RoundTripper) (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("%v", e)
}
}()
t.RegisterProtocol("https", rt)
return nil
}
// noDialH2RoundTripper is a RoundTripper which only tries to complete the request
// if there's already has a cached connection to the host.
// (The field is exported so it can be accessed via reflect from net/http; tested
// by TestNoDialH2RoundTripperType)
type noDialH2RoundTripper struct{ *Transport }
func (rt noDialH2RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
res, err := rt.Transport.RoundTrip(req)
if isNoCachedConnError(err) {
return nil, http.ErrSkipAltProtocol
}
return res, err
}
+31 -28
View File
@@ -20,41 +20,44 @@ import (
// TODO: Benchmark to determine if the pools are necessary. The GC may have // TODO: Benchmark to determine if the pools are necessary. The GC may have
// improved enough that we can instead allocate chunks like this: // improved enough that we can instead allocate chunks like this:
// make([]byte, max(16<<10, expectedBytesRemaining)) // make([]byte, max(16<<10, expectedBytesRemaining))
var ( var dataChunkPools = [...]sync.Pool{
dataChunkSizeClasses = []int{ {New: func() interface{} { return new([1 << 10]byte) }},
1 << 10, {New: func() interface{} { return new([2 << 10]byte) }},
2 << 10, {New: func() interface{} { return new([4 << 10]byte) }},
4 << 10, {New: func() interface{} { return new([8 << 10]byte) }},
8 << 10, {New: func() interface{} { return new([16 << 10]byte) }},
16 << 10, }
}
dataChunkPools = [...]sync.Pool{
{New: func() interface{} { return make([]byte, 1<<10) }},
{New: func() interface{} { return make([]byte, 2<<10) }},
{New: func() interface{} { return make([]byte, 4<<10) }},
{New: func() interface{} { return make([]byte, 8<<10) }},
{New: func() interface{} { return make([]byte, 16<<10) }},
}
)
func getDataBufferChunk(size int64) []byte { func getDataBufferChunk(size int64) []byte {
i := 0 switch {
for ; i < len(dataChunkSizeClasses)-1; i++ { case size <= 1<<10:
if size <= int64(dataChunkSizeClasses[i]) { return dataChunkPools[0].Get().(*[1 << 10]byte)[:]
break case size <= 2<<10:
} return dataChunkPools[1].Get().(*[2 << 10]byte)[:]
case size <= 4<<10:
return dataChunkPools[2].Get().(*[4 << 10]byte)[:]
case size <= 8<<10:
return dataChunkPools[3].Get().(*[8 << 10]byte)[:]
default:
return dataChunkPools[4].Get().(*[16 << 10]byte)[:]
} }
return dataChunkPools[i].Get().([]byte)
} }
func putDataBufferChunk(p []byte) { func putDataBufferChunk(p []byte) {
for i, n := range dataChunkSizeClasses { switch len(p) {
if len(p) == n { case 1 << 10:
dataChunkPools[i].Put(p) dataChunkPools[0].Put((*[1 << 10]byte)(p))
return case 2 << 10:
} dataChunkPools[1].Put((*[2 << 10]byte)(p))
case 4 << 10:
dataChunkPools[2].Put((*[4 << 10]byte)(p))
case 8 << 10:
dataChunkPools[3].Put((*[8 << 10]byte)(p))
case 16 << 10:
dataChunkPools[4].Put((*[16 << 10]byte)(p))
default:
panic(fmt.Sprintf("unexpected buffer len=%v", len(p)))
} }
panic(fmt.Sprintf("unexpected buffer len=%v", len(p)))
} }
// dataBuffer is an io.ReadWriter backed by a list of data chunks. // dataBuffer is an io.ReadWriter backed by a list of data chunks.
+13 -1
View File
@@ -53,6 +53,13 @@ func (e ErrCode) String() string {
return fmt.Sprintf("unknown error code 0x%x", uint32(e)) return fmt.Sprintf("unknown error code 0x%x", uint32(e))
} }
func (e ErrCode) stringToken() string {
if s, ok := errCodeName[e]; ok {
return s
}
return fmt.Sprintf("ERR_UNKNOWN_%d", uint32(e))
}
// ConnectionError is an error that results in the termination of the // ConnectionError is an error that results in the termination of the
// entire connection. // entire connection.
type ConnectionError ErrCode type ConnectionError ErrCode
@@ -67,6 +74,11 @@ type StreamError struct {
Cause error // optional additional detail Cause error // optional additional detail
} }
// errFromPeer is a sentinel error value for StreamError.Cause to
// indicate that the StreamError was sent from the peer over the wire
// and wasn't locally generated in the Transport.
var errFromPeer = errors.New("received from peer")
func streamError(id uint32, code ErrCode) StreamError { func streamError(id uint32, code ErrCode) StreamError {
return StreamError{StreamID: id, Code: code} return StreamError{StreamID: id, Code: code}
} }
@@ -124,7 +136,7 @@ func (e headerFieldNameError) Error() string {
type headerFieldValueError string type headerFieldValueError string
func (e headerFieldValueError) Error() string { func (e headerFieldValueError) Error() string {
return fmt.Sprintf("invalid header field value %q", string(e)) return fmt.Sprintf("invalid header field value for %q", string(e))
} }
var ( var (
+83 -13
View File
@@ -6,21 +6,91 @@
package http2 package http2
// flow is the flow control window's size. // inflowMinRefresh is the minimum number of bytes we'll send for a
type flow struct { // flow control window update.
// n is the number of DATA bytes we're allowed to send. const inflowMinRefresh = 4 << 10
// A flow is kept both on a conn and a per-stream.
n int32
// conn points to the shared connection-level flow that is // inflow accounts for an inbound flow control window.
// shared by all streams on that conn. It is nil for the flow // It tracks both the latest window sent to the peer (used for enforcement)
// that's on the conn directly. // and the accumulated unsent window.
conn *flow type inflow struct {
avail int32
unsent int32
} }
func (f *flow) setConnFlow(cf *flow) { f.conn = cf } // init sets the initial window.
func (f *inflow) init(n int32) {
f.avail = n
}
func (f *flow) available() int32 { // add adds n bytes to the window, with a maximum window size of max,
// indicating that the peer can now send us more data.
// For example, the user read from a {Request,Response} body and consumed
// some of the buffered data, so the peer can now send more.
// It returns the number of bytes to send in a WINDOW_UPDATE frame to the peer.
// Window updates are accumulated and sent when the unsent capacity
// is at least inflowMinRefresh or will at least double the peer's available window.
func (f *inflow) add(n int) (connAdd int32) {
if n < 0 {
panic("negative update")
}
unsent := int64(f.unsent) + int64(n)
// "A sender MUST NOT allow a flow-control window to exceed 2^31-1 octets."
// RFC 7540 Section 6.9.1.
const maxWindow = 1<<31 - 1
if unsent+int64(f.avail) > maxWindow {
panic("flow control update exceeds maximum window size")
}
f.unsent = int32(unsent)
if f.unsent < inflowMinRefresh && f.unsent < f.avail {
// If there aren't at least inflowMinRefresh bytes of window to send,
// and this update won't at least double the window, buffer the update for later.
return 0
}
f.avail += f.unsent
f.unsent = 0
return int32(unsent)
}
// take attempts to take n bytes from the peer's flow control window.
// It reports whether the window has available capacity.
func (f *inflow) take(n uint32) bool {
if n > uint32(f.avail) {
return false
}
f.avail -= int32(n)
return true
}
// takeInflows attempts to take n bytes from two inflows,
// typically connection-level and stream-level flows.
// It reports whether both windows have available capacity.
func takeInflows(f1, f2 *inflow, n uint32) bool {
if n > uint32(f1.avail) || n > uint32(f2.avail) {
return false
}
f1.avail -= int32(n)
f2.avail -= int32(n)
return true
}
// outflow is the outbound flow control window's size.
type outflow struct {
_ incomparable
// n is the number of DATA bytes we're allowed to send.
// An outflow is kept both on a conn and a per-stream.
n int32
// conn points to the shared connection-level outflow that is
// shared by all streams on that conn. It is nil for the outflow
// that's on the conn directly.
conn *outflow
}
func (f *outflow) setConnFlow(cf *outflow) { f.conn = cf }
func (f *outflow) available() int32 {
n := f.n n := f.n
if f.conn != nil && f.conn.n < n { if f.conn != nil && f.conn.n < n {
n = f.conn.n n = f.conn.n
@@ -28,7 +98,7 @@ func (f *flow) available() int32 {
return n return n
} }
func (f *flow) take(n int32) { func (f *outflow) take(n int32) {
if n > f.available() { if n > f.available() {
panic("internal error: took too much") panic("internal error: took too much")
} }
@@ -40,7 +110,7 @@ func (f *flow) take(n int32) {
// add adds n bytes (positive or negative) to the flow control window. // add adds n bytes (positive or negative) to the flow control window.
// It returns false if the sum would exceed 2^31-1. // It returns false if the sum would exceed 2^31-1.
func (f *flow) add(n int32) bool { func (f *outflow) add(n int32) bool {
sum := f.n + n sum := f.n + n
if (sum > n) == (f.n > 0) { if (sum > n) == (f.n > 0) {
f.n = sum f.n = sum
+210 -71
View File
@@ -23,7 +23,7 @@ const frameHeaderLen = 9
var padZeros = make([]byte, 255) // zeros for padding var padZeros = make([]byte, 255) // zeros for padding
// A FrameType is a registered frame type as defined in // A FrameType is a registered frame type as defined in
// http://http2.github.io/http2-spec/#rfc.section.11.2 // https://httpwg.org/specs/rfc7540.html#rfc.section.11.2
type FrameType uint8 type FrameType uint8
const ( const (
@@ -39,7 +39,7 @@ const (
FrameContinuation FrameType = 0x9 FrameContinuation FrameType = 0x9
) )
var frameName = map[FrameType]string{ var frameNames = [...]string{
FrameData: "DATA", FrameData: "DATA",
FrameHeaders: "HEADERS", FrameHeaders: "HEADERS",
FramePriority: "PRIORITY", FramePriority: "PRIORITY",
@@ -53,10 +53,10 @@ var frameName = map[FrameType]string{
} }
func (t FrameType) String() string { func (t FrameType) String() string {
if s, ok := frameName[t]; ok { if int(t) < len(frameNames) {
return s return frameNames[t]
} }
return fmt.Sprintf("UNKNOWN_FRAME_TYPE_%d", uint8(t)) return fmt.Sprintf("UNKNOWN_FRAME_TYPE_%d", t)
} }
// Flags is a bitmask of HTTP/2 flags. // Flags is a bitmask of HTTP/2 flags.
@@ -122,9 +122,9 @@ var flagName = map[FrameType]map[Flags]string{
// a frameParser parses a frame given its FrameHeader and payload // a frameParser parses a frame given its FrameHeader and payload
// bytes. The length of payload will always equal fh.Length (which // bytes. The length of payload will always equal fh.Length (which
// might be 0). // might be 0).
type frameParser func(fc *frameCache, fh FrameHeader, payload []byte) (Frame, error) type frameParser func(fc *frameCache, fh FrameHeader, countError func(string), payload []byte) (Frame, error)
var frameParsers = map[FrameType]frameParser{ var frameParsers = [...]frameParser{
FrameData: parseDataFrame, FrameData: parseDataFrame,
FrameHeaders: parseHeadersFrame, FrameHeaders: parseHeadersFrame,
FramePriority: parsePriorityFrame, FramePriority: parsePriorityFrame,
@@ -138,15 +138,15 @@ var frameParsers = map[FrameType]frameParser{
} }
func typeFrameParser(t FrameType) frameParser { func typeFrameParser(t FrameType) frameParser {
if f := frameParsers[t]; f != nil { if int(t) < len(frameParsers) {
return f return frameParsers[t]
} }
return parseUnknownFrame return parseUnknownFrame
} }
// A FrameHeader is the 9 byte header of all HTTP/2 frames. // A FrameHeader is the 9 byte header of all HTTP/2 frames.
// //
// See http://http2.github.io/http2-spec/#FrameHeader // See https://httpwg.org/specs/rfc7540.html#FrameHeader
type FrameHeader struct { type FrameHeader struct {
valid bool // caller can access []byte fields in the Frame valid bool // caller can access []byte fields in the Frame
@@ -225,6 +225,11 @@ var fhBytes = sync.Pool{
}, },
} }
func invalidHTTP1LookingFrameHeader() FrameHeader {
fh, _ := readFrameHeader(make([]byte, frameHeaderLen), strings.NewReader("HTTP/1.1 "))
return fh
}
// ReadFrameHeader reads 9 bytes from r and returns a FrameHeader. // ReadFrameHeader reads 9 bytes from r and returns a FrameHeader.
// Most users should use Framer.ReadFrame instead. // Most users should use Framer.ReadFrame instead.
func ReadFrameHeader(r io.Reader) (FrameHeader, error) { func ReadFrameHeader(r io.Reader) (FrameHeader, error) {
@@ -267,9 +272,16 @@ type Framer struct {
lastFrame Frame lastFrame Frame
errDetail error errDetail error
// countError is a non-nil func that's called on a frame parse
// error with some unique error path token. It's initialized
// from Transport.CountError or Server.CountError.
countError func(errToken string)
// lastHeaderStream is non-zero if the last frame was an // lastHeaderStream is non-zero if the last frame was an
// unfinished HEADERS/CONTINUATION. // unfinished HEADERS/CONTINUATION.
lastHeaderStream uint32 lastHeaderStream uint32
// lastFrameType holds the type of the last frame for verifying frame order.
lastFrameType FrameType
maxReadSize uint32 maxReadSize uint32
headerBuf [frameHeaderLen]byte headerBuf [frameHeaderLen]byte
@@ -337,7 +349,7 @@ func (fr *Framer) maxHeaderListSize() uint32 {
func (f *Framer) startWrite(ftype FrameType, flags Flags, streamID uint32) { func (f *Framer) startWrite(ftype FrameType, flags Flags, streamID uint32) {
// Write the FrameHeader. // Write the FrameHeader.
f.wbuf = append(f.wbuf[:0], f.wbuf = append(f.wbuf[:0],
0, // 3 bytes of length, filled in in endWrite 0, // 3 bytes of length, filled in endWrite
0, 0,
0, 0,
byte(ftype), byte(ftype),
@@ -426,6 +438,7 @@ func NewFramer(w io.Writer, r io.Reader) *Framer {
fr := &Framer{ fr := &Framer{
w: w, w: w,
r: r, r: r,
countError: func(string) {},
logReads: logFrameReads, logReads: logFrameReads,
logWrites: logFrameWrites, logWrites: logFrameWrites,
debugReadLoggerf: log.Printf, debugReadLoggerf: log.Printf,
@@ -477,39 +490,57 @@ func terminalReadFrameError(err error) bool {
return err != nil return err != nil
} }
// ReadFrame reads a single frame. The returned Frame is only valid // ReadFrameHeader reads the header of the next frame.
// until the next call to ReadFrame. // It reads the 9-byte fixed frame header, and does not read any portion of the
// frame payload. The caller is responsible for consuming the payload, either
// with ReadFrameForHeader or directly from the Framer's io.Reader.
// //
// If the frame is larger than previously set with SetMaxReadFrameSize, the // If the frame is larger than previously set with SetMaxReadFrameSize, it
// returned error is ErrFrameTooLarge. Other errors may be of type // returns the frame header and ErrFrameTooLarge.
// ConnectionError, StreamError, or anything else from the underlying //
// reader. // If the returned FrameHeader.StreamID is non-zero, it indicates the stream
func (fr *Framer) ReadFrame() (Frame, error) { // responsible for the error.
func (fr *Framer) ReadFrameHeader() (FrameHeader, error) {
fr.errDetail = nil fr.errDetail = nil
fh, err := readFrameHeader(fr.headerBuf[:], fr.r)
if err != nil {
return fh, err
}
if fh.Length > fr.maxReadSize {
if fh == invalidHTTP1LookingFrameHeader() {
return fh, fmt.Errorf("http2: failed reading the frame payload: %w, note that the frame header looked like an HTTP/1.1 header", ErrFrameTooLarge)
}
return fh, ErrFrameTooLarge
}
if err := fr.checkFrameOrder(fh); err != nil {
return fh, err
}
return fh, nil
}
// ReadFrameForHeader reads the payload for the frame with the given FrameHeader.
//
// It behaves identically to ReadFrame, other than not checking the maximum
// frame size.
func (fr *Framer) ReadFrameForHeader(fh FrameHeader) (Frame, error) {
if fr.lastFrame != nil { if fr.lastFrame != nil {
fr.lastFrame.invalidate() fr.lastFrame.invalidate()
} }
fh, err := readFrameHeader(fr.headerBuf[:], fr.r)
if err != nil {
return nil, err
}
if fh.Length > fr.maxReadSize {
return nil, ErrFrameTooLarge
}
payload := fr.getReadBuf(fh.Length) payload := fr.getReadBuf(fh.Length)
if _, err := io.ReadFull(fr.r, payload); err != nil { if _, err := io.ReadFull(fr.r, payload); err != nil {
if fh == invalidHTTP1LookingFrameHeader() {
return nil, fmt.Errorf("http2: failed reading the frame payload: %w, note that the frame header looked like an HTTP/1.1 header", err)
}
return nil, err return nil, err
} }
f, err := typeFrameParser(fh.Type)(fr.frameCache, fh, payload) f, err := typeFrameParser(fh.Type)(fr.frameCache, fh, fr.countError, payload)
if err != nil { if err != nil {
if ce, ok := err.(connError); ok { if ce, ok := err.(connError); ok {
return nil, fr.connError(ce.Code, ce.Reason) return nil, fr.connError(ce.Code, ce.Reason)
} }
return nil, err return nil, err
} }
if err := fr.checkFrameOrder(f); err != nil { fr.lastFrame = f
return nil, err
}
if fr.logReads { if fr.logReads {
fr.debugReadLoggerf("http2: Framer %p: read %v", fr, summarizeFrame(f)) fr.debugReadLoggerf("http2: Framer %p: read %v", fr, summarizeFrame(f))
} }
@@ -519,6 +550,24 @@ func (fr *Framer) ReadFrame() (Frame, error) {
return f, nil return f, nil
} }
// ReadFrame reads a single frame. The returned Frame is only valid
// until the next call to ReadFrame or ReadFrameBodyForHeader.
//
// If the frame is larger than previously set with SetMaxReadFrameSize, the
// returned error is ErrFrameTooLarge. Other errors may be of type
// ConnectionError, StreamError, or anything else from the underlying
// reader.
//
// If ReadFrame returns an error and a non-nil Frame, the Frame's StreamID
// indicates the stream responsible for the error.
func (fr *Framer) ReadFrame() (Frame, error) {
fh, err := fr.ReadFrameHeader()
if err != nil {
return nil, err
}
return fr.ReadFrameForHeader(fh)
}
// connError returns ConnectionError(code) but first // connError returns ConnectionError(code) but first
// stashes away a public reason to the caller can optionally relay it // stashes away a public reason to the caller can optionally relay it
// to the peer before hanging up on them. This might help others debug // to the peer before hanging up on them. This might help others debug
@@ -531,20 +580,19 @@ func (fr *Framer) connError(code ErrCode, reason string) error {
// checkFrameOrder reports an error if f is an invalid frame to return // checkFrameOrder reports an error if f is an invalid frame to return
// next from ReadFrame. Mostly it checks whether HEADERS and // next from ReadFrame. Mostly it checks whether HEADERS and
// CONTINUATION frames are contiguous. // CONTINUATION frames are contiguous.
func (fr *Framer) checkFrameOrder(f Frame) error { func (fr *Framer) checkFrameOrder(fh FrameHeader) error {
last := fr.lastFrame lastType := fr.lastFrameType
fr.lastFrame = f fr.lastFrameType = fh.Type
if fr.AllowIllegalReads { if fr.AllowIllegalReads {
return nil return nil
} }
fh := f.Header()
if fr.lastHeaderStream != 0 { if fr.lastHeaderStream != 0 {
if fh.Type != FrameContinuation { if fh.Type != FrameContinuation {
return fr.connError(ErrCodeProtocol, return fr.connError(ErrCodeProtocol,
fmt.Sprintf("got %s for stream %d; expected CONTINUATION following %s for stream %d", fmt.Sprintf("got %s for stream %d; expected CONTINUATION following %s for stream %d",
fh.Type, fh.StreamID, fh.Type, fh.StreamID,
last.Header().Type, fr.lastHeaderStream)) lastType, fr.lastHeaderStream))
} }
if fh.StreamID != fr.lastHeaderStream { if fh.StreamID != fr.lastHeaderStream {
return fr.connError(ErrCodeProtocol, return fr.connError(ErrCodeProtocol,
@@ -569,7 +617,7 @@ func (fr *Framer) checkFrameOrder(f Frame) error {
// A DataFrame conveys arbitrary, variable-length sequences of octets // A DataFrame conveys arbitrary, variable-length sequences of octets
// associated with a stream. // associated with a stream.
// See http://http2.github.io/http2-spec/#rfc.section.6.1 // See https://httpwg.org/specs/rfc7540.html#rfc.section.6.1
type DataFrame struct { type DataFrame struct {
FrameHeader FrameHeader
data []byte data []byte
@@ -588,13 +636,14 @@ func (f *DataFrame) Data() []byte {
return f.data return f.data
} }
func parseDataFrame(fc *frameCache, fh FrameHeader, payload []byte) (Frame, error) { func parseDataFrame(fc *frameCache, fh FrameHeader, countError func(string), payload []byte) (Frame, error) {
if fh.StreamID == 0 { if fh.StreamID == 0 {
// DATA frames MUST be associated with a stream. If a // DATA frames MUST be associated with a stream. If a
// DATA frame is received whose stream identifier // DATA frame is received whose stream identifier
// field is 0x0, the recipient MUST respond with a // field is 0x0, the recipient MUST respond with a
// connection error (Section 5.4.1) of type // connection error (Section 5.4.1) of type
// PROTOCOL_ERROR. // PROTOCOL_ERROR.
countError("frame_data_stream_0")
return nil, connError{ErrCodeProtocol, "DATA frame with stream ID 0"} return nil, connError{ErrCodeProtocol, "DATA frame with stream ID 0"}
} }
f := fc.getDataFrame() f := fc.getDataFrame()
@@ -605,6 +654,7 @@ func parseDataFrame(fc *frameCache, fh FrameHeader, payload []byte) (Frame, erro
var err error var err error
payload, padSize, err = readByte(payload) payload, padSize, err = readByte(payload)
if err != nil { if err != nil {
countError("frame_data_pad_byte_short")
return nil, err return nil, err
} }
} }
@@ -613,6 +663,7 @@ func parseDataFrame(fc *frameCache, fh FrameHeader, payload []byte) (Frame, erro
// length of the frame payload, the recipient MUST // length of the frame payload, the recipient MUST
// treat this as a connection error. // treat this as a connection error.
// Filed: https://github.com/http2/http2-spec/issues/610 // Filed: https://github.com/http2/http2-spec/issues/610
countError("frame_data_pad_too_big")
return nil, connError{ErrCodeProtocol, "pad size larger than data payload"} return nil, connError{ErrCodeProtocol, "pad size larger than data payload"}
} }
f.data = payload[:len(payload)-int(padSize)] f.data = payload[:len(payload)-int(padSize)]
@@ -643,7 +694,7 @@ func (f *Framer) WriteData(streamID uint32, endStream bool, data []byte) error {
return f.WriteDataPadded(streamID, endStream, data, nil) return f.WriteDataPadded(streamID, endStream, data, nil)
} }
// WriteData writes a DATA frame with optional padding. // WriteDataPadded writes a DATA frame with optional padding.
// //
// If pad is nil, the padding bit is not sent. // If pad is nil, the padding bit is not sent.
// The length of pad must not exceed 255 bytes. // The length of pad must not exceed 255 bytes.
@@ -653,6 +704,15 @@ func (f *Framer) WriteData(streamID uint32, endStream bool, data []byte) error {
// It is the caller's responsibility not to violate the maximum frame size // It is the caller's responsibility not to violate the maximum frame size
// and to not call other Write methods concurrently. // and to not call other Write methods concurrently.
func (f *Framer) WriteDataPadded(streamID uint32, endStream bool, data, pad []byte) error { func (f *Framer) WriteDataPadded(streamID uint32, endStream bool, data, pad []byte) error {
if err := f.startWriteDataPadded(streamID, endStream, data, pad); err != nil {
return err
}
return f.endWrite()
}
// startWriteDataPadded is WriteDataPadded, but only writes the frame to the Framer's internal buffer.
// The caller should call endWrite to flush the frame to the underlying writer.
func (f *Framer) startWriteDataPadded(streamID uint32, endStream bool, data, pad []byte) error {
if !validStreamID(streamID) && !f.AllowIllegalWrites { if !validStreamID(streamID) && !f.AllowIllegalWrites {
return errStreamID return errStreamID
} }
@@ -682,20 +742,20 @@ func (f *Framer) WriteDataPadded(streamID uint32, endStream bool, data, pad []by
} }
f.wbuf = append(f.wbuf, data...) f.wbuf = append(f.wbuf, data...)
f.wbuf = append(f.wbuf, pad...) f.wbuf = append(f.wbuf, pad...)
return f.endWrite() return nil
} }
// A SettingsFrame conveys configuration parameters that affect how // A SettingsFrame conveys configuration parameters that affect how
// endpoints communicate, such as preferences and constraints on peer // endpoints communicate, such as preferences and constraints on peer
// behavior. // behavior.
// //
// See http://http2.github.io/http2-spec/#SETTINGS // See https://httpwg.org/specs/rfc7540.html#SETTINGS
type SettingsFrame struct { type SettingsFrame struct {
FrameHeader FrameHeader
p []byte p []byte
} }
func parseSettingsFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, error) { func parseSettingsFrame(_ *frameCache, fh FrameHeader, countError func(string), p []byte) (Frame, error) {
if fh.Flags.Has(FlagSettingsAck) && fh.Length > 0 { if fh.Flags.Has(FlagSettingsAck) && fh.Length > 0 {
// When this (ACK 0x1) bit is set, the payload of the // When this (ACK 0x1) bit is set, the payload of the
// SETTINGS frame MUST be empty. Receipt of a // SETTINGS frame MUST be empty. Receipt of a
@@ -703,6 +763,7 @@ func parseSettingsFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, error)
// field value other than 0 MUST be treated as a // field value other than 0 MUST be treated as a
// connection error (Section 5.4.1) of type // connection error (Section 5.4.1) of type
// FRAME_SIZE_ERROR. // FRAME_SIZE_ERROR.
countError("frame_settings_ack_with_length")
return nil, ConnectionError(ErrCodeFrameSize) return nil, ConnectionError(ErrCodeFrameSize)
} }
if fh.StreamID != 0 { if fh.StreamID != 0 {
@@ -713,14 +774,17 @@ func parseSettingsFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, error)
// field is anything other than 0x0, the endpoint MUST // field is anything other than 0x0, the endpoint MUST
// respond with a connection error (Section 5.4.1) of // respond with a connection error (Section 5.4.1) of
// type PROTOCOL_ERROR. // type PROTOCOL_ERROR.
countError("frame_settings_has_stream")
return nil, ConnectionError(ErrCodeProtocol) return nil, ConnectionError(ErrCodeProtocol)
} }
if len(p)%6 != 0 { if len(p)%6 != 0 {
countError("frame_settings_mod_6")
// Expecting even number of 6 byte settings. // Expecting even number of 6 byte settings.
return nil, ConnectionError(ErrCodeFrameSize) return nil, ConnectionError(ErrCodeFrameSize)
} }
f := &SettingsFrame{FrameHeader: fh, p: p} f := &SettingsFrame{FrameHeader: fh, p: p}
if v, ok := f.Value(SettingInitialWindowSize); ok && v > (1<<31)-1 { if v, ok := f.Value(SettingInitialWindowSize); ok && v > (1<<31)-1 {
countError("frame_settings_window_size_too_big")
// Values above the maximum flow control window size of 2^31 - 1 MUST // Values above the maximum flow control window size of 2^31 - 1 MUST
// be treated as a connection error (Section 5.4.1) of type // be treated as a connection error (Section 5.4.1) of type
// FLOW_CONTROL_ERROR. // FLOW_CONTROL_ERROR.
@@ -824,7 +888,7 @@ func (f *Framer) WriteSettingsAck() error {
// A PingFrame is a mechanism for measuring a minimal round trip time // A PingFrame is a mechanism for measuring a minimal round trip time
// from the sender, as well as determining whether an idle connection // from the sender, as well as determining whether an idle connection
// is still functional. // is still functional.
// See http://http2.github.io/http2-spec/#rfc.section.6.7 // See https://httpwg.org/specs/rfc7540.html#rfc.section.6.7
type PingFrame struct { type PingFrame struct {
FrameHeader FrameHeader
Data [8]byte Data [8]byte
@@ -832,11 +896,13 @@ type PingFrame struct {
func (f *PingFrame) IsAck() bool { return f.Flags.Has(FlagPingAck) } func (f *PingFrame) IsAck() bool { return f.Flags.Has(FlagPingAck) }
func parsePingFrame(_ *frameCache, fh FrameHeader, payload []byte) (Frame, error) { func parsePingFrame(_ *frameCache, fh FrameHeader, countError func(string), payload []byte) (Frame, error) {
if len(payload) != 8 { if len(payload) != 8 {
countError("frame_ping_length")
return nil, ConnectionError(ErrCodeFrameSize) return nil, ConnectionError(ErrCodeFrameSize)
} }
if fh.StreamID != 0 { if fh.StreamID != 0 {
countError("frame_ping_has_stream")
return nil, ConnectionError(ErrCodeProtocol) return nil, ConnectionError(ErrCodeProtocol)
} }
f := &PingFrame{FrameHeader: fh} f := &PingFrame{FrameHeader: fh}
@@ -855,7 +921,7 @@ func (f *Framer) WritePing(ack bool, data [8]byte) error {
} }
// A GoAwayFrame informs the remote peer to stop creating streams on this connection. // A GoAwayFrame informs the remote peer to stop creating streams on this connection.
// See http://http2.github.io/http2-spec/#rfc.section.6.8 // See https://httpwg.org/specs/rfc7540.html#rfc.section.6.8
type GoAwayFrame struct { type GoAwayFrame struct {
FrameHeader FrameHeader
LastStreamID uint32 LastStreamID uint32
@@ -872,11 +938,13 @@ func (f *GoAwayFrame) DebugData() []byte {
return f.debugData return f.debugData
} }
func parseGoAwayFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, error) { func parseGoAwayFrame(_ *frameCache, fh FrameHeader, countError func(string), p []byte) (Frame, error) {
if fh.StreamID != 0 { if fh.StreamID != 0 {
countError("frame_goaway_has_stream")
return nil, ConnectionError(ErrCodeProtocol) return nil, ConnectionError(ErrCodeProtocol)
} }
if len(p) < 8 { if len(p) < 8 {
countError("frame_goaway_short")
return nil, ConnectionError(ErrCodeFrameSize) return nil, ConnectionError(ErrCodeFrameSize)
} }
return &GoAwayFrame{ return &GoAwayFrame{
@@ -912,19 +980,20 @@ func (f *UnknownFrame) Payload() []byte {
return f.p return f.p
} }
func parseUnknownFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, error) { func parseUnknownFrame(_ *frameCache, fh FrameHeader, countError func(string), p []byte) (Frame, error) {
return &UnknownFrame{fh, p}, nil return &UnknownFrame{fh, p}, nil
} }
// A WindowUpdateFrame is used to implement flow control. // A WindowUpdateFrame is used to implement flow control.
// See http://http2.github.io/http2-spec/#rfc.section.6.9 // See https://httpwg.org/specs/rfc7540.html#rfc.section.6.9
type WindowUpdateFrame struct { type WindowUpdateFrame struct {
FrameHeader FrameHeader
Increment uint32 // never read with high bit set Increment uint32 // never read with high bit set
} }
func parseWindowUpdateFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, error) { func parseWindowUpdateFrame(_ *frameCache, fh FrameHeader, countError func(string), p []byte) (Frame, error) {
if len(p) != 4 { if len(p) != 4 {
countError("frame_windowupdate_bad_len")
return nil, ConnectionError(ErrCodeFrameSize) return nil, ConnectionError(ErrCodeFrameSize)
} }
inc := binary.BigEndian.Uint32(p[:4]) & 0x7fffffff // mask off high reserved bit inc := binary.BigEndian.Uint32(p[:4]) & 0x7fffffff // mask off high reserved bit
@@ -936,8 +1005,10 @@ func parseWindowUpdateFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, err
// control window MUST be treated as a connection // control window MUST be treated as a connection
// error (Section 5.4.1). // error (Section 5.4.1).
if fh.StreamID == 0 { if fh.StreamID == 0 {
countError("frame_windowupdate_zero_inc_conn")
return nil, ConnectionError(ErrCodeProtocol) return nil, ConnectionError(ErrCodeProtocol)
} }
countError("frame_windowupdate_zero_inc_stream")
return nil, streamError(fh.StreamID, ErrCodeProtocol) return nil, streamError(fh.StreamID, ErrCodeProtocol)
} }
return &WindowUpdateFrame{ return &WindowUpdateFrame{
@@ -988,7 +1059,7 @@ func (f *HeadersFrame) HasPriority() bool {
return f.FrameHeader.Flags.Has(FlagHeadersPriority) return f.FrameHeader.Flags.Has(FlagHeadersPriority)
} }
func parseHeadersFrame(_ *frameCache, fh FrameHeader, p []byte) (_ Frame, err error) { func parseHeadersFrame(_ *frameCache, fh FrameHeader, countError func(string), p []byte) (_ Frame, err error) {
hf := &HeadersFrame{ hf := &HeadersFrame{
FrameHeader: fh, FrameHeader: fh,
} }
@@ -997,11 +1068,13 @@ func parseHeadersFrame(_ *frameCache, fh FrameHeader, p []byte) (_ Frame, err er
// is received whose stream identifier field is 0x0, the recipient MUST // is received whose stream identifier field is 0x0, the recipient MUST
// respond with a connection error (Section 5.4.1) of type // respond with a connection error (Section 5.4.1) of type
// PROTOCOL_ERROR. // PROTOCOL_ERROR.
countError("frame_headers_zero_stream")
return nil, connError{ErrCodeProtocol, "HEADERS frame with stream ID 0"} return nil, connError{ErrCodeProtocol, "HEADERS frame with stream ID 0"}
} }
var padLength uint8 var padLength uint8
if fh.Flags.Has(FlagHeadersPadded) { if fh.Flags.Has(FlagHeadersPadded) {
if p, padLength, err = readByte(p); err != nil { if p, padLength, err = readByte(p); err != nil {
countError("frame_headers_pad_short")
return return
} }
} }
@@ -1009,16 +1082,19 @@ func parseHeadersFrame(_ *frameCache, fh FrameHeader, p []byte) (_ Frame, err er
var v uint32 var v uint32
p, v, err = readUint32(p) p, v, err = readUint32(p)
if err != nil { if err != nil {
countError("frame_headers_prio_short")
return nil, err return nil, err
} }
hf.Priority.StreamDep = v & 0x7fffffff hf.Priority.StreamDep = v & 0x7fffffff
hf.Priority.Exclusive = (v != hf.Priority.StreamDep) // high bit was set hf.Priority.Exclusive = (v != hf.Priority.StreamDep) // high bit was set
p, hf.Priority.Weight, err = readByte(p) p, hf.Priority.Weight, err = readByte(p)
if err != nil { if err != nil {
countError("frame_headers_prio_weight_short")
return nil, err return nil, err
} }
} }
if len(p)-int(padLength) <= 0 { if len(p)-int(padLength) < 0 {
countError("frame_headers_pad_too_big")
return nil, streamError(fh.StreamID, ErrCodeProtocol) return nil, streamError(fh.StreamID, ErrCodeProtocol)
} }
hf.headerFragBuf = p[:len(p)-int(padLength)] hf.headerFragBuf = p[:len(p)-int(padLength)]
@@ -1098,13 +1174,22 @@ func (f *Framer) WriteHeaders(p HeadersFrameParam) error {
} }
// A PriorityFrame specifies the sender-advised priority of a stream. // A PriorityFrame specifies the sender-advised priority of a stream.
// See http://http2.github.io/http2-spec/#rfc.section.6.3 // See https://httpwg.org/specs/rfc7540.html#rfc.section.6.3
type PriorityFrame struct { type PriorityFrame struct {
FrameHeader FrameHeader
PriorityParam PriorityParam
} }
// PriorityParam are the stream prioritzation parameters. var defaultRFC9218Priority = PriorityParam{
incremental: 0,
urgency: 3,
}
// Note that HTTP/2 has had two different prioritization schemes, and
// PriorityParam struct below is a superset of both schemes. The exported
// symbols are from RFC 7540 and the non-exported ones are from RFC 9218.
// PriorityParam are the stream prioritization parameters.
type PriorityParam struct { type PriorityParam struct {
// StreamDep is a 31-bit stream identifier for the // StreamDep is a 31-bit stream identifier for the
// stream that this stream depends on. Zero means no // stream that this stream depends on. Zero means no
@@ -1119,17 +1204,33 @@ type PriorityParam struct {
// the spec, "Add one to the value to obtain a weight between // the spec, "Add one to the value to obtain a weight between
// 1 and 256." // 1 and 256."
Weight uint8 Weight uint8
// "The urgency (u) parameter value is Integer (see Section 3.3.1 of
// [STRUCTURED-FIELDS]), between 0 and 7 inclusive, in descending order of
// priority. The default is 3."
urgency uint8
// "The incremental (i) parameter value is Boolean (see Section 3.3.6 of
// [STRUCTURED-FIELDS]). It indicates if an HTTP response can be processed
// incrementally, i.e., provide some meaningful output as chunks of the
// response arrive."
//
// We use uint8 (i.e. 0 is false, 1 is true) instead of bool so we can
// avoid unnecessary type conversions and because either type takes 1 byte.
incremental uint8
} }
func (p PriorityParam) IsZero() bool { func (p PriorityParam) IsZero() bool {
return p == PriorityParam{} return p == PriorityParam{}
} }
func parsePriorityFrame(_ *frameCache, fh FrameHeader, payload []byte) (Frame, error) { func parsePriorityFrame(_ *frameCache, fh FrameHeader, countError func(string), payload []byte) (Frame, error) {
if fh.StreamID == 0 { if fh.StreamID == 0 {
countError("frame_priority_zero_stream")
return nil, connError{ErrCodeProtocol, "PRIORITY frame with stream ID 0"} return nil, connError{ErrCodeProtocol, "PRIORITY frame with stream ID 0"}
} }
if len(payload) != 5 { if len(payload) != 5 {
countError("frame_priority_bad_length")
return nil, connError{ErrCodeFrameSize, fmt.Sprintf("PRIORITY frame payload size was %d; want 5", len(payload))} return nil, connError{ErrCodeFrameSize, fmt.Sprintf("PRIORITY frame payload size was %d; want 5", len(payload))}
} }
v := binary.BigEndian.Uint32(payload[:4]) v := binary.BigEndian.Uint32(payload[:4])
@@ -1166,17 +1267,19 @@ func (f *Framer) WritePriority(streamID uint32, p PriorityParam) error {
} }
// A RSTStreamFrame allows for abnormal termination of a stream. // A RSTStreamFrame allows for abnormal termination of a stream.
// See http://http2.github.io/http2-spec/#rfc.section.6.4 // See https://httpwg.org/specs/rfc7540.html#rfc.section.6.4
type RSTStreamFrame struct { type RSTStreamFrame struct {
FrameHeader FrameHeader
ErrCode ErrCode ErrCode ErrCode
} }
func parseRSTStreamFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, error) { func parseRSTStreamFrame(_ *frameCache, fh FrameHeader, countError func(string), p []byte) (Frame, error) {
if len(p) != 4 { if len(p) != 4 {
countError("frame_rststream_bad_len")
return nil, ConnectionError(ErrCodeFrameSize) return nil, ConnectionError(ErrCodeFrameSize)
} }
if fh.StreamID == 0 { if fh.StreamID == 0 {
countError("frame_rststream_zero_stream")
return nil, ConnectionError(ErrCodeProtocol) return nil, ConnectionError(ErrCodeProtocol)
} }
return &RSTStreamFrame{fh, ErrCode(binary.BigEndian.Uint32(p[:4]))}, nil return &RSTStreamFrame{fh, ErrCode(binary.BigEndian.Uint32(p[:4]))}, nil
@@ -1196,14 +1299,15 @@ func (f *Framer) WriteRSTStream(streamID uint32, code ErrCode) error {
} }
// A ContinuationFrame is used to continue a sequence of header block fragments. // A ContinuationFrame is used to continue a sequence of header block fragments.
// See http://http2.github.io/http2-spec/#rfc.section.6.10 // See https://httpwg.org/specs/rfc7540.html#rfc.section.6.10
type ContinuationFrame struct { type ContinuationFrame struct {
FrameHeader FrameHeader
headerFragBuf []byte headerFragBuf []byte
} }
func parseContinuationFrame(_ *frameCache, fh FrameHeader, p []byte) (Frame, error) { func parseContinuationFrame(_ *frameCache, fh FrameHeader, countError func(string), p []byte) (Frame, error) {
if fh.StreamID == 0 { if fh.StreamID == 0 {
countError("frame_continuation_zero_stream")
return nil, connError{ErrCodeProtocol, "CONTINUATION frame with stream ID 0"} return nil, connError{ErrCodeProtocol, "CONTINUATION frame with stream ID 0"}
} }
return &ContinuationFrame{fh, p}, nil return &ContinuationFrame{fh, p}, nil
@@ -1236,7 +1340,7 @@ func (f *Framer) WriteContinuation(streamID uint32, endHeaders bool, headerBlock
} }
// A PushPromiseFrame is used to initiate a server stream. // A PushPromiseFrame is used to initiate a server stream.
// See http://http2.github.io/http2-spec/#rfc.section.6.6 // See https://httpwg.org/specs/rfc7540.html#rfc.section.6.6
type PushPromiseFrame struct { type PushPromiseFrame struct {
FrameHeader FrameHeader
PromiseID uint32 PromiseID uint32
@@ -1252,7 +1356,7 @@ func (f *PushPromiseFrame) HeadersEnded() bool {
return f.FrameHeader.Flags.Has(FlagPushPromiseEndHeaders) return f.FrameHeader.Flags.Has(FlagPushPromiseEndHeaders)
} }
func parsePushPromise(_ *frameCache, fh FrameHeader, p []byte) (_ Frame, err error) { func parsePushPromise(_ *frameCache, fh FrameHeader, countError func(string), p []byte) (_ Frame, err error) {
pp := &PushPromiseFrame{ pp := &PushPromiseFrame{
FrameHeader: fh, FrameHeader: fh,
} }
@@ -1263,6 +1367,7 @@ func parsePushPromise(_ *frameCache, fh FrameHeader, p []byte) (_ Frame, err err
// with. If the stream identifier field specifies the value // with. If the stream identifier field specifies the value
// 0x0, a recipient MUST respond with a connection error // 0x0, a recipient MUST respond with a connection error
// (Section 5.4.1) of type PROTOCOL_ERROR. // (Section 5.4.1) of type PROTOCOL_ERROR.
countError("frame_pushpromise_zero_stream")
return nil, ConnectionError(ErrCodeProtocol) return nil, ConnectionError(ErrCodeProtocol)
} }
// The PUSH_PROMISE frame includes optional padding. // The PUSH_PROMISE frame includes optional padding.
@@ -1270,18 +1375,21 @@ func parsePushPromise(_ *frameCache, fh FrameHeader, p []byte) (_ Frame, err err
var padLength uint8 var padLength uint8
if fh.Flags.Has(FlagPushPromisePadded) { if fh.Flags.Has(FlagPushPromisePadded) {
if p, padLength, err = readByte(p); err != nil { if p, padLength, err = readByte(p); err != nil {
countError("frame_pushpromise_pad_short")
return return
} }
} }
p, pp.PromiseID, err = readUint32(p) p, pp.PromiseID, err = readUint32(p)
if err != nil { if err != nil {
countError("frame_pushpromise_promiseid_short")
return return
} }
pp.PromiseID = pp.PromiseID & (1<<31 - 1) pp.PromiseID = pp.PromiseID & (1<<31 - 1)
if int(padLength) > len(p) { if int(padLength) > len(p) {
// like the DATA frame, error out if padding is longer than the body. // like the DATA frame, error out if padding is longer than the body.
countError("frame_pushpromise_pad_too_big")
return nil, ConnectionError(ErrCodeProtocol) return nil, ConnectionError(ErrCodeProtocol)
} }
pp.headerFragBuf = p[:len(p)-int(padLength)] pp.headerFragBuf = p[:len(p)-int(padLength)]
@@ -1444,7 +1552,7 @@ func (mh *MetaHeadersFrame) checkPseudos() error {
pf := mh.PseudoFields() pf := mh.PseudoFields()
for i, hf := range pf { for i, hf := range pf {
switch hf.Name { switch hf.Name {
case ":method", ":path", ":scheme", ":authority": case ":method", ":path", ":scheme", ":authority", ":protocol":
isRequest = true isRequest = true
case ":status": case ":status":
isResponse = true isResponse = true
@@ -1452,7 +1560,7 @@ func (mh *MetaHeadersFrame) checkPseudos() error {
return pseudoHeaderError(hf.Name) return pseudoHeaderError(hf.Name)
} }
// Check for duplicates. // Check for duplicates.
// This would be a bad algorithm, but N is 4. // This would be a bad algorithm, but N is 5.
// And this doesn't allocate. // And this doesn't allocate.
for _, hf2 := range pf[:i] { for _, hf2 := range pf[:i] {
if hf.Name == hf2.Name { if hf.Name == hf2.Name {
@@ -1467,19 +1575,18 @@ func (mh *MetaHeadersFrame) checkPseudos() error {
} }
func (fr *Framer) maxHeaderStringLen() int { func (fr *Framer) maxHeaderStringLen() int {
v := fr.maxHeaderListSize() v := int(fr.maxHeaderListSize())
if uint32(int(v)) == v { if v < 0 {
return int(v) // If maxHeaderListSize overflows an int, use no limit (0).
return 0
} }
// They had a crazy big number for MaxHeaderBytes anyway, return v
// so give them unlimited header lengths:
return 0
} }
// readMetaFrame returns 0 or more CONTINUATION frames from fr and // readMetaFrame returns 0 or more CONTINUATION frames from fr and
// merge them into into the provided hf and returns a MetaHeadersFrame // merge them into the provided hf and returns a MetaHeadersFrame
// with the decoded hpack values. // with the decoded hpack values.
func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) { func (fr *Framer) readMetaFrame(hf *HeadersFrame) (Frame, error) {
if fr.AllowIllegalReads { if fr.AllowIllegalReads {
return nil, errors.New("illegal use of AllowIllegalReads with ReadMetaHeaders") return nil, errors.New("illegal use of AllowIllegalReads with ReadMetaHeaders")
} }
@@ -1498,7 +1605,8 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) {
fr.debugReadLoggerf("http2: decoded hpack field %+v", hf) fr.debugReadLoggerf("http2: decoded hpack field %+v", hf)
} }
if !httpguts.ValidHeaderFieldValue(hf.Value) { if !httpguts.ValidHeaderFieldValue(hf.Value) {
invalid = headerFieldValueError(hf.Value) // Don't include the value in the error, because it may be sensitive.
invalid = headerFieldValueError(hf.Name)
} }
isPseudo := strings.HasPrefix(hf.Name, ":") isPseudo := strings.HasPrefix(hf.Name, ":")
if isPseudo { if isPseudo {
@@ -1521,6 +1629,7 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) {
if size > remainSize { if size > remainSize {
hdec.SetEmitEnabled(false) hdec.SetEmitEnabled(false)
mh.Truncated = true mh.Truncated = true
remainSize = 0
return return
} }
remainSize -= size remainSize -= size
@@ -1533,8 +1642,38 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) {
var hc headersOrContinuation = hf var hc headersOrContinuation = hf
for { for {
frag := hc.HeaderBlockFragment() frag := hc.HeaderBlockFragment()
// Avoid parsing large amounts of headers that we will then discard.
// If the sender exceeds the max header list size by too much,
// skip parsing the fragment and close the connection.
//
// "Too much" is either any CONTINUATION frame after we've already
// exceeded the max header list size (in which case remainSize is 0),
// or a frame whose encoded size is more than twice the remaining
// header list bytes we're willing to accept.
if int64(len(frag)) > int64(2*remainSize) {
if VerboseLogs {
log.Printf("http2: header list too large")
}
// It would be nice to send a RST_STREAM before sending the GOAWAY,
// but the structure of the server's frame writer makes this difficult.
return mh, ConnectionError(ErrCodeProtocol)
}
// Also close the connection after any CONTINUATION frame following an
// invalid header, since we stop tracking the size of the headers after
// an invalid one.
if invalid != nil {
if VerboseLogs {
log.Printf("http2: invalid header: %v", invalid)
}
// It would be nice to send a RST_STREAM before sending the GOAWAY,
// but the structure of the server's frame writer makes this difficult.
return mh, ConnectionError(ErrCodeProtocol)
}
if _, err := hdec.Write(frag); err != nil { if _, err := hdec.Write(frag); err != nil {
return nil, ConnectionError(ErrCodeCompression) return mh, ConnectionError(ErrCodeCompression)
} }
if hc.HeadersEnded() { if hc.HeadersEnded() {
@@ -1551,7 +1690,7 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) {
mh.HeadersFrame.invalidate() mh.HeadersFrame.invalidate()
if err := hdec.Close(); err != nil { if err := hdec.Close(); err != nil {
return nil, ConnectionError(ErrCodeCompression) return mh, ConnectionError(ErrCodeCompression)
} }
if invalid != nil { if invalid != nil {
fr.errDetail = invalid fr.errDetail = invalid
-26
View File
@@ -1,26 +0,0 @@
// Copyright 2018 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.
// +build go1.11
package http2
import "net/textproto"
func traceHasWroteHeaderField(trace *clientTrace) bool {
return trace != nil && trace.WroteHeaderField != nil
}
func traceWroteHeaderField(trace *clientTrace, k, v string) {
if trace != nil && trace.WroteHeaderField != nil {
trace.WroteHeaderField(k, []string{v})
}
}
func traceGot1xxResponseFunc(trace *clientTrace) func(int, textproto.MIMEHeader) error {
if trace != nil {
return trace.Got1xxResponse
}
return nil
}
-16
View File
@@ -1,16 +0,0 @@
// Copyright 2016 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.
// +build go1.6
package http2
import (
"net/http"
"time"
)
func transportExpectContinueTimeout(t1 *http.Transport) time.Duration {
return t1.ExpectContinueTimeout
}
-121
View File
@@ -1,121 +0,0 @@
// Copyright 2016 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.
// +build go1.7
package http2
import (
"context"
"net"
"net/http"
"net/http/httptrace"
"time"
)
type contextContext interface {
context.Context
}
var errCanceled = context.Canceled
func serverConnBaseContext(c net.Conn, opts *ServeConnOpts) (ctx contextContext, cancel func()) {
ctx, cancel = context.WithCancel(context.Background())
ctx = context.WithValue(ctx, http.LocalAddrContextKey, c.LocalAddr())
if hs := opts.baseConfig(); hs != nil {
ctx = context.WithValue(ctx, http.ServerContextKey, hs)
}
return
}
func contextWithCancel(ctx contextContext) (_ contextContext, cancel func()) {
return context.WithCancel(ctx)
}
func requestWithContext(req *http.Request, ctx contextContext) *http.Request {
return req.WithContext(ctx)
}
type clientTrace httptrace.ClientTrace
func reqContext(r *http.Request) context.Context { return r.Context() }
func (t *Transport) idleConnTimeout() time.Duration {
if t.t1 != nil {
return t.t1.IdleConnTimeout
}
return 0
}
func setResponseUncompressed(res *http.Response) { res.Uncompressed = true }
func traceGetConn(req *http.Request, hostPort string) {
trace := httptrace.ContextClientTrace(req.Context())
if trace == nil || trace.GetConn == nil {
return
}
trace.GetConn(hostPort)
}
func traceGotConn(req *http.Request, cc *ClientConn) {
trace := httptrace.ContextClientTrace(req.Context())
if trace == nil || trace.GotConn == nil {
return
}
ci := httptrace.GotConnInfo{Conn: cc.tconn}
cc.mu.Lock()
ci.Reused = cc.nextStreamID > 1
ci.WasIdle = len(cc.streams) == 0 && ci.Reused
if ci.WasIdle && !cc.lastActive.IsZero() {
ci.IdleTime = time.Now().Sub(cc.lastActive)
}
cc.mu.Unlock()
trace.GotConn(ci)
}
func traceWroteHeaders(trace *clientTrace) {
if trace != nil && trace.WroteHeaders != nil {
trace.WroteHeaders()
}
}
func traceGot100Continue(trace *clientTrace) {
if trace != nil && trace.Got100Continue != nil {
trace.Got100Continue()
}
}
func traceWait100Continue(trace *clientTrace) {
if trace != nil && trace.Wait100Continue != nil {
trace.Wait100Continue()
}
}
func traceWroteRequest(trace *clientTrace, err error) {
if trace != nil && trace.WroteRequest != nil {
trace.WroteRequest(httptrace.WroteRequestInfo{Err: err})
}
}
func traceFirstResponseByte(trace *clientTrace) {
if trace != nil && trace.GotFirstResponseByte != nil {
trace.GotFirstResponseByte()
}
}
func requestTrace(req *http.Request) *clientTrace {
trace := httptrace.ContextClientTrace(req.Context())
return (*clientTrace)(trace)
}
// Ping sends a PING frame to the server and waits for the ack.
func (cc *ClientConn) Ping(ctx context.Context) error {
return cc.ping(ctx)
}
// Shutdown gracefully closes the client connection, waiting for running streams to complete.
func (cc *ClientConn) Shutdown(ctx context.Context) error {
return cc.shutdown(ctx)
}
-36
View File
@@ -1,36 +0,0 @@
// Copyright 2016 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.
// +build go1.7,!go1.8
package http2
import "crypto/tls"
// temporary copy of Go 1.7's private tls.Config.clone:
func cloneTLSConfig(c *tls.Config) *tls.Config {
return &tls.Config{
Rand: c.Rand,
Time: c.Time,
Certificates: c.Certificates,
NameToCertificate: c.NameToCertificate,
GetCertificate: c.GetCertificate,
RootCAs: c.RootCAs,
NextProtos: c.NextProtos,
ServerName: c.ServerName,
ClientAuth: c.ClientAuth,
ClientCAs: c.ClientCAs,
InsecureSkipVerify: c.InsecureSkipVerify,
CipherSuites: c.CipherSuites,
PreferServerCipherSuites: c.PreferServerCipherSuites,
SessionTicketsDisabled: c.SessionTicketsDisabled,
SessionTicketKey: c.SessionTicketKey,
ClientSessionCache: c.ClientSessionCache,
MinVersion: c.MinVersion,
MaxVersion: c.MaxVersion,
CurvePreferences: c.CurvePreferences,
DynamicRecordSizingDisabled: c.DynamicRecordSizingDisabled,
Renegotiation: c.Renegotiation,
}
}
-56
View File
@@ -1,56 +0,0 @@
// Copyright 2015 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.
// +build go1.8
package http2
import (
"crypto/tls"
"io"
"net/http"
)
func cloneTLSConfig(c *tls.Config) *tls.Config {
c2 := c.Clone()
c2.GetClientCertificate = c.GetClientCertificate // golang.org/issue/19264
return c2
}
var _ http.Pusher = (*responseWriter)(nil)
// Push implements http.Pusher.
func (w *responseWriter) Push(target string, opts *http.PushOptions) error {
internalOpts := pushOptions{}
if opts != nil {
internalOpts.Method = opts.Method
internalOpts.Header = opts.Header
}
return w.push(target, internalOpts)
}
func configureServer18(h1 *http.Server, h2 *Server) error {
if h2.IdleTimeout == 0 {
if h1.IdleTimeout != 0 {
h2.IdleTimeout = h1.IdleTimeout
} else {
h2.IdleTimeout = h1.ReadTimeout
}
}
return nil
}
func shouldLogPanic(panicValue interface{}) bool {
return panicValue != nil && panicValue != http.ErrAbortHandler
}
func reqGetBody(req *http.Request) func() (io.ReadCloser, error) {
return req.GetBody
}
func reqBodyIsNoBody(body io.ReadCloser) bool {
return body == http.NoBody
}
func go18httpNoBody() io.ReadCloser { return http.NoBody } // for tests only
-16
View File
@@ -1,16 +0,0 @@
// Copyright 2015 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.
// +build go1.9
package http2
import (
"net/http"
)
func configureServer19(s *http.Server, conf *Server) error {
s.RegisterOnShutdown(conf.state.startGracefulShutdown)
return nil
}
+14 -3
View File
@@ -15,21 +15,32 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"sync" "sync"
"sync/atomic"
) )
var DebugGoroutines = os.Getenv("DEBUG_HTTP2_GOROUTINES") == "1" var DebugGoroutines = os.Getenv("DEBUG_HTTP2_GOROUTINES") == "1"
// Setting DebugGoroutines to false during a test to disable goroutine debugging
// results in race detector complaints when a test leaves goroutines running before
// returning. Tests shouldn't do this, of course, but when they do it generally shows
// up as infrequent, hard-to-debug flakes. (See #66519.)
//
// Disable goroutine debugging during individual tests with an atomic bool.
// (Note that it's safe to enable/disable debugging mid-test, so the actual race condition
// here is harmless.)
var disableDebugGoroutines atomic.Bool
type goroutineLock uint64 type goroutineLock uint64
func newGoroutineLock() goroutineLock { func newGoroutineLock() goroutineLock {
if !DebugGoroutines { if !DebugGoroutines || disableDebugGoroutines.Load() {
return 0 return 0
} }
return goroutineLock(curGoroutineID()) return goroutineLock(curGoroutineID())
} }
func (g goroutineLock) check() { func (g goroutineLock) check() {
if !DebugGoroutines { if !DebugGoroutines || disableDebugGoroutines.Load() {
return return
} }
if curGoroutineID() != uint64(g) { if curGoroutineID() != uint64(g) {
@@ -38,7 +49,7 @@ func (g goroutineLock) check() {
} }
func (g goroutineLock) checkNotOn() { func (g goroutineLock) checkNotOn() {
if !DebugGoroutines { if !DebugGoroutines || disableDebugGoroutines.Load() {
return return
} }
if curGoroutineID() == uint64(g) { if curGoroutineID() == uint64(g) {
+7 -2
View File
@@ -116,6 +116,11 @@ func (e *Encoder) SetMaxDynamicTableSize(v uint32) {
e.dynTab.setMaxSize(v) e.dynTab.setMaxSize(v)
} }
// MaxDynamicTableSize returns the current dynamic header table size.
func (e *Encoder) MaxDynamicTableSize() (v uint32) {
return e.dynTab.maxSize
}
// SetMaxDynamicTableSizeLimit changes the maximum value that can be // SetMaxDynamicTableSizeLimit changes the maximum value that can be
// specified in SetMaxDynamicTableSize to v. By default, it is set to // specified in SetMaxDynamicTableSize to v. By default, it is set to
// 4096, which is the same size of the default dynamic header table // 4096, which is the same size of the default dynamic header table
@@ -150,7 +155,7 @@ func appendIndexed(dst []byte, i uint64) []byte {
// extended buffer. // extended buffer.
// //
// If f.Sensitive is true, "Never Indexed" representation is used. If // If f.Sensitive is true, "Never Indexed" representation is used. If
// f.Sensitive is false and indexing is true, "Inremental Indexing" // f.Sensitive is false and indexing is true, "Incremental Indexing"
// representation is used. // representation is used.
func appendNewName(dst []byte, f HeaderField, indexing bool) []byte { func appendNewName(dst []byte, f HeaderField, indexing bool) []byte {
dst = append(dst, encodeTypeByte(indexing, f.Sensitive)) dst = append(dst, encodeTypeByte(indexing, f.Sensitive))
@@ -191,7 +196,7 @@ func appendTableSize(dst []byte, v uint32) []byte {
// bit prefix, to dst and returns the extended buffer. // bit prefix, to dst and returns the extended buffer.
// //
// See // See
// http://http2.github.io/http2-spec/compression.html#integer.representation // https://httpwg.org/specs/rfc7541.html#integer.representation
func appendVarInt(dst []byte, n byte, i uint64) []byte { func appendVarInt(dst []byte, n byte, i uint64) []byte {
k := uint64((1 << n) - 1) k := uint64((1 << n) - 1)
if i < k { if i < k {
+67 -40
View File
@@ -59,7 +59,7 @@ func (hf HeaderField) String() string {
// Size returns the size of an entry per RFC 7541 section 4.1. // Size returns the size of an entry per RFC 7541 section 4.1.
func (hf HeaderField) Size() uint32 { func (hf HeaderField) Size() uint32 {
// http://http2.github.io/http2-spec/compression.html#rfc.section.4.1 // https://httpwg.org/specs/rfc7541.html#rfc.section.4.1
// "The size of the dynamic table is the sum of the size of // "The size of the dynamic table is the sum of the size of
// its entries. The size of an entry is the sum of its name's // its entries. The size of an entry is the sum of its name's
// length in octets (as defined in Section 5.2), its value's // length in octets (as defined in Section 5.2), its value's
@@ -92,6 +92,8 @@ type Decoder struct {
// saveBuf is previous data passed to Write which we weren't able // saveBuf is previous data passed to Write which we weren't able
// to fully parse before. Unlike buf, we own this data. // to fully parse before. Unlike buf, we own this data.
saveBuf bytes.Buffer saveBuf bytes.Buffer
firstField bool // processing the first field of the header block
} }
// NewDecoder returns a new decoder with the provided maximum dynamic // NewDecoder returns a new decoder with the provided maximum dynamic
@@ -101,6 +103,7 @@ func NewDecoder(maxDynamicTableSize uint32, emitFunc func(f HeaderField)) *Decod
d := &Decoder{ d := &Decoder{
emit: emitFunc, emit: emitFunc,
emitEnabled: true, emitEnabled: true,
firstField: true,
} }
d.dynTab.table.init() d.dynTab.table.init()
d.dynTab.allowedMaxSize = maxDynamicTableSize d.dynTab.allowedMaxSize = maxDynamicTableSize
@@ -155,7 +158,7 @@ func (d *Decoder) SetAllowedMaxDynamicTableSize(v uint32) {
} }
type dynamicTable struct { type dynamicTable struct {
// http://http2.github.io/http2-spec/compression.html#rfc.section.2.3.2 // https://httpwg.org/specs/rfc7541.html#rfc.section.2.3.2
table headerFieldTable table headerFieldTable
size uint32 // in bytes size uint32 // in bytes
maxSize uint32 // current maxSize maxSize uint32 // current maxSize
@@ -208,7 +211,7 @@ func (d *Decoder) at(i uint64) (hf HeaderField, ok bool) {
return dt.ents[dt.len()-(int(i)-staticTable.len())], true return dt.ents[dt.len()-(int(i)-staticTable.len())], true
} }
// Decode decodes an entire block. // DecodeFull decodes an entire block.
// //
// TODO: remove this method and make it incremental later? This is // TODO: remove this method and make it incremental later? This is
// easier for debugging now. // easier for debugging now.
@@ -226,11 +229,15 @@ func (d *Decoder) DecodeFull(p []byte) ([]HeaderField, error) {
return hf, nil return hf, nil
} }
// Close declares that the decoding is complete and resets the Decoder
// to be reused again for a new header block. If there is any remaining
// data in the decoder's buffer, Close returns an error.
func (d *Decoder) Close() error { func (d *Decoder) Close() error {
if d.saveBuf.Len() > 0 { if d.saveBuf.Len() > 0 {
d.saveBuf.Reset() d.saveBuf.Reset()
return DecodingError{errors.New("truncated headers")} return DecodingError{errors.New("truncated headers")}
} }
d.firstField = true
return nil return nil
} }
@@ -266,6 +273,7 @@ func (d *Decoder) Write(p []byte) (n int, err error) {
d.saveBuf.Write(d.buf) d.saveBuf.Write(d.buf)
return len(p), nil return len(p), nil
} }
d.firstField = false
if err != nil { if err != nil {
break break
} }
@@ -299,27 +307,27 @@ func (d *Decoder) parseHeaderFieldRepr() error {
case b&128 != 0: case b&128 != 0:
// Indexed representation. // Indexed representation.
// High bit set? // High bit set?
// http://http2.github.io/http2-spec/compression.html#rfc.section.6.1 // https://httpwg.org/specs/rfc7541.html#rfc.section.6.1
return d.parseFieldIndexed() return d.parseFieldIndexed()
case b&192 == 64: case b&192 == 64:
// 6.2.1 Literal Header Field with Incremental Indexing // 6.2.1 Literal Header Field with Incremental Indexing
// 0b10xxxxxx: top two bits are 10 // 0b10xxxxxx: top two bits are 10
// http://http2.github.io/http2-spec/compression.html#rfc.section.6.2.1 // https://httpwg.org/specs/rfc7541.html#rfc.section.6.2.1
return d.parseFieldLiteral(6, indexedTrue) return d.parseFieldLiteral(6, indexedTrue)
case b&240 == 0: case b&240 == 0:
// 6.2.2 Literal Header Field without Indexing // 6.2.2 Literal Header Field without Indexing
// 0b0000xxxx: top four bits are 0000 // 0b0000xxxx: top four bits are 0000
// http://http2.github.io/http2-spec/compression.html#rfc.section.6.2.2 // https://httpwg.org/specs/rfc7541.html#rfc.section.6.2.2
return d.parseFieldLiteral(4, indexedFalse) return d.parseFieldLiteral(4, indexedFalse)
case b&240 == 16: case b&240 == 16:
// 6.2.3 Literal Header Field never Indexed // 6.2.3 Literal Header Field never Indexed
// 0b0001xxxx: top four bits are 0001 // 0b0001xxxx: top four bits are 0001
// http://http2.github.io/http2-spec/compression.html#rfc.section.6.2.3 // https://httpwg.org/specs/rfc7541.html#rfc.section.6.2.3
return d.parseFieldLiteral(4, indexedNever) return d.parseFieldLiteral(4, indexedNever)
case b&224 == 32: case b&224 == 32:
// 6.3 Dynamic Table Size Update // 6.3 Dynamic Table Size Update
// Top three bits are '001'. // Top three bits are '001'.
// http://http2.github.io/http2-spec/compression.html#rfc.section.6.3 // https://httpwg.org/specs/rfc7541.html#rfc.section.6.3
return d.parseDynamicTableSizeUpdate() return d.parseDynamicTableSizeUpdate()
} }
@@ -351,6 +359,7 @@ func (d *Decoder) parseFieldLiteral(n uint8, it indexType) error {
var hf HeaderField var hf HeaderField
wantStr := d.emitEnabled || it.indexed() wantStr := d.emitEnabled || it.indexed()
var undecodedName undecodedString
if nameIdx > 0 { if nameIdx > 0 {
ihf, ok := d.at(nameIdx) ihf, ok := d.at(nameIdx)
if !ok { if !ok {
@@ -358,15 +367,27 @@ func (d *Decoder) parseFieldLiteral(n uint8, it indexType) error {
} }
hf.Name = ihf.Name hf.Name = ihf.Name
} else { } else {
hf.Name, buf, err = d.readString(buf, wantStr) undecodedName, buf, err = d.readString(buf)
if err != nil { if err != nil {
return err return err
} }
} }
hf.Value, buf, err = d.readString(buf, wantStr) undecodedValue, buf, err := d.readString(buf)
if err != nil { if err != nil {
return err return err
} }
if wantStr {
if nameIdx <= 0 {
hf.Name, err = d.decodeString(undecodedName)
if err != nil {
return err
}
}
hf.Value, err = d.decodeString(undecodedValue)
if err != nil {
return err
}
}
d.buf = buf d.buf = buf
if it.indexed() { if it.indexed() {
d.dynTab.add(hf) d.dynTab.add(hf)
@@ -391,7 +412,7 @@ func (d *Decoder) callEmit(hf HeaderField) error {
func (d *Decoder) parseDynamicTableSizeUpdate() error { func (d *Decoder) parseDynamicTableSizeUpdate() error {
// RFC 7541, sec 4.2: This dynamic table size update MUST occur at the // RFC 7541, sec 4.2: This dynamic table size update MUST occur at the
// beginning of the first header block following the change to the dynamic table size. // beginning of the first header block following the change to the dynamic table size.
if d.dynTab.size > 0 { if !d.firstField && d.dynTab.size > 0 {
return DecodingError{errors.New("dynamic table size update MUST occur at the beginning of a header block")} return DecodingError{errors.New("dynamic table size update MUST occur at the beginning of a header block")}
} }
@@ -412,7 +433,7 @@ var errVarintOverflow = DecodingError{errors.New("varint integer overflow")}
// readVarInt reads an unsigned variable length integer off the // readVarInt reads an unsigned variable length integer off the
// beginning of p. n is the parameter as described in // beginning of p. n is the parameter as described in
// http://http2.github.io/http2-spec/compression.html#rfc.section.5.1. // https://httpwg.org/specs/rfc7541.html#rfc.section.5.1.
// //
// n must always be between 1 and 8. // n must always be between 1 and 8.
// //
@@ -451,46 +472,52 @@ func readVarInt(n byte, p []byte) (i uint64, remain []byte, err error) {
return 0, origP, errNeedMore return 0, origP, errNeedMore
} }
// readString decodes an hpack string from p. // readString reads an hpack string from p.
// //
// wantStr is whether s will be used. If false, decompression and // It returns a reference to the encoded string data to permit deferring decode costs
// []byte->string garbage are skipped if s will be ignored // until after the caller verifies all data is present.
// anyway. This does mean that huffman decoding errors for non-indexed func (d *Decoder) readString(p []byte) (u undecodedString, remain []byte, err error) {
// strings past the MAX_HEADER_LIST_SIZE are ignored, but the server
// is returning an error anyway, and because they're not indexed, the error
// won't affect the decoding state.
func (d *Decoder) readString(p []byte, wantStr bool) (s string, remain []byte, err error) {
if len(p) == 0 { if len(p) == 0 {
return "", p, errNeedMore return u, p, errNeedMore
} }
isHuff := p[0]&128 != 0 isHuff := p[0]&128 != 0
strLen, p, err := readVarInt(7, p) strLen, p, err := readVarInt(7, p)
if err != nil { if err != nil {
return "", p, err return u, p, err
} }
if d.maxStrLen != 0 && strLen > uint64(d.maxStrLen) { if d.maxStrLen != 0 && strLen > uint64(d.maxStrLen) {
return "", nil, ErrStringLength // Returning an error here means Huffman decoding errors
// for non-indexed strings past the maximum string length
// are ignored, but the server is returning an error anyway
// and because the string is not indexed the error will not
// affect the decoding state.
return u, nil, ErrStringLength
} }
if uint64(len(p)) < strLen { if uint64(len(p)) < strLen {
return "", p, errNeedMore return u, p, errNeedMore
} }
if !isHuff { u.isHuff = isHuff
if wantStr { u.b = p[:strLen]
s = string(p[:strLen]) return u, p[strLen:], nil
} }
return s, p[strLen:], nil
type undecodedString struct {
isHuff bool
b []byte
}
func (d *Decoder) decodeString(u undecodedString) (string, error) {
if !u.isHuff {
return string(u.b), nil
} }
buf := bufPool.Get().(*bytes.Buffer)
if wantStr { buf.Reset() // don't trust others
buf := bufPool.Get().(*bytes.Buffer) var s string
buf.Reset() // don't trust others err := huffmanDecode(buf, d.maxStrLen, u.b)
defer bufPool.Put(buf) if err == nil {
if err := huffmanDecode(buf, d.maxStrLen, p[:strLen]); err != nil {
buf.Reset()
return "", nil, err
}
s = buf.String() s = buf.String()
buf.Reset() // be nice to GC
} }
return s, p[strLen:], nil buf.Reset() // be nice to GC
bufPool.Put(buf)
return s, err
} }
+68 -64
View File
@@ -105,7 +105,14 @@ func huffmanDecode(buf *bytes.Buffer, maxLen int, v []byte) error {
return nil return nil
} }
// incomparable is a zero-width, non-comparable type. Adding it to a struct
// makes that struct also non-comparable, and generally doesn't add
// any size (as long as it's first).
type incomparable [0]func()
type node struct { type node struct {
_ incomparable
// children is non-nil for internal nodes // children is non-nil for internal nodes
children *[256]*node children *[256]*node
@@ -133,50 +140,79 @@ func buildRootHuffmanNode() {
panic("unexpected size") panic("unexpected size")
} }
lazyRootHuffmanNode = newInternalNode() lazyRootHuffmanNode = newInternalNode()
for i, code := range huffmanCodes { // allocate a leaf node for each of the 256 symbols
addDecoderNode(byte(i), code, huffmanCodeLen[i]) leaves := new([256]node)
}
}
func addDecoderNode(sym byte, code uint32, codeLen uint8) { for sym, code := range huffmanCodes {
cur := lazyRootHuffmanNode codeLen := huffmanCodeLen[sym]
for codeLen > 8 {
codeLen -= 8 cur := lazyRootHuffmanNode
i := uint8(code >> codeLen) for codeLen > 8 {
if cur.children[i] == nil { codeLen -= 8
cur.children[i] = newInternalNode() i := uint8(code >> codeLen)
if cur.children[i] == nil {
cur.children[i] = newInternalNode()
}
cur = cur.children[i]
}
shift := 8 - codeLen
start, end := int(uint8(code<<shift)), int(1<<shift)
leaves[sym].sym = byte(sym)
leaves[sym].codeLen = codeLen
for i := start; i < start+end; i++ {
cur.children[i] = &leaves[sym]
} }
cur = cur.children[i]
}
shift := 8 - codeLen
start, end := int(uint8(code<<shift)), int(1<<shift)
for i := start; i < start+end; i++ {
cur.children[i] = &node{sym: sym, codeLen: codeLen}
} }
} }
// AppendHuffmanString appends s, as encoded in Huffman codes, to dst // AppendHuffmanString appends s, as encoded in Huffman codes, to dst
// and returns the extended buffer. // and returns the extended buffer.
func AppendHuffmanString(dst []byte, s string) []byte { func AppendHuffmanString(dst []byte, s string) []byte {
rembits := uint8(8) // This relies on the maximum huffman code length being 30 (See tables.go huffmanCodeLen array)
// So if a uint64 buffer has less than 32 valid bits can always accommodate another huffmanCode.
var (
x uint64 // buffer
n uint // number valid of bits present in x
)
for i := 0; i < len(s); i++ { for i := 0; i < len(s); i++ {
if rembits == 8 { c := s[i]
dst = append(dst, 0) n += uint(huffmanCodeLen[c])
x <<= huffmanCodeLen[c] % 64
x |= uint64(huffmanCodes[c])
if n >= 32 {
n %= 32 // Normally would be -= 32 but %= 32 informs compiler 0 <= n <= 31 for upcoming shift
y := uint32(x >> n) // Compiler doesn't combine memory writes if y isn't uint32
dst = append(dst, byte(y>>24), byte(y>>16), byte(y>>8), byte(y))
} }
dst, rembits = appendByteToHuffmanCode(dst, rembits, s[i])
} }
// Add padding bits if necessary
if rembits < 8 { if over := n % 8; over > 0 {
// special EOS symbol const (
code := uint32(0x3fffffff) eosCode = 0x3fffffff
nbits := uint8(30) eosNBits = 30
eosPadByte = eosCode >> (eosNBits - 8)
t := uint8(code >> (nbits - rembits)) )
dst[len(dst)-1] |= t pad := 8 - over
x = (x << pad) | (eosPadByte >> over)
n += pad // 8 now divides into n exactly
} }
// n in (0, 8, 16, 24, 32)
return dst switch n / 8 {
case 0:
return dst
case 1:
return append(dst, byte(x))
case 2:
y := uint16(x)
return append(dst, byte(y>>8), byte(y))
case 3:
y := uint16(x >> 8)
return append(dst, byte(y>>8), byte(y), byte(x))
}
// case 4:
y := uint32(x)
return append(dst, byte(y>>24), byte(y>>16), byte(y>>8), byte(y))
} }
// HuffmanEncodeLength returns the number of bytes required to encode // HuffmanEncodeLength returns the number of bytes required to encode
@@ -188,35 +224,3 @@ func HuffmanEncodeLength(s string) uint64 {
} }
return (n + 7) / 8 return (n + 7) / 8
} }
// appendByteToHuffmanCode appends Huffman code for c to dst and
// returns the extended buffer and the remaining bits in the last
// element. The appending is not byte aligned and the remaining bits
// in the last element of dst is given in rembits.
func appendByteToHuffmanCode(dst []byte, rembits uint8, c byte) ([]byte, uint8) {
code := huffmanCodes[c]
nbits := huffmanCodeLen[c]
for {
if rembits > nbits {
t := uint8(code << (rembits - nbits))
dst[len(dst)-1] |= t
rembits -= nbits
break
}
t := uint8(code >> (nbits - rembits))
dst[len(dst)-1] |= t
nbits -= rembits
rembits = 8
if nbits == 0 {
break
}
dst = append(dst, 0)
}
return dst, rembits
}
+188
View File
@@ -0,0 +1,188 @@
// go generate gen.go
// Code generated by the command above; DO NOT EDIT.
package hpack
var staticTable = &headerFieldTable{
evictCount: 0,
byName: map[string]uint64{
":authority": 1,
":method": 3,
":path": 5,
":scheme": 7,
":status": 14,
"accept-charset": 15,
"accept-encoding": 16,
"accept-language": 17,
"accept-ranges": 18,
"accept": 19,
"access-control-allow-origin": 20,
"age": 21,
"allow": 22,
"authorization": 23,
"cache-control": 24,
"content-disposition": 25,
"content-encoding": 26,
"content-language": 27,
"content-length": 28,
"content-location": 29,
"content-range": 30,
"content-type": 31,
"cookie": 32,
"date": 33,
"etag": 34,
"expect": 35,
"expires": 36,
"from": 37,
"host": 38,
"if-match": 39,
"if-modified-since": 40,
"if-none-match": 41,
"if-range": 42,
"if-unmodified-since": 43,
"last-modified": 44,
"link": 45,
"location": 46,
"max-forwards": 47,
"proxy-authenticate": 48,
"proxy-authorization": 49,
"range": 50,
"referer": 51,
"refresh": 52,
"retry-after": 53,
"server": 54,
"set-cookie": 55,
"strict-transport-security": 56,
"transfer-encoding": 57,
"user-agent": 58,
"vary": 59,
"via": 60,
"www-authenticate": 61,
},
byNameValue: map[pairNameValue]uint64{
{name: ":authority", value: ""}: 1,
{name: ":method", value: "GET"}: 2,
{name: ":method", value: "POST"}: 3,
{name: ":path", value: "/"}: 4,
{name: ":path", value: "/index.html"}: 5,
{name: ":scheme", value: "http"}: 6,
{name: ":scheme", value: "https"}: 7,
{name: ":status", value: "200"}: 8,
{name: ":status", value: "204"}: 9,
{name: ":status", value: "206"}: 10,
{name: ":status", value: "304"}: 11,
{name: ":status", value: "400"}: 12,
{name: ":status", value: "404"}: 13,
{name: ":status", value: "500"}: 14,
{name: "accept-charset", value: ""}: 15,
{name: "accept-encoding", value: "gzip, deflate"}: 16,
{name: "accept-language", value: ""}: 17,
{name: "accept-ranges", value: ""}: 18,
{name: "accept", value: ""}: 19,
{name: "access-control-allow-origin", value: ""}: 20,
{name: "age", value: ""}: 21,
{name: "allow", value: ""}: 22,
{name: "authorization", value: ""}: 23,
{name: "cache-control", value: ""}: 24,
{name: "content-disposition", value: ""}: 25,
{name: "content-encoding", value: ""}: 26,
{name: "content-language", value: ""}: 27,
{name: "content-length", value: ""}: 28,
{name: "content-location", value: ""}: 29,
{name: "content-range", value: ""}: 30,
{name: "content-type", value: ""}: 31,
{name: "cookie", value: ""}: 32,
{name: "date", value: ""}: 33,
{name: "etag", value: ""}: 34,
{name: "expect", value: ""}: 35,
{name: "expires", value: ""}: 36,
{name: "from", value: ""}: 37,
{name: "host", value: ""}: 38,
{name: "if-match", value: ""}: 39,
{name: "if-modified-since", value: ""}: 40,
{name: "if-none-match", value: ""}: 41,
{name: "if-range", value: ""}: 42,
{name: "if-unmodified-since", value: ""}: 43,
{name: "last-modified", value: ""}: 44,
{name: "link", value: ""}: 45,
{name: "location", value: ""}: 46,
{name: "max-forwards", value: ""}: 47,
{name: "proxy-authenticate", value: ""}: 48,
{name: "proxy-authorization", value: ""}: 49,
{name: "range", value: ""}: 50,
{name: "referer", value: ""}: 51,
{name: "refresh", value: ""}: 52,
{name: "retry-after", value: ""}: 53,
{name: "server", value: ""}: 54,
{name: "set-cookie", value: ""}: 55,
{name: "strict-transport-security", value: ""}: 56,
{name: "transfer-encoding", value: ""}: 57,
{name: "user-agent", value: ""}: 58,
{name: "vary", value: ""}: 59,
{name: "via", value: ""}: 60,
{name: "www-authenticate", value: ""}: 61,
},
ents: []HeaderField{
{Name: ":authority", Value: "", Sensitive: false},
{Name: ":method", Value: "GET", Sensitive: false},
{Name: ":method", Value: "POST", Sensitive: false},
{Name: ":path", Value: "/", Sensitive: false},
{Name: ":path", Value: "/index.html", Sensitive: false},
{Name: ":scheme", Value: "http", Sensitive: false},
{Name: ":scheme", Value: "https", Sensitive: false},
{Name: ":status", Value: "200", Sensitive: false},
{Name: ":status", Value: "204", Sensitive: false},
{Name: ":status", Value: "206", Sensitive: false},
{Name: ":status", Value: "304", Sensitive: false},
{Name: ":status", Value: "400", Sensitive: false},
{Name: ":status", Value: "404", Sensitive: false},
{Name: ":status", Value: "500", Sensitive: false},
{Name: "accept-charset", Value: "", Sensitive: false},
{Name: "accept-encoding", Value: "gzip, deflate", Sensitive: false},
{Name: "accept-language", Value: "", Sensitive: false},
{Name: "accept-ranges", Value: "", Sensitive: false},
{Name: "accept", Value: "", Sensitive: false},
{Name: "access-control-allow-origin", Value: "", Sensitive: false},
{Name: "age", Value: "", Sensitive: false},
{Name: "allow", Value: "", Sensitive: false},
{Name: "authorization", Value: "", Sensitive: false},
{Name: "cache-control", Value: "", Sensitive: false},
{Name: "content-disposition", Value: "", Sensitive: false},
{Name: "content-encoding", Value: "", Sensitive: false},
{Name: "content-language", Value: "", Sensitive: false},
{Name: "content-length", Value: "", Sensitive: false},
{Name: "content-location", Value: "", Sensitive: false},
{Name: "content-range", Value: "", Sensitive: false},
{Name: "content-type", Value: "", Sensitive: false},
{Name: "cookie", Value: "", Sensitive: false},
{Name: "date", Value: "", Sensitive: false},
{Name: "etag", Value: "", Sensitive: false},
{Name: "expect", Value: "", Sensitive: false},
{Name: "expires", Value: "", Sensitive: false},
{Name: "from", Value: "", Sensitive: false},
{Name: "host", Value: "", Sensitive: false},
{Name: "if-match", Value: "", Sensitive: false},
{Name: "if-modified-since", Value: "", Sensitive: false},
{Name: "if-none-match", Value: "", Sensitive: false},
{Name: "if-range", Value: "", Sensitive: false},
{Name: "if-unmodified-since", Value: "", Sensitive: false},
{Name: "last-modified", Value: "", Sensitive: false},
{Name: "link", Value: "", Sensitive: false},
{Name: "location", Value: "", Sensitive: false},
{Name: "max-forwards", Value: "", Sensitive: false},
{Name: "proxy-authenticate", Value: "", Sensitive: false},
{Name: "proxy-authorization", Value: "", Sensitive: false},
{Name: "range", Value: "", Sensitive: false},
{Name: "referer", Value: "", Sensitive: false},
{Name: "refresh", Value: "", Sensitive: false},
{Name: "retry-after", Value: "", Sensitive: false},
{Name: "server", Value: "", Sensitive: false},
{Name: "set-cookie", Value: "", Sensitive: false},
{Name: "strict-transport-security", Value: "", Sensitive: false},
{Name: "transfer-encoding", Value: "", Sensitive: false},
{Name: "user-agent", Value: "", Sensitive: false},
{Name: "vary", Value: "", Sensitive: false},
{Name: "via", Value: "", Sensitive: false},
{Name: "www-authenticate", Value: "", Sensitive: false},
},
}
+1 -77
View File
@@ -96,8 +96,7 @@ func (t *headerFieldTable) evictOldest(n int) {
// meaning t.ents is reversed for dynamic tables. Hence, when t is a dynamic // meaning t.ents is reversed for dynamic tables. Hence, when t is a dynamic
// table, the return value i actually refers to the entry t.ents[t.len()-i]. // table, the return value i actually refers to the entry t.ents[t.len()-i].
// //
// All tables are assumed to be a dynamic tables except for the global // All tables are assumed to be a dynamic tables except for the global staticTable.
// staticTable pointer.
// //
// See Section 2.3.3. // See Section 2.3.3.
func (t *headerFieldTable) search(f HeaderField) (i uint64, nameValueMatch bool) { func (t *headerFieldTable) search(f HeaderField) (i uint64, nameValueMatch bool) {
@@ -125,81 +124,6 @@ func (t *headerFieldTable) idToIndex(id uint64) uint64 {
return k + 1 return k + 1
} }
// http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-07#appendix-B
var staticTable = newStaticTable()
var staticTableEntries = [...]HeaderField{
{Name: ":authority"},
{Name: ":method", Value: "GET"},
{Name: ":method", Value: "POST"},
{Name: ":path", Value: "/"},
{Name: ":path", Value: "/index.html"},
{Name: ":scheme", Value: "http"},
{Name: ":scheme", Value: "https"},
{Name: ":status", Value: "200"},
{Name: ":status", Value: "204"},
{Name: ":status", Value: "206"},
{Name: ":status", Value: "304"},
{Name: ":status", Value: "400"},
{Name: ":status", Value: "404"},
{Name: ":status", Value: "500"},
{Name: "accept-charset"},
{Name: "accept-encoding", Value: "gzip, deflate"},
{Name: "accept-language"},
{Name: "accept-ranges"},
{Name: "accept"},
{Name: "access-control-allow-origin"},
{Name: "age"},
{Name: "allow"},
{Name: "authorization"},
{Name: "cache-control"},
{Name: "content-disposition"},
{Name: "content-encoding"},
{Name: "content-language"},
{Name: "content-length"},
{Name: "content-location"},
{Name: "content-range"},
{Name: "content-type"},
{Name: "cookie"},
{Name: "date"},
{Name: "etag"},
{Name: "expect"},
{Name: "expires"},
{Name: "from"},
{Name: "host"},
{Name: "if-match"},
{Name: "if-modified-since"},
{Name: "if-none-match"},
{Name: "if-range"},
{Name: "if-unmodified-since"},
{Name: "last-modified"},
{Name: "link"},
{Name: "location"},
{Name: "max-forwards"},
{Name: "proxy-authenticate"},
{Name: "proxy-authorization"},
{Name: "range"},
{Name: "referer"},
{Name: "refresh"},
{Name: "retry-after"},
{Name: "server"},
{Name: "set-cookie"},
{Name: "strict-transport-security"},
{Name: "transfer-encoding"},
{Name: "user-agent"},
{Name: "vary"},
{Name: "via"},
{Name: "www-authenticate"},
}
func newStaticTable() *headerFieldTable {
t := &headerFieldTable{}
t.init()
for _, e := range staticTableEntries[:] {
t.addEntry(e)
}
return t
}
var huffmanCodes = [256]uint32{ var huffmanCodes = [256]uint32{
0x1ff8, 0x1ff8,
0x7fffd8, 0x7fffd8,
+82 -57
View File
@@ -11,9 +11,6 @@
// requires Go 1.6 or later) // requires Go 1.6 or later)
// //
// See https://http2.github.io/ for more information on HTTP/2. // See https://http2.github.io/ for more information on HTTP/2.
//
// See https://http2.golang.org/ for a test server running this code.
//
package http2 // import "golang.org/x/net/http2" package http2 // import "golang.org/x/net/http2"
import ( import (
@@ -21,13 +18,14 @@ import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"io" "net"
"net/http" "net/http"
"os" "os"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
"golang.org/x/net/http/httpguts" "golang.org/x/net/http/httpguts"
) )
@@ -36,7 +34,15 @@ var (
VerboseLogs bool VerboseLogs bool
logFrameWrites bool logFrameWrites bool
logFrameReads bool logFrameReads bool
inTests bool
// Enabling extended CONNECT by causes browsers to attempt to use
// WebSockets-over-HTTP/2. This results in problems when the server's websocket
// package doesn't support extended CONNECT.
//
// Disable extended CONNECT by default for now.
//
// Issue #71128.
disableExtendedConnectProtocol = true
) )
func init() { func init() {
@@ -49,6 +55,9 @@ func init() {
logFrameWrites = true logFrameWrites = true
logFrameReads = true logFrameReads = true
} }
if strings.Contains(e, "http2xconnect=1") {
disableExtendedConnectProtocol = false
}
} }
const ( const (
@@ -57,14 +66,14 @@ const (
ClientPreface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" ClientPreface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
// SETTINGS_MAX_FRAME_SIZE default // SETTINGS_MAX_FRAME_SIZE default
// http://http2.github.io/http2-spec/#rfc.section.6.5.2 // https://httpwg.org/specs/rfc7540.html#rfc.section.6.5.2
initialMaxFrameSize = 16384 initialMaxFrameSize = 16384
// NextProtoTLS is the NPN/ALPN protocol negotiated during // NextProtoTLS is the NPN/ALPN protocol negotiated during
// HTTP/2's TLS setup. // HTTP/2's TLS setup.
NextProtoTLS = "h2" NextProtoTLS = "h2"
// http://http2.github.io/http2-spec/#SettingValues // https://httpwg.org/specs/rfc7540.html#SettingValues
initialHeaderTableSize = 4096 initialHeaderTableSize = 4096
initialWindowSize = 65535 // 6.9.2 Initial Flow Control Window Size initialWindowSize = 65535 // 6.9.2 Initial Flow Control Window Size
@@ -113,7 +122,7 @@ func (st streamState) String() string {
// Setting is a setting parameter: which setting it is, and its value. // Setting is a setting parameter: which setting it is, and its value.
type Setting struct { type Setting struct {
// ID is which setting is being set. // ID is which setting is being set.
// See http://http2.github.io/http2-spec/#SettingValues // See https://httpwg.org/specs/rfc7540.html#SettingFormat
ID SettingID ID SettingID
// Val is the value. // Val is the value.
@@ -140,30 +149,36 @@ func (s Setting) Valid() error {
if s.Val < 16384 || s.Val > 1<<24-1 { if s.Val < 16384 || s.Val > 1<<24-1 {
return ConnectionError(ErrCodeProtocol) return ConnectionError(ErrCodeProtocol)
} }
case SettingEnableConnectProtocol:
if s.Val != 1 && s.Val != 0 {
return ConnectionError(ErrCodeProtocol)
}
} }
return nil return nil
} }
// A SettingID is an HTTP/2 setting as defined in // A SettingID is an HTTP/2 setting as defined in
// http://http2.github.io/http2-spec/#iana-settings // https://httpwg.org/specs/rfc7540.html#iana-settings
type SettingID uint16 type SettingID uint16
const ( const (
SettingHeaderTableSize SettingID = 0x1 SettingHeaderTableSize SettingID = 0x1
SettingEnablePush SettingID = 0x2 SettingEnablePush SettingID = 0x2
SettingMaxConcurrentStreams SettingID = 0x3 SettingMaxConcurrentStreams SettingID = 0x3
SettingInitialWindowSize SettingID = 0x4 SettingInitialWindowSize SettingID = 0x4
SettingMaxFrameSize SettingID = 0x5 SettingMaxFrameSize SettingID = 0x5
SettingMaxHeaderListSize SettingID = 0x6 SettingMaxHeaderListSize SettingID = 0x6
SettingEnableConnectProtocol SettingID = 0x8
) )
var settingName = map[SettingID]string{ var settingName = map[SettingID]string{
SettingHeaderTableSize: "HEADER_TABLE_SIZE", SettingHeaderTableSize: "HEADER_TABLE_SIZE",
SettingEnablePush: "ENABLE_PUSH", SettingEnablePush: "ENABLE_PUSH",
SettingMaxConcurrentStreams: "MAX_CONCURRENT_STREAMS", SettingMaxConcurrentStreams: "MAX_CONCURRENT_STREAMS",
SettingInitialWindowSize: "INITIAL_WINDOW_SIZE", SettingInitialWindowSize: "INITIAL_WINDOW_SIZE",
SettingMaxFrameSize: "MAX_FRAME_SIZE", SettingMaxFrameSize: "MAX_FRAME_SIZE",
SettingMaxHeaderListSize: "MAX_HEADER_LIST_SIZE", SettingMaxHeaderListSize: "MAX_HEADER_LIST_SIZE",
SettingEnableConnectProtocol: "ENABLE_CONNECT_PROTOCOL",
} }
func (s SettingID) String() string { func (s SettingID) String() string {
@@ -173,19 +188,15 @@ func (s SettingID) String() string {
return fmt.Sprintf("UNKNOWN_SETTING_%d", uint16(s)) return fmt.Sprintf("UNKNOWN_SETTING_%d", uint16(s))
} }
var (
errInvalidHeaderFieldName = errors.New("http2: invalid header field name")
errInvalidHeaderFieldValue = errors.New("http2: invalid header field value")
)
// validWireHeaderFieldName reports whether v is a valid header field // validWireHeaderFieldName reports whether v is a valid header field
// name (key). See httpguts.ValidHeaderName for the base rules. // name (key). See httpguts.ValidHeaderName for the base rules.
// //
// Further, http2 says: // Further, http2 says:
// "Just as in HTTP/1.x, header field names are strings of ASCII //
// characters that are compared in a case-insensitive // "Just as in HTTP/1.x, header field names are strings of ASCII
// fashion. However, header field names MUST be converted to // characters that are compared in a case-insensitive
// lowercase prior to their encoding in HTTP/2. " // fashion. However, header field names MUST be converted to
// lowercase prior to their encoding in HTTP/2. "
func validWireHeaderFieldName(v string) bool { func validWireHeaderFieldName(v string) bool {
if len(v) == 0 { if len(v) == 0 {
return false return false
@@ -216,12 +227,6 @@ type stringWriter interface {
WriteString(s string) (n int, err error) WriteString(s string) (n int, err error)
} }
// A gate lets two goroutines coordinate their activities.
type gate chan struct{}
func (g gate) Done() { g <- struct{}{} }
func (g gate) Wait() { <-g }
// A closeWaiter is like a sync.WaitGroup but only goes 1 to 0 (open to closed). // A closeWaiter is like a sync.WaitGroup but only goes 1 to 0 (open to closed).
type closeWaiter chan struct{} type closeWaiter chan struct{}
@@ -247,12 +252,17 @@ func (cw closeWaiter) Wait() {
// Its buffered writer is lazily allocated as needed, to minimize // Its buffered writer is lazily allocated as needed, to minimize
// idle memory usage with many connections. // idle memory usage with many connections.
type bufferedWriter struct { type bufferedWriter struct {
w io.Writer // immutable _ incomparable
bw *bufio.Writer // non-nil when data is buffered conn net.Conn // immutable
bw *bufio.Writer // non-nil when data is buffered
byteTimeout time.Duration // immutable, WriteByteTimeout
} }
func newBufferedWriter(w io.Writer) *bufferedWriter { func newBufferedWriter(conn net.Conn, timeout time.Duration) *bufferedWriter {
return &bufferedWriter{w: w} return &bufferedWriter{
conn: conn,
byteTimeout: timeout,
}
} }
// bufWriterPoolBufferSize is the size of bufio.Writer's // bufWriterPoolBufferSize is the size of bufio.Writer's
@@ -279,7 +289,7 @@ func (w *bufferedWriter) Available() int {
func (w *bufferedWriter) Write(p []byte) (n int, err error) { func (w *bufferedWriter) Write(p []byte) (n int, err error) {
if w.bw == nil { if w.bw == nil {
bw := bufWriterPool.Get().(*bufio.Writer) bw := bufWriterPool.Get().(*bufio.Writer)
bw.Reset(w.w) bw.Reset((*bufferedWriterTimeoutWriter)(w))
w.bw = bw w.bw = bw
} }
return w.bw.Write(p) return w.bw.Write(p)
@@ -297,6 +307,32 @@ func (w *bufferedWriter) Flush() error {
return err return err
} }
type bufferedWriterTimeoutWriter bufferedWriter
func (w *bufferedWriterTimeoutWriter) Write(p []byte) (n int, err error) {
return writeWithByteTimeout(w.conn, w.byteTimeout, p)
}
// writeWithByteTimeout writes to conn.
// If more than timeout passes without any bytes being written to the connection,
// the write fails.
func writeWithByteTimeout(conn net.Conn, timeout time.Duration, p []byte) (n int, err error) {
if timeout <= 0 {
return conn.Write(p)
}
for {
conn.SetWriteDeadline(time.Now().Add(timeout))
nn, err := conn.Write(p[n:])
n += nn
if n == len(p) || nn == 0 || !errors.Is(err, os.ErrDeadlineExceeded) {
// Either we finished the write, made no progress, or hit the deadline.
// Whichever it is, we're done now.
conn.SetWriteDeadline(time.Time{})
return n, err
}
}
}
func mustUint31(v int32) uint32 { func mustUint31(v int32) uint32 {
if v < 0 || v > 2147483647 { if v < 0 || v > 2147483647 {
panic("out of range") panic("out of range")
@@ -319,6 +355,7 @@ func bodyAllowedForStatus(status int) bool {
} }
type httpError struct { type httpError struct {
_ incomparable
msg string msg string
timeout bool timeout bool
} }
@@ -366,19 +403,7 @@ func (s *sorter) SortStrings(ss []string) {
s.v = save s.v = save
} }
// validPseudoPath reports whether v is a valid :path pseudo-header // incomparable is a zero-width, non-comparable type. Adding it to a struct
// value. It must be either: // makes that struct also non-comparable, and generally doesn't add
// // any size (as long as it's first).
// *) a non-empty string starting with '/' type incomparable [0]func()
// *) the string '*', for OPTIONS requests.
//
// For now this is only used a quick check for deciding when to clean
// up Opaque URLs before sending requests from the Transport.
// See golang.org/issue/16847
//
// We used to enforce that the path also didn't start with "//", but
// Google's GFE accepts such paths and Chrome sends them, so ignore
// that part of the spec. See golang.org/issue/19103.
func validPseudoPath(v string) bool {
return (len(v) > 0 && v[0] == '/') || v == "*"
}
-17
View File
@@ -1,17 +0,0 @@
// Copyright 2018 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.
// +build !go1.11
package http2
import "net/textproto"
func traceHasWroteHeaderField(trace *clientTrace) bool { return false }
func traceWroteHeaderField(trace *clientTrace, k, v string) {}
func traceGot1xxResponseFunc(trace *clientTrace) func(int, textproto.MIMEHeader) error {
return nil
}
-21
View File
@@ -1,21 +0,0 @@
// Copyright 2015 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.
// +build !go1.6
package http2
import (
"net/http"
"time"
)
func configureTransport(t1 *http.Transport) (*Transport, error) {
return nil, errTransportVersion
}
func transportExpectContinueTimeout(t1 *http.Transport) time.Duration {
return 0
}
-95
View File
@@ -1,95 +0,0 @@
// Copyright 2016 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.
// +build !go1.7
package http2
import (
"crypto/tls"
"errors"
"net"
"net/http"
"time"
)
type contextContext interface {
Done() <-chan struct{}
Err() error
}
var errCanceled = errors.New("canceled")
type fakeContext struct{}
func (fakeContext) Done() <-chan struct{} { return nil }
func (fakeContext) Err() error { panic("should not be called") }
func reqContext(r *http.Request) fakeContext {
return fakeContext{}
}
func setResponseUncompressed(res *http.Response) {
// Nothing.
}
type clientTrace struct{}
func requestTrace(*http.Request) *clientTrace { return nil }
func traceGetConn(*http.Request, string) {}
func traceGotConn(*http.Request, *ClientConn) {}
func traceFirstResponseByte(*clientTrace) {}
func traceWroteHeaders(*clientTrace) {}
func traceWroteRequest(*clientTrace, error) {}
func traceGot100Continue(trace *clientTrace) {}
func traceWait100Continue(trace *clientTrace) {}
func nop() {}
func serverConnBaseContext(c net.Conn, opts *ServeConnOpts) (ctx contextContext, cancel func()) {
return nil, nop
}
func contextWithCancel(ctx contextContext) (_ contextContext, cancel func()) {
return ctx, nop
}
func requestWithContext(req *http.Request, ctx contextContext) *http.Request {
return req
}
// temporary copy of Go 1.6's private tls.Config.clone:
func cloneTLSConfig(c *tls.Config) *tls.Config {
return &tls.Config{
Rand: c.Rand,
Time: c.Time,
Certificates: c.Certificates,
NameToCertificate: c.NameToCertificate,
GetCertificate: c.GetCertificate,
RootCAs: c.RootCAs,
NextProtos: c.NextProtos,
ServerName: c.ServerName,
ClientAuth: c.ClientAuth,
ClientCAs: c.ClientCAs,
InsecureSkipVerify: c.InsecureSkipVerify,
CipherSuites: c.CipherSuites,
PreferServerCipherSuites: c.PreferServerCipherSuites,
SessionTicketsDisabled: c.SessionTicketsDisabled,
SessionTicketKey: c.SessionTicketKey,
ClientSessionCache: c.ClientSessionCache,
MinVersion: c.MinVersion,
MaxVersion: c.MaxVersion,
CurvePreferences: c.CurvePreferences,
}
}
func (cc *ClientConn) Ping(ctx contextContext) error {
return cc.ping(ctx)
}
func (cc *ClientConn) Shutdown(ctx contextContext) error {
return cc.shutdown(ctx)
}
func (t *Transport) idleConnTimeout() time.Duration { return 0 }
-29
View File
@@ -1,29 +0,0 @@
// Copyright 2016 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.
// +build !go1.8
package http2
import (
"io"
"net/http"
)
func configureServer18(h1 *http.Server, h2 *Server) error {
// No IdleTimeout to sync prior to Go 1.8.
return nil
}
func shouldLogPanic(panicValue interface{}) bool {
return panicValue != nil
}
func reqGetBody(req *http.Request) func() (io.ReadCloser, error) {
return nil
}
func reqBodyIsNoBody(io.ReadCloser) bool { return false }
func go18httpNoBody() io.ReadCloser { return nil } // for tests only
-16
View File
@@ -1,16 +0,0 @@
// Copyright 2016 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.
// +build !go1.9
package http2
import (
"net/http"
)
func configureServer19(s *http.Server, conf *Server) error {
// not supported prior to go1.9
return nil
}
+26 -5
View File
@@ -17,6 +17,7 @@ type pipe struct {
mu sync.Mutex mu sync.Mutex
c sync.Cond // c.L lazily initialized to &p.mu c sync.Cond // c.L lazily initialized to &p.mu
b pipeBuffer // nil when done reading b pipeBuffer // nil when done reading
unread int // bytes unread when done
err error // read error once empty. non-nil means closed. err error // read error once empty. non-nil means closed.
breakErr error // immediate read error (caller doesn't see rest of b) breakErr error // immediate read error (caller doesn't see rest of b)
donec chan struct{} // closed on error donec chan struct{} // closed on error
@@ -29,11 +30,22 @@ type pipeBuffer interface {
io.Reader io.Reader
} }
// setBuffer initializes the pipe buffer.
// It has no effect if the pipe is already closed.
func (p *pipe) setBuffer(b pipeBuffer) {
p.mu.Lock()
defer p.mu.Unlock()
if p.err != nil || p.breakErr != nil {
return
}
p.b = b
}
func (p *pipe) Len() int { func (p *pipe) Len() int {
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
if p.b == nil { if p.b == nil {
return 0 return p.unread
} }
return p.b.Len() return p.b.Len()
} }
@@ -65,7 +77,10 @@ func (p *pipe) Read(d []byte) (n int, err error) {
} }
} }
var errClosedPipeWrite = errors.New("write on closed buffer") var (
errClosedPipeWrite = errors.New("write on closed buffer")
errUninitializedPipeWrite = errors.New("write on uninitialized buffer")
)
// Write copies bytes from p into the buffer and wakes a reader. // Write copies bytes from p into the buffer and wakes a reader.
// It is an error to write more data than the buffer can hold. // It is an error to write more data than the buffer can hold.
@@ -76,11 +91,14 @@ func (p *pipe) Write(d []byte) (n int, err error) {
p.c.L = &p.mu p.c.L = &p.mu
} }
defer p.c.Signal() defer p.c.Signal()
if p.err != nil { if p.err != nil || p.breakErr != nil {
return 0, errClosedPipeWrite return 0, errClosedPipeWrite
} }
if p.breakErr != nil { // pipe.setBuffer is never invoked, leaving the buffer uninitialized.
return len(d), nil // discard when there is no reader // We shouldn't try to write to an uninitialized pipe,
// but returning an error is better than panicking.
if p.b == nil {
return 0, errUninitializedPipeWrite
} }
return p.b.Write(d) return p.b.Write(d)
} }
@@ -117,6 +135,9 @@ func (p *pipe) closeWithError(dst *error, err error, fn func()) {
} }
p.readFn = fn p.readFn = fn
if dst == &p.breakErr { if dst == &p.breakErr {
if p.b != nil {
p.unread += p.b.Len()
}
p.b = nil p.b = nil
} }
*dst = err *dst = err
+843 -392
View File
File diff suppressed because it is too large Load Diff
+2004 -1018
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2024 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
import (
"crypto/tls"
"errors"
"net"
)
const nextProtoUnencryptedHTTP2 = "unencrypted_http2"
// unencryptedNetConnFromTLSConn retrieves a net.Conn wrapped in a *tls.Conn.
//
// TLSNextProto functions accept a *tls.Conn.
//
// When passing an unencrypted HTTP/2 connection to a TLSNextProto function,
// we pass a *tls.Conn with an underlying net.Conn containing the unencrypted connection.
// To be extra careful about mistakes (accidentally dropping TLS encryption in a place
// where we want it), the tls.Conn contains a net.Conn with an UnencryptedNetConn method
// that returns the actual connection we want to use.
func unencryptedNetConnFromTLSConn(tc *tls.Conn) (net.Conn, error) {
conner, ok := tc.NetConn().(interface {
UnencryptedNetConn() net.Conn
})
if !ok {
return nil, errors.New("http2: TLS conn unexpectedly found in unencrypted handoff")
}
return conner.UnencryptedNetConn(), nil
}
+18 -2
View File
@@ -13,6 +13,7 @@ import (
"golang.org/x/net/http/httpguts" "golang.org/x/net/http/httpguts"
"golang.org/x/net/http2/hpack" "golang.org/x/net/http2/hpack"
"golang.org/x/net/internal/httpcommon"
) )
// writeFramer is implemented by any type that is used to write frames. // writeFramer is implemented by any type that is used to write frames.
@@ -131,6 +132,16 @@ func (se StreamError) writeFrame(ctx writeContext) error {
func (se StreamError) staysWithinBuffer(max int) bool { return frameHeaderLen+4 <= max } func (se StreamError) staysWithinBuffer(max int) bool { return frameHeaderLen+4 <= max }
type writePing struct {
data [8]byte
}
func (w writePing) writeFrame(ctx writeContext) error {
return ctx.Framer().WritePing(false, w.data)
}
func (w writePing) staysWithinBuffer(max int) bool { return frameHeaderLen+len(w.data) <= max }
type writePingAck struct{ pf *PingFrame } type writePingAck struct{ pf *PingFrame }
func (w writePingAck) writeFrame(ctx writeContext) error { func (w writePingAck) writeFrame(ctx writeContext) error {
@@ -329,7 +340,7 @@ func (wu writeWindowUpdate) writeFrame(ctx writeContext) error {
} }
// encodeHeaders encodes an http.Header. If keys is not nil, then (k, h[k]) // encodeHeaders encodes an http.Header. If keys is not nil, then (k, h[k])
// is encoded only only if k is in keys. // is encoded only if k is in keys.
func encodeHeaders(enc *hpack.Encoder, h http.Header, keys []string) { func encodeHeaders(enc *hpack.Encoder, h http.Header, keys []string) {
if keys == nil { if keys == nil {
sorter := sorterPool.Get().(*sorter) sorter := sorterPool.Get().(*sorter)
@@ -341,7 +352,12 @@ func encodeHeaders(enc *hpack.Encoder, h http.Header, keys []string) {
} }
for _, k := range keys { for _, k := range keys {
vv := h[k] vv := h[k]
k = lowerHeader(k) k, ascii := httpcommon.LowerHeader(k)
if !ascii {
// Skip writing invalid headers. Per RFC 7540, Section 8.1.2, header
// field names have to be ASCII characters (just as in HTTP/1.x).
continue
}
if !validWireHeaderFieldName(k) { if !validWireHeaderFieldName(k) {
// Skip it as backup paranoia. Per // Skip it as backup paranoia. Per
// golang.org/issue/14048, these should // golang.org/issue/14048, these should
+62 -16
View File
@@ -32,7 +32,8 @@ type WriteScheduler interface {
// Pop dequeues the next frame to write. Returns false if no frames can // Pop dequeues the next frame to write. Returns false if no frames can
// be written. Frames with a given wr.StreamID() are Pop'd in the same // be written. Frames with a given wr.StreamID() are Pop'd in the same
// order they are Push'd. // order they are Push'd, except RST_STREAM frames. No frames should be
// discarded except by CloseStream.
Pop() (wr FrameWriteRequest, ok bool) Pop() (wr FrameWriteRequest, ok bool)
} }
@@ -41,6 +42,8 @@ type OpenStreamOptions struct {
// PusherID is zero if the stream was initiated by the client. Otherwise, // PusherID is zero if the stream was initiated by the client. Otherwise,
// PusherID names the stream that pushed the newly opened stream. // PusherID names the stream that pushed the newly opened stream.
PusherID uint32 PusherID uint32
// priority is used to set the priority of the newly opened stream.
priority PriorityParam
} }
// FrameWriteRequest is a request to write a frame. // FrameWriteRequest is a request to write a frame.
@@ -52,6 +55,7 @@ type FrameWriteRequest struct {
// stream is the stream on which this frame will be written. // stream is the stream on which this frame will be written.
// nil for non-stream frames like PING and SETTINGS. // nil for non-stream frames like PING and SETTINGS.
// nil for RST_STREAM streams, which use the StreamError.StreamID field instead.
stream *stream stream *stream
// done, if non-nil, must be a buffered channel with space for // done, if non-nil, must be a buffered channel with space for
@@ -76,6 +80,12 @@ func (wr FrameWriteRequest) StreamID() uint32 {
return wr.stream.id return wr.stream.id
} }
// isControl reports whether wr is a control frame for MaxQueuedControlFrames
// purposes. That includes non-stream frames and RST_STREAM frames.
func (wr FrameWriteRequest) isControl() bool {
return wr.stream == nil
}
// DataSize returns the number of flow control bytes that must be consumed // DataSize returns the number of flow control bytes that must be consumed
// to write this entire frame. This is 0 for non-DATA frames. // to write this entire frame. This is 0 for non-DATA frames.
func (wr FrameWriteRequest) DataSize() int { func (wr FrameWriteRequest) DataSize() int {
@@ -175,44 +185,75 @@ func (wr *FrameWriteRequest) replyToWriter(err error) {
} }
// writeQueue is used by implementations of WriteScheduler. // writeQueue is used by implementations of WriteScheduler.
//
// Each writeQueue contains a queue of FrameWriteRequests, meant to store all
// FrameWriteRequests associated with a given stream. This is implemented as a
// two-stage queue: currQueue[currPos:] and nextQueue. Removing an item is done
// by incrementing currPos of currQueue. Adding an item is done by appending it
// to the nextQueue. If currQueue is empty when trying to remove an item, we
// can swap currQueue and nextQueue to remedy the situation.
// This two-stage queue is analogous to the use of two lists in Okasaki's
// purely functional queue but without the overhead of reversing the list when
// swapping stages.
//
// writeQueue also contains prev and next, this can be used by implementations
// of WriteScheduler to construct data structures that represent the order of
// writing between different streams (e.g. circular linked list).
type writeQueue struct { type writeQueue struct {
s []FrameWriteRequest currQueue []FrameWriteRequest
nextQueue []FrameWriteRequest
currPos int
prev, next *writeQueue
} }
func (q *writeQueue) empty() bool { return len(q.s) == 0 } func (q *writeQueue) empty() bool {
return (len(q.currQueue) - q.currPos + len(q.nextQueue)) == 0
}
func (q *writeQueue) push(wr FrameWriteRequest) { func (q *writeQueue) push(wr FrameWriteRequest) {
q.s = append(q.s, wr) q.nextQueue = append(q.nextQueue, wr)
} }
func (q *writeQueue) shift() FrameWriteRequest { func (q *writeQueue) shift() FrameWriteRequest {
if len(q.s) == 0 { if q.empty() {
panic("invalid use of queue") panic("invalid use of queue")
} }
wr := q.s[0] if q.currPos >= len(q.currQueue) {
// TODO: less copy-happy queue. q.currQueue, q.currPos, q.nextQueue = q.nextQueue, 0, q.currQueue[:0]
copy(q.s, q.s[1:]) }
q.s[len(q.s)-1] = FrameWriteRequest{} wr := q.currQueue[q.currPos]
q.s = q.s[:len(q.s)-1] q.currQueue[q.currPos] = FrameWriteRequest{}
q.currPos++
return wr return wr
} }
func (q *writeQueue) peek() *FrameWriteRequest {
if q.currPos < len(q.currQueue) {
return &q.currQueue[q.currPos]
}
if len(q.nextQueue) > 0 {
return &q.nextQueue[0]
}
return nil
}
// consume consumes up to n bytes from q.s[0]. If the frame is // consume consumes up to n bytes from q.s[0]. If the frame is
// entirely consumed, it is removed from the queue. If the frame // entirely consumed, it is removed from the queue. If the frame
// is partially consumed, the frame is kept with the consumed // is partially consumed, the frame is kept with the consumed
// bytes removed. Returns true iff any bytes were consumed. // bytes removed. Returns true iff any bytes were consumed.
func (q *writeQueue) consume(n int32) (FrameWriteRequest, bool) { func (q *writeQueue) consume(n int32) (FrameWriteRequest, bool) {
if len(q.s) == 0 { if q.empty() {
return FrameWriteRequest{}, false return FrameWriteRequest{}, false
} }
consumed, rest, numresult := q.s[0].Consume(n) consumed, rest, numresult := q.peek().Consume(n)
switch numresult { switch numresult {
case 0: case 0:
return FrameWriteRequest{}, false return FrameWriteRequest{}, false
case 1: case 1:
q.shift() q.shift()
case 2: case 2:
q.s[0] = rest *q.peek() = rest
} }
return consumed, true return consumed, true
} }
@@ -221,10 +262,15 @@ type writeQueuePool []*writeQueue
// put inserts an unused writeQueue into the pool. // put inserts an unused writeQueue into the pool.
func (p *writeQueuePool) put(q *writeQueue) { func (p *writeQueuePool) put(q *writeQueue) {
for i := range q.s { for i := range q.currQueue {
q.s[i] = FrameWriteRequest{} q.currQueue[i] = FrameWriteRequest{}
} }
q.s = q.s[:0] for i := range q.nextQueue {
q.nextQueue[i] = FrameWriteRequest{}
}
q.currQueue = q.currQueue[:0]
q.nextQueue = q.nextQueue[:0]
q.currPos = 0
*p = append(*p, q) *p = append(*p, q)
} }
@@ -11,7 +11,7 @@ import (
) )
// RFC 7540, Section 5.3.5: the default weight is 16. // RFC 7540, Section 5.3.5: the default weight is 16.
const priorityDefaultWeight = 15 // 16 = 15 + 1 const priorityDefaultWeightRFC7540 = 15 // 16 = 15 + 1
// PriorityWriteSchedulerConfig configures a priorityWriteScheduler. // PriorityWriteSchedulerConfig configures a priorityWriteScheduler.
type PriorityWriteSchedulerConfig struct { type PriorityWriteSchedulerConfig struct {
@@ -66,8 +66,8 @@ func NewPriorityWriteScheduler(cfg *PriorityWriteSchedulerConfig) WriteScheduler
} }
} }
ws := &priorityWriteScheduler{ ws := &priorityWriteSchedulerRFC7540{
nodes: make(map[uint32]*priorityNode), nodes: make(map[uint32]*priorityNodeRFC7540),
maxClosedNodesInTree: cfg.MaxClosedNodesInTree, maxClosedNodesInTree: cfg.MaxClosedNodesInTree,
maxIdleNodesInTree: cfg.MaxIdleNodesInTree, maxIdleNodesInTree: cfg.MaxIdleNodesInTree,
enableWriteThrottle: cfg.ThrottleOutOfOrderWrites, enableWriteThrottle: cfg.ThrottleOutOfOrderWrites,
@@ -81,32 +81,32 @@ func NewPriorityWriteScheduler(cfg *PriorityWriteSchedulerConfig) WriteScheduler
return ws return ws
} }
type priorityNodeState int type priorityNodeStateRFC7540 int
const ( const (
priorityNodeOpen priorityNodeState = iota priorityNodeOpenRFC7540 priorityNodeStateRFC7540 = iota
priorityNodeClosed priorityNodeClosedRFC7540
priorityNodeIdle priorityNodeIdleRFC7540
) )
// priorityNode is a node in an HTTP/2 priority tree. // priorityNodeRFC7540 is a node in an HTTP/2 priority tree.
// Each node is associated with a single stream ID. // Each node is associated with a single stream ID.
// See RFC 7540, Section 5.3. // See RFC 7540, Section 5.3.
type priorityNode struct { type priorityNodeRFC7540 struct {
q writeQueue // queue of pending frames to write q writeQueue // queue of pending frames to write
id uint32 // id of the stream, or 0 for the root of the tree id uint32 // id of the stream, or 0 for the root of the tree
weight uint8 // the actual weight is weight+1, so the value is in [1,256] weight uint8 // the actual weight is weight+1, so the value is in [1,256]
state priorityNodeState // open | closed | idle state priorityNodeStateRFC7540 // open | closed | idle
bytes int64 // number of bytes written by this node, or 0 if closed bytes int64 // number of bytes written by this node, or 0 if closed
subtreeBytes int64 // sum(node.bytes) of all nodes in this subtree subtreeBytes int64 // sum(node.bytes) of all nodes in this subtree
// These links form the priority tree. // These links form the priority tree.
parent *priorityNode parent *priorityNodeRFC7540
kids *priorityNode // start of the kids list kids *priorityNodeRFC7540 // start of the kids list
prev, next *priorityNode // doubly-linked list of siblings prev, next *priorityNodeRFC7540 // doubly-linked list of siblings
} }
func (n *priorityNode) setParent(parent *priorityNode) { func (n *priorityNodeRFC7540) setParent(parent *priorityNodeRFC7540) {
if n == parent { if n == parent {
panic("setParent to self") panic("setParent to self")
} }
@@ -141,7 +141,7 @@ func (n *priorityNode) setParent(parent *priorityNode) {
} }
} }
func (n *priorityNode) addBytes(b int64) { func (n *priorityNodeRFC7540) addBytes(b int64) {
n.bytes += b n.bytes += b
for ; n != nil; n = n.parent { for ; n != nil; n = n.parent {
n.subtreeBytes += b n.subtreeBytes += b
@@ -149,12 +149,12 @@ func (n *priorityNode) addBytes(b int64) {
} }
// walkReadyInOrder iterates over the tree in priority order, calling f for each node // walkReadyInOrder iterates over the tree in priority order, calling f for each node
// with a non-empty write queue. When f returns true, this funcion returns true and the // with a non-empty write queue. When f returns true, this function returns true and the
// walk halts. tmp is used as scratch space for sorting. // walk halts. tmp is used as scratch space for sorting.
// //
// f(n, openParent) takes two arguments: the node to visit, n, and a bool that is true // f(n, openParent) takes two arguments: the node to visit, n, and a bool that is true
// if any ancestor p of n is still open (ignoring the root node). // if any ancestor p of n is still open (ignoring the root node).
func (n *priorityNode) walkReadyInOrder(openParent bool, tmp *[]*priorityNode, f func(*priorityNode, bool) bool) bool { func (n *priorityNodeRFC7540) walkReadyInOrder(openParent bool, tmp *[]*priorityNodeRFC7540, f func(*priorityNodeRFC7540, bool) bool) bool {
if !n.q.empty() && f(n, openParent) { if !n.q.empty() && f(n, openParent) {
return true return true
} }
@@ -165,7 +165,7 @@ func (n *priorityNode) walkReadyInOrder(openParent bool, tmp *[]*priorityNode, f
// Don't consider the root "open" when updating openParent since // Don't consider the root "open" when updating openParent since
// we can't send data frames on the root stream (only control frames). // we can't send data frames on the root stream (only control frames).
if n.id != 0 { if n.id != 0 {
openParent = openParent || (n.state == priorityNodeOpen) openParent = openParent || (n.state == priorityNodeOpenRFC7540)
} }
// Common case: only one kid or all kids have the same weight. // Common case: only one kid or all kids have the same weight.
@@ -195,7 +195,7 @@ func (n *priorityNode) walkReadyInOrder(openParent bool, tmp *[]*priorityNode, f
*tmp = append(*tmp, n.kids) *tmp = append(*tmp, n.kids)
n.kids.setParent(nil) n.kids.setParent(nil)
} }
sort.Sort(sortPriorityNodeSiblings(*tmp)) sort.Sort(sortPriorityNodeSiblingsRFC7540(*tmp))
for i := len(*tmp) - 1; i >= 0; i-- { for i := len(*tmp) - 1; i >= 0; i-- {
(*tmp)[i].setParent(n) // setParent inserts at the head of n.kids (*tmp)[i].setParent(n) // setParent inserts at the head of n.kids
} }
@@ -207,15 +207,15 @@ func (n *priorityNode) walkReadyInOrder(openParent bool, tmp *[]*priorityNode, f
return false return false
} }
type sortPriorityNodeSiblings []*priorityNode type sortPriorityNodeSiblingsRFC7540 []*priorityNodeRFC7540
func (z sortPriorityNodeSiblings) Len() int { return len(z) } func (z sortPriorityNodeSiblingsRFC7540) Len() int { return len(z) }
func (z sortPriorityNodeSiblings) Swap(i, k int) { z[i], z[k] = z[k], z[i] } func (z sortPriorityNodeSiblingsRFC7540) Swap(i, k int) { z[i], z[k] = z[k], z[i] }
func (z sortPriorityNodeSiblings) Less(i, k int) bool { func (z sortPriorityNodeSiblingsRFC7540) Less(i, k int) bool {
// Prefer the subtree that has sent fewer bytes relative to its weight. // Prefer the subtree that has sent fewer bytes relative to its weight.
// See sections 5.3.2 and 5.3.4. // See sections 5.3.2 and 5.3.4.
wi, bi := float64(z[i].weight+1), float64(z[i].subtreeBytes) wi, bi := float64(z[i].weight)+1, float64(z[i].subtreeBytes)
wk, bk := float64(z[k].weight+1), float64(z[k].subtreeBytes) wk, bk := float64(z[k].weight)+1, float64(z[k].subtreeBytes)
if bi == 0 && bk == 0 { if bi == 0 && bk == 0 {
return wi >= wk return wi >= wk
} }
@@ -225,13 +225,13 @@ func (z sortPriorityNodeSiblings) Less(i, k int) bool {
return bi/bk <= wi/wk return bi/bk <= wi/wk
} }
type priorityWriteScheduler struct { type priorityWriteSchedulerRFC7540 struct {
// root is the root of the priority tree, where root.id = 0. // root is the root of the priority tree, where root.id = 0.
// The root queues control frames that are not associated with any stream. // The root queues control frames that are not associated with any stream.
root priorityNode root priorityNodeRFC7540
// nodes maps stream ids to priority tree nodes. // nodes maps stream ids to priority tree nodes.
nodes map[uint32]*priorityNode nodes map[uint32]*priorityNodeRFC7540
// maxID is the maximum stream id in nodes. // maxID is the maximum stream id in nodes.
maxID uint32 maxID uint32
@@ -239,7 +239,7 @@ type priorityWriteScheduler struct {
// lists of nodes that have been closed or are idle, but are kept in // lists of nodes that have been closed or are idle, but are kept in
// the tree for improved prioritization. When the lengths exceed either // the tree for improved prioritization. When the lengths exceed either
// maxClosedNodesInTree or maxIdleNodesInTree, old nodes are discarded. // maxClosedNodesInTree or maxIdleNodesInTree, old nodes are discarded.
closedNodes, idleNodes []*priorityNode closedNodes, idleNodes []*priorityNodeRFC7540
// From the config. // From the config.
maxClosedNodesInTree int maxClosedNodesInTree int
@@ -248,19 +248,19 @@ type priorityWriteScheduler struct {
enableWriteThrottle bool enableWriteThrottle bool
// tmp is scratch space for priorityNode.walkReadyInOrder to reduce allocations. // tmp is scratch space for priorityNode.walkReadyInOrder to reduce allocations.
tmp []*priorityNode tmp []*priorityNodeRFC7540
// pool of empty queues for reuse. // pool of empty queues for reuse.
queuePool writeQueuePool queuePool writeQueuePool
} }
func (ws *priorityWriteScheduler) OpenStream(streamID uint32, options OpenStreamOptions) { func (ws *priorityWriteSchedulerRFC7540) OpenStream(streamID uint32, options OpenStreamOptions) {
// The stream may be currently idle but cannot be opened or closed. // The stream may be currently idle but cannot be opened or closed.
if curr := ws.nodes[streamID]; curr != nil { if curr := ws.nodes[streamID]; curr != nil {
if curr.state != priorityNodeIdle { if curr.state != priorityNodeIdleRFC7540 {
panic(fmt.Sprintf("stream %d already opened", streamID)) panic(fmt.Sprintf("stream %d already opened", streamID))
} }
curr.state = priorityNodeOpen curr.state = priorityNodeOpenRFC7540
return return
} }
@@ -272,11 +272,11 @@ func (ws *priorityWriteScheduler) OpenStream(streamID uint32, options OpenStream
if parent == nil { if parent == nil {
parent = &ws.root parent = &ws.root
} }
n := &priorityNode{ n := &priorityNodeRFC7540{
q: *ws.queuePool.get(), q: *ws.queuePool.get(),
id: streamID, id: streamID,
weight: priorityDefaultWeight, weight: priorityDefaultWeightRFC7540,
state: priorityNodeOpen, state: priorityNodeOpenRFC7540,
} }
n.setParent(parent) n.setParent(parent)
ws.nodes[streamID] = n ws.nodes[streamID] = n
@@ -285,24 +285,23 @@ func (ws *priorityWriteScheduler) OpenStream(streamID uint32, options OpenStream
} }
} }
func (ws *priorityWriteScheduler) CloseStream(streamID uint32) { func (ws *priorityWriteSchedulerRFC7540) CloseStream(streamID uint32) {
if streamID == 0 { if streamID == 0 {
panic("violation of WriteScheduler interface: cannot close stream 0") panic("violation of WriteScheduler interface: cannot close stream 0")
} }
if ws.nodes[streamID] == nil { if ws.nodes[streamID] == nil {
panic(fmt.Sprintf("violation of WriteScheduler interface: unknown stream %d", streamID)) panic(fmt.Sprintf("violation of WriteScheduler interface: unknown stream %d", streamID))
} }
if ws.nodes[streamID].state != priorityNodeOpen { if ws.nodes[streamID].state != priorityNodeOpenRFC7540 {
panic(fmt.Sprintf("violation of WriteScheduler interface: stream %d already closed", streamID)) panic(fmt.Sprintf("violation of WriteScheduler interface: stream %d already closed", streamID))
} }
n := ws.nodes[streamID] n := ws.nodes[streamID]
n.state = priorityNodeClosed n.state = priorityNodeClosedRFC7540
n.addBytes(-n.bytes) n.addBytes(-n.bytes)
q := n.q q := n.q
ws.queuePool.put(&q) ws.queuePool.put(&q)
n.q.s = nil
if ws.maxClosedNodesInTree > 0 { if ws.maxClosedNodesInTree > 0 {
ws.addClosedOrIdleNode(&ws.closedNodes, ws.maxClosedNodesInTree, n) ws.addClosedOrIdleNode(&ws.closedNodes, ws.maxClosedNodesInTree, n)
} else { } else {
@@ -310,7 +309,7 @@ func (ws *priorityWriteScheduler) CloseStream(streamID uint32) {
} }
} }
func (ws *priorityWriteScheduler) AdjustStream(streamID uint32, priority PriorityParam) { func (ws *priorityWriteSchedulerRFC7540) AdjustStream(streamID uint32, priority PriorityParam) {
if streamID == 0 { if streamID == 0 {
panic("adjustPriority on root") panic("adjustPriority on root")
} }
@@ -324,11 +323,11 @@ func (ws *priorityWriteScheduler) AdjustStream(streamID uint32, priority Priorit
return return
} }
ws.maxID = streamID ws.maxID = streamID
n = &priorityNode{ n = &priorityNodeRFC7540{
q: *ws.queuePool.get(), q: *ws.queuePool.get(),
id: streamID, id: streamID,
weight: priorityDefaultWeight, weight: priorityDefaultWeightRFC7540,
state: priorityNodeIdle, state: priorityNodeIdleRFC7540,
} }
n.setParent(&ws.root) n.setParent(&ws.root)
ws.nodes[streamID] = n ws.nodes[streamID] = n
@@ -340,7 +339,7 @@ func (ws *priorityWriteScheduler) AdjustStream(streamID uint32, priority Priorit
parent := ws.nodes[priority.StreamDep] parent := ws.nodes[priority.StreamDep]
if parent == nil { if parent == nil {
n.setParent(&ws.root) n.setParent(&ws.root)
n.weight = priorityDefaultWeight n.weight = priorityDefaultWeightRFC7540
return return
} }
@@ -381,18 +380,17 @@ func (ws *priorityWriteScheduler) AdjustStream(streamID uint32, priority Priorit
n.weight = priority.Weight n.weight = priority.Weight
} }
func (ws *priorityWriteScheduler) Push(wr FrameWriteRequest) { func (ws *priorityWriteSchedulerRFC7540) Push(wr FrameWriteRequest) {
var n *priorityNode var n *priorityNodeRFC7540
if id := wr.StreamID(); id == 0 { if wr.isControl() {
n = &ws.root n = &ws.root
} else { } else {
id := wr.StreamID()
n = ws.nodes[id] n = ws.nodes[id]
if n == nil { if n == nil {
// id is an idle or closed stream. wr should not be a HEADERS or // id is an idle or closed stream. wr should not be a HEADERS or
// DATA frame. However, wr can be a RST_STREAM. In this case, we // DATA frame. In other case, we push wr onto the root, rather
// push wr onto the root, rather than creating a new priorityNode, // than creating a new priorityNode.
// since RST_STREAM is tiny and the stream's priority is unknown
// anyway. See issue #17919.
if wr.DataSize() > 0 { if wr.DataSize() > 0 {
panic("add DATA on non-open stream") panic("add DATA on non-open stream")
} }
@@ -402,8 +400,8 @@ func (ws *priorityWriteScheduler) Push(wr FrameWriteRequest) {
n.q.push(wr) n.q.push(wr)
} }
func (ws *priorityWriteScheduler) Pop() (wr FrameWriteRequest, ok bool) { func (ws *priorityWriteSchedulerRFC7540) Pop() (wr FrameWriteRequest, ok bool) {
ws.root.walkReadyInOrder(false, &ws.tmp, func(n *priorityNode, openParent bool) bool { ws.root.walkReadyInOrder(false, &ws.tmp, func(n *priorityNodeRFC7540, openParent bool) bool {
limit := int32(math.MaxInt32) limit := int32(math.MaxInt32)
if openParent { if openParent {
limit = ws.writeThrottleLimit limit = ws.writeThrottleLimit
@@ -429,7 +427,7 @@ func (ws *priorityWriteScheduler) Pop() (wr FrameWriteRequest, ok bool) {
return wr, ok return wr, ok
} }
func (ws *priorityWriteScheduler) addClosedOrIdleNode(list *[]*priorityNode, maxSize int, n *priorityNode) { func (ws *priorityWriteSchedulerRFC7540) addClosedOrIdleNode(list *[]*priorityNodeRFC7540, maxSize int, n *priorityNodeRFC7540) {
if maxSize == 0 { if maxSize == 0 {
return return
} }
@@ -443,9 +441,9 @@ func (ws *priorityWriteScheduler) addClosedOrIdleNode(list *[]*priorityNode, max
*list = append(*list, n) *list = append(*list, n)
} }
func (ws *priorityWriteScheduler) removeNode(n *priorityNode) { func (ws *priorityWriteSchedulerRFC7540) removeNode(n *priorityNodeRFC7540) {
for k := n.kids; k != nil; k = k.next { for n.kids != nil {
k.setParent(n.parent) n.kids.setParent(n.parent)
} }
n.setParent(nil) n.setParent(nil)
delete(ws.nodes, n.id) delete(ws.nodes, n.id)
+209
View File
@@ -0,0 +1,209 @@
// Copyright 2025 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
import (
"fmt"
"math"
)
type streamMetadata struct {
location *writeQueue
priority PriorityParam
}
type priorityWriteSchedulerRFC9218 struct {
// control contains control frames (SETTINGS, PING, etc.).
control writeQueue
// heads contain the head of a circular list of streams.
// We put these heads within a nested array that represents urgency and
// incremental, as defined in
// https://www.rfc-editor.org/rfc/rfc9218.html#name-priority-parameters.
// 8 represents u=0 up to u=7, and 2 represents i=false and i=true.
heads [8][2]*writeQueue
// streams contains a mapping between each stream ID and their metadata, so
// we can quickly locate them when needing to, for example, adjust their
// priority.
streams map[uint32]streamMetadata
// queuePool are empty queues for reuse.
queuePool writeQueuePool
// prioritizeIncremental is used to determine whether we should prioritize
// incremental streams or not, when urgency is the same in a given Pop()
// call.
prioritizeIncremental bool
}
func newPriorityWriteSchedulerRFC9218() WriteScheduler {
ws := &priorityWriteSchedulerRFC9218{
streams: make(map[uint32]streamMetadata),
}
return ws
}
func (ws *priorityWriteSchedulerRFC9218) OpenStream(streamID uint32, opt OpenStreamOptions) {
if ws.streams[streamID].location != nil {
panic(fmt.Errorf("stream %d already opened", streamID))
}
q := ws.queuePool.get()
ws.streams[streamID] = streamMetadata{
location: q,
priority: opt.priority,
}
u, i := opt.priority.urgency, opt.priority.incremental
if ws.heads[u][i] == nil {
ws.heads[u][i] = q
q.next = q
q.prev = q
} else {
// Queues are stored in a ring.
// Insert the new stream before ws.head, putting it at the end of the list.
q.prev = ws.heads[u][i].prev
q.next = ws.heads[u][i]
q.prev.next = q
q.next.prev = q
}
}
func (ws *priorityWriteSchedulerRFC9218) CloseStream(streamID uint32) {
metadata := ws.streams[streamID]
q, u, i := metadata.location, metadata.priority.urgency, metadata.priority.incremental
if q == nil {
return
}
if q.next == q {
// This was the only open stream.
ws.heads[u][i] = nil
} else {
q.prev.next = q.next
q.next.prev = q.prev
if ws.heads[u][i] == q {
ws.heads[u][i] = q.next
}
}
delete(ws.streams, streamID)
ws.queuePool.put(q)
}
func (ws *priorityWriteSchedulerRFC9218) AdjustStream(streamID uint32, priority PriorityParam) {
metadata := ws.streams[streamID]
q, u, i := metadata.location, metadata.priority.urgency, metadata.priority.incremental
if q == nil {
return
}
// Remove stream from current location.
if q.next == q {
// This was the only open stream.
ws.heads[u][i] = nil
} else {
q.prev.next = q.next
q.next.prev = q.prev
if ws.heads[u][i] == q {
ws.heads[u][i] = q.next
}
}
// Insert stream to the new queue.
u, i = priority.urgency, priority.incremental
if ws.heads[u][i] == nil {
ws.heads[u][i] = q
q.next = q
q.prev = q
} else {
// Queues are stored in a ring.
// Insert the new stream before ws.head, putting it at the end of the list.
q.prev = ws.heads[u][i].prev
q.next = ws.heads[u][i]
q.prev.next = q
q.next.prev = q
}
// Update the metadata.
ws.streams[streamID] = streamMetadata{
location: q,
priority: priority,
}
}
func (ws *priorityWriteSchedulerRFC9218) Push(wr FrameWriteRequest) {
if wr.isControl() {
ws.control.push(wr)
return
}
q := ws.streams[wr.StreamID()].location
if q == nil {
// This is a closed stream.
// wr should not be a HEADERS or DATA frame.
// We push the request onto the control queue.
if wr.DataSize() > 0 {
panic("add DATA on non-open stream")
}
ws.control.push(wr)
return
}
q.push(wr)
}
func (ws *priorityWriteSchedulerRFC9218) Pop() (FrameWriteRequest, bool) {
// Control and RST_STREAM frames first.
if !ws.control.empty() {
return ws.control.shift(), true
}
// On the next Pop(), we want to prioritize incremental if we prioritized
// non-incremental request of the same urgency this time. Vice-versa.
// i.e. when there are incremental and non-incremental requests at the same
// priority, we give 50% of our bandwidth to the incremental ones in
// aggregate and 50% to the first non-incremental one (since
// non-incremental streams do not use round-robin writes).
ws.prioritizeIncremental = !ws.prioritizeIncremental
// Always prioritize lowest u (i.e. highest urgency level).
for u := range ws.heads {
for i := range ws.heads[u] {
// When we want to prioritize incremental, we try to pop i=true
// first before i=false when u is the same.
if ws.prioritizeIncremental {
i = (i + 1) % 2
}
q := ws.heads[u][i]
if q == nil {
continue
}
for {
if wr, ok := q.consume(math.MaxInt32); ok {
if i == 1 {
// For incremental streams, we update head to q.next so
// we can round-robin between multiple streams that can
// immediately benefit from partial writes.
ws.heads[u][i] = q.next
} else {
// For non-incremental streams, we try to finish one to
// completion rather than doing round-robin. However,
// we update head here so that if q.consume() is !ok
// (e.g. the stream has no more frame to consume), head
// is updated to the next q that has frames to consume
// on future iterations. This way, we do not prioritize
// writing to unavailable stream on next Pop() calls,
// preventing head-of-line blocking.
ws.heads[u][i] = q
}
return wr, true
}
q = q.next
if q == ws.heads[u][i] {
break
}
}
}
}
return FrameWriteRequest{}, false
}
+10 -5
View File
@@ -19,7 +19,8 @@ type randomWriteScheduler struct {
zero writeQueue zero writeQueue
// sq contains the stream-specific queues, keyed by stream ID. // sq contains the stream-specific queues, keyed by stream ID.
// When a stream is idle or closed, it's deleted from the map. // When a stream is idle, closed, or emptied, it's deleted
// from the map.
sq map[uint32]*writeQueue sq map[uint32]*writeQueue
// pool of empty queues for reuse. // pool of empty queues for reuse.
@@ -44,11 +45,11 @@ func (ws *randomWriteScheduler) AdjustStream(streamID uint32, priority PriorityP
} }
func (ws *randomWriteScheduler) Push(wr FrameWriteRequest) { func (ws *randomWriteScheduler) Push(wr FrameWriteRequest) {
id := wr.StreamID() if wr.isControl() {
if id == 0 {
ws.zero.push(wr) ws.zero.push(wr)
return return
} }
id := wr.StreamID()
q, ok := ws.sq[id] q, ok := ws.sq[id]
if !ok { if !ok {
q = ws.queuePool.get() q = ws.queuePool.get()
@@ -58,13 +59,17 @@ func (ws *randomWriteScheduler) Push(wr FrameWriteRequest) {
} }
func (ws *randomWriteScheduler) Pop() (FrameWriteRequest, bool) { func (ws *randomWriteScheduler) Pop() (FrameWriteRequest, bool) {
// Control frames first. // Control and RST_STREAM frames first.
if !ws.zero.empty() { if !ws.zero.empty() {
return ws.zero.shift(), true return ws.zero.shift(), true
} }
// Iterate over all non-idle streams until finding one that can be consumed. // Iterate over all non-idle streams until finding one that can be consumed.
for _, q := range ws.sq { for streamID, q := range ws.sq {
if wr, ok := q.consume(math.MaxInt32); ok { if wr, ok := q.consume(math.MaxInt32); ok {
if q.empty() {
delete(ws.sq, streamID)
ws.queuePool.put(q)
}
return wr, true return wr, true
} }
} }
+119
View File
@@ -0,0 +1,119 @@
// Copyright 2023 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
import (
"fmt"
"math"
)
type roundRobinWriteScheduler struct {
// control contains control frames (SETTINGS, PING, etc.).
control writeQueue
// streams maps stream ID to a queue.
streams map[uint32]*writeQueue
// stream queues are stored in a circular linked list.
// head is the next stream to write, or nil if there are no streams open.
head *writeQueue
// pool of empty queues for reuse.
queuePool writeQueuePool
}
// newRoundRobinWriteScheduler constructs a new write scheduler.
// The round robin scheduler prioritizes control frames
// like SETTINGS and PING over DATA frames.
// When there are no control frames to send, it performs a round-robin
// selection from the ready streams.
func newRoundRobinWriteScheduler() WriteScheduler {
ws := &roundRobinWriteScheduler{
streams: make(map[uint32]*writeQueue),
}
return ws
}
func (ws *roundRobinWriteScheduler) OpenStream(streamID uint32, options OpenStreamOptions) {
if ws.streams[streamID] != nil {
panic(fmt.Errorf("stream %d already opened", streamID))
}
q := ws.queuePool.get()
ws.streams[streamID] = q
if ws.head == nil {
ws.head = q
q.next = q
q.prev = q
} else {
// Queues are stored in a ring.
// Insert the new stream before ws.head, putting it at the end of the list.
q.prev = ws.head.prev
q.next = ws.head
q.prev.next = q
q.next.prev = q
}
}
func (ws *roundRobinWriteScheduler) CloseStream(streamID uint32) {
q := ws.streams[streamID]
if q == nil {
return
}
if q.next == q {
// This was the only open stream.
ws.head = nil
} else {
q.prev.next = q.next
q.next.prev = q.prev
if ws.head == q {
ws.head = q.next
}
}
delete(ws.streams, streamID)
ws.queuePool.put(q)
}
func (ws *roundRobinWriteScheduler) AdjustStream(streamID uint32, priority PriorityParam) {}
func (ws *roundRobinWriteScheduler) Push(wr FrameWriteRequest) {
if wr.isControl() {
ws.control.push(wr)
return
}
q := ws.streams[wr.StreamID()]
if q == nil {
// This is a closed stream.
// wr should not be a HEADERS or DATA frame.
// We push the request onto the control queue.
if wr.DataSize() > 0 {
panic("add DATA on non-open stream")
}
ws.control.push(wr)
return
}
q.push(wr)
}
func (ws *roundRobinWriteScheduler) Pop() (FrameWriteRequest, bool) {
// Control and RST_STREAM frames first.
if !ws.control.empty() {
return ws.control.shift(), true
}
if ws.head == nil {
return FrameWriteRequest{}, false
}
q := ws.head
for {
if wr, ok := q.consume(math.MaxInt32); ok {
ws.head = q.next
return wr, true
}
q = q.next
if q == ws.head {
break
}
}
return FrameWriteRequest{}, false
}
+13
View File
@@ -0,0 +1,13 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
// Copyright 2021 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.
//go:build go1.18
package idna
// Transitional processing is disabled by default in Go 1.18.
// https://golang.org/issue/47510
const transitionalLookup = false
@@ -4,14 +4,16 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build go1.10
// Package idna implements IDNA2008 using the compatibility processing // Package idna implements IDNA2008 using the compatibility processing
// defined by UTS (Unicode Technical Standard) #46, which defines a standard to // defined by UTS (Unicode Technical Standard) #46, which defines a standard to
// deal with the transition from IDNA2003. // deal with the transition from IDNA2003.
// //
// IDNA2008 (Internationalized Domain Names for Applications), is defined in RFC // IDNA2008 (Internationalized Domain Names for Applications), is defined in RFC
// 5890, RFC 5891, RFC 5892, RFC 5893 and RFC 5894. // 5890, RFC 5891, RFC 5892, RFC 5893 and RFC 5894.
// UTS #46 is defined in http://www.unicode.org/reports/tr46. // UTS #46 is defined in https://www.unicode.org/reports/tr46.
// See http://unicode.org/cldr/utility/idna.jsp for a visualization of the // See https://unicode.org/cldr/utility/idna.jsp for a visualization of the
// differences between these two standards. // differences between these two standards.
package idna // import "golang.org/x/net/idna" package idna // import "golang.org/x/net/idna"
@@ -56,23 +58,22 @@ type Option func(*options)
// Transitional sets a Profile to use the Transitional mapping as defined in UTS // Transitional sets a Profile to use the Transitional mapping as defined in UTS
// #46. This will cause, for example, "ß" to be mapped to "ss". Using the // #46. This will cause, for example, "ß" to be mapped to "ss". Using the
// transitional mapping provides a compromise between IDNA2003 and IDNA2008 // transitional mapping provides a compromise between IDNA2003 and IDNA2008
// compatibility. It is used by most browsers when resolving domain names. This // compatibility. It is used by some browsers when resolving domain names. This
// option is only meaningful if combined with MapForLookup. // option is only meaningful if combined with MapForLookup.
func Transitional(transitional bool) Option { func Transitional(transitional bool) Option {
return func(o *options) { o.transitional = true } return func(o *options) { o.transitional = transitional }
} }
// VerifyDNSLength sets whether a Profile should fail if any of the IDN parts // VerifyDNSLength sets whether a Profile should fail if any of the IDN parts
// are longer than allowed by the RFC. // are longer than allowed by the RFC.
//
// This option corresponds to the VerifyDnsLength flag in UTS #46.
func VerifyDNSLength(verify bool) Option { func VerifyDNSLength(verify bool) Option {
return func(o *options) { o.verifyDNSLength = verify } return func(o *options) { o.verifyDNSLength = verify }
} }
// RemoveLeadingDots removes leading label separators. Leading runes that map to // RemoveLeadingDots removes leading label separators. Leading runes that map to
// dots, such as U+3002 IDEOGRAPHIC FULL STOP, are removed as well. // dots, such as U+3002 IDEOGRAPHIC FULL STOP, are removed as well.
//
// This is the behavior suggested by the UTS #46 and is adopted by some
// browsers.
func RemoveLeadingDots(remove bool) Option { func RemoveLeadingDots(remove bool) Option {
return func(o *options) { o.removeLeadingDots = remove } return func(o *options) { o.removeLeadingDots = remove }
} }
@@ -80,6 +81,8 @@ func RemoveLeadingDots(remove bool) Option {
// ValidateLabels sets whether to check the mandatory label validation criteria // ValidateLabels sets whether to check the mandatory label validation criteria
// as defined in Section 5.4 of RFC 5891. This includes testing for correct use // as defined in Section 5.4 of RFC 5891. This includes testing for correct use
// of hyphens ('-'), normalization, validity of runes, and the context rules. // of hyphens ('-'), normalization, validity of runes, and the context rules.
// In particular, ValidateLabels also sets the CheckHyphens and CheckJoiners flags
// in UTS #46.
func ValidateLabels(enable bool) Option { func ValidateLabels(enable bool) Option {
return func(o *options) { return func(o *options) {
// Don't override existing mappings, but set one that at least checks // Don't override existing mappings, but set one that at least checks
@@ -88,25 +91,48 @@ func ValidateLabels(enable bool) Option {
o.mapping = normalize o.mapping = normalize
} }
o.trie = trie o.trie = trie
o.validateLabels = enable o.checkJoiners = enable
o.fromPuny = validateFromPunycode o.checkHyphens = enable
if enable {
o.fromPuny = validateFromPunycode
} else {
o.fromPuny = nil
}
}
}
// CheckHyphens sets whether to check for correct use of hyphens ('-') in
// labels. Most web browsers do not have this option set, since labels such as
// "r3---sn-apo3qvuoxuxbt-j5pe" are in common use.
//
// This option corresponds to the CheckHyphens flag in UTS #46.
func CheckHyphens(enable bool) Option {
return func(o *options) { o.checkHyphens = enable }
}
// CheckJoiners sets whether to check the ContextJ rules as defined in Appendix
// A of RFC 5892, concerning the use of joiner runes.
//
// This option corresponds to the CheckJoiners flag in UTS #46.
func CheckJoiners(enable bool) Option {
return func(o *options) {
o.trie = trie
o.checkJoiners = enable
} }
} }
// StrictDomainName limits the set of permissible ASCII characters to those // StrictDomainName limits the set of permissible ASCII characters to those
// allowed in domain names as defined in RFC 1034 (A-Z, a-z, 0-9 and the // allowed in domain names as defined in RFC 1034 (A-Z, a-z, 0-9 and the
// hyphen). This is set by default for MapForLookup and ValidateForRegistration. // hyphen). This is set by default for MapForLookup and ValidateForRegistration,
// but is only useful if ValidateLabels is set.
// //
// This option is useful, for instance, for browsers that allow characters // This option is useful, for instance, for browsers that allow characters
// outside this range, for example a '_' (U+005F LOW LINE). See // outside this range, for example a '_' (U+005F LOW LINE). See
// http://www.rfc-editor.org/std/std3.txt for more details This option // http://www.rfc-editor.org/std/std3.txt for more details.
// corresponds to the UseSTD3ASCIIRules option in UTS #46. //
// This option corresponds to the UseSTD3ASCIIRules flag in UTS #46.
func StrictDomainName(use bool) Option { func StrictDomainName(use bool) Option {
return func(o *options) { return func(o *options) { o.useSTD3Rules = use }
o.trie = trie
o.useSTD3Rules = use
o.fromPuny = validateFromPunycode
}
} }
// NOTE: the following options pull in tables. The tables should not be linked // NOTE: the following options pull in tables. The tables should not be linked
@@ -114,6 +140,8 @@ func StrictDomainName(use bool) Option {
// BidiRule enables the Bidi rule as defined in RFC 5893. Any application // BidiRule enables the Bidi rule as defined in RFC 5893. Any application
// that relies on proper validation of labels should include this rule. // that relies on proper validation of labels should include this rule.
//
// This option corresponds to the CheckBidi flag in UTS #46.
func BidiRule() Option { func BidiRule() Option {
return func(o *options) { o.bidirule = bidirule.ValidString } return func(o *options) { o.bidirule = bidirule.ValidString }
} }
@@ -149,7 +177,8 @@ func MapForLookup() Option {
type options struct { type options struct {
transitional bool transitional bool
useSTD3Rules bool useSTD3Rules bool
validateLabels bool checkHyphens bool
checkJoiners bool
verifyDNSLength bool verifyDNSLength bool
removeLeadingDots bool removeLeadingDots bool
@@ -222,8 +251,11 @@ func (p *Profile) String() string {
if p.useSTD3Rules { if p.useSTD3Rules {
s += ":UseSTD3Rules" s += ":UseSTD3Rules"
} }
if p.validateLabels { if p.checkHyphens {
s += ":ValidateLabels" s += ":CheckHyphens"
}
if p.checkJoiners {
s += ":CheckJoiners"
} }
if p.verifyDNSLength { if p.verifyDNSLength {
s += ":VerifyDNSLength" s += ":VerifyDNSLength"
@@ -251,26 +283,29 @@ var (
punycode = &Profile{} punycode = &Profile{}
lookup = &Profile{options{ lookup = &Profile{options{
transitional: true, transitional: transitionalLookup,
useSTD3Rules: true, useSTD3Rules: true,
validateLabels: true, checkHyphens: true,
trie: trie, checkJoiners: true,
fromPuny: validateFromPunycode, trie: trie,
mapping: validateAndMap, fromPuny: validateFromPunycode,
bidirule: bidirule.ValidString, mapping: validateAndMap,
bidirule: bidirule.ValidString,
}} }}
display = &Profile{options{ display = &Profile{options{
useSTD3Rules: true, useSTD3Rules: true,
validateLabels: true, checkHyphens: true,
trie: trie, checkJoiners: true,
fromPuny: validateFromPunycode, trie: trie,
mapping: validateAndMap, fromPuny: validateFromPunycode,
bidirule: bidirule.ValidString, mapping: validateAndMap,
bidirule: bidirule.ValidString,
}} }}
registration = &Profile{options{ registration = &Profile{options{
useSTD3Rules: true, useSTD3Rules: true,
validateLabels: true,
verifyDNSLength: true, verifyDNSLength: true,
checkHyphens: true,
checkJoiners: true,
trie: trie, trie: trie,
fromPuny: validateFromPunycode, fromPuny: validateFromPunycode,
mapping: validateRegistration, mapping: validateRegistration,
@@ -297,7 +332,7 @@ func (e runeError) Error() string {
} }
// process implements the algorithm described in section 4 of UTS #46, // process implements the algorithm described in section 4 of UTS #46,
// see http://www.unicode.org/reports/tr46. // see https://www.unicode.org/reports/tr46.
func (p *Profile) process(s string, toASCII bool) (string, error) { func (p *Profile) process(s string, toASCII bool) (string, error) {
var err error var err error
var isBidi bool var isBidi bool
@@ -337,7 +372,7 @@ func (p *Profile) process(s string, toASCII bool) (string, error) {
} }
isBidi = isBidi || bidirule.DirectionString(u) != bidi.LeftToRight isBidi = isBidi || bidirule.DirectionString(u) != bidi.LeftToRight
labels.set(u) labels.set(u)
if err == nil && p.validateLabels { if err == nil && p.fromPuny != nil {
err = p.fromPuny(p, u) err = p.fromPuny(p, u)
} }
if err == nil { if err == nil {
@@ -678,16 +713,18 @@ func (p *Profile) validateLabel(s string) (err error) {
} }
return nil return nil
} }
if !p.validateLabels { if p.checkHyphens {
if len(s) > 4 && s[2] == '-' && s[3] == '-' {
return &labelError{s, "V2"}
}
if s[0] == '-' || s[len(s)-1] == '-' {
return &labelError{s, "V3"}
}
}
if !p.checkJoiners {
return nil return nil
} }
trie := p.trie // p.validateLabels is only set if trie is set. trie := p.trie // p.checkJoiners is only set if trie is set.
if len(s) > 4 && s[2] == '-' && s[3] == '-' {
return &labelError{s, "V2"}
}
if s[0] == '-' || s[len(s)-1] == '-' {
return &labelError{s, "V3"}
}
// TODO: merge the use of this in the trie. // TODO: merge the use of this in the trie.
v, sz := trie.lookupString(s) v, sz := trie.lookupString(s)
x := info(v) x := info(v)
+717
View File
@@ -0,0 +1,717 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
// Copyright 2016 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.
//go:build !go1.10
// Package idna implements IDNA2008 using the compatibility processing
// defined by UTS (Unicode Technical Standard) #46, which defines a standard to
// deal with the transition from IDNA2003.
//
// IDNA2008 (Internationalized Domain Names for Applications), is defined in RFC
// 5890, RFC 5891, RFC 5892, RFC 5893 and RFC 5894.
// UTS #46 is defined in https://www.unicode.org/reports/tr46.
// See https://unicode.org/cldr/utility/idna.jsp for a visualization of the
// differences between these two standards.
package idna // import "golang.org/x/net/idna"
import (
"fmt"
"strings"
"unicode/utf8"
"golang.org/x/text/secure/bidirule"
"golang.org/x/text/unicode/norm"
)
// NOTE: Unlike common practice in Go APIs, the functions will return a
// sanitized domain name in case of errors. Browsers sometimes use a partially
// evaluated string as lookup.
// TODO: the current error handling is, in my opinion, the least opinionated.
// Other strategies are also viable, though:
// Option 1) Return an empty string in case of error, but allow the user to
// specify explicitly which errors to ignore.
// Option 2) Return the partially evaluated string if it is itself a valid
// string, otherwise return the empty string in case of error.
// Option 3) Option 1 and 2.
// Option 4) Always return an empty string for now and implement Option 1 as
// needed, and document that the return string may not be empty in case of
// error in the future.
// I think Option 1 is best, but it is quite opinionated.
// ToASCII is a wrapper for Punycode.ToASCII.
func ToASCII(s string) (string, error) {
return Punycode.process(s, true)
}
// ToUnicode is a wrapper for Punycode.ToUnicode.
func ToUnicode(s string) (string, error) {
return Punycode.process(s, false)
}
// An Option configures a Profile at creation time.
type Option func(*options)
// Transitional sets a Profile to use the Transitional mapping as defined in UTS
// #46. This will cause, for example, "ß" to be mapped to "ss". Using the
// transitional mapping provides a compromise between IDNA2003 and IDNA2008
// compatibility. It is used by some browsers when resolving domain names. This
// option is only meaningful if combined with MapForLookup.
func Transitional(transitional bool) Option {
return func(o *options) { o.transitional = transitional }
}
// VerifyDNSLength sets whether a Profile should fail if any of the IDN parts
// are longer than allowed by the RFC.
//
// This option corresponds to the VerifyDnsLength flag in UTS #46.
func VerifyDNSLength(verify bool) Option {
return func(o *options) { o.verifyDNSLength = verify }
}
// RemoveLeadingDots removes leading label separators. Leading runes that map to
// dots, such as U+3002 IDEOGRAPHIC FULL STOP, are removed as well.
func RemoveLeadingDots(remove bool) Option {
return func(o *options) { o.removeLeadingDots = remove }
}
// ValidateLabels sets whether to check the mandatory label validation criteria
// as defined in Section 5.4 of RFC 5891. This includes testing for correct use
// of hyphens ('-'), normalization, validity of runes, and the context rules.
// In particular, ValidateLabels also sets the CheckHyphens and CheckJoiners flags
// in UTS #46.
func ValidateLabels(enable bool) Option {
return func(o *options) {
// Don't override existing mappings, but set one that at least checks
// normalization if it is not set.
if o.mapping == nil && enable {
o.mapping = normalize
}
o.trie = trie
o.checkJoiners = enable
o.checkHyphens = enable
if enable {
o.fromPuny = validateFromPunycode
} else {
o.fromPuny = nil
}
}
}
// CheckHyphens sets whether to check for correct use of hyphens ('-') in
// labels. Most web browsers do not have this option set, since labels such as
// "r3---sn-apo3qvuoxuxbt-j5pe" are in common use.
//
// This option corresponds to the CheckHyphens flag in UTS #46.
func CheckHyphens(enable bool) Option {
return func(o *options) { o.checkHyphens = enable }
}
// CheckJoiners sets whether to check the ContextJ rules as defined in Appendix
// A of RFC 5892, concerning the use of joiner runes.
//
// This option corresponds to the CheckJoiners flag in UTS #46.
func CheckJoiners(enable bool) Option {
return func(o *options) {
o.trie = trie
o.checkJoiners = enable
}
}
// StrictDomainName limits the set of permissible ASCII characters to those
// allowed in domain names as defined in RFC 1034 (A-Z, a-z, 0-9 and the
// hyphen). This is set by default for MapForLookup and ValidateForRegistration,
// but is only useful if ValidateLabels is set.
//
// This option is useful, for instance, for browsers that allow characters
// outside this range, for example a '_' (U+005F LOW LINE). See
// http://www.rfc-editor.org/std/std3.txt for more details.
//
// This option corresponds to the UseSTD3ASCIIRules flag in UTS #46.
func StrictDomainName(use bool) Option {
return func(o *options) { o.useSTD3Rules = use }
}
// NOTE: the following options pull in tables. The tables should not be linked
// in as long as the options are not used.
// BidiRule enables the Bidi rule as defined in RFC 5893. Any application
// that relies on proper validation of labels should include this rule.
//
// This option corresponds to the CheckBidi flag in UTS #46.
func BidiRule() Option {
return func(o *options) { o.bidirule = bidirule.ValidString }
}
// ValidateForRegistration sets validation options to verify that a given IDN is
// properly formatted for registration as defined by Section 4 of RFC 5891.
func ValidateForRegistration() Option {
return func(o *options) {
o.mapping = validateRegistration
StrictDomainName(true)(o)
ValidateLabels(true)(o)
VerifyDNSLength(true)(o)
BidiRule()(o)
}
}
// MapForLookup sets validation and mapping options such that a given IDN is
// transformed for domain name lookup according to the requirements set out in
// Section 5 of RFC 5891. The mappings follow the recommendations of RFC 5894,
// RFC 5895 and UTS 46. It does not add the Bidi Rule. Use the BidiRule option
// to add this check.
//
// The mappings include normalization and mapping case, width and other
// compatibility mappings.
func MapForLookup() Option {
return func(o *options) {
o.mapping = validateAndMap
StrictDomainName(true)(o)
ValidateLabels(true)(o)
RemoveLeadingDots(true)(o)
}
}
type options struct {
transitional bool
useSTD3Rules bool
checkHyphens bool
checkJoiners bool
verifyDNSLength bool
removeLeadingDots bool
trie *idnaTrie
// fromPuny calls validation rules when converting A-labels to U-labels.
fromPuny func(p *Profile, s string) error
// mapping implements a validation and mapping step as defined in RFC 5895
// or UTS 46, tailored to, for example, domain registration or lookup.
mapping func(p *Profile, s string) (string, error)
// bidirule, if specified, checks whether s conforms to the Bidi Rule
// defined in RFC 5893.
bidirule func(s string) bool
}
// A Profile defines the configuration of a IDNA mapper.
type Profile struct {
options
}
func apply(o *options, opts []Option) {
for _, f := range opts {
f(o)
}
}
// New creates a new Profile.
//
// With no options, the returned Profile is the most permissive and equals the
// Punycode Profile. Options can be passed to further restrict the Profile. The
// MapForLookup and ValidateForRegistration options set a collection of options,
// for lookup and registration purposes respectively, which can be tailored by
// adding more fine-grained options, where later options override earlier
// options.
func New(o ...Option) *Profile {
p := &Profile{}
apply(&p.options, o)
return p
}
// ToASCII converts a domain or domain label to its ASCII form. For example,
// ToASCII("bücher.example.com") is "xn--bcher-kva.example.com", and
// ToASCII("golang") is "golang". If an error is encountered it will return
// an error and a (partially) processed result.
func (p *Profile) ToASCII(s string) (string, error) {
return p.process(s, true)
}
// ToUnicode converts a domain or domain label to its Unicode form. For example,
// ToUnicode("xn--bcher-kva.example.com") is "bücher.example.com", and
// ToUnicode("golang") is "golang". If an error is encountered it will return
// an error and a (partially) processed result.
func (p *Profile) ToUnicode(s string) (string, error) {
pp := *p
pp.transitional = false
return pp.process(s, false)
}
// String reports a string with a description of the profile for debugging
// purposes. The string format may change with different versions.
func (p *Profile) String() string {
s := ""
if p.transitional {
s = "Transitional"
} else {
s = "NonTransitional"
}
if p.useSTD3Rules {
s += ":UseSTD3Rules"
}
if p.checkHyphens {
s += ":CheckHyphens"
}
if p.checkJoiners {
s += ":CheckJoiners"
}
if p.verifyDNSLength {
s += ":VerifyDNSLength"
}
return s
}
var (
// Punycode is a Profile that does raw punycode processing with a minimum
// of validation.
Punycode *Profile = punycode
// Lookup is the recommended profile for looking up domain names, according
// to Section 5 of RFC 5891. The exact configuration of this profile may
// change over time.
Lookup *Profile = lookup
// Display is the recommended profile for displaying domain names.
// The configuration of this profile may change over time.
Display *Profile = display
// Registration is the recommended profile for checking whether a given
// IDN is valid for registration, according to Section 4 of RFC 5891.
Registration *Profile = registration
punycode = &Profile{}
lookup = &Profile{options{
transitional: true,
removeLeadingDots: true,
useSTD3Rules: true,
checkHyphens: true,
checkJoiners: true,
trie: trie,
fromPuny: validateFromPunycode,
mapping: validateAndMap,
bidirule: bidirule.ValidString,
}}
display = &Profile{options{
useSTD3Rules: true,
removeLeadingDots: true,
checkHyphens: true,
checkJoiners: true,
trie: trie,
fromPuny: validateFromPunycode,
mapping: validateAndMap,
bidirule: bidirule.ValidString,
}}
registration = &Profile{options{
useSTD3Rules: true,
verifyDNSLength: true,
checkHyphens: true,
checkJoiners: true,
trie: trie,
fromPuny: validateFromPunycode,
mapping: validateRegistration,
bidirule: bidirule.ValidString,
}}
// TODO: profiles
// Register: recommended for approving domain names: don't do any mappings
// but rather reject on invalid input. Bundle or block deviation characters.
)
type labelError struct{ label, code_ string }
func (e labelError) code() string { return e.code_ }
func (e labelError) Error() string {
return fmt.Sprintf("idna: invalid label %q", e.label)
}
type runeError rune
func (e runeError) code() string { return "P1" }
func (e runeError) Error() string {
return fmt.Sprintf("idna: disallowed rune %U", e)
}
// process implements the algorithm described in section 4 of UTS #46,
// see https://www.unicode.org/reports/tr46.
func (p *Profile) process(s string, toASCII bool) (string, error) {
var err error
if p.mapping != nil {
s, err = p.mapping(p, s)
}
// Remove leading empty labels.
if p.removeLeadingDots {
for ; len(s) > 0 && s[0] == '.'; s = s[1:] {
}
}
// It seems like we should only create this error on ToASCII, but the
// UTS 46 conformance tests suggests we should always check this.
if err == nil && p.verifyDNSLength && s == "" {
err = &labelError{s, "A4"}
}
labels := labelIter{orig: s}
for ; !labels.done(); labels.next() {
label := labels.label()
if label == "" {
// Empty labels are not okay. The label iterator skips the last
// label if it is empty.
if err == nil && p.verifyDNSLength {
err = &labelError{s, "A4"}
}
continue
}
if strings.HasPrefix(label, acePrefix) {
u, err2 := decode(label[len(acePrefix):])
if err2 != nil {
if err == nil {
err = err2
}
// Spec says keep the old label.
continue
}
labels.set(u)
if err == nil && p.fromPuny != nil {
err = p.fromPuny(p, u)
}
if err == nil {
// This should be called on NonTransitional, according to the
// spec, but that currently does not have any effect. Use the
// original profile to preserve options.
err = p.validateLabel(u)
}
} else if err == nil {
err = p.validateLabel(label)
}
}
if toASCII {
for labels.reset(); !labels.done(); labels.next() {
label := labels.label()
if !ascii(label) {
a, err2 := encode(acePrefix, label)
if err == nil {
err = err2
}
label = a
labels.set(a)
}
n := len(label)
if p.verifyDNSLength && err == nil && (n == 0 || n > 63) {
err = &labelError{label, "A4"}
}
}
}
s = labels.result()
if toASCII && p.verifyDNSLength && err == nil {
// Compute the length of the domain name minus the root label and its dot.
n := len(s)
if n > 0 && s[n-1] == '.' {
n--
}
if len(s) < 1 || n > 253 {
err = &labelError{s, "A4"}
}
}
return s, err
}
func normalize(p *Profile, s string) (string, error) {
return norm.NFC.String(s), nil
}
func validateRegistration(p *Profile, s string) (string, error) {
if !norm.NFC.IsNormalString(s) {
return s, &labelError{s, "V1"}
}
for i := 0; i < len(s); {
v, sz := trie.lookupString(s[i:])
// Copy bytes not copied so far.
switch p.simplify(info(v).category()) {
// TODO: handle the NV8 defined in the Unicode idna data set to allow
// for strict conformance to IDNA2008.
case valid, deviation:
case disallowed, mapped, unknown, ignored:
r, _ := utf8.DecodeRuneInString(s[i:])
return s, runeError(r)
}
i += sz
}
return s, nil
}
func validateAndMap(p *Profile, s string) (string, error) {
var (
err error
b []byte
k int
)
for i := 0; i < len(s); {
v, sz := trie.lookupString(s[i:])
start := i
i += sz
// Copy bytes not copied so far.
switch p.simplify(info(v).category()) {
case valid:
continue
case disallowed:
if err == nil {
r, _ := utf8.DecodeRuneInString(s[start:])
err = runeError(r)
}
continue
case mapped, deviation:
b = append(b, s[k:start]...)
b = info(v).appendMapping(b, s[start:i])
case ignored:
b = append(b, s[k:start]...)
// drop the rune
case unknown:
b = append(b, s[k:start]...)
b = append(b, "\ufffd"...)
}
k = i
}
if k == 0 {
// No changes so far.
s = norm.NFC.String(s)
} else {
b = append(b, s[k:]...)
if norm.NFC.QuickSpan(b) != len(b) {
b = norm.NFC.Bytes(b)
}
// TODO: the punycode converters require strings as input.
s = string(b)
}
return s, err
}
// A labelIter allows iterating over domain name labels.
type labelIter struct {
orig string
slice []string
curStart int
curEnd int
i int
}
func (l *labelIter) reset() {
l.curStart = 0
l.curEnd = 0
l.i = 0
}
func (l *labelIter) done() bool {
return l.curStart >= len(l.orig)
}
func (l *labelIter) result() string {
if l.slice != nil {
return strings.Join(l.slice, ".")
}
return l.orig
}
func (l *labelIter) label() string {
if l.slice != nil {
return l.slice[l.i]
}
p := strings.IndexByte(l.orig[l.curStart:], '.')
l.curEnd = l.curStart + p
if p == -1 {
l.curEnd = len(l.orig)
}
return l.orig[l.curStart:l.curEnd]
}
// next sets the value to the next label. It skips the last label if it is empty.
func (l *labelIter) next() {
l.i++
if l.slice != nil {
if l.i >= len(l.slice) || l.i == len(l.slice)-1 && l.slice[l.i] == "" {
l.curStart = len(l.orig)
}
} else {
l.curStart = l.curEnd + 1
if l.curStart == len(l.orig)-1 && l.orig[l.curStart] == '.' {
l.curStart = len(l.orig)
}
}
}
func (l *labelIter) set(s string) {
if l.slice == nil {
l.slice = strings.Split(l.orig, ".")
}
l.slice[l.i] = s
}
// acePrefix is the ASCII Compatible Encoding prefix.
const acePrefix = "xn--"
func (p *Profile) simplify(cat category) category {
switch cat {
case disallowedSTD3Mapped:
if p.useSTD3Rules {
cat = disallowed
} else {
cat = mapped
}
case disallowedSTD3Valid:
if p.useSTD3Rules {
cat = disallowed
} else {
cat = valid
}
case deviation:
if !p.transitional {
cat = valid
}
case validNV8, validXV8:
// TODO: handle V2008
cat = valid
}
return cat
}
func validateFromPunycode(p *Profile, s string) error {
if !norm.NFC.IsNormalString(s) {
return &labelError{s, "V1"}
}
for i := 0; i < len(s); {
v, sz := trie.lookupString(s[i:])
if c := p.simplify(info(v).category()); c != valid && c != deviation {
return &labelError{s, "V6"}
}
i += sz
}
return nil
}
const (
zwnj = "\u200c"
zwj = "\u200d"
)
type joinState int8
const (
stateStart joinState = iota
stateVirama
stateBefore
stateBeforeVirama
stateAfter
stateFAIL
)
var joinStates = [][numJoinTypes]joinState{
stateStart: {
joiningL: stateBefore,
joiningD: stateBefore,
joinZWNJ: stateFAIL,
joinZWJ: stateFAIL,
joinVirama: stateVirama,
},
stateVirama: {
joiningL: stateBefore,
joiningD: stateBefore,
},
stateBefore: {
joiningL: stateBefore,
joiningD: stateBefore,
joiningT: stateBefore,
joinZWNJ: stateAfter,
joinZWJ: stateFAIL,
joinVirama: stateBeforeVirama,
},
stateBeforeVirama: {
joiningL: stateBefore,
joiningD: stateBefore,
joiningT: stateBefore,
},
stateAfter: {
joiningL: stateFAIL,
joiningD: stateBefore,
joiningT: stateAfter,
joiningR: stateStart,
joinZWNJ: stateFAIL,
joinZWJ: stateFAIL,
joinVirama: stateAfter, // no-op as we can't accept joiners here
},
stateFAIL: {
0: stateFAIL,
joiningL: stateFAIL,
joiningD: stateFAIL,
joiningT: stateFAIL,
joiningR: stateFAIL,
joinZWNJ: stateFAIL,
joinZWJ: stateFAIL,
joinVirama: stateFAIL,
},
}
// validateLabel validates the criteria from Section 4.1. Item 1, 4, and 6 are
// already implicitly satisfied by the overall implementation.
func (p *Profile) validateLabel(s string) error {
if s == "" {
if p.verifyDNSLength {
return &labelError{s, "A4"}
}
return nil
}
if p.bidirule != nil && !p.bidirule(s) {
return &labelError{s, "B"}
}
if p.checkHyphens {
if len(s) > 4 && s[2] == '-' && s[3] == '-' {
return &labelError{s, "V2"}
}
if s[0] == '-' || s[len(s)-1] == '-' {
return &labelError{s, "V3"}
}
}
if !p.checkJoiners {
return nil
}
trie := p.trie // p.checkJoiners is only set if trie is set.
// TODO: merge the use of this in the trie.
v, sz := trie.lookupString(s)
x := info(v)
if x.isModifier() {
return &labelError{s, "V5"}
}
// Quickly return in the absence of zero-width (non) joiners.
if strings.Index(s, zwj) == -1 && strings.Index(s, zwnj) == -1 {
return nil
}
st := stateStart
for i := 0; ; {
jt := x.joinType()
if s[i:i+sz] == zwj {
jt = joinZWJ
} else if s[i:i+sz] == zwnj {
jt = joinZWNJ
}
st = joinStates[st][jt]
if x.isViramaModifier() {
st = joinStates[st][joinVirama]
}
if i += sz; i == len(s) {
break
}
v, sz = trie.lookupString(s[i:])
x = info(v)
}
if st == stateFAIL || st == stateAfter {
return &labelError{s, "C"}
}
return nil
}
func ascii(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] >= utf8.RuneSelf {
return false
}
}
return true
}
+11
View File
@@ -0,0 +1,11 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
// Copyright 2021 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.
//go:build !go1.18
package idna
const transitionalLookup = true
+25 -11
View File
@@ -49,6 +49,7 @@ func decode(encoded string) (string, error) {
} }
} }
i, n, bias := int32(0), initialN, initialBias i, n, bias := int32(0), initialN, initialBias
overflow := false
for pos < len(encoded) { for pos < len(encoded) {
oldI, w := i, int32(1) oldI, w := i, int32(1)
for k := base; ; k += base { for k := base; ; k += base {
@@ -60,29 +61,32 @@ func decode(encoded string) (string, error) {
return "", punyError(encoded) return "", punyError(encoded)
} }
pos++ pos++
i += digit * w i, overflow = madd(i, digit, w)
if i < 0 { if overflow {
return "", punyError(encoded) return "", punyError(encoded)
} }
t := k - bias t := k - bias
if t < tmin { if k <= bias {
t = tmin t = tmin
} else if t > tmax { } else if k >= bias+tmax {
t = tmax t = tmax
} }
if digit < t { if digit < t {
break break
} }
w *= base - t w, overflow = madd(0, w, base-t)
if w >= math.MaxInt32/base { if overflow {
return "", punyError(encoded) return "", punyError(encoded)
} }
} }
if len(output) >= 1024 {
return "", punyError(encoded)
}
x := int32(len(output) + 1) x := int32(len(output) + 1)
bias = adapt(i-oldI, x, oldI == 0) bias = adapt(i-oldI, x, oldI == 0)
n += i / x n += i / x
i %= x i %= x
if n > utf8.MaxRune || len(output) >= 1024 { if n < 0 || n > utf8.MaxRune {
return "", punyError(encoded) return "", punyError(encoded)
} }
output = append(output, 0) output = append(output, 0)
@@ -115,6 +119,7 @@ func encode(prefix, s string) (string, error) {
if b > 0 { if b > 0 {
output = append(output, '-') output = append(output, '-')
} }
overflow := false
for remaining != 0 { for remaining != 0 {
m := int32(0x7fffffff) m := int32(0x7fffffff)
for _, r := range s { for _, r := range s {
@@ -122,8 +127,8 @@ func encode(prefix, s string) (string, error) {
m = r m = r
} }
} }
delta += (m - n) * (h + 1) delta, overflow = madd(delta, m-n, h+1)
if delta < 0 { if overflow {
return "", punyError(s) return "", punyError(s)
} }
n = m n = m
@@ -141,9 +146,9 @@ func encode(prefix, s string) (string, error) {
q := delta q := delta
for k := base; ; k += base { for k := base; ; k += base {
t := k - bias t := k - bias
if t < tmin { if k <= bias {
t = tmin t = tmin
} else if t > tmax { } else if k >= bias+tmax {
t = tmax t = tmax
} }
if q < t { if q < t {
@@ -164,6 +169,15 @@ func encode(prefix, s string) (string, error) {
return string(output), nil return string(output), nil
} }
// madd computes a + (b * c), detecting overflow.
func madd(a, b, c int32) (next int32, overflow bool) {
p := int64(b) * int64(c)
if p > math.MaxInt32-int64(a) {
return 0, true
}
return a + int32(p), false
}
func decodeDigit(x byte) (digit int32, ok bool) { func decodeDigit(x byte) (digit int32, ok bool) {
switch { switch {
case '0' <= x && x <= '9': case '0' <= x && x <= '9':
@@ -1,11 +1,13 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
//go:build go1.10 && !go1.13
package idna package idna
// UnicodeVersion is the Unicode version from which the tables in this package are derived. // UnicodeVersion is the Unicode version from which the tables in this package are derived.
const UnicodeVersion = "10.0.0" const UnicodeVersion = "10.0.0"
var mappings string = "" + // Size: 8176 bytes var mappings string = "" + // Size: 8175 bytes
"\x00\x01 \x03 ̈\x01a\x03 ̄\x012\x013\x03 ́\x03 ̧\x011\x01o\x051⁄4\x051⁄2" + "\x00\x01 \x03 ̈\x01a\x03 ̄\x012\x013\x03 ́\x03 ̧\x011\x01o\x051⁄4\x051⁄2" +
"\x053⁄4\x03i̇\x03l·\x03ʼn\x01s\x03dž\x03ⱥ\x03ⱦ\x01h\x01j\x01r\x01w\x01y" + "\x053⁄4\x03i̇\x03l·\x03ʼn\x01s\x03dž\x03ⱥ\x03ⱦ\x01h\x01j\x01r\x01w\x01y" +
"\x03 ̆\x03 ̇\x03 ̊\x03 ̨\x03 ̃\x03 ̋\x01l\x01x\x04̈́\x03 ι\x01;\x05 ̈́" + "\x03 ̆\x03 ̇\x03 ̊\x03 ̨\x03 ̃\x03 ̋\x01l\x01x\x04̈́\x03 ι\x01;\x05 ̈́" +
@@ -4554,4 +4556,4 @@ var idnaSparseValues = [1915]valueRange{
{value: 0x0040, lo: 0xb0, hi: 0xbf}, {value: 0x0040, lo: 0xb0, hi: 0xbf},
} }
// Total table size 42115 bytes (41KiB); checksum: F4A1FA4E // Total table size 42114 bytes (41KiB); checksum: 355A58A4
+4653
View File
File diff suppressed because it is too large Load Diff
+4733
View File
File diff suppressed because it is too large Load Diff
+4959
View File
File diff suppressed because it is too large Load Diff
+5144
View File
File diff suppressed because it is too large Load Diff
+4486
View File
File diff suppressed because it is too large Load Diff
-21
View File
@@ -6,27 +6,6 @@
package idna package idna
// appendMapping appends the mapping for the respective rune. isMapped must be
// true. A mapping is a categorization of a rune as defined in UTS #46.
func (c info) appendMapping(b []byte, s string) []byte {
index := int(c >> indexShift)
if c&xorBit == 0 {
s := mappings[index:]
return append(b, s[1:s[0]+1]...)
}
b = append(b, s...)
if c&inlineXOR == inlineXOR {
// TODO: support and handle two-byte inline masks
b[len(b)-1] ^= byte(index)
} else {
for p := len(b) - int(xorData[index]); p < len(b); p++ {
index++
b[p] ^= xorData[index]
}
}
return b
}
// Sparse block handling code. // Sparse block handling code.
type valueRange struct { type valueRange struct {
+30
View File
@@ -0,0 +1,30 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
// Copyright 2016 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.
//go:build !go1.16
package idna
// appendMapping appends the mapping for the respective rune. isMapped must be
// true. A mapping is a categorization of a rune as defined in UTS #46.
func (c info) appendMapping(b []byte, s string) []byte {
index := int(c >> indexShift)
if c&xorBit == 0 {
s := mappings[index:]
return append(b, s[1:s[0]+1]...)
}
b = append(b, s...)
if c&inlineXOR == inlineXOR {
// TODO: support and handle two-byte inline masks
b[len(b)-1] ^= byte(index)
} else {
for p := len(b) - int(xorData[index]); p < len(b); p++ {
index++
b[p] ^= xorData[index]
}
}
return b
}
+30
View File
@@ -0,0 +1,30 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
// Copyright 2016 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.
//go:build go1.16
package idna
// appendMapping appends the mapping for the respective rune. isMapped must be
// true. A mapping is a categorization of a rune as defined in UTS #46.
func (c info) appendMapping(b []byte, s string) []byte {
index := int(c >> indexShift)
if c&xorBit == 0 {
p := index
return append(b, mappings[mappingIndex[p]:mappingIndex[p+1]]...)
}
b = append(b, s...)
if c&inlineXOR == inlineXOR {
// TODO: support and handle two-byte inline masks
b[len(b)-1] ^= byte(index)
} else {
for p := len(b) - int(xorData[index]); p < len(b); p++ {
index++
b[p] ^= xorData[index]
}
}
return b
}
+17 -17
View File
@@ -17,23 +17,23 @@ package idna
// //
// The per-rune values have the following format: // The per-rune values have the following format:
// //
// if mapped { // if mapped {
// if inlinedXOR { // if inlinedXOR {
// 15..13 inline XOR marker // 15..13 inline XOR marker
// 12..11 unused // 12..11 unused
// 10..3 inline XOR mask // 10..3 inline XOR mask
// } else { // } else {
// 15..3 index into xor or mapping table // 15..3 index into xor or mapping table
// } // }
// } else { // } else {
// 15..14 unused // 15..14 unused
// 13 mayNeedNorm // 13 mayNeedNorm
// 12..11 attributes // 12..11 attributes
// 10..8 joining type // 10..8 joining type
// 7..3 category type // 7..3 category type
// } // }
// 2 use xor pattern // 2 use xor pattern
// 1..0 mapped category // 1..0 mapped category
// //
// See the definitions below for a more detailed description of the various // See the definitions below for a more detailed description of the various
// bits. // bits.
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2025 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 httpcommon
import "strings"
// The HTTP protocols are defined in terms of ASCII, not Unicode. This file
// contains helper functions which may use Unicode-aware functions which would
// otherwise be unsafe and could introduce vulnerabilities if used improperly.
// asciiEqualFold is strings.EqualFold, ASCII only. It reports whether s and t
// are equal, ASCII-case-insensitively.
func asciiEqualFold(s, t string) bool {
if len(s) != len(t) {
return false
}
for i := 0; i < len(s); i++ {
if lower(s[i]) != lower(t[i]) {
return false
}
}
return true
}
// lower returns the ASCII lowercase version of b.
func lower(b byte) byte {
if 'A' <= b && b <= 'Z' {
return b + ('a' - 'A')
}
return b
}
// isASCIIPrint returns whether s is ASCII and printable according to
// https://tools.ietf.org/html/rfc20#section-4.2.
func isASCIIPrint(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] < ' ' || s[i] > '~' {
return false
}
}
return true
}
// asciiToLower returns the lowercase version of s if s is ASCII and printable,
// and whether or not it was.
func asciiToLower(s string) (lower string, ok bool) {
if !isASCIIPrint(s) {
return "", false
}
return strings.ToLower(s), true
}
@@ -1,12 +1,11 @@
// Copyright 2014 The Go Authors. All rights reserved. // Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package http2 package httpcommon
import ( import (
"net/http" "net/textproto"
"strings"
"sync" "sync"
) )
@@ -28,7 +27,14 @@ func buildCommonHeaderMaps() {
"accept-language", "accept-language",
"accept-ranges", "accept-ranges",
"age", "age",
"access-control-allow-credentials",
"access-control-allow-headers",
"access-control-allow-methods",
"access-control-allow-origin", "access-control-allow-origin",
"access-control-expose-headers",
"access-control-max-age",
"access-control-request-headers",
"access-control-request-method",
"allow", "allow",
"authorization", "authorization",
"cache-control", "cache-control",
@@ -54,6 +60,7 @@ func buildCommonHeaderMaps() {
"link", "link",
"location", "location",
"max-forwards", "max-forwards",
"origin",
"proxy-authenticate", "proxy-authenticate",
"proxy-authorization", "proxy-authorization",
"range", "range",
@@ -69,20 +76,40 @@ func buildCommonHeaderMaps() {
"vary", "vary",
"via", "via",
"www-authenticate", "www-authenticate",
"x-forwarded-for",
"x-forwarded-proto",
} }
commonLowerHeader = make(map[string]string, len(common)) commonLowerHeader = make(map[string]string, len(common))
commonCanonHeader = make(map[string]string, len(common)) commonCanonHeader = make(map[string]string, len(common))
for _, v := range common { for _, v := range common {
chk := http.CanonicalHeaderKey(v) chk := textproto.CanonicalMIMEHeaderKey(v)
commonLowerHeader[chk] = v commonLowerHeader[chk] = v
commonCanonHeader[v] = chk commonCanonHeader[v] = chk
} }
} }
func lowerHeader(v string) string { // LowerHeader returns the lowercase form of a header name,
// used on the wire for HTTP/2 and HTTP/3 requests.
func LowerHeader(v string) (lower string, ascii bool) {
buildCommonHeaderMapsOnce() buildCommonHeaderMapsOnce()
if s, ok := commonLowerHeader[v]; ok { if s, ok := commonLowerHeader[v]; ok {
return s, true
}
return asciiToLower(v)
}
// CanonicalHeader canonicalizes a header name. (For example, "host" becomes "Host".)
func CanonicalHeader(v string) string {
buildCommonHeaderMapsOnce()
if s, ok := commonCanonHeader[v]; ok {
return s return s
} }
return strings.ToLower(v) return textproto.CanonicalMIMEHeaderKey(v)
}
// CachedCanonicalHeader returns the canonical form of a well-known header name.
func CachedCanonicalHeader(v string) (string, bool) {
buildCommonHeaderMapsOnce()
s, ok := commonCanonHeader[v]
return s, ok
} }
+467
View File
@@ -0,0 +1,467 @@
// Copyright 2025 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 httpcommon
import (
"context"
"errors"
"fmt"
"net/http/httptrace"
"net/textproto"
"net/url"
"sort"
"strconv"
"strings"
"golang.org/x/net/http/httpguts"
"golang.org/x/net/http2/hpack"
)
var (
ErrRequestHeaderListSize = errors.New("request header list larger than peer's advertised limit")
)
// Request is a subset of http.Request.
// It'd be simpler to pass an *http.Request, of course, but we can't depend on net/http
// without creating a dependency cycle.
type Request struct {
URL *url.URL
Method string
Host string
Header map[string][]string
Trailer map[string][]string
ActualContentLength int64 // 0 means 0, -1 means unknown
}
// EncodeHeadersParam is parameters to EncodeHeaders.
type EncodeHeadersParam struct {
Request Request
// AddGzipHeader indicates that an "accept-encoding: gzip" header should be
// added to the request.
AddGzipHeader bool
// PeerMaxHeaderListSize, when non-zero, is the peer's MAX_HEADER_LIST_SIZE setting.
PeerMaxHeaderListSize uint64
// DefaultUserAgent is the User-Agent header to send when the request
// neither contains a User-Agent nor disables it.
DefaultUserAgent string
}
// EncodeHeadersResult is the result of EncodeHeaders.
type EncodeHeadersResult struct {
HasBody bool
HasTrailers bool
}
// EncodeHeaders constructs request headers common to HTTP/2 and HTTP/3.
// It validates a request and calls headerf with each pseudo-header and header
// for the request.
// The headerf function is called with the validated, canonicalized header name.
func EncodeHeaders(ctx context.Context, param EncodeHeadersParam, headerf func(name, value string)) (res EncodeHeadersResult, _ error) {
req := param.Request
// Check for invalid connection-level headers.
if err := checkConnHeaders(req.Header); err != nil {
return res, err
}
if req.URL == nil {
return res, errors.New("Request.URL is nil")
}
host := req.Host
if host == "" {
host = req.URL.Host
}
host, err := httpguts.PunycodeHostPort(host)
if err != nil {
return res, err
}
if !httpguts.ValidHostHeader(host) {
return res, errors.New("invalid Host header")
}
// isNormalConnect is true if this is a non-extended CONNECT request.
isNormalConnect := false
var protocol string
if vv := req.Header[":protocol"]; len(vv) > 0 {
protocol = vv[0]
}
if req.Method == "CONNECT" && protocol == "" {
isNormalConnect = true
} else if protocol != "" && req.Method != "CONNECT" {
return res, errors.New("invalid :protocol header in non-CONNECT request")
}
// Validate the path, except for non-extended CONNECT requests which have no path.
var path string
if !isNormalConnect {
path = req.URL.RequestURI()
if !validPseudoPath(path) {
orig := path
path = strings.TrimPrefix(path, req.URL.Scheme+"://"+host)
if !validPseudoPath(path) {
if req.URL.Opaque != "" {
return res, fmt.Errorf("invalid request :path %q from URL.Opaque = %q", orig, req.URL.Opaque)
} else {
return res, fmt.Errorf("invalid request :path %q", orig)
}
}
}
}
// Check for any invalid headers+trailers and return an error before we
// potentially pollute our hpack state. (We want to be able to
// continue to reuse the hpack encoder for future requests)
if err := validateHeaders(req.Header); err != "" {
return res, fmt.Errorf("invalid HTTP header %s", err)
}
if err := validateHeaders(req.Trailer); err != "" {
return res, fmt.Errorf("invalid HTTP trailer %s", err)
}
trailers, err := commaSeparatedTrailers(req.Trailer)
if err != nil {
return res, err
}
enumerateHeaders := func(f func(name, value string)) {
// 8.1.2.3 Request Pseudo-Header Fields
// The :path pseudo-header field includes the path and query parts of the
// target URI (the path-absolute production and optionally a '?' character
// followed by the query production, see Sections 3.3 and 3.4 of
// [RFC3986]).
f(":authority", host)
m := req.Method
if m == "" {
m = "GET"
}
f(":method", m)
if !isNormalConnect {
f(":path", path)
f(":scheme", req.URL.Scheme)
}
if protocol != "" {
f(":protocol", protocol)
}
if trailers != "" {
f("trailer", trailers)
}
var didUA bool
for k, vv := range req.Header {
if asciiEqualFold(k, "host") || asciiEqualFold(k, "content-length") {
// Host is :authority, already sent.
// Content-Length is automatic, set below.
continue
} else if asciiEqualFold(k, "connection") ||
asciiEqualFold(k, "proxy-connection") ||
asciiEqualFold(k, "transfer-encoding") ||
asciiEqualFold(k, "upgrade") ||
asciiEqualFold(k, "keep-alive") {
// Per 8.1.2.2 Connection-Specific Header
// Fields, don't send connection-specific
// fields. We have already checked if any
// are error-worthy so just ignore the rest.
continue
} else if asciiEqualFold(k, "user-agent") {
// Match Go's http1 behavior: at most one
// User-Agent. If set to nil or empty string,
// then omit it. Otherwise if not mentioned,
// include the default (below).
didUA = true
if len(vv) < 1 {
continue
}
vv = vv[:1]
if vv[0] == "" {
continue
}
} else if asciiEqualFold(k, "cookie") {
// Per 8.1.2.5 To allow for better compression efficiency, the
// Cookie header field MAY be split into separate header fields,
// each with one or more cookie-pairs.
for _, v := range vv {
for {
p := strings.IndexByte(v, ';')
if p < 0 {
break
}
f("cookie", v[:p])
p++
// strip space after semicolon if any.
for p+1 <= len(v) && v[p] == ' ' {
p++
}
v = v[p:]
}
if len(v) > 0 {
f("cookie", v)
}
}
continue
} else if k == ":protocol" {
// :protocol pseudo-header was already sent above.
continue
}
for _, v := range vv {
f(k, v)
}
}
if shouldSendReqContentLength(req.Method, req.ActualContentLength) {
f("content-length", strconv.FormatInt(req.ActualContentLength, 10))
}
if param.AddGzipHeader {
f("accept-encoding", "gzip")
}
if !didUA {
f("user-agent", param.DefaultUserAgent)
}
}
// Do a first pass over the headers counting bytes to ensure
// we don't exceed cc.peerMaxHeaderListSize. This is done as a
// separate pass before encoding the headers to prevent
// modifying the hpack state.
if param.PeerMaxHeaderListSize > 0 {
hlSize := uint64(0)
enumerateHeaders(func(name, value string) {
hf := hpack.HeaderField{Name: name, Value: value}
hlSize += uint64(hf.Size())
})
if hlSize > param.PeerMaxHeaderListSize {
return res, ErrRequestHeaderListSize
}
}
trace := httptrace.ContextClientTrace(ctx)
// Header list size is ok. Write the headers.
enumerateHeaders(func(name, value string) {
name, ascii := LowerHeader(name)
if !ascii {
// Skip writing invalid headers. Per RFC 7540, Section 8.1.2, header
// field names have to be ASCII characters (just as in HTTP/1.x).
return
}
headerf(name, value)
if trace != nil && trace.WroteHeaderField != nil {
trace.WroteHeaderField(name, []string{value})
}
})
res.HasBody = req.ActualContentLength != 0
res.HasTrailers = trailers != ""
return res, nil
}
// IsRequestGzip reports whether we should add an Accept-Encoding: gzip header
// for a request.
func IsRequestGzip(method string, header map[string][]string, disableCompression bool) bool {
// TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere?
if !disableCompression &&
len(header["Accept-Encoding"]) == 0 &&
len(header["Range"]) == 0 &&
method != "HEAD" {
// Request gzip only, not deflate. Deflate is ambiguous and
// not as universally supported anyway.
// See: https://zlib.net/zlib_faq.html#faq39
//
// Note that we don't request this for HEAD requests,
// due to a bug in nginx:
// http://trac.nginx.org/nginx/ticket/358
// https://golang.org/issue/5522
//
// We don't request gzip if the request is for a range, since
// auto-decoding a portion of a gzipped document will just fail
// anyway. See https://golang.org/issue/8923
return true
}
return false
}
// checkConnHeaders checks whether req has any invalid connection-level headers.
//
// https://www.rfc-editor.org/rfc/rfc9114.html#section-4.2-3
// https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.2-1
//
// Certain headers are special-cased as okay but not transmitted later.
// For example, we allow "Transfer-Encoding: chunked", but drop the header when encoding.
func checkConnHeaders(h map[string][]string) error {
if vv := h["Upgrade"]; len(vv) > 0 && (vv[0] != "" && vv[0] != "chunked") {
return fmt.Errorf("invalid Upgrade request header: %q", vv)
}
if vv := h["Transfer-Encoding"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && vv[0] != "chunked") {
return fmt.Errorf("invalid Transfer-Encoding request header: %q", vv)
}
if vv := h["Connection"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && !asciiEqualFold(vv[0], "close") && !asciiEqualFold(vv[0], "keep-alive")) {
return fmt.Errorf("invalid Connection request header: %q", vv)
}
return nil
}
func commaSeparatedTrailers(trailer map[string][]string) (string, error) {
keys := make([]string, 0, len(trailer))
for k := range trailer {
k = CanonicalHeader(k)
switch k {
case "Transfer-Encoding", "Trailer", "Content-Length":
return "", fmt.Errorf("invalid Trailer key %q", k)
}
keys = append(keys, k)
}
if len(keys) > 0 {
sort.Strings(keys)
return strings.Join(keys, ","), nil
}
return "", nil
}
// validPseudoPath reports whether v is a valid :path pseudo-header
// value. It must be either:
//
// - a non-empty string starting with '/'
// - the string '*', for OPTIONS requests.
//
// For now this is only used a quick check for deciding when to clean
// up Opaque URLs before sending requests from the Transport.
// See golang.org/issue/16847
//
// We used to enforce that the path also didn't start with "//", but
// Google's GFE accepts such paths and Chrome sends them, so ignore
// that part of the spec. See golang.org/issue/19103.
func validPseudoPath(v string) bool {
return (len(v) > 0 && v[0] == '/') || v == "*"
}
func validateHeaders(hdrs map[string][]string) string {
for k, vv := range hdrs {
if !httpguts.ValidHeaderFieldName(k) && k != ":protocol" {
return fmt.Sprintf("name %q", k)
}
for _, v := range vv {
if !httpguts.ValidHeaderFieldValue(v) {
// Don't include the value in the error,
// because it may be sensitive.
return fmt.Sprintf("value for header %q", k)
}
}
}
return ""
}
// shouldSendReqContentLength reports whether we should send
// a "content-length" request header. This logic is basically a copy of the net/http
// transferWriter.shouldSendContentLength.
// The contentLength is the corrected contentLength (so 0 means actually 0, not unknown).
// -1 means unknown.
func shouldSendReqContentLength(method string, contentLength int64) bool {
if contentLength > 0 {
return true
}
if contentLength < 0 {
return false
}
// For zero bodies, whether we send a content-length depends on the method.
// It also kinda doesn't matter for http2 either way, with END_STREAM.
switch method {
case "POST", "PUT", "PATCH":
return true
default:
return false
}
}
// ServerRequestParam is parameters to NewServerRequest.
type ServerRequestParam struct {
Method string
Scheme, Authority, Path string
Protocol string
Header map[string][]string
}
// ServerRequestResult is the result of NewServerRequest.
type ServerRequestResult struct {
// Various http.Request fields.
URL *url.URL
RequestURI string
Trailer map[string][]string
NeedsContinue bool // client provided an "Expect: 100-continue" header
// If the request should be rejected, this is a short string suitable for passing
// to the http2 package's CountError function.
// It might be a bit odd to return errors this way rather than returning an error,
// but this ensures we don't forget to include a CountError reason.
InvalidReason string
}
func NewServerRequest(rp ServerRequestParam) ServerRequestResult {
needsContinue := httpguts.HeaderValuesContainsToken(rp.Header["Expect"], "100-continue")
if needsContinue {
delete(rp.Header, "Expect")
}
// Merge Cookie headers into one "; "-delimited value.
if cookies := rp.Header["Cookie"]; len(cookies) > 1 {
rp.Header["Cookie"] = []string{strings.Join(cookies, "; ")}
}
// Setup Trailers
var trailer map[string][]string
for _, v := range rp.Header["Trailer"] {
for _, key := range strings.Split(v, ",") {
key = textproto.CanonicalMIMEHeaderKey(textproto.TrimString(key))
switch key {
case "Transfer-Encoding", "Trailer", "Content-Length":
// Bogus. (copy of http1 rules)
// Ignore.
default:
if trailer == nil {
trailer = make(map[string][]string)
}
trailer[key] = nil
}
}
}
delete(rp.Header, "Trailer")
// "':authority' MUST NOT include the deprecated userinfo subcomponent
// for "http" or "https" schemed URIs."
// https://www.rfc-editor.org/rfc/rfc9113.html#section-8.3.1-2.3.8
if strings.IndexByte(rp.Authority, '@') != -1 && (rp.Scheme == "http" || rp.Scheme == "https") {
return ServerRequestResult{
InvalidReason: "userinfo_in_authority",
}
}
var url_ *url.URL
var requestURI string
if rp.Method == "CONNECT" && rp.Protocol == "" {
url_ = &url.URL{Host: rp.Authority}
requestURI = rp.Authority // mimic HTTP/1 server behavior
} else {
var err error
url_, err = url.ParseRequestURI(rp.Path)
if err != nil {
return ServerRequestResult{
InvalidReason: "bad_path",
}
}
requestURI = rp.Path
}
return ServerRequestResult{
URL: url_,
NeedsContinue: needsContinue,
RequestURI: requestURI,
Trailer: trailer,
}
}
-3
View File
@@ -1,3 +0,0 @@
# This source code refers to The Go Authors for copyright purposes.
# The master list of authors is in the main Go distribution,
# visible at http://tip.golang.org/AUTHORS.
-3
View File
@@ -1,3 +0,0 @@
# This source code was written by the Go contributors.
# The master list of contributors is in the main Go distribution,
# visible at http://tip.golang.org/CONTRIBUTORS.
+2 -2
View File
@@ -1,4 +1,4 @@
Copyright (c) 2009 The Go Authors. All rights reserved. Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are modification, are permitted provided that the following conditions are
@@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
copyright notice, this list of conditions and the following disclaimer copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the in the documentation and/or other materials provided with the
distribution. distribution.
* Neither the name of Google Inc. nor the names of its * Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from contributors may be used to endorse or promote products derived from
this software without specific prior written permission. this software without specific prior written permission.
+1 -1
View File
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build go1.10 //go:build go1.10
package bidirule package bidirule
+1 -1
View File
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build !go1.10 //go:build !go1.10
package bidirule package bidirule
+8 -4
View File
@@ -78,8 +78,8 @@ type SpanningTransformer interface {
// considering the error err. // considering the error err.
// //
// A nil error means that all input bytes are known to be identical to the // A nil error means that all input bytes are known to be identical to the
// output produced by the Transformer. A nil error can be be returned // output produced by the Transformer. A nil error can be returned
// regardless of whether atEOF is true. If err is nil, then then n must // regardless of whether atEOF is true. If err is nil, then n must
// equal len(src); the converse is not necessarily true. // equal len(src); the converse is not necessarily true.
// //
// ErrEndOfSpan means that the Transformer output may differ from the // ErrEndOfSpan means that the Transformer output may differ from the
@@ -493,7 +493,7 @@ func (c *chain) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err erro
return dstL.n, srcL.p, err return dstL.n, srcL.p, err
} }
// Deprecated: use runes.Remove instead. // Deprecated: Use runes.Remove instead.
func RemoveFunc(f func(r rune) bool) Transformer { func RemoveFunc(f func(r rune) bool) Transformer {
return removeF(f) return removeF(f)
} }
@@ -648,7 +648,8 @@ func String(t Transformer, s string) (result string, n int, err error) {
// Transform the remaining input, growing dst and src buffers as necessary. // Transform the remaining input, growing dst and src buffers as necessary.
for { for {
n := copy(src, s[pSrc:]) n := copy(src, s[pSrc:])
nDst, nSrc, err := t.Transform(dst[pDst:], src[:n], pSrc+n == len(s)) atEOF := pSrc+n == len(s)
nDst, nSrc, err := t.Transform(dst[pDst:], src[:n], atEOF)
pDst += nDst pDst += nDst
pSrc += nSrc pSrc += nSrc
@@ -659,6 +660,9 @@ func String(t Transformer, s string) (result string, n int, err error) {
dst = grow(dst, pDst) dst = grow(dst, pDst)
} }
} else if err == ErrShortSrc { } else if err == ErrShortSrc {
if atEOF {
return string(dst[:pDst]), pSrc, err
}
if nSrc == 0 { if nSrc == 0 {
src = grow(src, 0) src = grow(src, 0)
} }
+192 -31
View File
@@ -6,21 +6,20 @@
// Package bidi contains functionality for bidirectional text support. // Package bidi contains functionality for bidirectional text support.
// //
// See http://www.unicode.org/reports/tr9. // See https://www.unicode.org/reports/tr9.
// //
// NOTE: UNDER CONSTRUCTION. This API may change in backwards incompatible ways // NOTE: UNDER CONSTRUCTION. This API may change in backwards incompatible ways
// and without notice. // and without notice.
package bidi // import "golang.org/x/text/unicode/bidi" package bidi // import "golang.org/x/text/unicode/bidi"
// TODO: // TODO
// The following functionality would not be hard to implement, but hinges on
// the definition of a Segmenter interface. For now this is up to the user.
// - Iterate over paragraphs
// - Segmenter to iterate over runs directly from a given text.
// Also:
// - Transformer for reordering? // - Transformer for reordering?
// - Transformer (validator, really) for Bidi Rule. // - Transformer (validator, really) for Bidi Rule.
import (
"bytes"
)
// This API tries to avoid dealing with embedding levels for now. Under the hood // This API tries to avoid dealing with embedding levels for now. Under the hood
// these will be computed, but the question is to which extent the user should // these will be computed, but the question is to which extent the user should
// know they exist. We should at some point allow the user to specify an // know they exist. We should at some point allow the user to specify an
@@ -49,7 +48,9 @@ const (
Neutral Neutral
) )
type options struct{} type options struct {
defaultDirection Direction
}
// An Option is an option for Bidi processing. // An Option is an option for Bidi processing.
type Option func(*options) type Option func(*options)
@@ -66,12 +67,62 @@ type Option func(*options)
// DefaultDirection sets the default direction for a Paragraph. The direction is // DefaultDirection sets the default direction for a Paragraph. The direction is
// overridden if the text contains directional characters. // overridden if the text contains directional characters.
func DefaultDirection(d Direction) Option { func DefaultDirection(d Direction) Option {
panic("unimplemented") return func(opts *options) {
opts.defaultDirection = d
}
} }
// A Paragraph holds a single Paragraph for Bidi processing. // A Paragraph holds a single Paragraph for Bidi processing.
type Paragraph struct { type Paragraph struct {
// buffers p []byte
o Ordering
opts []Option
types []Class
pairTypes []bracketType
pairValues []rune
runes []rune
options options
}
// Initialize the p.pairTypes, p.pairValues and p.types from the input previously
// set by p.SetBytes() or p.SetString(). Also limit the input up to (and including) a paragraph
// separator (bidi class B).
//
// The function p.Order() needs these values to be set, so this preparation could be postponed.
// But since the SetBytes and SetStrings functions return the length of the input up to the paragraph
// separator, the whole input needs to be processed anyway and should not be done twice.
//
// The function has the same return values as SetBytes() / SetString()
func (p *Paragraph) prepareInput() (n int, err error) {
p.runes = bytes.Runes(p.p)
bytecount := 0
// clear slices from previous SetString or SetBytes
p.pairTypes = nil
p.pairValues = nil
p.types = nil
for _, r := range p.runes {
props, i := LookupRune(r)
bytecount += i
cls := props.Class()
if cls == B {
return bytecount, nil
}
p.types = append(p.types, cls)
if props.IsOpeningBracket() {
p.pairTypes = append(p.pairTypes, bpOpen)
p.pairValues = append(p.pairValues, r)
} else if props.IsBracket() {
// this must be a closing bracket,
// since IsOpeningBracket is not true
p.pairTypes = append(p.pairTypes, bpClose)
p.pairValues = append(p.pairValues, r)
} else {
p.pairTypes = append(p.pairTypes, bpNone)
p.pairValues = append(p.pairValues, 0)
}
}
return bytecount, nil
} }
// SetBytes configures p for the given paragraph text. It replaces text // SetBytes configures p for the given paragraph text. It replaces text
@@ -80,70 +131,150 @@ type Paragraph struct {
// consumed from b including this separator. Error may be non-nil if options are // consumed from b including this separator. Error may be non-nil if options are
// given. // given.
func (p *Paragraph) SetBytes(b []byte, opts ...Option) (n int, err error) { func (p *Paragraph) SetBytes(b []byte, opts ...Option) (n int, err error) {
panic("unimplemented") p.p = b
p.opts = opts
return p.prepareInput()
} }
// SetString configures p for the given paragraph text. It replaces text // SetString configures s for the given paragraph text. It replaces text
// previously set by SetBytes or SetString. If b contains a paragraph separator // previously set by SetBytes or SetString. If s contains a paragraph separator
// it will only process the first paragraph and report the number of bytes // it will only process the first paragraph and report the number of bytes
// consumed from b including this separator. Error may be non-nil if options are // consumed from s including this separator. Error may be non-nil if options are
// given. // given.
func (p *Paragraph) SetString(s string, opts ...Option) (n int, err error) { func (p *Paragraph) SetString(s string, opts ...Option) (n int, err error) {
panic("unimplemented") p.p = []byte(s)
p.opts = opts
return p.prepareInput()
} }
// IsLeftToRight reports whether the principle direction of rendering for this // IsLeftToRight reports whether the principle direction of rendering for this
// paragraphs is left-to-right. If this returns false, the principle direction // paragraphs is left-to-right. If this returns false, the principle direction
// of rendering is right-to-left. // of rendering is right-to-left.
func (p *Paragraph) IsLeftToRight() bool { func (p *Paragraph) IsLeftToRight() bool {
panic("unimplemented") return p.Direction() == LeftToRight
} }
// Direction returns the direction of the text of this paragraph. // Direction returns the direction of the text of this paragraph.
// //
// The direction may be LeftToRight, RightToLeft, Mixed, or Neutral. // The direction may be LeftToRight, RightToLeft, Mixed, or Neutral.
func (p *Paragraph) Direction() Direction { func (p *Paragraph) Direction() Direction {
panic("unimplemented") return p.o.Direction()
} }
// TODO: what happens if the position is > len(input)? This should return an error.
// RunAt reports the Run at the given position of the input text. // RunAt reports the Run at the given position of the input text.
// //
// This method can be used for computing line breaks on paragraphs. // This method can be used for computing line breaks on paragraphs.
func (p *Paragraph) RunAt(pos int) Run { func (p *Paragraph) RunAt(pos int) Run {
panic("unimplemented") c := 0
runNumber := 0
for i, r := range p.o.runes {
c += len(r)
if pos < c {
runNumber = i
}
}
return p.o.Run(runNumber)
}
func calculateOrdering(levels []level, runes []rune) Ordering {
var curDir Direction
prevDir := Neutral
prevI := 0
o := Ordering{}
// lvl = 0,2,4,...: left to right
// lvl = 1,3,5,...: right to left
for i, lvl := range levels {
if lvl%2 == 0 {
curDir = LeftToRight
} else {
curDir = RightToLeft
}
if curDir != prevDir {
if i > 0 {
o.runes = append(o.runes, runes[prevI:i])
o.directions = append(o.directions, prevDir)
o.startpos = append(o.startpos, prevI)
}
prevI = i
prevDir = curDir
}
}
o.runes = append(o.runes, runes[prevI:])
o.directions = append(o.directions, prevDir)
o.startpos = append(o.startpos, prevI)
return o
} }
// Order computes the visual ordering of all the runs in a Paragraph. // Order computes the visual ordering of all the runs in a Paragraph.
func (p *Paragraph) Order() (Ordering, error) { func (p *Paragraph) Order() (Ordering, error) {
panic("unimplemented") if len(p.types) == 0 {
return Ordering{}, nil
}
for _, fn := range p.opts {
fn(&p.options)
}
lvl := level(-1)
if p.options.defaultDirection == RightToLeft {
lvl = 1
}
para, err := newParagraph(p.types, p.pairTypes, p.pairValues, lvl)
if err != nil {
return Ordering{}, err
}
levels := para.getLevels([]int{len(p.types)})
p.o = calculateOrdering(levels, p.runes)
return p.o, nil
} }
// Line computes the visual ordering of runs for a single line starting and // Line computes the visual ordering of runs for a single line starting and
// ending at the given positions in the original text. // ending at the given positions in the original text.
func (p *Paragraph) Line(start, end int) (Ordering, error) { func (p *Paragraph) Line(start, end int) (Ordering, error) {
panic("unimplemented") lineTypes := p.types[start:end]
para, err := newParagraph(lineTypes, p.pairTypes[start:end], p.pairValues[start:end], -1)
if err != nil {
return Ordering{}, err
}
levels := para.getLevels([]int{len(lineTypes)})
o := calculateOrdering(levels, p.runes[start:end])
return o, nil
} }
// An Ordering holds the computed visual order of runs of a Paragraph. Calling // An Ordering holds the computed visual order of runs of a Paragraph. Calling
// SetBytes or SetString on the originating Paragraph invalidates an Ordering. // SetBytes or SetString on the originating Paragraph invalidates an Ordering.
// The methods of an Ordering should only be called by one goroutine at a time. // The methods of an Ordering should only be called by one goroutine at a time.
type Ordering struct{} type Ordering struct {
runes [][]rune
directions []Direction
startpos []int
}
// Direction reports the directionality of the runs. // Direction reports the directionality of the runs.
// //
// The direction may be LeftToRight, RightToLeft, Mixed, or Neutral. // The direction may be LeftToRight, RightToLeft, Mixed, or Neutral.
func (o *Ordering) Direction() Direction { func (o *Ordering) Direction() Direction {
panic("unimplemented") return o.directions[0]
} }
// NumRuns returns the number of runs. // NumRuns returns the number of runs.
func (o *Ordering) NumRuns() int { func (o *Ordering) NumRuns() int {
panic("unimplemented") return len(o.runes)
} }
// Run returns the ith run within the ordering. // Run returns the ith run within the ordering.
func (o *Ordering) Run(i int) Run { func (o *Ordering) Run(i int) Run {
panic("unimplemented") r := Run{
runes: o.runes[i],
direction: o.directions[i],
startpos: o.startpos[i],
}
return r
} }
// TODO: perhaps with options. // TODO: perhaps with options.
@@ -155,16 +286,19 @@ func (o *Ordering) Run(i int) Run {
// A Run is a continuous sequence of characters of a single direction. // A Run is a continuous sequence of characters of a single direction.
type Run struct { type Run struct {
runes []rune
direction Direction
startpos int
} }
// String returns the text of the run in its original order. // String returns the text of the run in its original order.
func (r *Run) String() string { func (r *Run) String() string {
panic("unimplemented") return string(r.runes)
} }
// Bytes returns the text of the run in its original order. // Bytes returns the text of the run in its original order.
func (r *Run) Bytes() []byte { func (r *Run) Bytes() []byte {
panic("unimplemented") return []byte(r.String())
} }
// TODO: methods for // TODO: methods for
@@ -174,25 +308,52 @@ func (r *Run) Bytes() []byte {
// Direction reports the direction of the run. // Direction reports the direction of the run.
func (r *Run) Direction() Direction { func (r *Run) Direction() Direction {
panic("unimplemented") return r.direction
} }
// Position of the Run within the text passed to SetBytes or SetString of the // Pos returns the position of the Run within the text passed to SetBytes or SetString of the
// originating Paragraph value. // originating Paragraph value.
func (r *Run) Pos() (start, end int) { func (r *Run) Pos() (start, end int) {
panic("unimplemented") return r.startpos, r.startpos + len(r.runes) - 1
} }
// AppendReverse reverses the order of characters of in, appends them to out, // AppendReverse reverses the order of characters of in, appends them to out,
// and returns the result. Modifiers will still follow the runes they modify. // and returns the result. Modifiers will still follow the runes they modify.
// Brackets are replaced with their counterparts. // Brackets are replaced with their counterparts.
func AppendReverse(out, in []byte) []byte { func AppendReverse(out, in []byte) []byte {
panic("unimplemented") ret := make([]byte, len(in)+len(out))
copy(ret, out)
inRunes := bytes.Runes(in)
for i, r := range inRunes {
prop, _ := LookupRune(r)
if prop.IsBracket() {
inRunes[i] = prop.reverseBracket(r)
}
}
for i, j := 0, len(inRunes)-1; i < j; i, j = i+1, j-1 {
inRunes[i], inRunes[j] = inRunes[j], inRunes[i]
}
copy(ret[len(out):], string(inRunes))
return ret
} }
// ReverseString reverses the order of characters in s and returns a new string. // ReverseString reverses the order of characters in s and returns a new string.
// Modifiers will still follow the runes they modify. Brackets are replaced with // Modifiers will still follow the runes they modify. Brackets are replaced with
// their counterparts. // their counterparts.
func ReverseString(s string) string { func ReverseString(s string) string {
panic("unimplemented") input := []rune(s)
li := len(input)
ret := make([]rune, li)
for i, r := range input {
prop, _ := LookupRune(r)
if prop.IsBracket() {
ret[li-i-1] = prop.reverseBracket(r)
} else {
ret[li-i-1] = r
}
}
return string(ret)
} }
+2 -2
View File
@@ -12,7 +12,7 @@ import (
// This file contains a port of the reference implementation of the // This file contains a port of the reference implementation of the
// Bidi Parentheses Algorithm: // Bidi Parentheses Algorithm:
// http://www.unicode.org/Public/PROGRAMS/BidiReferenceJava/BidiPBAReference.java // https://www.unicode.org/Public/PROGRAMS/BidiReferenceJava/BidiPBAReference.java
// //
// The implementation in this file covers definitions BD14-BD16 and rule N0 // The implementation in this file covers definitions BD14-BD16 and rule N0
// of UAX#9. // of UAX#9.
@@ -246,7 +246,7 @@ func (p *bracketPairer) getStrongTypeN0(index int) Class {
// assuming the given embedding direction. // assuming the given embedding direction.
// //
// It returns ON if no strong type is found. If a single strong type is found, // It returns ON if no strong type is found. If a single strong type is found,
// it returns this this type. Otherwise it returns the embedding direction. // it returns this type. Otherwise it returns the embedding direction.
// //
// TODO: use separate type for "strong" directionality. // TODO: use separate type for "strong" directionality.
func (p *bracketPairer) classifyPairContent(loc bracketPair, dirEmbed Class) Class { func (p *bracketPairer) classifyPairContent(loc bracketPair, dirEmbed Class) Class {
+57 -51
View File
@@ -4,10 +4,13 @@
package bidi package bidi
import "log" import (
"fmt"
"log"
)
// This implementation is a port based on the reference implementation found at: // This implementation is a port based on the reference implementation found at:
// http://www.unicode.org/Public/PROGRAMS/BidiReferenceJava/ // https://www.unicode.org/Public/PROGRAMS/BidiReferenceJava/
// //
// described in Unicode Bidirectional Algorithm (UAX #9). // described in Unicode Bidirectional Algorithm (UAX #9).
// //
@@ -97,13 +100,20 @@ type paragraph struct {
// rune (suggested is the rune of the open bracket for opening and matching // rune (suggested is the rune of the open bracket for opening and matching
// close brackets, after normalization). The embedding levels are optional, but // close brackets, after normalization). The embedding levels are optional, but
// may be supplied to encode embedding levels of styled text. // may be supplied to encode embedding levels of styled text.
// func newParagraph(types []Class, pairTypes []bracketType, pairValues []rune, levels level) (*paragraph, error) {
// TODO: return an error. var err error
func newParagraph(types []Class, pairTypes []bracketType, pairValues []rune, levels level) *paragraph { if err = validateTypes(types); err != nil {
validateTypes(types) return nil, err
validatePbTypes(pairTypes) }
validatePbValues(pairValues, pairTypes) if err = validatePbTypes(pairTypes); err != nil {
validateParagraphEmbeddingLevel(levels) return nil, err
}
if err = validatePbValues(pairValues, pairTypes); err != nil {
return nil, err
}
if err = validateParagraphEmbeddingLevel(levels); err != nil {
return nil, err
}
p := &paragraph{ p := &paragraph{
initialTypes: append([]Class(nil), types...), initialTypes: append([]Class(nil), types...),
@@ -115,7 +125,7 @@ func newParagraph(types []Class, pairTypes []bracketType, pairValues []rune, lev
resultTypes: append([]Class(nil), types...), resultTypes: append([]Class(nil), types...),
} }
p.run() p.run()
return p return p, nil
} }
func (p *paragraph) Len() int { return len(p.initialTypes) } func (p *paragraph) Len() int { return len(p.initialTypes) }
@@ -183,14 +193,14 @@ func (p *paragraph) run() {
// //
// At the end of this function: // At the end of this function:
// //
// - The member variable matchingPDI is set to point to the index of the // - The member variable matchingPDI is set to point to the index of the
// matching PDI character for each isolate initiator character. If there is // matching PDI character for each isolate initiator character. If there is
// no matching PDI, it is set to the length of the input text. For other // no matching PDI, it is set to the length of the input text. For other
// characters, it is set to -1. // characters, it is set to -1.
// - The member variable matchingIsolateInitiator is set to point to the // - The member variable matchingIsolateInitiator is set to point to the
// index of the matching isolate initiator character for each PDI character. // index of the matching isolate initiator character for each PDI character.
// If there is no matching isolate initiator, or the character is not a PDI, // If there is no matching isolate initiator, or the character is not a PDI,
// it is set to -1. // it is set to -1.
func (p *paragraph) determineMatchingIsolates() { func (p *paragraph) determineMatchingIsolates() {
p.matchingPDI = make([]int, p.Len()) p.matchingPDI = make([]int, p.Len())
p.matchingIsolateInitiator = make([]int, p.Len()) p.matchingIsolateInitiator = make([]int, p.Len())
@@ -417,15 +427,8 @@ type isolatingRunSequence struct {
func (i *isolatingRunSequence) Len() int { return len(i.indexes) } func (i *isolatingRunSequence) Len() int { return len(i.indexes) }
func maxLevel(a, b level) level {
if a > b {
return a
}
return b
}
// Rule X10, second bullet: Determine the start-of-sequence (sos) and end-of-sequence (eos) types, // Rule X10, second bullet: Determine the start-of-sequence (sos) and end-of-sequence (eos) types,
// either L or R, for each isolating run sequence. // either L or R, for each isolating run sequence.
func (p *paragraph) isolatingRunSequence(indexes []int) *isolatingRunSequence { func (p *paragraph) isolatingRunSequence(indexes []int) *isolatingRunSequence {
length := len(indexes) length := len(indexes)
types := make([]Class, length) types := make([]Class, length)
@@ -464,8 +467,8 @@ func (p *paragraph) isolatingRunSequence(indexes []int) *isolatingRunSequence {
indexes: indexes, indexes: indexes,
types: types, types: types,
level: level, level: level,
sos: typeForLevel(maxLevel(prevLevel, level)), sos: typeForLevel(max(prevLevel, level)),
eos: typeForLevel(maxLevel(succLevel, level)), eos: typeForLevel(max(succLevel, level)),
} }
} }
@@ -480,15 +483,15 @@ func (s *isolatingRunSequence) resolveWeakTypes() {
// Rule W1. // Rule W1.
// Changes all NSMs. // Changes all NSMs.
preceedingCharacterType := s.sos precedingCharacterType := s.sos
for i, t := range s.types { for i, t := range s.types {
if t == NSM { if t == NSM {
s.types[i] = preceedingCharacterType s.types[i] = precedingCharacterType
} else { } else {
if t.in(LRI, RLI, FSI, PDI) { // if t.in(LRI, RLI, FSI, PDI) {
preceedingCharacterType = ON // precedingCharacterType = ON
} // }
preceedingCharacterType = t precedingCharacterType = t
} }
} }
@@ -895,7 +898,7 @@ func (p *paragraph) getLevels(linebreaks []int) []level {
// Lines are concatenated from left to right. So for example, the fifth // Lines are concatenated from left to right. So for example, the fifth
// character from the left on the third line is // character from the left on the third line is
// //
// getReordering(linebreaks)[linebreaks[1] + 4] // getReordering(linebreaks)[linebreaks[1] + 4]
// //
// (linebreaks[1] is the position after the last character of the second // (linebreaks[1] is the position after the last character of the second
// line, which is also the index of the first character on the third line, // line, which is also the index of the first character on the third line,
@@ -1001,58 +1004,61 @@ func typeForLevel(level level) Class {
return R return R
} }
// TODO: change validation to not panic func validateTypes(types []Class) error {
func validateTypes(types []Class) {
if len(types) == 0 { if len(types) == 0 {
log.Panic("types is null") return fmt.Errorf("types is null")
} }
for i, t := range types[:len(types)-1] { for i, t := range types[:len(types)-1] {
if t == B { if t == B {
log.Panicf("B type before end of paragraph at index: %d", i) return fmt.Errorf("B type before end of paragraph at index: %d", i)
} }
} }
return nil
} }
func validateParagraphEmbeddingLevel(embeddingLevel level) { func validateParagraphEmbeddingLevel(embeddingLevel level) error {
if embeddingLevel != implicitLevel && if embeddingLevel != implicitLevel &&
embeddingLevel != 0 && embeddingLevel != 0 &&
embeddingLevel != 1 { embeddingLevel != 1 {
log.Panicf("illegal paragraph embedding level: %d", embeddingLevel) return fmt.Errorf("illegal paragraph embedding level: %d", embeddingLevel)
} }
return nil
} }
func validateLineBreaks(linebreaks []int, textLength int) { func validateLineBreaks(linebreaks []int, textLength int) error {
prev := 0 prev := 0
for i, next := range linebreaks { for i, next := range linebreaks {
if next <= prev { if next <= prev {
log.Panicf("bad linebreak: %d at index: %d", next, i) return fmt.Errorf("bad linebreak: %d at index: %d", next, i)
} }
prev = next prev = next
} }
if prev != textLength { if prev != textLength {
log.Panicf("last linebreak was %d, want %d", prev, textLength) return fmt.Errorf("last linebreak was %d, want %d", prev, textLength)
} }
return nil
} }
func validatePbTypes(pairTypes []bracketType) { func validatePbTypes(pairTypes []bracketType) error {
if len(pairTypes) == 0 { if len(pairTypes) == 0 {
log.Panic("pairTypes is null") return fmt.Errorf("pairTypes is null")
} }
for i, pt := range pairTypes { for i, pt := range pairTypes {
switch pt { switch pt {
case bpNone, bpOpen, bpClose: case bpNone, bpOpen, bpClose:
default: default:
log.Panicf("illegal pairType value at %d: %v", i, pairTypes[i]) return fmt.Errorf("illegal pairType value at %d: %v", i, pairTypes[i])
} }
} }
return nil
} }
func validatePbValues(pairValues []rune, pairTypes []bracketType) { func validatePbValues(pairValues []rune, pairTypes []bracketType) error {
if pairValues == nil { if pairValues == nil {
log.Panic("pairValues is null") return fmt.Errorf("pairValues is null")
} }
if len(pairTypes) != len(pairValues) { if len(pairTypes) != len(pairValues) {
log.Panic("pairTypes is different length from pairValues") return fmt.Errorf("pairTypes is different length from pairValues")
} }
return nil
} }
-133
View File
@@ -1,133 +0,0 @@
// Copyright 2015 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.
// +build ignore
package main
import (
"flag"
"log"
"golang.org/x/text/internal/gen"
"golang.org/x/text/internal/triegen"
"golang.org/x/text/internal/ucd"
)
var outputFile = flag.String("out", "tables.go", "output file")
func main() {
gen.Init()
gen.Repackage("gen_trieval.go", "trieval.go", "bidi")
gen.Repackage("gen_ranges.go", "ranges_test.go", "bidi")
genTables()
}
// bidiClass names and codes taken from class "bc" in
// http://www.unicode.org/Public/8.0.0/ucd/PropertyValueAliases.txt
var bidiClass = map[string]Class{
"AL": AL, // ArabicLetter
"AN": AN, // ArabicNumber
"B": B, // ParagraphSeparator
"BN": BN, // BoundaryNeutral
"CS": CS, // CommonSeparator
"EN": EN, // EuropeanNumber
"ES": ES, // EuropeanSeparator
"ET": ET, // EuropeanTerminator
"L": L, // LeftToRight
"NSM": NSM, // NonspacingMark
"ON": ON, // OtherNeutral
"R": R, // RightToLeft
"S": S, // SegmentSeparator
"WS": WS, // WhiteSpace
"FSI": Control,
"PDF": Control,
"PDI": Control,
"LRE": Control,
"LRI": Control,
"LRO": Control,
"RLE": Control,
"RLI": Control,
"RLO": Control,
}
func genTables() {
if numClass > 0x0F {
log.Fatalf("Too many Class constants (%#x > 0x0F).", numClass)
}
w := gen.NewCodeWriter()
defer w.WriteVersionedGoFile(*outputFile, "bidi")
gen.WriteUnicodeVersion(w)
t := triegen.NewTrie("bidi")
// Build data about bracket mapping. These bits need to be or-ed with
// any other bits.
orMask := map[rune]uint64{}
xorMap := map[rune]int{}
xorMasks := []rune{0} // First value is no-op.
ucd.Parse(gen.OpenUCDFile("BidiBrackets.txt"), func(p *ucd.Parser) {
r1 := p.Rune(0)
r2 := p.Rune(1)
xor := r1 ^ r2
if _, ok := xorMap[xor]; !ok {
xorMap[xor] = len(xorMasks)
xorMasks = append(xorMasks, xor)
}
entry := uint64(xorMap[xor]) << xorMaskShift
switch p.String(2) {
case "o":
entry |= openMask
case "c", "n":
default:
log.Fatalf("Unknown bracket class %q.", p.String(2))
}
orMask[r1] = entry
})
w.WriteComment(`
xorMasks contains masks to be xor-ed with brackets to get the reverse
version.`)
w.WriteVar("xorMasks", xorMasks)
done := map[rune]bool{}
insert := func(r rune, c Class) {
if !done[r] {
t.Insert(r, orMask[r]|uint64(c))
done[r] = true
}
}
// Insert the derived BiDi properties.
ucd.Parse(gen.OpenUCDFile("extracted/DerivedBidiClass.txt"), func(p *ucd.Parser) {
r := p.Rune(0)
class, ok := bidiClass[p.String(1)]
if !ok {
log.Fatalf("%U: Unknown BiDi class %q", r, p.String(1))
}
insert(r, class)
})
visitDefaults(insert)
// TODO: use sparse blocks. This would reduce table size considerably
// from the looks of it.
sz, err := t.Gen(w)
if err != nil {
log.Fatal(err)
}
w.Size += sz
}
// dummy values to make methods in gen_common compile. The real versions
// will be generated by this file to tables.go.
var (
xorMasks []rune
)
-57
View File
@@ -1,57 +0,0 @@
// Copyright 2015 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.
// +build ignore
package main
import (
"unicode"
"golang.org/x/text/internal/gen"
"golang.org/x/text/internal/ucd"
"golang.org/x/text/unicode/rangetable"
)
// These tables are hand-extracted from:
// http://www.unicode.org/Public/8.0.0/ucd/extracted/DerivedBidiClass.txt
func visitDefaults(fn func(r rune, c Class)) {
// first write default values for ranges listed above.
visitRunes(fn, AL, []rune{
0x0600, 0x07BF, // Arabic
0x08A0, 0x08FF, // Arabic Extended-A
0xFB50, 0xFDCF, // Arabic Presentation Forms
0xFDF0, 0xFDFF,
0xFE70, 0xFEFF,
0x0001EE00, 0x0001EEFF, // Arabic Mathematical Alpha Symbols
})
visitRunes(fn, R, []rune{
0x0590, 0x05FF, // Hebrew
0x07C0, 0x089F, // Nko et al.
0xFB1D, 0xFB4F,
0x00010800, 0x00010FFF, // Cypriot Syllabary et. al.
0x0001E800, 0x0001EDFF,
0x0001EF00, 0x0001EFFF,
})
visitRunes(fn, ET, []rune{ // European Terminator
0x20A0, 0x20Cf, // Currency symbols
})
rangetable.Visit(unicode.Noncharacter_Code_Point, func(r rune) {
fn(r, BN) // Boundary Neutral
})
ucd.Parse(gen.OpenUCDFile("DerivedCoreProperties.txt"), func(p *ucd.Parser) {
if p.String(1) == "Default_Ignorable_Code_Point" {
fn(p.Rune(0), BN) // Boundary Neutral
}
})
}
func visitRunes(fn func(r rune, c Class), c Class, runes []rune) {
for i := 0; i < len(runes); i += 2 {
lo, hi := runes[i], runes[i+1]
for j := lo; j <= hi; j++ {
fn(j, c)
}
}
}
-64
View File
@@ -1,64 +0,0 @@
// Copyright 2015 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.
// +build ignore
package main
// Class is the Unicode BiDi class. Each rune has a single class.
type Class uint
const (
L Class = iota // LeftToRight
R // RightToLeft
EN // EuropeanNumber
ES // EuropeanSeparator
ET // EuropeanTerminator
AN // ArabicNumber
CS // CommonSeparator
B // ParagraphSeparator
S // SegmentSeparator
WS // WhiteSpace
ON // OtherNeutral
BN // BoundaryNeutral
NSM // NonspacingMark
AL // ArabicLetter
Control // Control LRO - PDI
numClass
LRO // LeftToRightOverride
RLO // RightToLeftOverride
LRE // LeftToRightEmbedding
RLE // RightToLeftEmbedding
PDF // PopDirectionalFormat
LRI // LeftToRightIsolate
RLI // RightToLeftIsolate
FSI // FirstStrongIsolate
PDI // PopDirectionalIsolate
unknownClass = ^Class(0)
)
var controlToClass = map[rune]Class{
0x202D: LRO, // LeftToRightOverride,
0x202E: RLO, // RightToLeftOverride,
0x202A: LRE, // LeftToRightEmbedding,
0x202B: RLE, // RightToLeftEmbedding,
0x202C: PDF, // PopDirectionalFormat,
0x2066: LRI, // LeftToRightIsolate,
0x2067: RLI, // RightToLeftIsolate,
0x2068: FSI, // FirstStrongIsolate,
0x2069: PDI, // PopDirectionalIsolate,
}
// A trie entry has the following bits:
// 7..5 XOR mask for brackets
// 4 1: Bracket open, 0: Bracket close
// 3..0 Class type
const (
openMask = 0x10
xorMaskShift = 5
)
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
// +build go1.10 //go:build go1.10 && !go1.13
package bidi package bidi
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
// +build !go1.10 //go:build !go1.10
package bidi package bidi
-12
View File
@@ -37,18 +37,6 @@ const (
unknownClass = ^Class(0) unknownClass = ^Class(0)
) )
var controlToClass = map[rune]Class{
0x202D: LRO, // LeftToRightOverride,
0x202E: RLO, // RightToLeftOverride,
0x202A: LRE, // LeftToRightEmbedding,
0x202B: RLE, // RightToLeftEmbedding,
0x202C: PDF, // PopDirectionalFormat,
0x2066: LRI, // LeftToRightIsolate,
0x2067: RLI, // RightToLeftIsolate,
0x2068: FSI, // FirstStrongIsolate,
0x2069: PDI, // PopDirectionalIsolate,
}
// A trie entry has the following bits: // A trie entry has the following bits:
// 7..5 XOR mask for brackets // 7..5 XOR mask for brackets
// 4 1: Bracket open, 0: Bracket close // 4 1: Bracket open, 0: Bracket close
+6 -2
View File
@@ -407,7 +407,7 @@ func decomposeHangul(buf []byte, r rune) int {
// decomposeHangul algorithmically decomposes a Hangul rune into // decomposeHangul algorithmically decomposes a Hangul rune into
// its Jamo components. // its Jamo components.
// See http://unicode.org/reports/tr15/#Hangul for details on decomposing Hangul. // See https://unicode.org/reports/tr15/#Hangul for details on decomposing Hangul.
func (rb *reorderBuffer) decomposeHangul(r rune) { func (rb *reorderBuffer) decomposeHangul(r rune) {
r -= hangulBase r -= hangulBase
x := r % jamoTCount x := r % jamoTCount
@@ -420,7 +420,7 @@ func (rb *reorderBuffer) decomposeHangul(r rune) {
} }
// combineHangul algorithmically combines Jamo character components into Hangul. // combineHangul algorithmically combines Jamo character components into Hangul.
// See http://unicode.org/reports/tr15/#Hangul for details on combining Hangul. // See https://unicode.org/reports/tr15/#Hangul for details on combining Hangul.
func (rb *reorderBuffer) combineHangul(s, i, k int) { func (rb *reorderBuffer) combineHangul(s, i, k int) {
b := rb.rune[:] b := rb.rune[:]
bn := rb.nrune bn := rb.nrune
@@ -461,6 +461,10 @@ func (rb *reorderBuffer) combineHangul(s, i, k int) {
// It should only be used to recompose a single segment, as it will not // It should only be used to recompose a single segment, as it will not
// handle alternations between Hangul and non-Hangul characters correctly. // handle alternations between Hangul and non-Hangul characters correctly.
func (rb *reorderBuffer) compose() { func (rb *reorderBuffer) compose() {
// Lazily load the map used by the combine func below, but do
// it outside of the loop.
recompMapOnce.Do(buildRecompMap)
// UAX #15, section X5 , including Corrigendum #5 // UAX #15, section X5 , including Corrigendum #5
// "In any character sequence beginning with starter S, a character C is // "In any character sequence beginning with starter S, a character C is
// blocked from S if and only if there is some character B between S // blocked from S if and only if there is some character B between S
+25 -5
View File
@@ -4,6 +4,8 @@
package norm package norm
import "encoding/binary"
// This file contains Form-specific logic and wrappers for data in tables.go. // This file contains Form-specific logic and wrappers for data in tables.go.
// Rune info is stored in a separate trie per composing form. A composing form // Rune info is stored in a separate trie per composing form. A composing form
@@ -11,7 +13,7 @@ package norm
// a rune to a uint16. The values take two forms. For v >= 0x8000: // a rune to a uint16. The values take two forms. For v >= 0x8000:
// bits // bits
// 15: 1 (inverse of NFD_QC bit of qcInfo) // 15: 1 (inverse of NFD_QC bit of qcInfo)
// 13..7: qcInfo (see below). isYesD is always true (no decompostion). // 13..7: qcInfo (see below). isYesD is always true (no decomposition).
// 6..0: ccc (compressed CCC value). // 6..0: ccc (compressed CCC value).
// For v < 0x8000, the respective rune has a decomposition and v is an index // For v < 0x8000, the respective rune has a decomposition and v is an index
// into a byte array of UTF-8 decomposition sequences and additional info and // into a byte array of UTF-8 decomposition sequences and additional info and
@@ -108,10 +110,11 @@ func (p Properties) BoundaryAfter() bool {
} }
// We pack quick check data in 4 bits: // We pack quick check data in 4 bits:
// 5: Combines forward (0 == false, 1 == true) //
// 4..3: NFC_QC Yes(00), No (10), or Maybe (11) // 5: Combines forward (0 == false, 1 == true)
// 2: NFD_QC Yes (0) or No (1). No also means there is a decomposition. // 4..3: NFC_QC Yes(00), No (10), or Maybe (11)
// 1..0: Number of trailing non-starters. // 2: NFD_QC Yes (0) or No (1). No also means there is a decomposition.
// 1..0: Number of trailing non-starters.
// //
// When all 4 bits are zero, the character is inert, meaning it is never // When all 4 bits are zero, the character is inert, meaning it is never
// influenced by normalization. // influenced by normalization.
@@ -178,6 +181,17 @@ func (p Properties) TrailCCC() uint8 {
return ccc[p.tccc] return ccc[p.tccc]
} }
func buildRecompMap() {
recompMap = make(map[uint32]rune, len(recompMapPacked)/8)
var buf [8]byte
for i := 0; i < len(recompMapPacked); i += 8 {
copy(buf[:], recompMapPacked[i:i+8])
key := binary.BigEndian.Uint32(buf[:4])
val := binary.BigEndian.Uint32(buf[4:])
recompMap[key] = rune(val)
}
}
// Recomposition // Recomposition
// We use 32-bit keys instead of 64-bit for the two codepoint keys. // We use 32-bit keys instead of 64-bit for the two codepoint keys.
// This clips off the bits of three entries, but we know this will not // This clips off the bits of three entries, but we know this will not
@@ -186,8 +200,14 @@ func (p Properties) TrailCCC() uint8 {
// Note that the recomposition map for NFC and NFKC are identical. // Note that the recomposition map for NFC and NFKC are identical.
// combine returns the combined rune or 0 if it doesn't exist. // combine returns the combined rune or 0 if it doesn't exist.
//
// The caller is responsible for calling
// recompMapOnce.Do(buildRecompMap) sometime before this is called.
func combine(a, b rune) rune { func combine(a, b rune) rune {
key := uint32(uint16(a))<<16 + uint32(uint16(b)) key := uint32(uint16(a))<<16 + uint32(uint16(b))
if recompMap == nil {
panic("caller error") // see func comment
}
return recompMap[key] return recompMap[key]
} }
+2 -1
View File
@@ -128,8 +128,9 @@ func (i *Iter) Next() []byte {
func nextASCIIBytes(i *Iter) []byte { func nextASCIIBytes(i *Iter) []byte {
p := i.p + 1 p := i.p + 1
if p >= i.rb.nsrc { if p >= i.rb.nsrc {
p0 := i.p
i.setDone() i.setDone()
return i.rb.src.bytes[i.p:p] return i.rb.src.bytes[p0:p]
} }
if i.rb.src.bytes[p] < utf8.RuneSelf { if i.rb.src.bytes[p] < utf8.RuneSelf {
p0 := i.p p0 := i.p
-976
View File
@@ -1,976 +0,0 @@
// Copyright 2011 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.
// +build ignore
// Normalization table generator.
// Data read from the web.
// See forminfo.go for a description of the trie values associated with each rune.
package main
import (
"bytes"
"flag"
"fmt"
"io"
"log"
"sort"
"strconv"
"strings"
"golang.org/x/text/internal/gen"
"golang.org/x/text/internal/triegen"
"golang.org/x/text/internal/ucd"
)
func main() {
gen.Init()
loadUnicodeData()
compactCCC()
loadCompositionExclusions()
completeCharFields(FCanonical)
completeCharFields(FCompatibility)
computeNonStarterCounts()
verifyComputed()
printChars()
testDerived()
printTestdata()
makeTables()
}
var (
tablelist = flag.String("tables",
"all",
"comma-separated list of which tables to generate; "+
"can be 'decomp', 'recomp', 'info' and 'all'")
test = flag.Bool("test",
false,
"test existing tables against DerivedNormalizationProps and generate test data for regression testing")
verbose = flag.Bool("verbose",
false,
"write data to stdout as it is parsed")
)
const MaxChar = 0x10FFFF // anything above this shouldn't exist
// Quick Check properties of runes allow us to quickly
// determine whether a rune may occur in a normal form.
// For a given normal form, a rune may be guaranteed to occur
// verbatim (QC=Yes), may or may not combine with another
// rune (QC=Maybe), or may not occur (QC=No).
type QCResult int
const (
QCUnknown QCResult = iota
QCYes
QCNo
QCMaybe
)
func (r QCResult) String() string {
switch r {
case QCYes:
return "Yes"
case QCNo:
return "No"
case QCMaybe:
return "Maybe"
}
return "***UNKNOWN***"
}
const (
FCanonical = iota // NFC or NFD
FCompatibility // NFKC or NFKD
FNumberOfFormTypes
)
const (
MComposed = iota // NFC or NFKC
MDecomposed // NFD or NFKD
MNumberOfModes
)
// This contains only the properties we're interested in.
type Char struct {
name string
codePoint rune // if zero, this index is not a valid code point.
ccc uint8 // canonical combining class
origCCC uint8
excludeInComp bool // from CompositionExclusions.txt
compatDecomp bool // it has a compatibility expansion
nTrailingNonStarters uint8
nLeadingNonStarters uint8 // must be equal to trailing if non-zero
forms [FNumberOfFormTypes]FormInfo // For FCanonical and FCompatibility
state State
}
var chars = make([]Char, MaxChar+1)
var cccMap = make(map[uint8]uint8)
func (c Char) String() string {
buf := new(bytes.Buffer)
fmt.Fprintf(buf, "%U [%s]:\n", c.codePoint, c.name)
fmt.Fprintf(buf, " ccc: %v\n", c.ccc)
fmt.Fprintf(buf, " excludeInComp: %v\n", c.excludeInComp)
fmt.Fprintf(buf, " compatDecomp: %v\n", c.compatDecomp)
fmt.Fprintf(buf, " state: %v\n", c.state)
fmt.Fprintf(buf, " NFC:\n")
fmt.Fprint(buf, c.forms[FCanonical])
fmt.Fprintf(buf, " NFKC:\n")
fmt.Fprint(buf, c.forms[FCompatibility])
return buf.String()
}
// In UnicodeData.txt, some ranges are marked like this:
// 3400;<CJK Ideograph Extension A, First>;Lo;0;L;;;;;N;;;;;
// 4DB5;<CJK Ideograph Extension A, Last>;Lo;0;L;;;;;N;;;;;
// parseCharacter keeps a state variable indicating the weirdness.
type State int
const (
SNormal State = iota // known to be zero for the type
SFirst
SLast
SMissing
)
var lastChar = rune('\u0000')
func (c Char) isValid() bool {
return c.codePoint != 0 && c.state != SMissing
}
type FormInfo struct {
quickCheck [MNumberOfModes]QCResult // index: MComposed or MDecomposed
verified [MNumberOfModes]bool // index: MComposed or MDecomposed
combinesForward bool // May combine with rune on the right
combinesBackward bool // May combine with rune on the left
isOneWay bool // Never appears in result
inDecomp bool // Some decompositions result in this char.
decomp Decomposition
expandedDecomp Decomposition
}
func (f FormInfo) String() string {
buf := bytes.NewBuffer(make([]byte, 0))
fmt.Fprintf(buf, " quickCheck[C]: %v\n", f.quickCheck[MComposed])
fmt.Fprintf(buf, " quickCheck[D]: %v\n", f.quickCheck[MDecomposed])
fmt.Fprintf(buf, " cmbForward: %v\n", f.combinesForward)
fmt.Fprintf(buf, " cmbBackward: %v\n", f.combinesBackward)
fmt.Fprintf(buf, " isOneWay: %v\n", f.isOneWay)
fmt.Fprintf(buf, " inDecomp: %v\n", f.inDecomp)
fmt.Fprintf(buf, " decomposition: %X\n", f.decomp)
fmt.Fprintf(buf, " expandedDecomp: %X\n", f.expandedDecomp)
return buf.String()
}
type Decomposition []rune
func parseDecomposition(s string, skipfirst bool) (a []rune, err error) {
decomp := strings.Split(s, " ")
if len(decomp) > 0 && skipfirst {
decomp = decomp[1:]
}
for _, d := range decomp {
point, err := strconv.ParseUint(d, 16, 64)
if err != nil {
return a, err
}
a = append(a, rune(point))
}
return a, nil
}
func loadUnicodeData() {
f := gen.OpenUCDFile("UnicodeData.txt")
defer f.Close()
p := ucd.New(f)
for p.Next() {
r := p.Rune(ucd.CodePoint)
char := &chars[r]
char.ccc = uint8(p.Uint(ucd.CanonicalCombiningClass))
decmap := p.String(ucd.DecompMapping)
exp, err := parseDecomposition(decmap, false)
isCompat := false
if err != nil {
if len(decmap) > 0 {
exp, err = parseDecomposition(decmap, true)
if err != nil {
log.Fatalf(`%U: bad decomp |%v|: "%s"`, r, decmap, err)
}
isCompat = true
}
}
char.name = p.String(ucd.Name)
char.codePoint = r
char.forms[FCompatibility].decomp = exp
if !isCompat {
char.forms[FCanonical].decomp = exp
} else {
char.compatDecomp = true
}
if len(decmap) > 0 {
char.forms[FCompatibility].decomp = exp
}
}
if err := p.Err(); err != nil {
log.Fatal(err)
}
}
// compactCCC converts the sparse set of CCC values to a continguous one,
// reducing the number of bits needed from 8 to 6.
func compactCCC() {
m := make(map[uint8]uint8)
for i := range chars {
c := &chars[i]
m[c.ccc] = 0
}
cccs := []int{}
for v, _ := range m {
cccs = append(cccs, int(v))
}
sort.Ints(cccs)
for i, c := range cccs {
cccMap[uint8(i)] = uint8(c)
m[uint8(c)] = uint8(i)
}
for i := range chars {
c := &chars[i]
c.origCCC = c.ccc
c.ccc = m[c.ccc]
}
if len(m) >= 1<<6 {
log.Fatalf("too many difference CCC values: %d >= 64", len(m))
}
}
// CompositionExclusions.txt has form:
// 0958 # ...
// See http://unicode.org/reports/tr44/ for full explanation
func loadCompositionExclusions() {
f := gen.OpenUCDFile("CompositionExclusions.txt")
defer f.Close()
p := ucd.New(f)
for p.Next() {
c := &chars[p.Rune(0)]
if c.excludeInComp {
log.Fatalf("%U: Duplicate entry in exclusions.", c.codePoint)
}
c.excludeInComp = true
}
if e := p.Err(); e != nil {
log.Fatal(e)
}
}
// hasCompatDecomp returns true if any of the recursive
// decompositions contains a compatibility expansion.
// In this case, the character may not occur in NFK*.
func hasCompatDecomp(r rune) bool {
c := &chars[r]
if c.compatDecomp {
return true
}
for _, d := range c.forms[FCompatibility].decomp {
if hasCompatDecomp(d) {
return true
}
}
return false
}
// Hangul related constants.
const (
HangulBase = 0xAC00
HangulEnd = 0xD7A4 // hangulBase + Jamo combinations (19 * 21 * 28)
JamoLBase = 0x1100
JamoLEnd = 0x1113
JamoVBase = 0x1161
JamoVEnd = 0x1176
JamoTBase = 0x11A8
JamoTEnd = 0x11C3
JamoLVTCount = 19 * 21 * 28
JamoTCount = 28
)
func isHangul(r rune) bool {
return HangulBase <= r && r < HangulEnd
}
func isHangulWithoutJamoT(r rune) bool {
if !isHangul(r) {
return false
}
r -= HangulBase
return r < JamoLVTCount && r%JamoTCount == 0
}
func ccc(r rune) uint8 {
return chars[r].ccc
}
// Insert a rune in a buffer, ordered by Canonical Combining Class.
func insertOrdered(b Decomposition, r rune) Decomposition {
n := len(b)
b = append(b, 0)
cc := ccc(r)
if cc > 0 {
// Use bubble sort.
for ; n > 0; n-- {
if ccc(b[n-1]) <= cc {
break
}
b[n] = b[n-1]
}
}
b[n] = r
return b
}
// Recursively decompose.
func decomposeRecursive(form int, r rune, d Decomposition) Decomposition {
dcomp := chars[r].forms[form].decomp
if len(dcomp) == 0 {
return insertOrdered(d, r)
}
for _, c := range dcomp {
d = decomposeRecursive(form, c, d)
}
return d
}
func completeCharFields(form int) {
// Phase 0: pre-expand decomposition.
for i := range chars {
f := &chars[i].forms[form]
if len(f.decomp) == 0 {
continue
}
exp := make(Decomposition, 0)
for _, c := range f.decomp {
exp = decomposeRecursive(form, c, exp)
}
f.expandedDecomp = exp
}
// Phase 1: composition exclusion, mark decomposition.
for i := range chars {
c := &chars[i]
f := &c.forms[form]
// Marks script-specific exclusions and version restricted.
f.isOneWay = c.excludeInComp
// Singletons
f.isOneWay = f.isOneWay || len(f.decomp) == 1
// Non-starter decompositions
if len(f.decomp) > 1 {
chk := c.ccc != 0 || chars[f.decomp[0]].ccc != 0
f.isOneWay = f.isOneWay || chk
}
// Runes that decompose into more than two runes.
f.isOneWay = f.isOneWay || len(f.decomp) > 2
if form == FCompatibility {
f.isOneWay = f.isOneWay || hasCompatDecomp(c.codePoint)
}
for _, r := range f.decomp {
chars[r].forms[form].inDecomp = true
}
}
// Phase 2: forward and backward combining.
for i := range chars {
c := &chars[i]
f := &c.forms[form]
if !f.isOneWay && len(f.decomp) == 2 {
f0 := &chars[f.decomp[0]].forms[form]
f1 := &chars[f.decomp[1]].forms[form]
if !f0.isOneWay {
f0.combinesForward = true
}
if !f1.isOneWay {
f1.combinesBackward = true
}
}
if isHangulWithoutJamoT(rune(i)) {
f.combinesForward = true
}
}
// Phase 3: quick check values.
for i := range chars {
c := &chars[i]
f := &c.forms[form]
switch {
case len(f.decomp) > 0:
f.quickCheck[MDecomposed] = QCNo
case isHangul(rune(i)):
f.quickCheck[MDecomposed] = QCNo
default:
f.quickCheck[MDecomposed] = QCYes
}
switch {
case f.isOneWay:
f.quickCheck[MComposed] = QCNo
case (i & 0xffff00) == JamoLBase:
f.quickCheck[MComposed] = QCYes
if JamoLBase <= i && i < JamoLEnd {
f.combinesForward = true
}
if JamoVBase <= i && i < JamoVEnd {
f.quickCheck[MComposed] = QCMaybe
f.combinesBackward = true
f.combinesForward = true
}
if JamoTBase <= i && i < JamoTEnd {
f.quickCheck[MComposed] = QCMaybe
f.combinesBackward = true
}
case !f.combinesBackward:
f.quickCheck[MComposed] = QCYes
default:
f.quickCheck[MComposed] = QCMaybe
}
}
}
func computeNonStarterCounts() {
// Phase 4: leading and trailing non-starter count
for i := range chars {
c := &chars[i]
runes := []rune{rune(i)}
// We always use FCompatibility so that the CGJ insertion points do not
// change for repeated normalizations with different forms.
if exp := c.forms[FCompatibility].expandedDecomp; len(exp) > 0 {
runes = exp
}
// We consider runes that combine backwards to be non-starters for the
// purpose of Stream-Safe Text Processing.
for _, r := range runes {
if cr := &chars[r]; cr.ccc == 0 && !cr.forms[FCompatibility].combinesBackward {
break
}
c.nLeadingNonStarters++
}
for i := len(runes) - 1; i >= 0; i-- {
if cr := &chars[runes[i]]; cr.ccc == 0 && !cr.forms[FCompatibility].combinesBackward {
break
}
c.nTrailingNonStarters++
}
if c.nTrailingNonStarters > 3 {
log.Fatalf("%U: Decomposition with more than 3 (%d) trailing modifiers (%U)", i, c.nTrailingNonStarters, runes)
}
if isHangul(rune(i)) {
c.nTrailingNonStarters = 2
if isHangulWithoutJamoT(rune(i)) {
c.nTrailingNonStarters = 1
}
}
if l, t := c.nLeadingNonStarters, c.nTrailingNonStarters; l > 0 && l != t {
log.Fatalf("%U: number of leading and trailing non-starters should be equal (%d vs %d)", i, l, t)
}
if t := c.nTrailingNonStarters; t > 3 {
log.Fatalf("%U: number of trailing non-starters is %d > 3", t)
}
}
}
func printBytes(w io.Writer, b []byte, name string) {
fmt.Fprintf(w, "// %s: %d bytes\n", name, len(b))
fmt.Fprintf(w, "var %s = [...]byte {", name)
for i, c := range b {
switch {
case i%64 == 0:
fmt.Fprintf(w, "\n// Bytes %x - %x\n", i, i+63)
case i%8 == 0:
fmt.Fprintf(w, "\n")
}
fmt.Fprintf(w, "0x%.2X, ", c)
}
fmt.Fprint(w, "\n}\n\n")
}
// See forminfo.go for format.
func makeEntry(f *FormInfo, c *Char) uint16 {
e := uint16(0)
if r := c.codePoint; HangulBase <= r && r < HangulEnd {
e |= 0x40
}
if f.combinesForward {
e |= 0x20
}
if f.quickCheck[MDecomposed] == QCNo {
e |= 0x4
}
switch f.quickCheck[MComposed] {
case QCYes:
case QCNo:
e |= 0x10
case QCMaybe:
e |= 0x18
default:
log.Fatalf("Illegal quickcheck value %v.", f.quickCheck[MComposed])
}
e |= uint16(c.nTrailingNonStarters)
return e
}
// decompSet keeps track of unique decompositions, grouped by whether
// the decomposition is followed by a trailing and/or leading CCC.
type decompSet [7]map[string]bool
const (
normalDecomp = iota
firstMulti
firstCCC
endMulti
firstLeadingCCC
firstCCCZeroExcept
firstStarterWithNLead
lastDecomp
)
var cname = []string{"firstMulti", "firstCCC", "endMulti", "firstLeadingCCC", "firstCCCZeroExcept", "firstStarterWithNLead", "lastDecomp"}
func makeDecompSet() decompSet {
m := decompSet{}
for i := range m {
m[i] = make(map[string]bool)
}
return m
}
func (m *decompSet) insert(key int, s string) {
m[key][s] = true
}
func printCharInfoTables(w io.Writer) int {
mkstr := func(r rune, f *FormInfo) (int, string) {
d := f.expandedDecomp
s := string([]rune(d))
if max := 1 << 6; len(s) >= max {
const msg = "%U: too many bytes in decomposition: %d >= %d"
log.Fatalf(msg, r, len(s), max)
}
head := uint8(len(s))
if f.quickCheck[MComposed] != QCYes {
head |= 0x40
}
if f.combinesForward {
head |= 0x80
}
s = string([]byte{head}) + s
lccc := ccc(d[0])
tccc := ccc(d[len(d)-1])
cc := ccc(r)
if cc != 0 && lccc == 0 && tccc == 0 {
log.Fatalf("%U: trailing and leading ccc are 0 for non-zero ccc %d", r, cc)
}
if tccc < lccc && lccc != 0 {
const msg = "%U: lccc (%d) must be <= tcc (%d)"
log.Fatalf(msg, r, lccc, tccc)
}
index := normalDecomp
nTrail := chars[r].nTrailingNonStarters
nLead := chars[r].nLeadingNonStarters
if tccc > 0 || lccc > 0 || nTrail > 0 {
tccc <<= 2
tccc |= nTrail
s += string([]byte{tccc})
index = endMulti
for _, r := range d[1:] {
if ccc(r) == 0 {
index = firstCCC
}
}
if lccc > 0 || nLead > 0 {
s += string([]byte{lccc})
if index == firstCCC {
log.Fatalf("%U: multi-segment decomposition not supported for decompositions with leading CCC != 0", r)
}
index = firstLeadingCCC
}
if cc != lccc {
if cc != 0 {
log.Fatalf("%U: for lccc != ccc, expected ccc to be 0; was %d", r, cc)
}
index = firstCCCZeroExcept
}
} else if len(d) > 1 {
index = firstMulti
}
return index, s
}
decompSet := makeDecompSet()
const nLeadStr = "\x00\x01" // 0-byte length and tccc with nTrail.
decompSet.insert(firstStarterWithNLead, nLeadStr)
// Store the uniqued decompositions in a byte buffer,
// preceded by their byte length.
for _, c := range chars {
for _, f := range c.forms {
if len(f.expandedDecomp) == 0 {
continue
}
if f.combinesBackward {
log.Fatalf("%U: combinesBackward and decompose", c.codePoint)
}
index, s := mkstr(c.codePoint, &f)
decompSet.insert(index, s)
}
}
decompositions := bytes.NewBuffer(make([]byte, 0, 10000))
size := 0
positionMap := make(map[string]uint16)
decompositions.WriteString("\000")
fmt.Fprintln(w, "const (")
for i, m := range decompSet {
sa := []string{}
for s := range m {
sa = append(sa, s)
}
sort.Strings(sa)
for _, s := range sa {
p := decompositions.Len()
decompositions.WriteString(s)
positionMap[s] = uint16(p)
}
if cname[i] != "" {
fmt.Fprintf(w, "%s = 0x%X\n", cname[i], decompositions.Len())
}
}
fmt.Fprintln(w, "maxDecomp = 0x8000")
fmt.Fprintln(w, ")")
b := decompositions.Bytes()
printBytes(w, b, "decomps")
size += len(b)
varnames := []string{"nfc", "nfkc"}
for i := 0; i < FNumberOfFormTypes; i++ {
trie := triegen.NewTrie(varnames[i])
for r, c := range chars {
f := c.forms[i]
d := f.expandedDecomp
if len(d) != 0 {
_, key := mkstr(c.codePoint, &f)
trie.Insert(rune(r), uint64(positionMap[key]))
if c.ccc != ccc(d[0]) {
// We assume the lead ccc of a decomposition !=0 in this case.
if ccc(d[0]) == 0 {
log.Fatalf("Expected leading CCC to be non-zero; ccc is %d", c.ccc)
}
}
} else if c.nLeadingNonStarters > 0 && len(f.expandedDecomp) == 0 && c.ccc == 0 && !f.combinesBackward {
// Handle cases where it can't be detected that the nLead should be equal
// to nTrail.
trie.Insert(c.codePoint, uint64(positionMap[nLeadStr]))
} else if v := makeEntry(&f, &c)<<8 | uint16(c.ccc); v != 0 {
trie.Insert(c.codePoint, uint64(0x8000|v))
}
}
sz, err := trie.Gen(w, triegen.Compact(&normCompacter{name: varnames[i]}))
if err != nil {
log.Fatal(err)
}
size += sz
}
return size
}
func contains(sa []string, s string) bool {
for _, a := range sa {
if a == s {
return true
}
}
return false
}
func makeTables() {
w := &bytes.Buffer{}
size := 0
if *tablelist == "" {
return
}
list := strings.Split(*tablelist, ",")
if *tablelist == "all" {
list = []string{"recomp", "info"}
}
// Compute maximum decomposition size.
max := 0
for _, c := range chars {
if n := len(string(c.forms[FCompatibility].expandedDecomp)); n > max {
max = n
}
}
fmt.Fprintln(w, "const (")
fmt.Fprintln(w, "\t// Version is the Unicode edition from which the tables are derived.")
fmt.Fprintf(w, "\tVersion = %q\n", gen.UnicodeVersion())
fmt.Fprintln(w)
fmt.Fprintln(w, "\t// MaxTransformChunkSize indicates the maximum number of bytes that Transform")
fmt.Fprintln(w, "\t// may need to write atomically for any Form. Making a destination buffer at")
fmt.Fprintln(w, "\t// least this size ensures that Transform can always make progress and that")
fmt.Fprintln(w, "\t// the user does not need to grow the buffer on an ErrShortDst.")
fmt.Fprintf(w, "\tMaxTransformChunkSize = %d+maxNonStarters*4\n", len(string(0x034F))+max)
fmt.Fprintln(w, ")\n")
// Print the CCC remap table.
size += len(cccMap)
fmt.Fprintf(w, "var ccc = [%d]uint8{", len(cccMap))
for i := 0; i < len(cccMap); i++ {
if i%8 == 0 {
fmt.Fprintln(w)
}
fmt.Fprintf(w, "%3d, ", cccMap[uint8(i)])
}
fmt.Fprintln(w, "\n}\n")
if contains(list, "info") {
size += printCharInfoTables(w)
}
if contains(list, "recomp") {
// Note that we use 32 bit keys, instead of 64 bit.
// This clips the bits of three entries, but we know
// this won't cause a collision. The compiler will catch
// any changes made to UnicodeData.txt that introduces
// a collision.
// Note that the recomposition map for NFC and NFKC
// are identical.
// Recomposition map
nrentries := 0
for _, c := range chars {
f := c.forms[FCanonical]
if !f.isOneWay && len(f.decomp) > 0 {
nrentries++
}
}
sz := nrentries * 8
size += sz
fmt.Fprintf(w, "// recompMap: %d bytes (entries only)\n", sz)
fmt.Fprintln(w, "var recompMap = map[uint32]rune{")
for i, c := range chars {
f := c.forms[FCanonical]
d := f.decomp
if !f.isOneWay && len(d) > 0 {
key := uint32(uint16(d[0]))<<16 + uint32(uint16(d[1]))
fmt.Fprintf(w, "0x%.8X: 0x%.4X,\n", key, i)
}
}
fmt.Fprintf(w, "}\n\n")
}
fmt.Fprintf(w, "// Total size of tables: %dKB (%d bytes)\n", (size+512)/1024, size)
gen.WriteVersionedGoFile("tables.go", "norm", w.Bytes())
}
func printChars() {
if *verbose {
for _, c := range chars {
if !c.isValid() || c.state == SMissing {
continue
}
fmt.Println(c)
}
}
}
// verifyComputed does various consistency tests.
func verifyComputed() {
for i, c := range chars {
for _, f := range c.forms {
isNo := (f.quickCheck[MDecomposed] == QCNo)
if (len(f.decomp) > 0) != isNo && !isHangul(rune(i)) {
log.Fatalf("%U: NF*D QC must be No if rune decomposes", i)
}
isMaybe := f.quickCheck[MComposed] == QCMaybe
if f.combinesBackward != isMaybe {
log.Fatalf("%U: NF*C QC must be Maybe if combinesBackward", i)
}
if len(f.decomp) > 0 && f.combinesForward && isMaybe {
log.Fatalf("%U: NF*C QC must be Yes or No if combinesForward and decomposes", i)
}
if len(f.expandedDecomp) != 0 {
continue
}
if a, b := c.nLeadingNonStarters > 0, (c.ccc > 0 || f.combinesBackward); a != b {
// We accept these runes to be treated differently (it only affects
// segment breaking in iteration, most likely on improper use), but
// reconsider if more characters are added.
// U+FF9E HALFWIDTH KATAKANA VOICED SOUND MARK;Lm;0;L;<narrow> 3099;;;;N;;;;;
// U+FF9F HALFWIDTH KATAKANA SEMI-VOICED SOUND MARK;Lm;0;L;<narrow> 309A;;;;N;;;;;
// U+3133 HANGUL LETTER KIYEOK-SIOS;Lo;0;L;<compat> 11AA;;;;N;HANGUL LETTER GIYEOG SIOS;;;;
// U+318E HANGUL LETTER ARAEAE;Lo;0;L;<compat> 11A1;;;;N;HANGUL LETTER ALAE AE;;;;
// U+FFA3 HALFWIDTH HANGUL LETTER KIYEOK-SIOS;Lo;0;L;<narrow> 3133;;;;N;HALFWIDTH HANGUL LETTER GIYEOG SIOS;;;;
// U+FFDC HALFWIDTH HANGUL LETTER I;Lo;0;L;<narrow> 3163;;;;N;;;;;
if i != 0xFF9E && i != 0xFF9F && !(0x3133 <= i && i <= 0x318E) && !(0xFFA3 <= i && i <= 0xFFDC) {
log.Fatalf("%U: nLead was %v; want %v", i, a, b)
}
}
}
nfc := c.forms[FCanonical]
nfkc := c.forms[FCompatibility]
if nfc.combinesBackward != nfkc.combinesBackward {
log.Fatalf("%U: Cannot combine combinesBackward\n", c.codePoint)
}
}
}
// Use values in DerivedNormalizationProps.txt to compare against the
// values we computed.
// DerivedNormalizationProps.txt has form:
// 00C0..00C5 ; NFD_QC; N # ...
// 0374 ; NFD_QC; N # ...
// See http://unicode.org/reports/tr44/ for full explanation
func testDerived() {
f := gen.OpenUCDFile("DerivedNormalizationProps.txt")
defer f.Close()
p := ucd.New(f)
for p.Next() {
r := p.Rune(0)
c := &chars[r]
var ftype, mode int
qt := p.String(1)
switch qt {
case "NFC_QC":
ftype, mode = FCanonical, MComposed
case "NFD_QC":
ftype, mode = FCanonical, MDecomposed
case "NFKC_QC":
ftype, mode = FCompatibility, MComposed
case "NFKD_QC":
ftype, mode = FCompatibility, MDecomposed
default:
continue
}
var qr QCResult
switch p.String(2) {
case "Y":
qr = QCYes
case "N":
qr = QCNo
case "M":
qr = QCMaybe
default:
log.Fatalf(`Unexpected quick check value "%s"`, p.String(2))
}
if got := c.forms[ftype].quickCheck[mode]; got != qr {
log.Printf("%U: FAILED %s (was %v need %v)\n", r, qt, got, qr)
}
c.forms[ftype].verified[mode] = true
}
if err := p.Err(); err != nil {
log.Fatal(err)
}
// Any unspecified value must be QCYes. Verify this.
for i, c := range chars {
for j, fd := range c.forms {
for k, qr := range fd.quickCheck {
if !fd.verified[k] && qr != QCYes {
m := "%U: FAIL F:%d M:%d (was %v need Yes) %s\n"
log.Printf(m, i, j, k, qr, c.name)
}
}
}
}
}
var testHeader = `const (
Yes = iota
No
Maybe
)
type formData struct {
qc uint8
combinesForward bool
decomposition string
}
type runeData struct {
r rune
ccc uint8
nLead uint8
nTrail uint8
f [2]formData // 0: canonical; 1: compatibility
}
func f(qc uint8, cf bool, dec string) [2]formData {
return [2]formData{{qc, cf, dec}, {qc, cf, dec}}
}
func g(qc, qck uint8, cf, cfk bool, d, dk string) [2]formData {
return [2]formData{{qc, cf, d}, {qck, cfk, dk}}
}
var testData = []runeData{
`
func printTestdata() {
type lastInfo struct {
ccc uint8
nLead uint8
nTrail uint8
f string
}
last := lastInfo{}
w := &bytes.Buffer{}
fmt.Fprintf(w, testHeader)
for r, c := range chars {
f := c.forms[FCanonical]
qc, cf, d := f.quickCheck[MComposed], f.combinesForward, string(f.expandedDecomp)
f = c.forms[FCompatibility]
qck, cfk, dk := f.quickCheck[MComposed], f.combinesForward, string(f.expandedDecomp)
s := ""
if d == dk && qc == qck && cf == cfk {
s = fmt.Sprintf("f(%s, %v, %q)", qc, cf, d)
} else {
s = fmt.Sprintf("g(%s, %s, %v, %v, %q, %q)", qc, qck, cf, cfk, d, dk)
}
current := lastInfo{c.ccc, c.nLeadingNonStarters, c.nTrailingNonStarters, s}
if last != current {
fmt.Fprintf(w, "\t{0x%x, %d, %d, %d, %s},\n", r, c.origCCC, c.nLeadingNonStarters, c.nTrailingNonStarters, s)
last = current
}
}
fmt.Fprintln(w, "}")
gen.WriteVersionedGoFile("data_test.go", "norm", w.Bytes())
}
+8 -7
View File
@@ -18,19 +18,20 @@ import (
// A Form denotes a canonical representation of Unicode code points. // A Form denotes a canonical representation of Unicode code points.
// The Unicode-defined normalization and equivalence forms are: // The Unicode-defined normalization and equivalence forms are:
// //
// NFC Unicode Normalization Form C // NFC Unicode Normalization Form C
// NFD Unicode Normalization Form D // NFD Unicode Normalization Form D
// NFKC Unicode Normalization Form KC // NFKC Unicode Normalization Form KC
// NFKD Unicode Normalization Form KD // NFKD Unicode Normalization Form KD
// //
// For a Form f, this documentation uses the notation f(x) to mean // For a Form f, this documentation uses the notation f(x) to mean
// the bytes or string x converted to the given form. // the bytes or string x converted to the given form.
// A position n in x is called a boundary if conversion to the form can // A position n in x is called a boundary if conversion to the form can
// proceed independently on both sides: // proceed independently on both sides:
// f(x) == append(f(x[0:n]), f(x[n:])...)
// //
// References: http://unicode.org/reports/tr15/ and // f(x) == append(f(x[0:n]), f(x[n:])...)
// http://unicode.org/notes/tn5/. //
// References: https://unicode.org/reports/tr15/ and
// https://unicode.org/notes/tn5/.
type Form int type Form int
const ( const (
+2 -2
View File
@@ -60,8 +60,8 @@ func (w *normWriter) Close() error {
} }
// Writer returns a new writer that implements Write(b) // Writer returns a new writer that implements Write(b)
// by writing f(b) to w. The returned writer may use an // by writing f(b) to w. The returned writer may use an
// an internal buffer to maintain state across Write calls. // internal buffer to maintain state across Write calls.
// Calling its Close method writes any buffered data to w. // Calling its Close method writes any buffered data to w.
func (f Form) Writer(w io.Writer) io.WriteCloser { func (f Form) Writer(w io.Writer) io.WriteCloser {
wr := &normWriter{rb: reorderBuffer{}, w: w} wr := &normWriter{rb: reorderBuffer{}, w: w}
+948 -944
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+948 -944
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -18,7 +18,6 @@ func (Form) Reset() {}
// Users should either catch ErrShortDst and allow dst to grow or have dst be at // Users should either catch ErrShortDst and allow dst to grow or have dst be at
// least of size MaxTransformChunkSize to be guaranteed of progress. // least of size MaxTransformChunkSize to be guaranteed of progress.
func (f Form) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { func (f Form) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
n := 0
// Cap the maximum number of src bytes to check. // Cap the maximum number of src bytes to check.
b := src b := src
eof := atEOF eof := atEOF
@@ -27,13 +26,14 @@ func (f Form) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error)
eof = false eof = false
b = b[:ns] b = b[:ns]
} }
i, ok := formTable[f].quickSpan(inputBytes(b), n, len(b), eof) i, ok := formTable[f].quickSpan(inputBytes(b), 0, len(b), eof)
n += copy(dst[n:], b[n:i]) n := copy(dst, b[:i])
if !ok { if !ok {
nDst, nSrc, err = f.transform(dst[n:], src[n:], atEOF) nDst, nSrc, err = f.transform(dst[n:], src[n:], atEOF)
return nDst + n, nSrc + n, err return nDst + n, nSrc + n, err
} }
if n < len(src) && !atEOF {
if err == nil && n < len(src) && !atEOF {
err = transform.ErrShortSrc err = transform.ErrShortSrc
} }
return n, n, err return n, n, err
@@ -79,7 +79,7 @@ func (f Form) transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error)
nSrc += n nSrc += n
nDst += n nDst += n
if ok { if ok {
if n < rb.nsrc && !atEOF { if err == nil && n < rb.nsrc && !atEOF {
err = transform.ErrShortSrc err = transform.ErrShortSrc
} }
return nDst, nSrc, err return nDst, nSrc, err
+1 -1
View File
@@ -29,7 +29,7 @@ var (
nfkcData = newNfkcTrie(0) nfkcData = newNfkcTrie(0)
) )
// lookupValue determines the type of block n and looks up the value for b. // lookup determines the type of block n and looks up the value for b.
// For n < t.cutoff, the block is a simple lookup table. Otherwise, the block // For n < t.cutoff, the block is a simple lookup table. Otherwise, the block
// is a list of ranges with an accompanying value. Given a matching range r, // is a list of ranges with an accompanying value. Given a matching range r,
// the value for b is by r.value + (b - r.lo) * stride. // the value for b is by r.value + (b - r.lo) * stride.
-117
View File
@@ -1,117 +0,0 @@
// Copyright 2011 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.
// +build ignore
// Trie table generator.
// Used by make*tables tools to generate a go file with trie data structures
// for mapping UTF-8 to a 16-bit value. All but the last byte in a UTF-8 byte
// sequence are used to lookup offsets in the index table to be used for the
// next byte. The last byte is used to index into a table with 16-bit values.
package main
import (
"fmt"
"io"
)
const maxSparseEntries = 16
type normCompacter struct {
sparseBlocks [][]uint64
sparseOffset []uint16
sparseCount int
name string
}
func mostFrequentStride(a []uint64) int {
counts := make(map[int]int)
var v int
for _, x := range a {
if stride := int(x) - v; v != 0 && stride >= 0 {
counts[stride]++
}
v = int(x)
}
var maxs, maxc int
for stride, cnt := range counts {
if cnt > maxc || (cnt == maxc && stride < maxs) {
maxs, maxc = stride, cnt
}
}
return maxs
}
func countSparseEntries(a []uint64) int {
stride := mostFrequentStride(a)
var v, count int
for _, tv := range a {
if int(tv)-v != stride {
if tv != 0 {
count++
}
}
v = int(tv)
}
return count
}
func (c *normCompacter) Size(v []uint64) (sz int, ok bool) {
if n := countSparseEntries(v); n <= maxSparseEntries {
return (n+1)*4 + 2, true
}
return 0, false
}
func (c *normCompacter) Store(v []uint64) uint32 {
h := uint32(len(c.sparseOffset))
c.sparseBlocks = append(c.sparseBlocks, v)
c.sparseOffset = append(c.sparseOffset, uint16(c.sparseCount))
c.sparseCount += countSparseEntries(v) + 1
return h
}
func (c *normCompacter) Handler() string {
return c.name + "Sparse.lookup"
}
func (c *normCompacter) Print(w io.Writer) (retErr error) {
p := func(f string, x ...interface{}) {
if _, err := fmt.Fprintf(w, f, x...); retErr == nil && err != nil {
retErr = err
}
}
ls := len(c.sparseBlocks)
p("// %sSparseOffset: %d entries, %d bytes\n", c.name, ls, ls*2)
p("var %sSparseOffset = %#v\n\n", c.name, c.sparseOffset)
ns := c.sparseCount
p("// %sSparseValues: %d entries, %d bytes\n", c.name, ns, ns*4)
p("var %sSparseValues = [%d]valueRange {", c.name, ns)
for i, b := range c.sparseBlocks {
p("\n// Block %#x, offset %#x", i, c.sparseOffset[i])
var v int
stride := mostFrequentStride(b)
n := countSparseEntries(b)
p("\n{value:%#04x,lo:%#02x},", stride, uint8(n))
for i, nv := range b {
if int(nv)-v != stride {
if v != 0 {
p(",hi:%#02x},", 0x80+i-1)
}
if nv != 0 {
p("\n{value:%#04x,lo:%#02x", nv, 0x80+i)
}
}
v = int(nv)
}
if v != 0 {
p(",hi:%#02x},", 0x80+len(b)-1)
}
}
p("\n}\n\n")
return
}
+27 -4
View File
@@ -1,10 +1,33 @@
# golang.org/x/net v0.0.0-20181017193950-04a2e542c03f # github.com/google/go-cmp v0.6.0
golang.org/x/net/http2 ## explicit
# github.com/yuin/goldmark v1.4.13
## explicit
# golang.org/x/crypto v0.46.0
## explicit
# golang.org/x/mod v0.31.0
## explicit
# golang.org/x/net v0.48.0
## explicit; go 1.24.0
golang.org/x/net/http/httpguts golang.org/x/net/http/httpguts
golang.org/x/net/http2
golang.org/x/net/http2/hpack golang.org/x/net/http2/hpack
golang.org/x/net/idna golang.org/x/net/idna
# golang.org/x/text v0.3.0 golang.org/x/net/internal/httpcommon
# golang.org/x/sync v0.19.0
## explicit
# golang.org/x/sys v0.39.0
## explicit
# golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc
## explicit
# golang.org/x/term v0.38.0
## explicit
# golang.org/x/text v0.33.0
## explicit; go 1.24.0
golang.org/x/text/secure/bidirule golang.org/x/text/secure/bidirule
golang.org/x/text/transform
golang.org/x/text/unicode/bidi golang.org/x/text/unicode/bidi
golang.org/x/text/unicode/norm golang.org/x/text/unicode/norm
golang.org/x/text/transform # golang.org/x/tools v0.40.0
## explicit
# golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
## explicit