updated dependencies
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details

This commit is contained in:
Paul 2024-04-20 09:40:17 +02:00
parent 0fa2d41f52
commit 2adc293678
54 changed files with 2337 additions and 894 deletions

16
go.mod
View File

@ -1,25 +1,25 @@
module git.paulbsd.com/paulbsd/qrz
go 1.21
go 1.22
require (
github.com/antchfx/htmlquery v1.3.0
github.com/antchfx/xpath v1.2.5 // indirect
github.com/antchfx/htmlquery v1.3.1
github.com/antchfx/xpath v1.3.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/labstack/echo/v4 v4.11.4
github.com/labstack/echo/v4 v4.12.0
github.com/lib/pq v1.10.9
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.23.0 // indirect
github.com/robfig/cron v1.2.0
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/net v0.22.0
golang.org/x/sys v0.18.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/net v0.24.0
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/ini.v1 v1.67.0
xorm.io/builder v0.3.13 // indirect
xorm.io/xorm v1.3.8
xorm.io/xorm v1.3.9
)
require (

15
go.sum
View File

@ -19,11 +19,15 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E=
github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8=
github.com/antchfx/htmlquery v1.3.1 h1:wm0LxjLMsZhRHfQKKZscDf2COyH4vDYA3wyH+qZ+Ylc=
github.com/antchfx/htmlquery v1.3.1/go.mod h1:PTj+f1V2zksPlwNt7uVvZPsxpKNa7mlVliCRxLX6Nx8=
github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.2.4 h1:dW1HB/JxKvGtJ9WyVGJ0sIoEcqftV3SqIstujI+B9XY=
github.com/antchfx/xpath v1.2.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.2.5 h1:hqZ+wtQ+KIOV/S3bGZcIhpgYC26um2bZYP2KVGcR7VY=
github.com/antchfx/xpath v1.2.5/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.3.0 h1:nTMlzGAK3IJ0bPpME2urTuFL76o4A96iYvoKFHRXJgc=
github.com/antchfx/xpath v1.3.0/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
@ -257,6 +261,8 @@ github.com/labstack/echo/v4 v4.11.2 h1:T+cTLQxWCDfqDEoydYm5kCobjmHwOwcv4OJAPHilm
github.com/labstack/echo/v4 v4.11.2/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws=
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
@ -494,6 +500,8 @@ golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@ -529,6 +537,7 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
@ -543,6 +552,8 @@ golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -609,6 +620,8 @@ golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -863,3 +876,5 @@ xorm.io/xorm v1.3.4 h1:vWFKzR3DhGUDl5b4srhUjhDwjxkZAc4C7BFszpu0swI=
xorm.io/xorm v1.3.4/go.mod h1:qFJGFoVYbbIdnz2vaL5OxSQ2raleMpyRRalnq3n9OJo=
xorm.io/xorm v1.3.8 h1:CJmplmWqfSRpLWSPMmqz+so8toBp3m7ehuRehIWedZo=
xorm.io/xorm v1.3.8/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
xorm.io/xorm v1.3.9 h1:TUovzS0ko+IQ1XnNLfs5dqK1cJl1H5uHpWbWqAQ04nU=
xorm.io/xorm v1.3.9/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=

View File

@ -1,16 +0,0 @@
language: go
go:
- 1.9.x
- 1.12.x
- 1.13.x
install:
- go get golang.org/x/net/html/charset
- go get golang.org/x/net/html
- go get github.com/antchfx/xpath
- go get github.com/mattn/goveralls
- go get github.com/golang/groupcache
script:
- $HOME/gopath/bin/goveralls -service=travis-ci

View File

@ -1,36 +1,32 @@
htmlquery
====
[![Build Status](https://travis-ci.org/antchfx/htmlquery.svg?branch=master)](https://travis-ci.org/antchfx/htmlquery)
[![Coverage Status](https://coveralls.io/repos/github/antchfx/htmlquery/badge.svg?branch=master)](https://coveralls.io/github/antchfx/htmlquery?branch=master)
# htmlquery
[![Build Status](https://github.com/antchfx/htmlquery/actions/workflows/testing.yml/badge.svg)](https://github.com/antchfx/htmlquery/actions/workflows/testing.yml)
[![GoDoc](https://godoc.org/github.com/antchfx/htmlquery?status.svg)](https://godoc.org/github.com/antchfx/htmlquery)
[![Go Report Card](https://goreportcard.com/badge/github.com/antchfx/htmlquery)](https://goreportcard.com/report/github.com/antchfx/htmlquery)
Overview
====
# Overview
`htmlquery` is an XPath query package for HTML, lets you extract data or evaluate from HTML documents by an XPath expression.
`htmlquery` built-in the query object caching feature based on [LRU](https://godoc.org/github.com/golang/groupcache/lru), this feature will caching the recently used XPATH query string. Enable query caching can avoid re-compile XPath expression each query.
`htmlquery` built-in the query object caching feature based on [LRU](https://godoc.org/github.com/golang/groupcache/lru), this feature will caching the recently used XPATH query string. Enable query caching can avoid re-compile XPath expression each query.
You can visit this page to learn about the supported XPath(1.0/2.0) syntax. https://github.com/antchfx/xpath
XPath query packages for Go
===
# XPath query packages for Go
| Name | Description |
| ------------------------------------------------- | ----------------------------------------- |
| [htmlquery](https://github.com/antchfx/htmlquery) | XPath query package for the HTML document |
| [xmlquery](https://github.com/antchfx/xmlquery) | XPath query package for the XML document |
| [jsonquery](https://github.com/antchfx/jsonquery) | XPath query package for the JSON document |
Installation
====
# Installation
```
go get github.com/antchfx/htmlquery
```
Getting Started
====
# Getting Started
#### Query, returns matched elements or error.
@ -70,15 +66,15 @@ list := htmlquery.Find(doc, "//a")
#### Find all A elements that have `href` attribute.
```go
list := htmlquery.Find(doc, "//a[@href]")
list := htmlquery.Find(doc, "//a[@href]")
```
#### Find all A elements with `href` attribute and only return `href` value.
```go
list := htmlquery.Find(doc, "//a/@href")
list := htmlquery.Find(doc, "//a/@href")
for _ , n := range list{
fmt.Println(htmlquery.SelectAttr(n, "href")) // output @href value
fmt.Println(htmlquery.InnerText(n)) // output @href value
}
```
@ -89,6 +85,7 @@ a := htmlquery.FindOne(doc, "//a[3]")
```
### Find children element (img) under A `href` and print the source
```go
a := htmlquery.FindOne(doc, "//a")
img := htmlquery.FindOne(a, "//img")
@ -103,9 +100,7 @@ v := expr.Evaluate(htmlquery.CreateXPathNavigator(doc)).(float64)
fmt.Printf("total count is %f", v)
```
Quick Starts
===
# Quick Starts
```go
func main() {
@ -127,9 +122,7 @@ func main() {
}
```
FAQ
====
# FAQ
#### `Find()` vs `QueryAll()`, which is better?
@ -158,6 +151,6 @@ BenchmarkDisableSelectorCache-4 500000 3162 ns/op
htmlquery.DisableSelectorCache = true
```
Questions
===
# Questions
Please let me know if you have any questions.

View File

@ -5,6 +5,8 @@ package htmlquery
import (
"bufio"
"compress/gzip"
"compress/zlib"
"fmt"
"io"
"net/http"
@ -88,15 +90,44 @@ func QuerySelectorAll(top *html.Node, selector *xpath.Expr) []*html.Node {
return elems
}
// LoadURL loads the HTML document from the specified URL.
// LoadURL loads the HTML document from the specified URL. Default enabling gzip on a HTTP request.
func LoadURL(url string) (*html.Node, error) {
resp, err := http.Get(url)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Enable gzip compression.
req.Header.Add("Accept-Encoding", "gzip")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
var reader io.ReadCloser
r, err := charset.NewReader(resp.Body, resp.Header.Get("Content-Type"))
defer func() {
if reader != nil {
reader.Close()
}
}()
encoding := resp.Header.Get("Content-Encoding")
switch encoding {
case "gzip":
reader, err = gzip.NewReader(resp.Body)
if err != nil {
return nil, err
}
case "deflate":
reader, err = zlib.NewReader(resp.Body)
if err != nil {
return nil, err
}
case "":
reader = resp.Body
default:
return nil, fmt.Errorf("%s compression is not support", encoding)
}
r, err := charset.NewReader(reader, resp.Header.Get("Content-Type"))
if err != nil {
return nil, err
}

View File

@ -1,12 +0,0 @@
language: go
go:
- 1.6
- 1.9
- '1.10'
install:
- go get github.com/mattn/goveralls
script:
- $HOME/gopath/bin/goveralls -service=travis-ci

View File

@ -1,14 +1,13 @@
XPath
====
# XPath
[![GoDoc](https://godoc.org/github.com/antchfx/xpath?status.svg)](https://godoc.org/github.com/antchfx/xpath)
[![Coverage Status](https://coveralls.io/repos/github/antchfx/xpath/badge.svg?branch=master)](https://coveralls.io/github/antchfx/xpath?branch=master)
[![Build Status](https://travis-ci.org/antchfx/xpath.svg?branch=master)](https://travis-ci.org/antchfx/xpath)
[![Build Status](https://github.com/antchfx/xpath/actions/workflows/testing.yml/badge.svg)](https://github.com/antchfx/xpath/actions/workflows/testing.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/antchfx/xpath)](https://goreportcard.com/report/github.com/antchfx/xpath)
XPath is Go package provides selecting nodes from XML, HTML or other documents using XPath expression.
Implementation
===
# Implementation
- [htmlquery](https://github.com/antchfx/htmlquery) - an XPath query package for HTML document
@ -16,8 +15,7 @@ Implementation
- [jsonquery](https://github.com/antchfx/jsonquery) - an XPath query package for JSON document
Supported Features
===
# Supported Features
#### The basic XPath patterns.
@ -63,11 +61,14 @@ Supported Features
- `child::*` : The child axis selects children of the current node.
- `child::node()`: Selects all the children of the context node.
- `child::text()`: Selects all text node children of the context node.
- `descendant::*` : The descendant axis selects descendants of the current node. It is equivalent to '//'.
- `descendant-or-self::*` : Selects descendants including the current node.
- `attribute::*` : Selects attributes of the current element. It is equivalent to @*
- `attribute::*` : Selects attributes of the current element. It is equivalent to @\*
- `following-sibling::*` : Selects nodes after the current node.
@ -87,27 +88,27 @@ Supported Features
#### Expressions
The gxpath supported three types: number, boolean, string.
The gxpath supported three types: number, boolean, string.
- `path` : Selects nodes based on the path.
- `a = b` : Standard comparisons.
* a = b True if a equals b.
* a != b True if a is not equal to b.
* a < b True if a is less than b.
* a <= b True if a is less than or equal to b.
* a > b True if a is greater than b.
* a >= b True if a is greater than or equal to b.
- `a = b` : True if a equals b.
- `a != b` : True if a is not equal to b.
- `a < b` : True if a is less than b.
- `a <= b` : True if a is less than or equal to b.
- `a > b` : True if a is greater than b.
- `a >= b` : True if a is greater than or equal to b.
- `a + b` : Arithmetic expressions.
* `- a` Unary minus
* a + b Add
* a - b Substract
* a * b Multiply
* a div b Divide
* a mod b Floating point mod, like Java.
- `- a` Unary minus
- `a + b` : Addition
- `a - b` : Subtraction
- `a * b` : Multiplication
- `a div b` : Division
- `a mod b` : Modulus (division remainder)
- `a or b` : Boolean `or` operation.
@ -117,49 +118,50 @@ Supported Features
- `fun(arg1, ..., argn)` : Function calls:
| Function | Supported |
| --- | --- |
`boolean()`| ✓ |
`ceiling()`| ✓ |
`choose()`| ✗ |
`concat()`| ✓ |
`contains()`| ✓ |
`count()`| ✓ |
`current()`| ✗ |
`document()`| ✗ |
`element-available()`| ✗ |
`ends-with()`| ✓ |
`false()`| ✓ |
`floor()`| ✓ |
`format-number()`| ✗ |
`function-available()`| ✗ |
`generate-id()`| ✗ |
`id()`| ✗ |
`key()`| ✗ |
`lang()`| ✗ |
`last()`| ✓ |
`local-name()`| ✓ |
`matches()`| ✓ |
`name()`| ✓ |
`namespace-uri()`| ✓ |
`normalize-space()`| ✓ |
`not()`| ✓ |
`number()`| ✓ |
`position()`| ✓ |
`replace()`| ✓ |
`reverse()`| ✓ |
`round()`| ✓ |
`starts-with()`| ✓ |
`string()`| ✓ |
`string-join()`[^1]| ✓ |
`string-length()`| ✓ |
`substring()`| ✓ |
`substring-after()`| ✓ |
`substring-before()`| ✓ |
`sum()`| ✓ |
`system-property()`| ✗ |
`translate()`| ✓ |
`true()`| ✓ |
`unparsed-entity-url()` | ✗ |
| Function | Supported |
| ----------------------- | --------- |
| `boolean()` | ✓ |
| `ceiling()` | ✓ |
| `choose()` | ✗ |
| `concat()` | ✓ |
| `contains()` | ✓ |
| `count()` | ✓ |
| `current()` | ✗ |
| `document()` | ✗ |
| `element-available()` | ✗ |
| `ends-with()` | ✓ |
| `false()` | ✓ |
| `floor()` | ✓ |
| `format-number()` | ✗ |
| `function-available()` | ✗ |
| `generate-id()` | ✗ |
| `id()` | ✗ |
| `key()` | ✗ |
| `lang()` | ✗ |
| `last()` | ✓ |
| `local-name()` | ✓ |
| `lower-case()`[^1] | ✓ |
| `matches()` | ✓ |
| `name()` | ✓ |
| `namespace-uri()` | ✓ |
| `normalize-space()` | ✓ |
| `not()` | ✓ |
| `number()` | ✓ |
| `position()` | ✓ |
| `replace()` | ✓ |
| `reverse()` | ✓ |
| `round()` | ✓ |
| `starts-with()` | ✓ |
| `string()` | ✓ |
| `string-join()`[^1] | ✓ |
| `string-length()` | ✓ |
| `substring()` | ✓ |
| `substring-after()` | ✓ |
| `substring-before()` | ✓ |
| `sum()` | ✓ |
| `system-property()` | ✗ |
| `translate()` | ✓ |
| `true()` | ✓ |
| `unparsed-entity-url()` | ✗ |
[^1]: XPath-2.0 expression
[^1]: XPath-2.0 expression

View File

@ -7,15 +7,39 @@ import (
type flag int
const (
noneFlag flag = iota
filterFlag
)
var flagsEnum = struct {
None flag
SmartDesc flag
PosFilter flag
Filter flag
Condition flag
}{
None: 0,
SmartDesc: 1,
PosFilter: 2,
Filter: 4,
Condition: 8,
}
type builderProp int
var builderProps = struct {
None builderProp
PosFilter builderProp
HasPosition builderProp
HasLast builderProp
NonFlat builderProp
}{
None: 0,
PosFilter: 1,
HasPosition: 2,
HasLast: 4,
NonFlat: 8,
}
// builder provides building an XPath expressions.
type builder struct {
depth int
flag flag
parseDepth int
firstInput query
}
@ -63,23 +87,26 @@ func axisPredicate(root *axisNode) func(NodeNavigator) bool {
return predicate
}
// processAxisNode processes a query for the XPath axis node.
func (b *builder) processAxisNode(root *axisNode) (query, error) {
// processAxis processes a query for the XPath axis node.
func (b *builder) processAxis(root *axisNode, flags flag, props *builderProp) (query, error) {
var (
err error
qyInput query
qyOutput query
predicate = axisPredicate(root)
err error
qyInput query
qyOutput query
)
b.firstInput = nil
predicate := axisPredicate(root)
if root.Input == nil {
qyInput = &contextQuery{}
*props = builderProps.None
} else {
inputFlags := flagsEnum.None
if root.AxeType == "child" && (root.Input.Type() == nodeAxis) {
if input := root.Input.(*axisNode); input.AxeType == "descendant-or-self" {
var qyGrandInput query
if input.Input != nil {
qyGrandInput, _ = b.processNode(input.Input)
qyGrandInput, _ = b.processNode(input.Input, flagsEnum.SmartDesc, props)
} else {
qyGrandInput = &contextQuery{}
}
@ -94,14 +121,14 @@ func (b *builder) processAxisNode(root *axisNode) (query, error) {
}
return v
}
// fix `//*[contains(@id,"food")]//*[contains(@id,"food")]`, see https://github.com/antchfx/htmlquery/issues/52
// Skip the current node(Self:false) for the next descendants nodes.
_, ok := qyGrandInput.(*contextQuery)
qyOutput = &descendantQuery{Input: qyGrandInput, Predicate: filter, Self: ok}
qyOutput = &descendantQuery{name: root.LocalName, Input: qyGrandInput, Predicate: filter, Self: false}
*props |= builderProps.NonFlat
return qyOutput, nil
}
} else if ((flags & flagsEnum.Filter) == 0) && (root.AxeType == "descendant" || root.AxeType == "descendant-or-self") {
inputFlags |= flagsEnum.SmartDesc
}
qyInput, err = b.processNode(root.Input)
qyInput, err = b.processNode(root.Input, inputFlags, props)
if err != nil {
return nil, err
}
@ -109,11 +136,13 @@ func (b *builder) processAxisNode(root *axisNode) (query, error) {
switch root.AxeType {
case "ancestor":
qyOutput = &ancestorQuery{Input: qyInput, Predicate: predicate}
qyOutput = &ancestorQuery{name: root.LocalName, Input: qyInput, Predicate: predicate}
*props |= builderProps.NonFlat
case "ancestor-or-self":
qyOutput = &ancestorQuery{Input: qyInput, Predicate: predicate, Self: true}
qyOutput = &ancestorQuery{name: root.LocalName, Input: qyInput, Predicate: predicate, Self: true}
*props |= builderProps.NonFlat
case "attribute":
qyOutput = &attributeQuery{Input: qyInput, Predicate: predicate}
qyOutput = &attributeQuery{name: root.LocalName, Input: qyInput, Predicate: predicate}
case "child":
filter := func(n NodeNavigator) bool {
v := predicate(n)
@ -127,19 +156,35 @@ func (b *builder) processAxisNode(root *axisNode) (query, error) {
}
return v
}
qyOutput = &childQuery{Input: qyInput, Predicate: filter}
if (*props & builderProps.NonFlat) == 0 {
qyOutput = &childQuery{name: root.LocalName, Input: qyInput, Predicate: filter}
} else {
qyOutput = &cachedChildQuery{name: root.LocalName, Input: qyInput, Predicate: filter}
}
case "descendant":
qyOutput = &descendantQuery{Input: qyInput, Predicate: predicate}
if (flags & flagsEnum.SmartDesc) != flagsEnum.None {
qyOutput = &descendantOverDescendantQuery{name: root.LocalName, Input: qyInput, MatchSelf: false, Predicate: predicate}
} else {
qyOutput = &descendantQuery{name: root.LocalName, Input: qyInput, Predicate: predicate}
}
*props |= builderProps.NonFlat
case "descendant-or-self":
qyOutput = &descendantQuery{Input: qyInput, Predicate: predicate, Self: true}
if (flags & flagsEnum.SmartDesc) != flagsEnum.None {
qyOutput = &descendantOverDescendantQuery{name: root.LocalName, Input: qyInput, MatchSelf: true, Predicate: predicate}
} else {
qyOutput = &descendantQuery{name: root.LocalName, Input: qyInput, Predicate: predicate, Self: true}
}
*props |= builderProps.NonFlat
case "following":
qyOutput = &followingQuery{Input: qyInput, Predicate: predicate}
*props |= builderProps.NonFlat
case "following-sibling":
qyOutput = &followingQuery{Input: qyInput, Predicate: predicate, Sibling: true}
case "parent":
qyOutput = &parentQuery{Input: qyInput, Predicate: predicate}
case "preceding":
qyOutput = &precedingQuery{Input: qyInput, Predicate: predicate}
*props |= builderProps.NonFlat
case "preceding-sibling":
qyOutput = &precedingQuery{Input: qyInput, Predicate: predicate, Sibling: true}
case "self":
@ -153,56 +198,182 @@ func (b *builder) processAxisNode(root *axisNode) (query, error) {
return qyOutput, nil
}
// processFilterNode builds query for the XPath filter predicate.
func (b *builder) processFilterNode(root *filterNode) (query, error) {
b.flag |= filterFlag
func canBeNumber(q query) bool {
if q.ValueType() != xpathResultType.Any {
return q.ValueType() == xpathResultType.Number
}
return true
}
qyInput, err := b.processNode(root.Input)
// processFilterNode builds query for the XPath filter predicate.
func (b *builder) processFilter(root *filterNode, flags flag, props *builderProp) (query, error) {
first := (flags & flagsEnum.Filter) == 0
qyInput, err := b.processNode(root.Input, (flags | flagsEnum.Filter), props)
if err != nil {
return nil, err
}
qyCond, err := b.processNode(root.Condition)
firstInput := b.firstInput
var propsCond builderProp
cond, err := b.processNode(root.Condition, flags, &propsCond)
if err != nil {
return nil, err
}
qyOutput := &filterQuery{Input: qyInput, Predicate: qyCond}
return qyOutput, nil
// Checking whether is number
if canBeNumber(cond) || ((propsCond & (builderProps.HasPosition | builderProps.HasLast)) != 0) {
propsCond |= builderProps.HasPosition
flags |= flagsEnum.PosFilter
}
if root.Input.Type() != nodeFilter {
*props &= ^builderProps.PosFilter
}
if (propsCond & builderProps.HasPosition) != 0 {
*props |= builderProps.PosFilter
}
merge := (qyInput.Properties() & queryProps.Merge) != 0
if (propsCond & builderProps.HasPosition) != builderProps.None {
if (propsCond & builderProps.HasLast) != 0 {
// https://github.com/antchfx/xpath/issues/76
// https://github.com/antchfx/xpath/issues/78
if qyFunc, ok := cond.(*functionQuery); ok {
switch qyFunc.Input.(type) {
case *filterQuery:
cond = &lastQuery{Input: qyFunc.Input}
}
}
}
}
if first && firstInput != nil {
if merge && ((*props & builderProps.PosFilter) != 0) {
qyInput = &filterQuery{Input: qyInput, Predicate: cond, NoPosition: false}
var (
rootQuery = &contextQuery{}
parent query
)
switch axisQuery := firstInput.(type) {
case *ancestorQuery:
if _, ok := axisQuery.Input.(*contextQuery); !ok {
parent = axisQuery.Input
axisQuery.Input = rootQuery
}
case *attributeQuery:
if _, ok := axisQuery.Input.(*contextQuery); !ok {
parent = axisQuery.Input
axisQuery.Input = rootQuery
}
case *childQuery:
if _, ok := axisQuery.Input.(*contextQuery); !ok {
parent = axisQuery.Input
axisQuery.Input = rootQuery
}
case *cachedChildQuery:
if _, ok := axisQuery.Input.(*contextQuery); !ok {
parent = axisQuery.Input
axisQuery.Input = rootQuery
}
case *descendantQuery:
if _, ok := axisQuery.Input.(*contextQuery); !ok {
parent = axisQuery.Input
axisQuery.Input = rootQuery
}
case *followingQuery:
if _, ok := axisQuery.Input.(*contextQuery); !ok {
parent = axisQuery.Input
axisQuery.Input = rootQuery
}
case *precedingQuery:
if _, ok := axisQuery.Input.(*contextQuery); !ok {
parent = axisQuery.Input
axisQuery.Input = rootQuery
}
case *parentQuery:
if _, ok := axisQuery.Input.(*contextQuery); !ok {
parent = axisQuery.Input
axisQuery.Input = rootQuery
}
case *selfQuery:
if _, ok := axisQuery.Input.(*contextQuery); !ok {
parent = axisQuery.Input
axisQuery.Input = rootQuery
}
case *groupQuery:
if _, ok := axisQuery.Input.(*contextQuery); !ok {
parent = axisQuery.Input
axisQuery.Input = rootQuery
}
case *descendantOverDescendantQuery:
if _, ok := axisQuery.Input.(*contextQuery); !ok {
parent = axisQuery.Input
axisQuery.Input = rootQuery
}
}
b.firstInput = nil
if parent != nil {
return &mergeQuery{Input: parent, Child: qyInput}, nil
}
return qyInput, nil
}
b.firstInput = nil
}
resultQuery := &filterQuery{
Input: qyInput,
Predicate: cond,
NoPosition: (propsCond & builderProps.HasPosition) == 0,
}
return resultQuery, nil
}
// processFunctionNode processes query for the XPath function node.
func (b *builder) processFunctionNode(root *functionNode) (query, error) {
func (b *builder) processFunction(root *functionNode, props *builderProp) (query, error) {
// Reset builder props
*props = builderProps.None
var qyOutput query
switch root.FuncName {
case "lower-case":
arg, err := b.processNode(root.Args[0], flagsEnum.None, props)
if err != nil {
return nil, err
}
qyOutput = &functionQuery{Input: arg, Func: lowerCaseFunc}
case "starts-with":
arg1, err := b.processNode(root.Args[0])
arg1, err := b.processNode(root.Args[0], flagsEnum.None, props)
if err != nil {
return nil, err
}
arg2, err := b.processNode(root.Args[1])
arg2, err := b.processNode(root.Args[1], flagsEnum.None, props)
if err != nil {
return nil, err
}
qyOutput = &functionQuery{Input: b.firstInput, Func: startwithFunc(arg1, arg2)}
qyOutput = &functionQuery{Func: startwithFunc(arg1, arg2)}
case "ends-with":
arg1, err := b.processNode(root.Args[0])
arg1, err := b.processNode(root.Args[0], flagsEnum.None, props)
if err != nil {
return nil, err
}
arg2, err := b.processNode(root.Args[1])
arg2, err := b.processNode(root.Args[1], flagsEnum.None, props)
if err != nil {
return nil, err
}
qyOutput = &functionQuery{Input: b.firstInput, Func: endwithFunc(arg1, arg2)}
qyOutput = &functionQuery{Func: endwithFunc(arg1, arg2)}
case "contains":
arg1, err := b.processNode(root.Args[0])
arg1, err := b.processNode(root.Args[0], flagsEnum.None, props)
if err != nil {
return nil, err
}
arg2, err := b.processNode(root.Args[1])
arg2, err := b.processNode(root.Args[1], flagsEnum.None, props)
if err != nil {
return nil, err
}
qyOutput = &functionQuery{Input: b.firstInput, Func: containsFunc(arg1, arg2)}
qyOutput = &functionQuery{Func: containsFunc(arg1, arg2)}
case "matches":
//matches(string , pattern)
if len(root.Args) != 2 {
@ -212,10 +383,10 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
arg1, arg2 query
err error
)
if arg1, err = b.processNode(root.Args[0]); err != nil {
if arg1, err = b.processNode(root.Args[0], flagsEnum.None, props); err != nil {
return nil, err
}
if arg2, err = b.processNode(root.Args[1]); err != nil {
if arg2, err = b.processNode(root.Args[1], flagsEnum.None, props); err != nil {
return nil, err
}
// Issue #92, testing the regular expression before.
@ -224,7 +395,7 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
return nil, fmt.Errorf("matches() got error. %v", err)
}
}
qyOutput = &functionQuery{Input: b.firstInput, Func: matchesFunc(arg1, arg2)}
qyOutput = &functionQuery{Func: matchesFunc(arg1, arg2)}
case "substring":
//substring( string , start [, length] )
if len(root.Args) < 2 {
@ -234,18 +405,18 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
arg1, arg2, arg3 query
err error
)
if arg1, err = b.processNode(root.Args[0]); err != nil {
if arg1, err = b.processNode(root.Args[0], flagsEnum.None, props); err != nil {
return nil, err
}
if arg2, err = b.processNode(root.Args[1]); err != nil {
if arg2, err = b.processNode(root.Args[1], flagsEnum.None, props); err != nil {
return nil, err
}
if len(root.Args) == 3 {
if arg3, err = b.processNode(root.Args[2]); err != nil {
if arg3, err = b.processNode(root.Args[2], flagsEnum.None, props); err != nil {
return nil, err
}
}
qyOutput = &functionQuery{Input: b.firstInput, Func: substringFunc(arg1, arg2, arg3)}
qyOutput = &functionQuery{Func: substringFunc(arg1, arg2, arg3)}
case "substring-before", "substring-after":
//substring-xxxx( haystack, needle )
if len(root.Args) != 2 {
@ -255,31 +426,30 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
arg1, arg2 query
err error
)
if arg1, err = b.processNode(root.Args[0]); err != nil {
if arg1, err = b.processNode(root.Args[0], flagsEnum.None, props); err != nil {
return nil, err
}
if arg2, err = b.processNode(root.Args[1]); err != nil {
if arg2, err = b.processNode(root.Args[1], flagsEnum.None, props); err != nil {
return nil, err
}
qyOutput = &functionQuery{
Input: b.firstInput,
Func: substringIndFunc(arg1, arg2, root.FuncName == "substring-after"),
Func: substringIndFunc(arg1, arg2, root.FuncName == "substring-after"),
}
case "string-length":
// string-length( [string] )
if len(root.Args) < 1 {
return nil, errors.New("xpath: string-length function must have at least one parameter")
}
arg1, err := b.processNode(root.Args[0])
arg1, err := b.processNode(root.Args[0], flagsEnum.None, props)
if err != nil {
return nil, err
}
qyOutput = &functionQuery{Input: b.firstInput, Func: stringLengthFunc(arg1)}
qyOutput = &functionQuery{Func: stringLengthFunc(arg1)}
case "normalize-space":
if len(root.Args) == 0 {
return nil, errors.New("xpath: normalize-space function must have at least one parameter")
}
argQuery, err := b.processNode(root.Args[0])
argQuery, err := b.processNode(root.Args[0], flagsEnum.None, props)
if err != nil {
return nil, err
}
@ -293,16 +463,16 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
arg1, arg2, arg3 query
err error
)
if arg1, err = b.processNode(root.Args[0]); err != nil {
if arg1, err = b.processNode(root.Args[0], flagsEnum.None, props); err != nil {
return nil, err
}
if arg2, err = b.processNode(root.Args[1]); err != nil {
if arg2, err = b.processNode(root.Args[1], flagsEnum.None, props); err != nil {
return nil, err
}
if arg3, err = b.processNode(root.Args[2]); err != nil {
if arg3, err = b.processNode(root.Args[2], flagsEnum.None, props); err != nil {
return nil, err
}
qyOutput = &functionQuery{Input: b.firstInput, Func: replaceFunc(arg1, arg2, arg3)}
qyOutput = &functionQuery{Func: replaceFunc(arg1, arg2, arg3)}
case "translate":
//translate( string , string, string )
if len(root.Args) != 3 {
@ -312,21 +482,21 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
arg1, arg2, arg3 query
err error
)
if arg1, err = b.processNode(root.Args[0]); err != nil {
if arg1, err = b.processNode(root.Args[0], flagsEnum.None, props); err != nil {
return nil, err
}
if arg2, err = b.processNode(root.Args[1]); err != nil {
if arg2, err = b.processNode(root.Args[1], flagsEnum.None, props); err != nil {
return nil, err
}
if arg3, err = b.processNode(root.Args[2]); err != nil {
if arg3, err = b.processNode(root.Args[2], flagsEnum.None, props); err != nil {
return nil, err
}
qyOutput = &functionQuery{Input: b.firstInput, Func: translateFunc(arg1, arg2, arg3)}
qyOutput = &functionQuery{Func: translateFunc(arg1, arg2, arg3)}
case "not":
if len(root.Args) == 0 {
return nil, errors.New("xpath: not function must have at least one parameter")
}
argQuery, err := b.processNode(root.Args[0])
argQuery, err := b.processNode(root.Args[0], flagsEnum.None, props)
if err != nil {
return nil, err
}
@ -340,46 +510,46 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
err error
)
if len(root.Args) == 1 {
arg, err = b.processNode(root.Args[0])
arg, err = b.processNode(root.Args[0], flagsEnum.None, props)
if err != nil {
return nil, err
}
}
switch root.FuncName {
case "name":
qyOutput = &functionQuery{Input: b.firstInput, Func: nameFunc(arg)}
qyOutput = &functionQuery{Func: nameFunc(arg)}
case "local-name":
qyOutput = &functionQuery{Input: b.firstInput, Func: localNameFunc(arg)}
qyOutput = &functionQuery{Func: localNameFunc(arg)}
case "namespace-uri":
qyOutput = &functionQuery{Input: b.firstInput, Func: namespaceFunc(arg)}
qyOutput = &functionQuery{Func: namespaceFunc(arg)}
}
case "true", "false":
val := root.FuncName == "true"
qyOutput = &functionQuery{
Input: b.firstInput,
Func: func(_ query, _ iterator) interface{} {
return val
},
}
case "last":
switch typ := b.firstInput.(type) {
case *groupQuery, *filterQuery:
// https://github.com/antchfx/xpath/issues/76
// https://github.com/antchfx/xpath/issues/78
qyOutput = &lastQuery{Input: typ}
default:
qyOutput = &functionQuery{Input: b.firstInput, Func: lastFunc}
}
//switch typ := b.firstInput.(type) {
//case *groupQuery, *filterQuery:
// https://github.com/antchfx/xpath/issues/76
// https://github.com/antchfx/xpath/issues/78
//qyOutput = &lastQuery{Input: typ}
//default:
qyOutput = &functionQuery{Func: lastFunc}
//}
*props |= builderProps.HasLast
case "position":
qyOutput = &functionQuery{Input: b.firstInput, Func: positionFunc}
qyOutput = &functionQuery{Func: positionFunc}
*props |= builderProps.HasPosition
case "boolean", "number", "string":
inp := b.firstInput
var inp query
if len(root.Args) > 1 {
return nil, fmt.Errorf("xpath: %s function must have at most one parameter", root.FuncName)
}
if len(root.Args) == 1 {
argQuery, err := b.processNode(root.Args[0])
argQuery, err := b.processNode(root.Args[0], flagsEnum.None, props)
if err != nil {
return nil, err
}
@ -396,13 +566,10 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
}
qyOutput = f
case "count":
//if b.firstInput == nil {
// return nil, errors.New("xpath: expression must evaluate to node-set")
//}
if len(root.Args) == 0 {
return nil, fmt.Errorf("xpath: count(node-sets) function must with have parameters node-sets")
}
argQuery, err := b.processNode(root.Args[0])
argQuery, err := b.processNode(root.Args[0], flagsEnum.None, props)
if err != nil {
return nil, err
}
@ -411,7 +578,7 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
if len(root.Args) == 0 {
return nil, fmt.Errorf("xpath: sum(node-sets) function must with have parameters node-sets")
}
argQuery, err := b.processNode(root.Args[0])
argQuery, err := b.processNode(root.Args[0], flagsEnum.None, props)
if err != nil {
return nil, err
}
@ -420,7 +587,7 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
if len(root.Args) == 0 {
return nil, fmt.Errorf("xpath: ceiling(node-sets) function must with have parameters node-sets")
}
argQuery, err := b.processNode(root.Args[0])
argQuery, err := b.processNode(root.Args[0], flagsEnum.None, props)
if err != nil {
return nil, err
}
@ -440,18 +607,18 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
}
var args []query
for _, v := range root.Args {
q, err := b.processNode(v)
q, err := b.processNode(v, flagsEnum.None, props)
if err != nil {
return nil, err
}
args = append(args, q)
}
qyOutput = &functionQuery{Input: b.firstInput, Func: concatFunc(args...)}
qyOutput = &functionQuery{Func: concatFunc(args...)}
case "reverse":
if len(root.Args) == 0 {
return nil, fmt.Errorf("xpath: reverse(node-sets) function must with have parameters node-sets")
}
argQuery, err := b.processNode(root.Args[0])
argQuery, err := b.processNode(root.Args[0], flagsEnum.None, props)
if err != nil {
return nil, err
}
@ -460,11 +627,11 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
if len(root.Args) != 2 {
return nil, fmt.Errorf("xpath: string-join(node-sets, separator) function requires node-set and argument")
}
argQuery, err := b.processNode(root.Args[0])
argQuery, err := b.processNode(root.Args[0], flagsEnum.None, props)
if err != nil {
return nil, err
}
arg1, err := b.processNode(root.Args[1])
arg1, err := b.processNode(root.Args[1], flagsEnum.None, props)
if err != nil {
return nil, err
}
@ -472,18 +639,29 @@ func (b *builder) processFunctionNode(root *functionNode) (query, error) {
default:
return nil, fmt.Errorf("not yet support this function %s()", root.FuncName)
}
if funcQuery, ok := qyOutput.(*functionQuery); ok && funcQuery.Input == nil {
funcQuery.Input = b.firstInput
}
return qyOutput, nil
}
func (b *builder) processOperatorNode(root *operatorNode) (query, error) {
left, err := b.processNode(root.Left)
func (b *builder) processOperator(root *operatorNode, props *builderProp) (query, error) {
var (
leftProp builderProp
rightProp builderProp
)
left, err := b.processNode(root.Left, flagsEnum.None, &leftProp)
if err != nil {
return nil, err
}
right, err := b.processNode(root.Right)
right, err := b.processNode(root.Right, flagsEnum.None, &rightProp)
if err != nil {
return nil, err
}
*props = leftProp | rightProp
var qyOutput query
switch root.Op {
case "+", "-", "*", "div", "mod": // Numeric operator
@ -525,41 +703,45 @@ func (b *builder) processOperatorNode(root *operatorNode) (query, error) {
}
qyOutput = &booleanQuery{Left: left, Right: right, IsOr: isOr}
case "|":
*props |= builderProps.NonFlat
qyOutput = &unionQuery{Left: left, Right: right}
}
return qyOutput, nil
}
func (b *builder) processNode(root node) (q query, err error) {
if b.depth = b.depth + 1; b.depth > 1024 {
func (b *builder) processNode(root node, flags flag, props *builderProp) (q query, err error) {
if b.parseDepth = b.parseDepth + 1; b.parseDepth > 1024 {
err = errors.New("the xpath expressions is too complex")
return
}
*props = builderProps.None
switch root.Type() {
case nodeConstantOperand:
n := root.(*operandNode)
q = &constantQuery{Val: n.Val}
case nodeRoot:
q = &contextQuery{Root: true}
q = &absoluteQuery{}
case nodeAxis:
q, err = b.processAxisNode(root.(*axisNode))
q, err = b.processAxis(root.(*axisNode), flags, props)
b.firstInput = q
case nodeFilter:
q, err = b.processFilterNode(root.(*filterNode))
q, err = b.processFilter(root.(*filterNode), flags, props)
b.firstInput = q
case nodeFunction:
q, err = b.processFunctionNode(root.(*functionNode))
q, err = b.processFunction(root.(*functionNode), props)
case nodeOperator:
q, err = b.processOperatorNode(root.(*operatorNode))
q, err = b.processOperator(root.(*operatorNode), props)
case nodeGroup:
q, err = b.processNode(root.(*groupNode).Input)
q, err = b.processNode(root.(*groupNode).Input, flagsEnum.None, props)
if err != nil {
return
}
q = &groupQuery{Input: q}
b.firstInput = q
if b.firstInput == nil {
b.firstInput = q
}
}
b.parseDepth--
return
}
@ -579,5 +761,6 @@ func build(expr string, namespaces map[string]string) (q query, err error) {
}()
root := parse(expr, namespaces)
b := &builder{}
return b.processNode(root)
props := builderProps.None
return b.processNode(root, flagsEnum.None, &props)
}

View File

@ -645,3 +645,9 @@ func stringJoinFunc(arg1 query) func(query, iterator) interface{} {
return strings.Join(parts, separator)
}
}
// lower-case is XPATH function that converts a string to lower case.
func lowerCaseFunc(q query, t iterator) interface{} {
v := functionArgs(q).Evaluate(t)
return strings.ToLower(asString(t, v))
}

View File

@ -1,40 +1,12 @@
package xpath
import (
"fmt"
"reflect"
"strconv"
)
// The XPath number operator function list.
// valueType is a return value type.
type valueType int
const (
booleanType valueType = iota
numberType
stringType
nodeSetType
)
func getValueType(i interface{}) valueType {
v := reflect.ValueOf(i)
switch v.Kind() {
case reflect.Float64:
return numberType
case reflect.String:
return stringType
case reflect.Bool:
return booleanType
default:
if _, ok := i.(query); ok {
return nodeSetType
}
}
panic(fmt.Errorf("xpath unknown value type: %v", v.Kind()))
}
type logical func(iterator, string, interface{}, interface{}) bool
var logicalFuncs = [][]logical{
@ -228,50 +200,50 @@ func cmpBooleanBoolean(t iterator, op string, m, n interface{}) bool {
// eqFunc is an `=` operator.
func eqFunc(t iterator, m, n interface{}) interface{} {
t1 := getValueType(m)
t2 := getValueType(n)
t1 := getXPathType(m)
t2 := getXPathType(n)
return logicalFuncs[t1][t2](t, "=", m, n)
}
// gtFunc is an `>` operator.
func gtFunc(t iterator, m, n interface{}) interface{} {
t1 := getValueType(m)
t2 := getValueType(n)
t1 := getXPathType(m)
t2 := getXPathType(n)
return logicalFuncs[t1][t2](t, ">", m, n)
}
// geFunc is an `>=` operator.
func geFunc(t iterator, m, n interface{}) interface{} {
t1 := getValueType(m)
t2 := getValueType(n)
t1 := getXPathType(m)
t2 := getXPathType(n)
return logicalFuncs[t1][t2](t, ">=", m, n)
}
// ltFunc is an `<` operator.
func ltFunc(t iterator, m, n interface{}) interface{} {
t1 := getValueType(m)
t2 := getValueType(n)
t1 := getXPathType(m)
t2 := getXPathType(n)
return logicalFuncs[t1][t2](t, "<", m, n)
}
// leFunc is an `<=` operator.
func leFunc(t iterator, m, n interface{}) interface{} {
t1 := getValueType(m)
t2 := getValueType(n)
t1 := getXPathType(m)
t2 := getXPathType(n)
return logicalFuncs[t1][t2](t, "<=", m, n)
}
// neFunc is an `!=` operator.
func neFunc(t iterator, m, n interface{}) interface{} {
t1 := getValueType(m)
t2 := getValueType(n)
t1 := getXPathType(m)
t2 := getXPathType(n)
return logicalFuncs[t1][t2](t, "!=", m, n)
}
// orFunc is an `or` operator.
var orFunc = func(t iterator, m, n interface{}) interface{} {
t1 := getValueType(m)
t2 := getValueType(n)
t1 := getXPathType(m)
t2 := getXPathType(n)
return logicalFuncs[t1][t2](t, "or", m, n)
}

View File

@ -7,6 +7,44 @@ import (
"reflect"
)
// The return type of the XPath expression.
type resultType int
var xpathResultType = struct {
Boolean resultType
// A numeric value
Number resultType
String resultType
// A node collection.
NodeSet resultType
// Any of the XPath node types.
Any resultType
}{
Boolean: 0,
Number: 1,
String: 2,
NodeSet: 3,
Any: 4,
}
type queryProp int
var queryProps = struct {
None queryProp
Position queryProp
Count queryProp
Cached queryProp
Reverse queryProp
Merge queryProp
}{
None: 0,
Position: 1,
Count: 2,
Cached: 4,
Reverse: 8,
Merge: 16,
}
type iterator interface {
Current() NodeNavigator
}
@ -20,12 +58,15 @@ type query interface {
Evaluate(iterator) interface{}
Clone() query
// ValueType returns the value type of the current query.
ValueType() resultType
Properties() queryProp
}
// nopQuery is an empty query that always return nil for any query.
type nopQuery struct {
query
}
type nopQuery struct{}
func (nopQuery) Select(iterator) NodeNavigator { return nil }
@ -33,21 +74,23 @@ func (nopQuery) Evaluate(iterator) interface{} { return nil }
func (nopQuery) Clone() query { return nopQuery{} }
func (nopQuery) ValueType() resultType { return xpathResultType.NodeSet }
func (nopQuery) Properties() queryProp {
return queryProps.Merge | queryProps.Position | queryProps.Count | queryProps.Cached
}
// contextQuery is returns current node on the iterator object query.
type contextQuery struct {
count int
Root bool // Moving to root-level node in the current context iterator.
}
func (c *contextQuery) Select(t iterator) (n NodeNavigator) {
if c.count == 0 {
c.count++
n = t.Current().Copy()
if c.Root {
n.MoveToRoot()
}
func (c *contextQuery) Select(t iterator) NodeNavigator {
if c.count > 0 {
return nil
}
return n
c.count++
return t.Current().Copy()
}
func (c *contextQuery) Evaluate(iterator) interface{} {
@ -56,12 +99,53 @@ func (c *contextQuery) Evaluate(iterator) interface{} {
}
func (c *contextQuery) Clone() query {
return &contextQuery{Root: c.Root}
return &contextQuery{}
}
func (c *contextQuery) ValueType() resultType {
return xpathResultType.NodeSet
}
func (c *contextQuery) Properties() queryProp {
return queryProps.Merge | queryProps.Position | queryProps.Count | queryProps.Cached
}
type absoluteQuery struct {
count int
}
func (a *absoluteQuery) Select(t iterator) (n NodeNavigator) {
if a.count > 0 {
return
}
a.count++
n = t.Current().Copy()
n.MoveToRoot()
return
}
func (a *absoluteQuery) Evaluate(t iterator) interface{} {
a.count = 0
return a
}
func (a *absoluteQuery) Clone() query {
return &absoluteQuery{}
}
func (a *absoluteQuery) ValueType() resultType {
return xpathResultType.NodeSet
}
func (a *absoluteQuery) Properties() queryProp {
return queryProps.Merge | queryProps.Position | queryProps.Count | queryProps.Cached
}
// ancestorQuery is an XPath ancestor node query.(ancestor::*|ancestor-self::*)
type ancestorQuery struct {
name string
iterator func() NodeNavigator
table map[uint64]bool
Self bool
Input query
@ -69,6 +153,10 @@ type ancestorQuery struct {
}
func (a *ancestorQuery) Select(t iterator) NodeNavigator {
if a.table == nil {
a.table = make(map[uint64]bool)
}
for {
if a.iterator == nil {
node := a.Input.Select(t)
@ -78,24 +166,27 @@ func (a *ancestorQuery) Select(t iterator) NodeNavigator {
first := true
node = node.Copy()
a.iterator = func() NodeNavigator {
if first && a.Self {
if first {
first = false
if a.Predicate(node) {
if a.Self && a.Predicate(node) {
return node
}
}
for node.MoveToParent() {
if !a.Predicate(node) {
continue
if a.Predicate(node) {
return node
}
return node
}
return nil
}
}
if node := a.iterator(); node != nil {
return node
for node := a.iterator(); node != nil; node = a.iterator() {
node_id := getHashCode(node.Copy())
if _, ok := a.table[node_id]; !ok {
a.table[node_id] = true
return node
}
}
a.iterator = nil
}
@ -112,11 +203,20 @@ func (a *ancestorQuery) Test(n NodeNavigator) bool {
}
func (a *ancestorQuery) Clone() query {
return &ancestorQuery{Self: a.Self, Input: a.Input.Clone(), Predicate: a.Predicate}
return &ancestorQuery{name: a.name, Self: a.Self, Input: a.Input.Clone(), Predicate: a.Predicate}
}
func (a *ancestorQuery) ValueType() resultType {
return xpathResultType.NodeSet
}
func (a *ancestorQuery) Properties() queryProp {
return queryProps.Position | queryProps.Count | queryProps.Cached | queryProps.Merge | queryProps.Reverse
}
// attributeQuery is an XPath attribute node query.(@*)
type attributeQuery struct {
name string
iterator func() NodeNavigator
Input query
@ -162,11 +262,20 @@ func (a *attributeQuery) Test(n NodeNavigator) bool {
}
func (a *attributeQuery) Clone() query {
return &attributeQuery{Input: a.Input.Clone(), Predicate: a.Predicate}
return &attributeQuery{name: a.name, Input: a.Input.Clone(), Predicate: a.Predicate}
}
func (a *attributeQuery) ValueType() resultType {
return xpathResultType.NodeSet
}
func (a *attributeQuery) Properties() queryProp {
return queryProps.Merge
}
// childQuery is an XPath child node query.(child::*)
type childQuery struct {
name string
posit int
iterator func() NodeNavigator
@ -216,7 +325,15 @@ func (c *childQuery) Test(n NodeNavigator) bool {
}
func (c *childQuery) Clone() query {
return &childQuery{Input: c.Input.Clone(), Predicate: c.Predicate}
return &childQuery{name: c.name, Input: c.Input.Clone(), Predicate: c.Predicate}
}
func (c *childQuery) ValueType() resultType {
return xpathResultType.NodeSet
}
func (c *childQuery) Properties() queryProp {
return queryProps.Merge
}
// position returns a position of current NodeNavigator.
@ -224,8 +341,75 @@ func (c *childQuery) position() int {
return c.posit
}
type cachedChildQuery struct {
name string
posit int
iterator func() NodeNavigator
Input query
Predicate func(NodeNavigator) bool
}
func (c *cachedChildQuery) Select(t iterator) NodeNavigator {
for {
if c.iterator == nil {
c.posit = 0
node := c.Input.Select(t)
if node == nil {
return nil
}
node = node.Copy()
first := true
c.iterator = func() NodeNavigator {
for {
if (first && !node.MoveToChild()) || (!first && !node.MoveToNext()) {
return nil
}
first = false
if c.Predicate(node) {
return node
}
}
}
}
if node := c.iterator(); node != nil {
c.posit++
return node
}
c.iterator = nil
}
}
func (c *cachedChildQuery) Evaluate(t iterator) interface{} {
c.Input.Evaluate(t)
c.iterator = nil
return c
}
func (c *cachedChildQuery) position() int {
return c.posit
}
func (c *cachedChildQuery) Test(n NodeNavigator) bool {
return c.Predicate(n)
}
func (c *cachedChildQuery) Clone() query {
return &childQuery{name: c.name, Input: c.Input.Clone(), Predicate: c.Predicate}
}
func (c *cachedChildQuery) ValueType() resultType {
return xpathResultType.NodeSet
}
func (c *cachedChildQuery) Properties() queryProp {
return queryProps.Merge
}
// descendantQuery is an XPath descendant node query.(descendant::* | descendant-or-self::*)
type descendantQuery struct {
name string
iterator func() NodeNavigator
posit int
level int
@ -245,14 +429,11 @@ func (d *descendantQuery) Select(t iterator) NodeNavigator {
}
node = node.Copy()
d.level = 0
positmap := make(map[int]int)
first := true
d.iterator = func() NodeNavigator {
if first && d.Self {
if first {
first = false
if d.Predicate(node) {
d.posit = 1
positmap[d.level] = 1
if d.Self && d.Predicate(node) {
return node
}
}
@ -260,7 +441,6 @@ func (d *descendantQuery) Select(t iterator) NodeNavigator {
for {
if node.MoveToChild() {
d.level = d.level + 1
positmap[d.level] = 0
} else {
for {
if d.level == 0 {
@ -274,8 +454,6 @@ func (d *descendantQuery) Select(t iterator) NodeNavigator {
}
}
if d.Predicate(node) {
positmap[d.level]++
d.posit = positmap[d.level]
return node
}
}
@ -283,6 +461,7 @@ func (d *descendantQuery) Select(t iterator) NodeNavigator {
}
if node := d.iterator(); node != nil {
d.posit++
return node
}
d.iterator = nil
@ -309,7 +488,15 @@ func (d *descendantQuery) depth() int {
}
func (d *descendantQuery) Clone() query {
return &descendantQuery{Self: d.Self, Input: d.Input.Clone(), Predicate: d.Predicate}
return &descendantQuery{name: d.name, Self: d.Self, Input: d.Input.Clone(), Predicate: d.Predicate}
}
func (d *descendantQuery) ValueType() resultType {
return xpathResultType.NodeSet
}
func (d *descendantQuery) Properties() queryProp {
return queryProps.Merge
}
// followingQuery is an XPath following node query.(following::*|following-sibling::*)
@ -390,6 +577,14 @@ func (f *followingQuery) Clone() query {
return &followingQuery{Input: f.Input.Clone(), Sibling: f.Sibling, Predicate: f.Predicate}
}
func (f *followingQuery) ValueType() resultType {
return xpathResultType.NodeSet
}
func (f *followingQuery) Properties() queryProp {
return queryProps.Merge
}
func (f *followingQuery) position() int {
return f.posit
}
@ -471,6 +666,14 @@ func (p *precedingQuery) Clone() query {
return &precedingQuery{Input: p.Input.Clone(), Sibling: p.Sibling, Predicate: p.Predicate}
}
func (p *precedingQuery) ValueType() resultType {
return xpathResultType.NodeSet
}
func (p *precedingQuery) Properties() queryProp {
return queryProps.Merge | queryProps.Reverse
}
func (p *precedingQuery) position() int {
return p.posit
}
@ -503,6 +706,14 @@ func (p *parentQuery) Clone() query {
return &parentQuery{Input: p.Input.Clone(), Predicate: p.Predicate}
}
func (p *parentQuery) ValueType() resultType {
return xpathResultType.NodeSet
}
func (p *parentQuery) Properties() queryProp {
return queryProps.Position | queryProps.Count | queryProps.Cached | queryProps.Merge
}
func (p *parentQuery) Test(n NodeNavigator) bool {
return p.Predicate(n)
}
@ -539,12 +750,22 @@ func (s *selfQuery) Clone() query {
return &selfQuery{Input: s.Input.Clone(), Predicate: s.Predicate}
}
func (s *selfQuery) ValueType() resultType {
return xpathResultType.NodeSet
}
func (s *selfQuery) Properties() queryProp {
return queryProps.Merge
}
// filterQuery is an XPath query for predicate filter.
type filterQuery struct {
Input query
Predicate query
posit int
positmap map[int]int
Input query
Predicate query
NoPosition bool
posit int
positmap map[int]int
}
func (f *filterQuery) do(t iterator) bool {
@ -602,6 +823,14 @@ func (f *filterQuery) Clone() query {
return &filterQuery{Input: f.Input.Clone(), Predicate: f.Predicate.Clone()}
}
func (f *filterQuery) ValueType() resultType {
return xpathResultType.NodeSet
}
func (f *filterQuery) Properties() queryProp {
return (queryProps.Position | f.Input.Properties()) & (queryProps.Reverse | queryProps.Merge)
}
// functionQuery is an XPath function that returns a computed value for
// the Evaluate call of the current NodeNavigator node. Select call isn't
// applicable for functionQuery.
@ -624,6 +853,14 @@ func (f *functionQuery) Clone() query {
return &functionQuery{Input: f.Input.Clone(), Func: f.Func}
}
func (f *functionQuery) ValueType() resultType {
return xpathResultType.Any
}
func (f *functionQuery) Properties() queryProp {
return queryProps.Merge
}
// transformFunctionQuery diffs from functionQuery where the latter computes a scalar
// value (number,string,boolean) for the current NodeNavigator node while the former
// (transformFunctionQuery) performs a mapping or transform of the current NodeNavigator
@ -652,6 +889,14 @@ func (f *transformFunctionQuery) Clone() query {
return &transformFunctionQuery{Input: f.Input.Clone(), Func: f.Func}
}
func (f *transformFunctionQuery) ValueType() resultType {
return xpathResultType.Any
}
func (f *transformFunctionQuery) Properties() queryProp {
return queryProps.Merge
}
// constantQuery is an XPath constant operand.
type constantQuery struct {
Val interface{}
@ -669,6 +914,14 @@ func (c *constantQuery) Clone() query {
return c
}
func (c *constantQuery) ValueType() resultType {
return getXPathType(c.Val)
}
func (c *constantQuery) Properties() queryProp {
return queryProps.Position | queryProps.Count | queryProps.Cached | queryProps.Merge
}
type groupQuery struct {
posit int
@ -692,6 +945,14 @@ func (g *groupQuery) Clone() query {
return &groupQuery{Input: g.Input.Clone()}
}
func (g *groupQuery) ValueType() resultType {
return g.Input.ValueType()
}
func (g *groupQuery) Properties() queryProp {
return queryProps.Position
}
func (g *groupQuery) position() int {
return g.posit
}
@ -726,6 +987,14 @@ func (l *logicalQuery) Clone() query {
return &logicalQuery{Left: l.Left.Clone(), Right: l.Right.Clone(), Do: l.Do}
}
func (l *logicalQuery) ValueType() resultType {
return xpathResultType.Boolean
}
func (l *logicalQuery) Properties() queryProp {
return queryProps.Merge
}
// numericQuery is an XPath numeric operator expression.
type numericQuery struct {
Left, Right query
@ -747,6 +1016,14 @@ func (n *numericQuery) Clone() query {
return &numericQuery{Left: n.Left.Clone(), Right: n.Right.Clone(), Do: n.Do}
}
func (n *numericQuery) ValueType() resultType {
return xpathResultType.Number
}
func (n *numericQuery) Properties() queryProp {
return queryProps.Merge
}
type booleanQuery struct {
IsOr bool
Left, Right query
@ -837,6 +1114,14 @@ func (b *booleanQuery) Clone() query {
return &booleanQuery{IsOr: b.IsOr, Left: b.Left.Clone(), Right: b.Right.Clone()}
}
func (b *booleanQuery) ValueType() resultType {
return xpathResultType.Boolean
}
func (b *booleanQuery) Properties() queryProp {
return queryProps.Merge
}
type unionQuery struct {
Left, Right query
iterator func() NodeNavigator
@ -894,6 +1179,14 @@ func (u *unionQuery) Clone() query {
return &unionQuery{Left: u.Left.Clone(), Right: u.Right.Clone()}
}
func (u *unionQuery) ValueType() resultType {
return xpathResultType.NodeSet
}
func (u *unionQuery) Properties() queryProp {
return queryProps.Merge
}
type lastQuery struct {
buffer []NodeNavigator
counted bool
@ -923,6 +1216,147 @@ func (q *lastQuery) Clone() query {
return &lastQuery{Input: q.Input.Clone()}
}
func (q *lastQuery) ValueType() resultType {
return xpathResultType.Number
}
func (q *lastQuery) Properties() queryProp {
return queryProps.Merge
}
type descendantOverDescendantQuery struct {
name string
level int
posit int
currentNode NodeNavigator
Input query
MatchSelf bool
Predicate func(NodeNavigator) bool
}
func (d *descendantOverDescendantQuery) moveToFirstChild() bool {
if d.currentNode.MoveToChild() {
d.level++
return true
}
return false
}
func (d *descendantOverDescendantQuery) moveUpUntilNext() bool {
for !d.currentNode.MoveToNext() {
d.level--
if d.level == 0 {
return false
}
d.currentNode.MoveToParent()
}
return true
}
func (d *descendantOverDescendantQuery) Select(t iterator) NodeNavigator {
for {
if d.level == 0 {
node := d.Input.Select(t)
if node == nil {
return nil
}
d.currentNode = node.Copy()
d.posit = 0
if d.MatchSelf && d.Predicate(d.currentNode) {
d.posit = 1
return d.currentNode
}
d.moveToFirstChild()
} else if !d.moveUpUntilNext() {
continue
}
for ok := true; ok; ok = d.moveToFirstChild() {
if d.Predicate(d.currentNode) {
d.posit++
return d.currentNode
}
}
}
}
func (d *descendantOverDescendantQuery) Evaluate(t iterator) interface{} {
d.Input.Evaluate(t)
return d
}
func (d *descendantOverDescendantQuery) Clone() query {
return &descendantOverDescendantQuery{Input: d.Input.Clone(), Predicate: d.Predicate, MatchSelf: d.MatchSelf}
}
func (d *descendantOverDescendantQuery) ValueType() resultType {
return xpathResultType.NodeSet
}
func (d *descendantOverDescendantQuery) Properties() queryProp {
return queryProps.Merge
}
func (d *descendantOverDescendantQuery) position() int {
return d.posit
}
type mergeQuery struct {
Input query
Child query
iterator func() NodeNavigator
}
func (m *mergeQuery) Select(t iterator) NodeNavigator {
for {
if m.iterator == nil {
root := m.Input.Select(t)
if root == nil {
return nil
}
m.Child.Evaluate(t)
root = root.Copy()
t.Current().MoveTo(root)
var list []NodeNavigator
for node := m.Child.Select(t); node != nil; node = m.Child.Select(t) {
list = append(list, node.Copy())
}
i := 0
m.iterator = func() NodeNavigator {
if i >= len(list) {
return nil
}
result := list[i]
i++
return result
}
}
if node := m.iterator(); node != nil {
return node
}
m.iterator = nil
}
}
func (m *mergeQuery) Evaluate(t iterator) interface{} {
m.Input.Evaluate(t)
return m
}
func (m *mergeQuery) Clone() query {
return &mergeQuery{Input: m.Input.Clone(), Child: m.Child.Clone()}
}
func (m *mergeQuery) ValueType() resultType {
return xpathResultType.NodeSet
}
func (m *mergeQuery) Properties() queryProp {
return queryProps.Position | queryProps.Count | queryProps.Cached | queryProps.Merge
}
func getHashCode(n NodeNavigator) uint64 {
var sb bytes.Buffer
switch n.NodeType() {
@ -958,7 +1392,7 @@ func getHashCode(n NodeNavigator) uint64 {
}
}
h := fnv.New64a()
h.Write([]byte(sb.String()))
h.Write(sb.Bytes())
return h.Sum64()
}
@ -981,3 +1415,20 @@ func getNodeDepth(q query) int {
}
return 0
}
func getXPathType(i interface{}) resultType {
v := reflect.ValueOf(i)
switch v.Kind() {
case reflect.Float64:
return xpathResultType.Number
case reflect.String:
return xpathResultType.String
case reflect.Bool:
return xpathResultType.Boolean
default:
if _, ok := i.(query); ok {
return xpathResultType.NodeSet
}
}
panic(fmt.Errorf("xpath unknown value type: %v", v.Kind()))
}

View File

@ -74,6 +74,7 @@ type NodeNavigator interface {
type NodeIterator struct {
node NodeNavigator
query query
table map[uint64]bool
}
// Current returns current node which matched.
@ -83,14 +84,22 @@ func (t *NodeIterator) Current() NodeNavigator {
// MoveNext moves Navigator to the next match node.
func (t *NodeIterator) MoveNext() bool {
n := t.query.Select(t)
if n != nil {
for {
n := t.query.Select(t)
if n == nil {
return false
}
if !t.node.MoveTo(n) {
t.node = n.Copy()
}
// https://github.com/antchfx/xpath/issues/94
id := getHashCode(n.Copy())
if _, ok := t.table[id]; ok {
continue
}
t.table[id] = true
return true
}
return false
}
// Select selects a node set using the specified XPath expression.
@ -121,14 +130,14 @@ func (expr *Expr) Evaluate(root NodeNavigator) interface{} {
val := expr.q.Evaluate(iteratorFunc(func() NodeNavigator { return root }))
switch val.(type) {
case query:
return &NodeIterator{query: expr.q.Clone(), node: root}
return &NodeIterator{query: expr.q.Clone(), node: root, table: make(map[uint64]bool)}
}
return val
}
// Select selects a node set using the specified XPath expression.
func (expr *Expr) Select(root NodeNavigator) *NodeIterator {
return &NodeIterator{query: expr.q.Clone(), node: root}
return &NodeIterator{query: expr.q.Clone(), node: root, table: make(map[uint64]bool)}
}
// String returns XPath expression string.

View File

@ -1,5 +1,36 @@
# Changelog
## v4.12.0 - 2024-04-15
**Security**
* Update golang.org/x/net dep because of [GO-2024-2687](https://pkg.go.dev/vuln/GO-2024-2687) by @aldas in https://github.com/labstack/echo/pull/2625
**Enhancements**
* binder: make binding to Map work better with string destinations by @aldas in https://github.com/labstack/echo/pull/2554
* README.md: add Encore as sponsor by @marcuskohlberg in https://github.com/labstack/echo/pull/2579
* Reorder paragraphs in README.md by @aldas in https://github.com/labstack/echo/pull/2581
* CI: upgrade actions/checkout to v4 by @aldas in https://github.com/labstack/echo/pull/2584
* Remove default charset from 'application/json' Content-Type header by @doortts in https://github.com/labstack/echo/pull/2568
* CI: Use Go 1.22 by @aldas in https://github.com/labstack/echo/pull/2588
* binder: allow binding to a nil map by @georgmu in https://github.com/labstack/echo/pull/2574
* Add Skipper Unit Test In BasicBasicAuthConfig and Add More Detail Explanation regarding BasicAuthValidator by @RyoKusnadi in https://github.com/labstack/echo/pull/2461
* fix some typos by @teslaedison in https://github.com/labstack/echo/pull/2603
* fix: some typos by @pomadev in https://github.com/labstack/echo/pull/2596
* Allow ResponseWriters to unwrap writers when flushing/hijacking by @aldas in https://github.com/labstack/echo/pull/2595
* Add SPDX licence comments to files. by @aldas in https://github.com/labstack/echo/pull/2604
* Upgrade deps by @aldas in https://github.com/labstack/echo/pull/2605
* Change type definition blocks to single declarations. This helps copy… by @aldas in https://github.com/labstack/echo/pull/2606
* Fix Real IP logic by @cl-bvl in https://github.com/labstack/echo/pull/2550
* Default binder can use `UnmarshalParams(params []string) error` inter… by @aldas in https://github.com/labstack/echo/pull/2607
* Default binder can bind pointer to slice as struct field. For example `*[]string` by @aldas in https://github.com/labstack/echo/pull/2608
* Remove maxparam dependence from Context by @aldas in https://github.com/labstack/echo/pull/2611
* When route is registered with empty path it is normalized to `/`. by @aldas in https://github.com/labstack/echo/pull/2616
* proxy middleware should use httputil.ReverseProxy for SSE requests by @aldas in https://github.com/labstack/echo/pull/2624
## v4.11.4 - 2023-12-20
**Security**

View File

@ -31,6 +31,6 @@ benchmark: ## Run benchmarks
help: ## Display this help screen
@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
goversion ?= "1.17"
test_version: ## Run tests inside Docker with given version (defaults to 1.17 oldest supported). Example: make test_version goversion=1.17
goversion ?= "1.19"
test_version: ## Run tests inside Docker with given version (defaults to 1.19 oldest supported). Example: make test_version goversion=1.19
@docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make init check"

View File

@ -9,20 +9,18 @@
[![Twitter](https://img.shields.io/badge/twitter-@labstack-55acee.svg?style=flat-square)](https://twitter.com/labstack)
[![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/labstack/echo/master/LICENSE)
## Supported Go versions
## Echo
Latest version of Echo supports last four Go major [releases](https://go.dev/doc/devel/release) and might work with
older versions.
High performance, extensible, minimalist Go web framework.
As of version 4.0.0, Echo is available as a [Go module](https://github.com/golang/go/wiki/Modules).
Therefore a Go version capable of understanding /vN suffixed imports is required:
* [Official website](https://echo.labstack.com)
* [Quick start](https://echo.labstack.com/docs/quick-start)
* [Middlewares](https://echo.labstack.com/docs/category/middleware)
Any of these versions will allow you to import Echo as `github.com/labstack/echo/v4` which is the recommended
way of using Echo going forward.
Help and questions: [Github Discussions](https://github.com/labstack/echo/discussions)
For older versions, please use the latest v3 tag.
## Feature Overview
### Feature Overview
- Optimized HTTP router which smartly prioritize routes
- Build robust and scalable RESTful APIs
@ -38,6 +36,18 @@ For older versions, please use the latest v3 tag.
- Automatic TLS via Lets Encrypt
- HTTP/2 support
## Sponsors
<div>
<a href="https://encore.dev" style="display: inline-flex; align-items: center; gap: 10px">
<img src="https://user-images.githubusercontent.com/78424526/214602214-52e0483a-b5fc-4d4c-b03e-0b7b23e012df.svg" height="28px" alt="encore icon"></img>
<b>Encore the platform for building Go-based cloud backends</b>
</a>
</div>
<br/>
Click [here](https://github.com/sponsors/labstack) for more information on sponsorship.
## Benchmarks
Date: 2020/11/11<br>
@ -57,6 +67,7 @@ The benchmarks above were run on an Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz
// go get github.com/labstack/echo/{version}
go get github.com/labstack/echo/v4
```
Latest version of Echo supports last four Go major [releases](https://go.dev/doc/devel/release) and might work with older versions.
### Example
@ -117,10 +128,6 @@ of middlewares in this list.
Please send a PR to add your own library here.
## Help
- [Forum](https://github.com/labstack/echo/discussions)
## Contribute
**Use issues for everything**

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo
import (
@ -11,23 +14,28 @@ import (
"strings"
)
type (
// Binder is the interface that wraps the Bind method.
Binder interface {
Bind(i interface{}, c Context) error
}
// Binder is the interface that wraps the Bind method.
type Binder interface {
Bind(i interface{}, c Context) error
}
// DefaultBinder is the default implementation of the Binder interface.
DefaultBinder struct{}
// DefaultBinder is the default implementation of the Binder interface.
type DefaultBinder struct{}
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
// Types that don't implement this, but do implement encoding.TextUnmarshaler
// will use that interface instead.
BindUnmarshaler interface {
// UnmarshalParam decodes and assigns a value from an form or query param.
UnmarshalParam(param string) error
}
)
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
// Types that don't implement this, but do implement encoding.TextUnmarshaler
// will use that interface instead.
type BindUnmarshaler interface {
// UnmarshalParam decodes and assigns a value from an form or query param.
UnmarshalParam(param string) error
}
// bindMultipleUnmarshaler is used by binder to unmarshal multiple values from request at once to
// type implementing this interface. For example request could have multiple query fields `?a=1&a=2&b=test` in that case
// for `a` following slice `["1", "2"] will be passed to unmarshaller.
type bindMultipleUnmarshaler interface {
UnmarshalParams(params []string) error
}
// BindPathParams binds path params to bindable object
func (b *DefaultBinder) BindPathParams(c Context, i interface{}) error {
@ -131,10 +139,29 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
typ := reflect.TypeOf(destination).Elem()
val := reflect.ValueOf(destination).Elem()
// Map
if typ.Kind() == reflect.Map {
// Support binding to limited Map destinations:
// - map[string][]string,
// - map[string]string <-- (binds first value from data slice)
// - map[string]interface{}
// You are better off binding to struct but there are user who want this map feature. Source of data for these cases are:
// params,query,header,form as these sources produce string values, most of the time slice of strings, actually.
if typ.Kind() == reflect.Map && typ.Key().Kind() == reflect.String {
k := typ.Elem().Kind()
isElemInterface := k == reflect.Interface
isElemString := k == reflect.String
isElemSliceOfStrings := k == reflect.Slice && typ.Elem().Elem().Kind() == reflect.String
if !(isElemSliceOfStrings || isElemString || isElemInterface) {
return nil
}
if val.IsNil() {
val.Set(reflect.MakeMap(typ))
}
for k, v := range data {
val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v[0]))
if isElemString {
val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v[0]))
} else {
val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v))
}
}
return nil
}
@ -161,14 +188,14 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
}
structFieldKind := structField.Kind()
inputFieldName := typeField.Tag.Get(tag)
if typeField.Anonymous && structField.Kind() == reflect.Struct && inputFieldName != "" {
if typeField.Anonymous && structFieldKind == reflect.Struct && inputFieldName != "" {
// if anonymous struct with query/param/form tags, report an error
return errors.New("query/param/form tags are not allowed with anonymous struct field")
}
if inputFieldName == "" {
// If tag is nil, we inspect if the field is a not BindUnmarshaler struct and try to bind data into it (might contains fields with tags).
// structs that implement BindUnmarshaler are binded only when they have explicit tag
// structs that implement BindUnmarshaler are bound only when they have explicit tag
if _, ok := structField.Addr().Interface().(BindUnmarshaler); !ok && structFieldKind == reflect.Struct {
if err := b.bindData(structField.Addr().Interface(), data, tag); err != nil {
return err
@ -197,27 +224,46 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
continue
}
// Call this first, in case we're dealing with an alias to an array type
if ok, err := unmarshalField(typeField.Type.Kind(), inputValue[0], structField); ok {
// NOTE: algorithm here is not particularly sophisticated. It probably does not work with absurd types like `**[]*int`
// but it is smart enough to handle niche cases like `*int`,`*[]string`,`[]*int` .
// try unmarshalling first, in case we're dealing with an alias to an array type
if ok, err := unmarshalInputsToField(typeField.Type.Kind(), inputValue, structField); ok {
if err != nil {
return err
}
continue
}
numElems := len(inputValue)
if structFieldKind == reflect.Slice && numElems > 0 {
if ok, err := unmarshalInputToField(typeField.Type.Kind(), inputValue[0], structField); ok {
if err != nil {
return err
}
continue
}
// we could be dealing with pointer to slice `*[]string` so dereference it. There are wierd OpenAPI generators
// that could create struct fields like that.
if structFieldKind == reflect.Pointer {
structFieldKind = structField.Elem().Kind()
structField = structField.Elem()
}
if structFieldKind == reflect.Slice {
sliceOf := structField.Type().Elem().Kind()
numElems := len(inputValue)
slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
for j := 0; j < numElems; j++ {
if err := setWithProperType(sliceOf, inputValue[j], slice.Index(j)); err != nil {
return err
}
}
val.Field(i).Set(slice)
} else if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
return err
structField.Set(slice)
continue
}
if err := setWithProperType(structFieldKind, inputValue[0], structField); err != nil {
return err
}
}
return nil
@ -225,7 +271,7 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error {
// But also call it here, in case we're dealing with an array of BindUnmarshalers
if ok, err := unmarshalField(valueKind, val, structField); ok {
if ok, err := unmarshalInputToField(valueKind, val, structField); ok {
return err
}
@ -266,35 +312,41 @@ func setWithProperType(valueKind reflect.Kind, val string, structField reflect.V
return nil
}
func unmarshalField(valueKind reflect.Kind, val string, field reflect.Value) (bool, error) {
switch valueKind {
case reflect.Ptr:
return unmarshalFieldPtr(val, field)
default:
return unmarshalFieldNonPtr(val, field)
func unmarshalInputsToField(valueKind reflect.Kind, values []string, field reflect.Value) (bool, error) {
if valueKind == reflect.Ptr {
if field.IsNil() {
field.Set(reflect.New(field.Type().Elem()))
}
field = field.Elem()
}
fieldIValue := field.Addr().Interface()
unmarshaler, ok := fieldIValue.(bindMultipleUnmarshaler)
if !ok {
return false, nil
}
return true, unmarshaler.UnmarshalParams(values)
}
func unmarshalFieldNonPtr(value string, field reflect.Value) (bool, error) {
fieldIValue := field.Addr().Interface()
if unmarshaler, ok := fieldIValue.(BindUnmarshaler); ok {
return true, unmarshaler.UnmarshalParam(value)
func unmarshalInputToField(valueKind reflect.Kind, val string, field reflect.Value) (bool, error) {
if valueKind == reflect.Ptr {
if field.IsNil() {
field.Set(reflect.New(field.Type().Elem()))
}
field = field.Elem()
}
if unmarshaler, ok := fieldIValue.(encoding.TextUnmarshaler); ok {
return true, unmarshaler.UnmarshalText([]byte(value))
fieldIValue := field.Addr().Interface()
switch unmarshaler := fieldIValue.(type) {
case BindUnmarshaler:
return true, unmarshaler.UnmarshalParam(val)
case encoding.TextUnmarshaler:
return true, unmarshaler.UnmarshalText([]byte(val))
}
return false, nil
}
func unmarshalFieldPtr(value string, field reflect.Value) (bool, error) {
if field.IsNil() {
// Initialize the pointer to a nil value
field.Set(reflect.New(field.Type().Elem()))
}
return unmarshalFieldNonPtr(value, field.Elem())
}
func setIntField(value string, bitSize int, field reflect.Value) error {
if value == "" {
value = "0"

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo
import (

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo
import (
@ -13,204 +16,216 @@ import (
"sync"
)
type (
// Context represents the context of the current HTTP request. It holds request and
// response objects, path, path parameters, data and registered handler.
Context interface {
// Request returns `*http.Request`.
Request() *http.Request
// Context represents the context of the current HTTP request. It holds request and
// response objects, path, path parameters, data and registered handler.
type Context interface {
// Request returns `*http.Request`.
Request() *http.Request
// SetRequest sets `*http.Request`.
SetRequest(r *http.Request)
// SetRequest sets `*http.Request`.
SetRequest(r *http.Request)
// SetResponse sets `*Response`.
SetResponse(r *Response)
// SetResponse sets `*Response`.
SetResponse(r *Response)
// Response returns `*Response`.
Response() *Response
// Response returns `*Response`.
Response() *Response
// IsTLS returns true if HTTP connection is TLS otherwise false.
IsTLS() bool
// IsTLS returns true if HTTP connection is TLS otherwise false.
IsTLS() bool
// IsWebSocket returns true if HTTP connection is WebSocket otherwise false.
IsWebSocket() bool
// IsWebSocket returns true if HTTP connection is WebSocket otherwise false.
IsWebSocket() bool
// Scheme returns the HTTP protocol scheme, `http` or `https`.
Scheme() string
// Scheme returns the HTTP protocol scheme, `http` or `https`.
Scheme() string
// RealIP returns the client's network address based on `X-Forwarded-For`
// or `X-Real-IP` request header.
// The behavior can be configured using `Echo#IPExtractor`.
RealIP() string
// RealIP returns the client's network address based on `X-Forwarded-For`
// or `X-Real-IP` request header.
// The behavior can be configured using `Echo#IPExtractor`.
RealIP() string
// Path returns the registered path for the handler.
Path() string
// Path returns the registered path for the handler.
Path() string
// SetPath sets the registered path for the handler.
SetPath(p string)
// SetPath sets the registered path for the handler.
SetPath(p string)
// Param returns path parameter by name.
Param(name string) string
// Param returns path parameter by name.
Param(name string) string
// ParamNames returns path parameter names.
ParamNames() []string
// ParamNames returns path parameter names.
ParamNames() []string
// SetParamNames sets path parameter names.
SetParamNames(names ...string)
// SetParamNames sets path parameter names.
SetParamNames(names ...string)
// ParamValues returns path parameter values.
ParamValues() []string
// ParamValues returns path parameter values.
ParamValues() []string
// SetParamValues sets path parameter values.
SetParamValues(values ...string)
// SetParamValues sets path parameter values.
SetParamValues(values ...string)
// QueryParam returns the query param for the provided name.
QueryParam(name string) string
// QueryParam returns the query param for the provided name.
QueryParam(name string) string
// QueryParams returns the query parameters as `url.Values`.
QueryParams() url.Values
// QueryParams returns the query parameters as `url.Values`.
QueryParams() url.Values
// QueryString returns the URL query string.
QueryString() string
// QueryString returns the URL query string.
QueryString() string
// FormValue returns the form field value for the provided name.
FormValue(name string) string
// FormValue returns the form field value for the provided name.
FormValue(name string) string
// FormParams returns the form parameters as `url.Values`.
FormParams() (url.Values, error)
// FormParams returns the form parameters as `url.Values`.
FormParams() (url.Values, error)
// FormFile returns the multipart form file for the provided name.
FormFile(name string) (*multipart.FileHeader, error)
// FormFile returns the multipart form file for the provided name.
FormFile(name string) (*multipart.FileHeader, error)
// MultipartForm returns the multipart form.
MultipartForm() (*multipart.Form, error)
// MultipartForm returns the multipart form.
MultipartForm() (*multipart.Form, error)
// Cookie returns the named cookie provided in the request.
Cookie(name string) (*http.Cookie, error)
// Cookie returns the named cookie provided in the request.
Cookie(name string) (*http.Cookie, error)
// SetCookie adds a `Set-Cookie` header in HTTP response.
SetCookie(cookie *http.Cookie)
// SetCookie adds a `Set-Cookie` header in HTTP response.
SetCookie(cookie *http.Cookie)
// Cookies returns the HTTP cookies sent with the request.
Cookies() []*http.Cookie
// Cookies returns the HTTP cookies sent with the request.
Cookies() []*http.Cookie
// Get retrieves data from the context.
Get(key string) interface{}
// Get retrieves data from the context.
Get(key string) interface{}
// Set saves data in the context.
Set(key string, val interface{})
// Set saves data in the context.
Set(key string, val interface{})
// Bind binds path params, query params and the request body into provided type `i`. The default binder
// binds body based on Content-Type header.
Bind(i interface{}) error
// Bind binds path params, query params and the request body into provided type `i`. The default binder
// binds body based on Content-Type header.
Bind(i interface{}) error
// Validate validates provided `i`. It is usually called after `Context#Bind()`.
// Validator must be registered using `Echo#Validator`.
Validate(i interface{}) error
// Validate validates provided `i`. It is usually called after `Context#Bind()`.
// Validator must be registered using `Echo#Validator`.
Validate(i interface{}) error
// Render renders a template with data and sends a text/html response with status
// code. Renderer must be registered using `Echo.Renderer`.
Render(code int, name string, data interface{}) error
// Render renders a template with data and sends a text/html response with status
// code. Renderer must be registered using `Echo.Renderer`.
Render(code int, name string, data interface{}) error
// HTML sends an HTTP response with status code.
HTML(code int, html string) error
// HTML sends an HTTP response with status code.
HTML(code int, html string) error
// HTMLBlob sends an HTTP blob response with status code.
HTMLBlob(code int, b []byte) error
// HTMLBlob sends an HTTP blob response with status code.
HTMLBlob(code int, b []byte) error
// String sends a string response with status code.
String(code int, s string) error
// String sends a string response with status code.
String(code int, s string) error
// JSON sends a JSON response with status code.
JSON(code int, i interface{}) error
// JSON sends a JSON response with status code.
JSON(code int, i interface{}) error
// JSONPretty sends a pretty-print JSON with status code.
JSONPretty(code int, i interface{}, indent string) error
// JSONPretty sends a pretty-print JSON with status code.
JSONPretty(code int, i interface{}, indent string) error
// JSONBlob sends a JSON blob response with status code.
JSONBlob(code int, b []byte) error
// JSONBlob sends a JSON blob response with status code.
JSONBlob(code int, b []byte) error
// JSONP sends a JSONP response with status code. It uses `callback` to construct
// the JSONP payload.
JSONP(code int, callback string, i interface{}) error
// JSONP sends a JSONP response with status code. It uses `callback` to construct
// the JSONP payload.
JSONP(code int, callback string, i interface{}) error
// JSONPBlob sends a JSONP blob response with status code. It uses `callback`
// to construct the JSONP payload.
JSONPBlob(code int, callback string, b []byte) error
// JSONPBlob sends a JSONP blob response with status code. It uses `callback`
// to construct the JSONP payload.
JSONPBlob(code int, callback string, b []byte) error
// XML sends an XML response with status code.
XML(code int, i interface{}) error
// XML sends an XML response with status code.
XML(code int, i interface{}) error
// XMLPretty sends a pretty-print XML with status code.
XMLPretty(code int, i interface{}, indent string) error
// XMLPretty sends a pretty-print XML with status code.
XMLPretty(code int, i interface{}, indent string) error
// XMLBlob sends an XML blob response with status code.
XMLBlob(code int, b []byte) error
// XMLBlob sends an XML blob response with status code.
XMLBlob(code int, b []byte) error
// Blob sends a blob response with status code and content type.
Blob(code int, contentType string, b []byte) error
// Blob sends a blob response with status code and content type.
Blob(code int, contentType string, b []byte) error
// Stream sends a streaming response with status code and content type.
Stream(code int, contentType string, r io.Reader) error
// Stream sends a streaming response with status code and content type.
Stream(code int, contentType string, r io.Reader) error
// File sends a response with the content of the file.
File(file string) error
// File sends a response with the content of the file.
File(file string) error
// Attachment sends a response as attachment, prompting client to save the
// file.
Attachment(file string, name string) error
// Attachment sends a response as attachment, prompting client to save the
// file.
Attachment(file string, name string) error
// Inline sends a response as inline, opening the file in the browser.
Inline(file string, name string) error
// Inline sends a response as inline, opening the file in the browser.
Inline(file string, name string) error
// NoContent sends a response with no body and a status code.
NoContent(code int) error
// NoContent sends a response with no body and a status code.
NoContent(code int) error
// Redirect redirects the request to a provided URL with status code.
Redirect(code int, url string) error
// Redirect redirects the request to a provided URL with status code.
Redirect(code int, url string) error
// Error invokes the registered global HTTP error handler. Generally used by middleware.
// A side-effect of calling global error handler is that now Response has been committed (sent to the client) and
// middlewares up in chain can not change Response status code or Response body anymore.
//
// Avoid using this method in handlers as no middleware will be able to effectively handle errors after that.
Error(err error)
// Error invokes the registered global HTTP error handler. Generally used by middleware.
// A side-effect of calling global error handler is that now Response has been committed (sent to the client) and
// middlewares up in chain can not change Response status code or Response body anymore.
//
// Avoid using this method in handlers as no middleware will be able to effectively handle errors after that.
Error(err error)
// Handler returns the matched handler by router.
Handler() HandlerFunc
// Handler returns the matched handler by router.
Handler() HandlerFunc
// SetHandler sets the matched handler by router.
SetHandler(h HandlerFunc)
// SetHandler sets the matched handler by router.
SetHandler(h HandlerFunc)
// Logger returns the `Logger` instance.
Logger() Logger
// Logger returns the `Logger` instance.
Logger() Logger
// SetLogger Set the logger
SetLogger(l Logger)
// SetLogger Set the logger
SetLogger(l Logger)
// Echo returns the `Echo` instance.
Echo() *Echo
// Echo returns the `Echo` instance.
Echo() *Echo
// Reset resets the context after request completes. It must be called along
// with `Echo#AcquireContext()` and `Echo#ReleaseContext()`.
// See `Echo#ServeHTTP()`
Reset(r *http.Request, w http.ResponseWriter)
}
// Reset resets the context after request completes. It must be called along
// with `Echo#AcquireContext()` and `Echo#ReleaseContext()`.
// See `Echo#ServeHTTP()`
Reset(r *http.Request, w http.ResponseWriter)
}
context struct {
request *http.Request
response *Response
path string
pnames []string
pvalues []string
query url.Values
handler HandlerFunc
store Map
echo *Echo
logger Logger
lock sync.RWMutex
}
)
type context struct {
request *http.Request
response *Response
query url.Values
echo *Echo
logger Logger
store Map
lock sync.RWMutex
// following fields are set by Router
// path is route path that Router matched. It is empty string where there is no route match.
// Route registered with RouteNotFound is considered as a match and path therefore is not empty.
path string
// pnames length is tied to param count for the matched route
pnames []string
// Usually echo.Echo is sizing pvalues but there could be user created middlewares that decide to
// overwrite parameter by calling SetParamNames + SetParamValues.
// When echo.Echo allocated that slice it length/capacity is tied to echo.Echo.maxParam value.
//
// It is important that pvalues size is always equal or bigger to pnames length.
pvalues []string
handler HandlerFunc
}
const (
// ContextKeyHeaderAllow is set by Router for getting value for `Allow` header in later stages of handler call chain.
@ -329,13 +344,9 @@ func (c *context) SetParamNames(names ...string) {
c.pnames = names
l := len(names)
if *c.echo.maxParam < l {
*c.echo.maxParam = l
}
if len(c.pvalues) < l {
// Keeping the old pvalues just for backward compatibility, but it sounds that doesn't make sense to keep them,
// probably those values will be overriden in a Context#SetParamValues
// probably those values will be overridden in a Context#SetParamValues
newPvalues := make([]string, l)
copy(newPvalues, c.pvalues)
c.pvalues = newPvalues
@ -347,11 +358,11 @@ func (c *context) ParamValues() []string {
}
func (c *context) SetParamValues(values ...string) {
// NOTE: Don't just set c.pvalues = values, because it has to have length c.echo.maxParam at all times
// NOTE: Don't just set c.pvalues = values, because it has to have length c.echo.maxParam (or bigger) at all times
// It will brake the Router#Find code
limit := len(values)
if limit > *c.echo.maxParam {
limit = *c.echo.maxParam
if limit > len(c.pvalues) {
c.pvalues = make([]string, limit)
}
for i := 0; i < limit; i++ {
c.pvalues[i] = values[i]
@ -489,7 +500,7 @@ func (c *context) jsonPBlob(code int, callback string, i interface{}) (err error
}
func (c *context) json(code int, i interface{}, indent string) error {
c.writeContentType(MIMEApplicationJSONCharsetUTF8)
c.writeContentType(MIMEApplicationJSON)
c.response.Status = code
return c.echo.JSONSerializer.Serialize(c, i, indent)
}
@ -507,7 +518,7 @@ func (c *context) JSONPretty(code int, i interface{}, indent string) (err error)
}
func (c *context) JSONBlob(code int, b []byte) (err error) {
return c.Blob(code, MIMEApplicationJSONCharsetUTF8, b)
return c.Blob(code, MIMEApplicationJSON, b)
}
func (c *context) JSONP(code int, callback string, i interface{}) (err error) {
@ -642,8 +653,8 @@ func (c *context) Reset(r *http.Request, w http.ResponseWriter) {
c.path = ""
c.pnames = nil
c.logger = nil
// NOTE: Don't reset because it has to have length c.echo.maxParam at all times
for i := 0; i < *c.echo.maxParam; i++ {
// NOTE: Don't reset because it has to have length c.echo.maxParam (or bigger) at all times
for i := 0; i < len(c.pvalues); i++ {
c.pvalues[i] = ""
}
}

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo
import (

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
/*
Package echo implements high performance, minimalist Go web framework.
@ -60,97 +63,95 @@ import (
"golang.org/x/net/http2/h2c"
)
type (
// Echo is the top-level framework instance.
//
// Goroutine safety: Do not mutate Echo instance fields after server has started. Accessing these
// fields from handlers/middlewares and changing field values at the same time leads to data-races.
// Adding new routes after the server has been started is also not safe!
Echo struct {
filesystem
common
// startupMutex is mutex to lock Echo instance access during server configuration and startup. Useful for to get
// listener address info (on which interface/port was listener binded) without having data races.
startupMutex sync.RWMutex
colorer *color.Color
// Echo is the top-level framework instance.
//
// Goroutine safety: Do not mutate Echo instance fields after server has started. Accessing these
// fields from handlers/middlewares and changing field values at the same time leads to data-races.
// Adding new routes after the server has been started is also not safe!
type Echo struct {
filesystem
common
// startupMutex is mutex to lock Echo instance access during server configuration and startup. Useful for to get
// listener address info (on which interface/port was listener bound) without having data races.
startupMutex sync.RWMutex
colorer *color.Color
// premiddleware are middlewares that are run before routing is done. In case a pre-middleware returns
// an error the router is not executed and the request will end up in the global error handler.
premiddleware []MiddlewareFunc
middleware []MiddlewareFunc
maxParam *int
router *Router
routers map[string]*Router
pool sync.Pool
// premiddleware are middlewares that are run before routing is done. In case a pre-middleware returns
// an error the router is not executed and the request will end up in the global error handler.
premiddleware []MiddlewareFunc
middleware []MiddlewareFunc
maxParam *int
router *Router
routers map[string]*Router
pool sync.Pool
StdLogger *stdLog.Logger
Server *http.Server
TLSServer *http.Server
Listener net.Listener
TLSListener net.Listener
AutoTLSManager autocert.Manager
DisableHTTP2 bool
Debug bool
HideBanner bool
HidePort bool
HTTPErrorHandler HTTPErrorHandler
Binder Binder
JSONSerializer JSONSerializer
Validator Validator
Renderer Renderer
Logger Logger
IPExtractor IPExtractor
ListenerNetwork string
StdLogger *stdLog.Logger
Server *http.Server
TLSServer *http.Server
Listener net.Listener
TLSListener net.Listener
AutoTLSManager autocert.Manager
DisableHTTP2 bool
Debug bool
HideBanner bool
HidePort bool
HTTPErrorHandler HTTPErrorHandler
Binder Binder
JSONSerializer JSONSerializer
Validator Validator
Renderer Renderer
Logger Logger
IPExtractor IPExtractor
ListenerNetwork string
// OnAddRouteHandler is called when Echo adds new route to specific host router.
OnAddRouteHandler func(host string, route Route, handler HandlerFunc, middleware []MiddlewareFunc)
}
// OnAddRouteHandler is called when Echo adds new route to specific host router.
OnAddRouteHandler func(host string, route Route, handler HandlerFunc, middleware []MiddlewareFunc)
}
// Route contains a handler and information for matching against requests.
Route struct {
Method string `json:"method"`
Path string `json:"path"`
Name string `json:"name"`
}
// Route contains a handler and information for matching against requests.
type Route struct {
Method string `json:"method"`
Path string `json:"path"`
Name string `json:"name"`
}
// HTTPError represents an error that occurred while handling a request.
HTTPError struct {
Code int `json:"-"`
Message interface{} `json:"message"`
Internal error `json:"-"` // Stores the error returned by an external dependency
}
// HTTPError represents an error that occurred while handling a request.
type HTTPError struct {
Code int `json:"-"`
Message interface{} `json:"message"`
Internal error `json:"-"` // Stores the error returned by an external dependency
}
// MiddlewareFunc defines a function to process middleware.
MiddlewareFunc func(next HandlerFunc) HandlerFunc
// MiddlewareFunc defines a function to process middleware.
type MiddlewareFunc func(next HandlerFunc) HandlerFunc
// HandlerFunc defines a function to serve HTTP requests.
HandlerFunc func(c Context) error
// HandlerFunc defines a function to serve HTTP requests.
type HandlerFunc func(c Context) error
// HTTPErrorHandler is a centralized HTTP error handler.
HTTPErrorHandler func(err error, c Context)
// HTTPErrorHandler is a centralized HTTP error handler.
type HTTPErrorHandler func(err error, c Context)
// Validator is the interface that wraps the Validate function.
Validator interface {
Validate(i interface{}) error
}
// Validator is the interface that wraps the Validate function.
type Validator interface {
Validate(i interface{}) error
}
// JSONSerializer is the interface that encodes and decodes JSON to and from interfaces.
JSONSerializer interface {
Serialize(c Context, i interface{}, indent string) error
Deserialize(c Context, i interface{}) error
}
// JSONSerializer is the interface that encodes and decodes JSON to and from interfaces.
type JSONSerializer interface {
Serialize(c Context, i interface{}, indent string) error
Deserialize(c Context, i interface{}) error
}
// Renderer is the interface that wraps the Render function.
Renderer interface {
Render(io.Writer, string, interface{}, Context) error
}
// Renderer is the interface that wraps the Render function.
type Renderer interface {
Render(io.Writer, string, interface{}, Context) error
}
// Map defines a generic map of type `map[string]interface{}`.
Map map[string]interface{}
// Map defines a generic map of type `map[string]interface{}`.
type Map map[string]interface{}
// Common struct for Echo & Group.
common struct{}
)
// Common struct for Echo & Group.
type common struct{}
// HTTP methods
// NOTE: Deprecated, please use the stdlib constants directly instead.
@ -169,7 +170,12 @@ const (
// MIME types
const (
MIMEApplicationJSON = "application/json"
// MIMEApplicationJSON JavaScript Object Notation (JSON) https://www.rfc-editor.org/rfc/rfc8259
MIMEApplicationJSON = "application/json"
// Deprecated: Please use MIMEApplicationJSON instead. JSON should be encoded using UTF-8 by default.
// No "charset" parameter is defined for this registration.
// Adding one really has no effect on compliant recipients.
// See RFC 8259, section 8.1. https://datatracker.ietf.org/doc/html/rfc8259#section-8.1
MIMEApplicationJSONCharsetUTF8 = MIMEApplicationJSON + "; " + charsetUTF8
MIMEApplicationJavaScript = "application/javascript"
MIMEApplicationJavaScriptCharsetUTF8 = MIMEApplicationJavaScript + "; " + charsetUTF8
@ -259,7 +265,7 @@ const (
const (
// Version of Echo
Version = "4.11.4"
Version = "4.12.0"
website = "https://echo.labstack.com"
// http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo
banner = `
@ -274,21 +280,19 @@ ____________________________________O/_______
`
)
var (
methods = [...]string{
http.MethodConnect,
http.MethodDelete,
http.MethodGet,
http.MethodHead,
http.MethodOptions,
http.MethodPatch,
http.MethodPost,
PROPFIND,
http.MethodPut,
http.MethodTrace,
REPORT,
}
)
var methods = [...]string{
http.MethodConnect,
http.MethodDelete,
http.MethodGet,
http.MethodHead,
http.MethodOptions,
http.MethodPatch,
http.MethodPost,
PROPFIND,
http.MethodPut,
http.MethodTrace,
REPORT,
}
// Errors
var (
@ -341,22 +345,23 @@ var (
ErrInvalidListenerNetwork = errors.New("invalid listener network")
)
// Error handlers
var (
NotFoundHandler = func(c Context) error {
return ErrNotFound
}
// NotFoundHandler is the handler that router uses in case there was no matching route found. Returns an error that results
// HTTP 404 status code.
var NotFoundHandler = func(c Context) error {
return ErrNotFound
}
MethodNotAllowedHandler = func(c Context) error {
// See RFC 7231 section 7.4.1: An origin server MUST generate an Allow field in a 405 (Method Not Allowed)
// response and MAY do so in any other response. For disabled resources an empty Allow header may be returned
routerAllowMethods, ok := c.Get(ContextKeyHeaderAllow).(string)
if ok && routerAllowMethods != "" {
c.Response().Header().Set(HeaderAllow, routerAllowMethods)
}
return ErrMethodNotAllowed
// MethodNotAllowedHandler is the handler thar router uses in case there was no matching route found but there was
// another matching routes for that requested URL. Returns an error that results HTTP 405 Method Not Allowed status code.
var MethodNotAllowedHandler = func(c Context) error {
// See RFC 7231 section 7.4.1: An origin server MUST generate an Allow field in a 405 (Method Not Allowed)
// response and MAY do so in any other response. For disabled resources an empty Allow header may be returned
routerAllowMethods, ok := c.Get(ContextKeyHeaderAllow).(string)
if ok && routerAllowMethods != "" {
c.Response().Header().Set(HeaderAllow, routerAllowMethods)
}
)
return ErrMethodNotAllowed
}
// New creates an instance of Echo.
func New() (e *Echo) {
@ -414,7 +419,7 @@ func (e *Echo) Routers() map[string]*Router {
//
// NOTE: In case errors happens in middleware call-chain that is returning from handler (which did not return an error).
// When handler has already sent response (ala c.JSON()) and there is error in middleware that is returning from
// handler. Then the error that global error handler received will be ignored because we have already "commited" the
// handler. Then the error that global error handler received will be ignored because we have already "committed" the
// response and status code header has been sent to the client.
func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo
import (

View File

@ -1,21 +1,22 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo
import (
"net/http"
)
type (
// Group is a set of sub-routes for a specified route. It can be used for inner
// routes that share a common middleware or functionality that should be separate
// from the parent echo instance while still inheriting from it.
Group struct {
common
host string
prefix string
middleware []MiddlewareFunc
echo *Echo
}
)
// Group is a set of sub-routes for a specified route. It can be used for inner
// routes that share a common middleware or functionality that should be separate
// from the parent echo instance while still inheriting from it.
type Group struct {
common
host string
prefix string
middleware []MiddlewareFunc
echo *Echo
}
// Use implements `Echo#Use()` for sub-routes within the Group.
func (g *Group) Use(middleware ...MiddlewareFunc) {

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo
import (

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo
import (
@ -64,7 +67,7 @@ XFF: "x" "x, a" "x, a, b"
```
In this case, use **first _untrustable_ IP reading from right**. Never use first one reading from left, as it is
configurable by client. Here "trustable" means "you are sure the IP address belongs to your infrastructre".
configurable by client. Here "trustable" means "you are sure the IP address belongs to your infrastructure".
In above example, if `b` and `c` are trustable, the IP address of the client is `a` for both cases, never be `x`.
In Echo, use `ExtractIPFromXFFHeader(...TrustOption)`.
@ -225,15 +228,21 @@ func extractIP(req *http.Request) string {
func ExtractIPFromRealIPHeader(options ...TrustOption) IPExtractor {
checker := newIPChecker(options)
return func(req *http.Request) string {
directIP := extractIP(req)
realIP := req.Header.Get(HeaderXRealIP)
if realIP != "" {
if realIP == "" {
return directIP
}
if checker.trust(net.ParseIP(directIP)) {
realIP = strings.TrimPrefix(realIP, "[")
realIP = strings.TrimSuffix(realIP, "]")
if ip := net.ParseIP(realIP); ip != nil && checker.trust(ip) {
if rIP := net.ParseIP(realIP); rIP != nil {
return realIP
}
}
return extractIP(req)
return directIP
}
}

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo
import (

View File

@ -1,41 +1,41 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo
import (
"io"
"github.com/labstack/gommon/log"
"io"
)
type (
// Logger defines the logging interface.
Logger interface {
Output() io.Writer
SetOutput(w io.Writer)
Prefix() string
SetPrefix(p string)
Level() log.Lvl
SetLevel(v log.Lvl)
SetHeader(h string)
Print(i ...interface{})
Printf(format string, args ...interface{})
Printj(j log.JSON)
Debug(i ...interface{})
Debugf(format string, args ...interface{})
Debugj(j log.JSON)
Info(i ...interface{})
Infof(format string, args ...interface{})
Infoj(j log.JSON)
Warn(i ...interface{})
Warnf(format string, args ...interface{})
Warnj(j log.JSON)
Error(i ...interface{})
Errorf(format string, args ...interface{})
Errorj(j log.JSON)
Fatal(i ...interface{})
Fatalj(j log.JSON)
Fatalf(format string, args ...interface{})
Panic(i ...interface{})
Panicj(j log.JSON)
Panicf(format string, args ...interface{})
}
)
// Logger defines the logging interface.
type Logger interface {
Output() io.Writer
SetOutput(w io.Writer)
Prefix() string
SetPrefix(p string)
Level() log.Lvl
SetLevel(v log.Lvl)
SetHeader(h string)
Print(i ...interface{})
Printf(format string, args ...interface{})
Printj(j log.JSON)
Debug(i ...interface{})
Debugf(format string, args ...interface{})
Debugj(j log.JSON)
Info(i ...interface{})
Infof(format string, args ...interface{})
Infoj(j log.JSON)
Warn(i ...interface{})
Warnf(format string, args ...interface{})
Warnj(j log.JSON)
Error(i ...interface{})
Errorf(format string, args ...interface{})
Errorj(j log.JSON)
Fatal(i ...interface{})
Fatalj(j log.JSON)
Fatalf(format string, args ...interface{})
Panic(i ...interface{})
Panicj(j log.JSON)
Panicf(format string, args ...interface{})
}

View File

@ -1,25 +1,27 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo
import (
"bufio"
"errors"
"net"
"net/http"
)
type (
// Response wraps an http.ResponseWriter and implements its interface to be used
// by an HTTP handler to construct an HTTP response.
// See: https://golang.org/pkg/net/http/#ResponseWriter
Response struct {
echo *Echo
beforeFuncs []func()
afterFuncs []func()
Writer http.ResponseWriter
Status int
Size int64
Committed bool
}
)
// Response wraps an http.ResponseWriter and implements its interface to be used
// by an HTTP handler to construct an HTTP response.
// See: https://golang.org/pkg/net/http/#ResponseWriter
type Response struct {
echo *Echo
beforeFuncs []func()
afterFuncs []func()
Writer http.ResponseWriter
Status int
Size int64
Committed bool
}
// NewResponse creates a new instance of Response.
func NewResponse(w http.ResponseWriter, e *Echo) (r *Response) {
@ -84,14 +86,17 @@ func (r *Response) Write(b []byte) (n int, err error) {
// buffered data to the client.
// See [http.Flusher](https://golang.org/pkg/net/http/#Flusher)
func (r *Response) Flush() {
r.Writer.(http.Flusher).Flush()
err := responseControllerFlush(r.Writer)
if err != nil && errors.Is(err, http.ErrNotSupported) {
panic(errors.New("response writer flushing is not supported"))
}
}
// Hijack implements the http.Hijacker interface to allow an HTTP handler to
// take over the connection.
// See [http.Hijacker](https://golang.org/pkg/net/http/#Hijacker)
func (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return r.Writer.(http.Hijacker).Hijack()
return responseControllerHijack(r.Writer)
}
// Unwrap returns the original http.ResponseWriter.

View File

@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
//go:build !go1.20
package echo
import (
"bufio"
"fmt"
"net"
"net/http"
)
// TODO: remove when Go 1.23 is released and we do not support 1.19 anymore
func responseControllerFlush(rw http.ResponseWriter) error {
for {
switch t := rw.(type) {
case interface{ FlushError() error }:
return t.FlushError()
case http.Flusher:
t.Flush()
return nil
case interface{ Unwrap() http.ResponseWriter }:
rw = t.Unwrap()
default:
return fmt.Errorf("%w", http.ErrNotSupported)
}
}
}
// TODO: remove when Go 1.23 is released and we do not support 1.19 anymore
func responseControllerHijack(rw http.ResponseWriter) (net.Conn, *bufio.ReadWriter, error) {
for {
switch t := rw.(type) {
case http.Hijacker:
return t.Hijack()
case interface{ Unwrap() http.ResponseWriter }:
rw = t.Unwrap()
default:
return nil, nil, fmt.Errorf("%w", http.ErrNotSupported)
}
}
}

View File

@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
//go:build go1.20
package echo
import (
"bufio"
"net"
"net/http"
)
func responseControllerFlush(rw http.ResponseWriter) error {
return http.NewResponseController(rw).Flush()
}
func responseControllerHijack(rw http.ResponseWriter) (net.Conn, *bufio.ReadWriter, error) {
return http.NewResponseController(rw).Hijack()
}

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo
import (
@ -6,56 +9,58 @@ import (
"net/http"
)
type (
// Router is the registry of all registered routes for an `Echo` instance for
// request matching and URL path parameter parsing.
Router struct {
tree *node
routes map[string]*Route
echo *Echo
}
node struct {
kind kind
label byte
prefix string
parent *node
staticChildren children
originalPath string
methods *routeMethods
paramChild *node
anyChild *node
paramsCount int
// isLeaf indicates that node does not have child routes
isLeaf bool
// isHandler indicates that node has at least one handler registered to it
isHandler bool
// Router is the registry of all registered routes for an `Echo` instance for
// request matching and URL path parameter parsing.
type Router struct {
tree *node
routes map[string]*Route
echo *Echo
}
// notFoundHandler is handler registered with RouteNotFound method and is executed for 404 cases
notFoundHandler *routeMethod
}
kind uint8
children []*node
routeMethod struct {
ppath string
pnames []string
handler HandlerFunc
}
routeMethods struct {
connect *routeMethod
delete *routeMethod
get *routeMethod
head *routeMethod
options *routeMethod
patch *routeMethod
post *routeMethod
propfind *routeMethod
put *routeMethod
trace *routeMethod
report *routeMethod
anyOther map[string]*routeMethod
allowHeader string
}
)
type node struct {
kind kind
label byte
prefix string
parent *node
staticChildren children
originalPath string
methods *routeMethods
paramChild *node
anyChild *node
paramsCount int
// isLeaf indicates that node does not have child routes
isLeaf bool
// isHandler indicates that node has at least one handler registered to it
isHandler bool
// notFoundHandler is handler registered with RouteNotFound method and is executed for 404 cases
notFoundHandler *routeMethod
}
type kind uint8
type children []*node
type routeMethod struct {
ppath string
pnames []string
handler HandlerFunc
}
type routeMethods struct {
connect *routeMethod
delete *routeMethod
get *routeMethod
head *routeMethod
options *routeMethod
patch *routeMethod
post *routeMethod
propfind *routeMethod
put *routeMethod
trace *routeMethod
report *routeMethod
anyOther map[string]*routeMethod
allowHeader string
}
const (
staticKind kind = iota
@ -180,8 +185,18 @@ func (r *Router) Reverse(name string, params ...interface{}) string {
return uri.String()
}
func normalizePathSlash(path string) string {
if path == "" {
path = "/"
} else if path[0] != '/' {
path = "/" + path
}
return path
}
func (r *Router) add(method, path, name string, h HandlerFunc) *Route {
r.Add(method, path, h)
path = normalizePathSlash(path)
r.insert(method, path, h)
route := &Route{
Method: method,
@ -194,13 +209,11 @@ func (r *Router) add(method, path, name string, h HandlerFunc) *Route {
// Add registers a new route for method and path with matching handler.
func (r *Router) Add(method, path string, h HandlerFunc) {
// Validate path
if path == "" {
path = "/"
}
if path[0] != '/' {
path = "/" + path
}
r.insert(method, normalizePathSlash(path), h)
}
func (r *Router) insert(method, path string, h HandlerFunc) {
path = normalizePathSlash(path)
pnames := []string{} // Param names
ppath := path // Pristine path
@ -219,7 +232,7 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
}
j := i + 1
r.insert(method, path[:i], staticKind, routeMethod{})
r.insertNode(method, path[:i], staticKind, routeMethod{})
for ; i < lcpIndex && path[i] != '/'; i++ {
}
@ -229,21 +242,21 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
if i == lcpIndex {
// path node is last fragment of route path. ie. `/users/:id`
r.insert(method, path[:i], paramKind, routeMethod{ppath, pnames, h})
r.insertNode(method, path[:i], paramKind, routeMethod{ppath, pnames, h})
} else {
r.insert(method, path[:i], paramKind, routeMethod{})
r.insertNode(method, path[:i], paramKind, routeMethod{})
}
} else if path[i] == '*' {
r.insert(method, path[:i], staticKind, routeMethod{})
r.insertNode(method, path[:i], staticKind, routeMethod{})
pnames = append(pnames, "*")
r.insert(method, path[:i+1], anyKind, routeMethod{ppath, pnames, h})
r.insertNode(method, path[:i+1], anyKind, routeMethod{ppath, pnames, h})
}
}
r.insert(method, path, staticKind, routeMethod{ppath, pnames, h})
r.insertNode(method, path, staticKind, routeMethod{ppath, pnames, h})
}
func (r *Router) insert(method, path string, t kind, rm routeMethod) {
func (r *Router) insertNode(method, path string, t kind, rm routeMethod) {
// Adjust max param
paramLen := len(rm.pnames)
if *r.echo.maxParam < paramLen {

View File

@ -1564,6 +1564,7 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) {
if size > remainSize {
hdec.SetEmitEnabled(false)
mh.Truncated = true
remainSize = 0
return
}
remainSize -= size
@ -1576,6 +1577,36 @@ func (fr *Framer) readMetaFrame(hf *HeadersFrame) (*MetaHeadersFrame, error) {
var hc headersOrContinuation = hf
for {
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 nil, 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 nil, ConnectionError(ErrCodeProtocol)
}
if _, err := hdec.Write(frag); err != nil {
return nil, ConnectionError(ErrCodeCompression)
}

View File

@ -77,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.
// It is an error to write more data than the buffer can hold.
@ -91,6 +94,12 @@ func (p *pipe) Write(d []byte) (n int, err error) {
if p.err != nil || p.breakErr != nil {
return 0, errClosedPipeWrite
}
// pipe.setBuffer is never invoked, leaving the buffer uninitialized.
// 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)
}

View File

@ -124,6 +124,7 @@ type Server struct {
// IdleTimeout specifies how long until idle clients should be
// closed with a GOAWAY frame. PING frames are not considered
// activity for the purposes of IdleTimeout.
// If zero or negative, there is no timeout.
IdleTimeout time.Duration
// MaxUploadBufferPerConnection is the size of the initial flow
@ -434,7 +435,7 @@ func (s *Server) ServeConn(c net.Conn, opts *ServeConnOpts) {
// passes the connection off to us with the deadline already set.
// Write deadlines are set per stream in serverConn.newStream.
// Disarm the net.Conn write deadline here.
if sc.hs.WriteTimeout != 0 {
if sc.hs.WriteTimeout > 0 {
sc.conn.SetWriteDeadline(time.Time{})
}
@ -924,7 +925,7 @@ func (sc *serverConn) serve() {
sc.setConnState(http.StateActive)
sc.setConnState(http.StateIdle)
if sc.srv.IdleTimeout != 0 {
if sc.srv.IdleTimeout > 0 {
sc.idleTimer = time.AfterFunc(sc.srv.IdleTimeout, sc.onIdleTimer)
defer sc.idleTimer.Stop()
}
@ -1637,7 +1638,7 @@ func (sc *serverConn) closeStream(st *stream, err error) {
delete(sc.streams, st.id)
if len(sc.streams) == 0 {
sc.setConnState(http.StateIdle)
if sc.srv.IdleTimeout != 0 {
if sc.srv.IdleTimeout > 0 {
sc.idleTimer.Reset(sc.srv.IdleTimeout)
}
if h1ServerKeepAlivesDisabled(sc.hs) {
@ -2017,7 +2018,7 @@ func (sc *serverConn) processHeaders(f *MetaHeadersFrame) error {
// similar to how the http1 server works. Here it's
// technically more like the http1 Server's ReadHeaderTimeout
// (in Go 1.8), though. That's a more sane option anyway.
if sc.hs.ReadTimeout != 0 {
if sc.hs.ReadTimeout > 0 {
sc.conn.SetReadDeadline(time.Time{})
st.readDeadline = time.AfterFunc(sc.hs.ReadTimeout, st.onReadTimeout)
}
@ -2038,7 +2039,7 @@ func (sc *serverConn) upgradeRequest(req *http.Request) {
// Disable any read deadline set by the net/http package
// prior to the upgrade.
if sc.hs.ReadTimeout != 0 {
if sc.hs.ReadTimeout > 0 {
sc.conn.SetReadDeadline(time.Time{})
}
@ -2116,7 +2117,7 @@ func (sc *serverConn) newStream(id, pusherID uint32, state streamState) *stream
st.flow.conn = &sc.flow // link to conn-level counter
st.flow.add(sc.initialStreamSendWindowSize)
st.inflow.init(sc.srv.initialStreamRecvWindowSize())
if sc.hs.WriteTimeout != 0 {
if sc.hs.WriteTimeout > 0 {
st.writeDeadline = time.AfterFunc(sc.hs.WriteTimeout, st.onWriteTimeout)
}

331
vendor/golang.org/x/net/http2/testsync.go generated vendored Normal file
View File

@ -0,0 +1,331 @@
// 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 (
"context"
"sync"
"time"
)
// testSyncHooks coordinates goroutines in tests.
//
// For example, a call to ClientConn.RoundTrip involves several goroutines, including:
// - the goroutine running RoundTrip;
// - the clientStream.doRequest goroutine, which writes the request; and
// - the clientStream.readLoop goroutine, which reads the response.
//
// Using testSyncHooks, a test can start a RoundTrip and identify when all these goroutines
// are blocked waiting for some condition such as reading the Request.Body or waiting for
// flow control to become available.
//
// The testSyncHooks also manage timers and synthetic time in tests.
// This permits us to, for example, start a request and cause it to time out waiting for
// response headers without resorting to time.Sleep calls.
type testSyncHooks struct {
// active/inactive act as a mutex and condition variable.
//
// - neither chan contains a value: testSyncHooks is locked.
// - active contains a value: unlocked, and at least one goroutine is not blocked
// - inactive contains a value: unlocked, and all goroutines are blocked
active chan struct{}
inactive chan struct{}
// goroutine counts
total int // total goroutines
condwait map[*sync.Cond]int // blocked in sync.Cond.Wait
blocked []*testBlockedGoroutine // otherwise blocked
// fake time
now time.Time
timers []*fakeTimer
// Transport testing: Report various events.
newclientconn func(*ClientConn)
newstream func(*clientStream)
}
// testBlockedGoroutine is a blocked goroutine.
type testBlockedGoroutine struct {
f func() bool // blocked until f returns true
ch chan struct{} // closed when unblocked
}
func newTestSyncHooks() *testSyncHooks {
h := &testSyncHooks{
active: make(chan struct{}, 1),
inactive: make(chan struct{}, 1),
condwait: map[*sync.Cond]int{},
}
h.inactive <- struct{}{}
h.now = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
return h
}
// lock acquires the testSyncHooks mutex.
func (h *testSyncHooks) lock() {
select {
case <-h.active:
case <-h.inactive:
}
}
// waitInactive waits for all goroutines to become inactive.
func (h *testSyncHooks) waitInactive() {
for {
<-h.inactive
if !h.unlock() {
break
}
}
}
// unlock releases the testSyncHooks mutex.
// It reports whether any goroutines are active.
func (h *testSyncHooks) unlock() (active bool) {
// Look for a blocked goroutine which can be unblocked.
blocked := h.blocked[:0]
unblocked := false
for _, b := range h.blocked {
if !unblocked && b.f() {
unblocked = true
close(b.ch)
} else {
blocked = append(blocked, b)
}
}
h.blocked = blocked
// Count goroutines blocked on condition variables.
condwait := 0
for _, count := range h.condwait {
condwait += count
}
if h.total > condwait+len(blocked) {
h.active <- struct{}{}
return true
} else {
h.inactive <- struct{}{}
return false
}
}
// goRun starts a new goroutine.
func (h *testSyncHooks) goRun(f func()) {
h.lock()
h.total++
h.unlock()
go func() {
defer func() {
h.lock()
h.total--
h.unlock()
}()
f()
}()
}
// blockUntil indicates that a goroutine is blocked waiting for some condition to become true.
// It waits until f returns true before proceeding.
//
// Example usage:
//
// h.blockUntil(func() bool {
// // Is the context done yet?
// select {
// case <-ctx.Done():
// default:
// return false
// }
// return true
// })
// // Wait for the context to become done.
// <-ctx.Done()
//
// The function f passed to blockUntil must be non-blocking and idempotent.
func (h *testSyncHooks) blockUntil(f func() bool) {
if f() {
return
}
ch := make(chan struct{})
h.lock()
h.blocked = append(h.blocked, &testBlockedGoroutine{
f: f,
ch: ch,
})
h.unlock()
<-ch
}
// broadcast is sync.Cond.Broadcast.
func (h *testSyncHooks) condBroadcast(cond *sync.Cond) {
h.lock()
delete(h.condwait, cond)
h.unlock()
cond.Broadcast()
}
// broadcast is sync.Cond.Wait.
func (h *testSyncHooks) condWait(cond *sync.Cond) {
h.lock()
h.condwait[cond]++
h.unlock()
}
// newTimer creates a new fake timer.
func (h *testSyncHooks) newTimer(d time.Duration) timer {
h.lock()
defer h.unlock()
t := &fakeTimer{
hooks: h,
when: h.now.Add(d),
c: make(chan time.Time),
}
h.timers = append(h.timers, t)
return t
}
// afterFunc creates a new fake AfterFunc timer.
func (h *testSyncHooks) afterFunc(d time.Duration, f func()) timer {
h.lock()
defer h.unlock()
t := &fakeTimer{
hooks: h,
when: h.now.Add(d),
f: f,
}
h.timers = append(h.timers, t)
return t
}
func (h *testSyncHooks) contextWithTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(ctx)
t := h.afterFunc(d, cancel)
return ctx, func() {
t.Stop()
cancel()
}
}
func (h *testSyncHooks) timeUntilEvent() time.Duration {
h.lock()
defer h.unlock()
var next time.Time
for _, t := range h.timers {
if next.IsZero() || t.when.Before(next) {
next = t.when
}
}
if d := next.Sub(h.now); d > 0 {
return d
}
return 0
}
// advance advances time and causes synthetic timers to fire.
func (h *testSyncHooks) advance(d time.Duration) {
h.lock()
defer h.unlock()
h.now = h.now.Add(d)
timers := h.timers[:0]
for _, t := range h.timers {
t := t // remove after go.mod depends on go1.22
t.mu.Lock()
switch {
case t.when.After(h.now):
timers = append(timers, t)
case t.when.IsZero():
// stopped timer
default:
t.when = time.Time{}
if t.c != nil {
close(t.c)
}
if t.f != nil {
h.total++
go func() {
defer func() {
h.lock()
h.total--
h.unlock()
}()
t.f()
}()
}
}
t.mu.Unlock()
}
h.timers = timers
}
// A timer wraps a time.Timer, or a synthetic equivalent in tests.
// Unlike time.Timer, timer is single-use: The timer channel is closed when the timer expires.
type timer interface {
C() <-chan time.Time
Stop() bool
Reset(d time.Duration) bool
}
// timeTimer implements timer using real time.
type timeTimer struct {
t *time.Timer
c chan time.Time
}
// newTimeTimer creates a new timer using real time.
func newTimeTimer(d time.Duration) timer {
ch := make(chan time.Time)
t := time.AfterFunc(d, func() {
close(ch)
})
return &timeTimer{t, ch}
}
// newTimeAfterFunc creates an AfterFunc timer using real time.
func newTimeAfterFunc(d time.Duration, f func()) timer {
return &timeTimer{
t: time.AfterFunc(d, f),
}
}
func (t timeTimer) C() <-chan time.Time { return t.c }
func (t timeTimer) Stop() bool { return t.t.Stop() }
func (t timeTimer) Reset(d time.Duration) bool { return t.t.Reset(d) }
// fakeTimer implements timer using fake time.
type fakeTimer struct {
hooks *testSyncHooks
mu sync.Mutex
when time.Time // when the timer will fire
c chan time.Time // closed when the timer fires; mutually exclusive with f
f func() // called when the timer fires; mutually exclusive with c
}
func (t *fakeTimer) C() <-chan time.Time { return t.c }
func (t *fakeTimer) Stop() bool {
t.mu.Lock()
defer t.mu.Unlock()
stopped := t.when.IsZero()
t.when = time.Time{}
return stopped
}
func (t *fakeTimer) Reset(d time.Duration) bool {
if t.c != nil || t.f == nil {
panic("fakeTimer only supports Reset on AfterFunc timers")
}
t.mu.Lock()
defer t.mu.Unlock()
t.hooks.lock()
defer t.hooks.unlock()
active := !t.when.IsZero()
t.when = t.hooks.now.Add(d)
if !active {
t.hooks.timers = append(t.hooks.timers, t)
}
return active
}

View File

@ -147,6 +147,12 @@ type Transport struct {
// waiting for their turn.
StrictMaxConcurrentStreams bool
// IdleConnTimeout is the maximum amount of time an idle
// (keep-alive) connection will remain idle before closing
// itself.
// Zero means no limit.
IdleConnTimeout time.Duration
// ReadIdleTimeout is the timeout after which a health check using ping
// frame will be carried out if no frame is received on the connection.
// Note that a ping response will is considered a received frame, so if
@ -178,6 +184,8 @@ type Transport struct {
connPoolOnce sync.Once
connPoolOrDef ClientConnPool // non-nil version of ConnPool
syncHooks *testSyncHooks
}
func (t *Transport) maxHeaderListSize() uint32 {
@ -302,7 +310,7 @@ type ClientConn struct {
readerErr error // set before readerDone is closed
idleTimeout time.Duration // or 0 for never
idleTimer *time.Timer
idleTimer timer
mu sync.Mutex // guards following
cond *sync.Cond // hold mu; broadcast on flow/closed changes
@ -344,6 +352,60 @@ type ClientConn struct {
werr error // first write error that has occurred
hbuf bytes.Buffer // HPACK encoder writes into this
henc *hpack.Encoder
syncHooks *testSyncHooks // can be nil
}
// Hook points used for testing.
// Outside of tests, cc.syncHooks is nil and these all have minimal implementations.
// Inside tests, see the testSyncHooks function docs.
// goRun starts a new goroutine.
func (cc *ClientConn) goRun(f func()) {
if cc.syncHooks != nil {
cc.syncHooks.goRun(f)
return
}
go f()
}
// condBroadcast is cc.cond.Broadcast.
func (cc *ClientConn) condBroadcast() {
if cc.syncHooks != nil {
cc.syncHooks.condBroadcast(cc.cond)
}
cc.cond.Broadcast()
}
// condWait is cc.cond.Wait.
func (cc *ClientConn) condWait() {
if cc.syncHooks != nil {
cc.syncHooks.condWait(cc.cond)
}
cc.cond.Wait()
}
// newTimer creates a new time.Timer, or a synthetic timer in tests.
func (cc *ClientConn) newTimer(d time.Duration) timer {
if cc.syncHooks != nil {
return cc.syncHooks.newTimer(d)
}
return newTimeTimer(d)
}
// afterFunc creates a new time.AfterFunc timer, or a synthetic timer in tests.
func (cc *ClientConn) afterFunc(d time.Duration, f func()) timer {
if cc.syncHooks != nil {
return cc.syncHooks.afterFunc(d, f)
}
return newTimeAfterFunc(d, f)
}
func (cc *ClientConn) contextWithTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) {
if cc.syncHooks != nil {
return cc.syncHooks.contextWithTimeout(ctx, d)
}
return context.WithTimeout(ctx, d)
}
// clientStream is the state for a single HTTP/2 stream. One of these
@ -425,7 +487,7 @@ func (cs *clientStream) abortStreamLocked(err error) {
// TODO(dneil): Clean up tests where cs.cc.cond is nil.
if cs.cc.cond != nil {
// Wake up writeRequestBody if it is waiting on flow control.
cs.cc.cond.Broadcast()
cs.cc.condBroadcast()
}
}
@ -435,7 +497,7 @@ func (cs *clientStream) abortRequestBodyWrite() {
defer cc.mu.Unlock()
if cs.reqBody != nil && cs.reqBodyClosed == nil {
cs.closeReqBodyLocked()
cc.cond.Broadcast()
cc.condBroadcast()
}
}
@ -445,10 +507,10 @@ func (cs *clientStream) closeReqBodyLocked() {
}
cs.reqBodyClosed = make(chan struct{})
reqBodyClosed := cs.reqBodyClosed
go func() {
cs.cc.goRun(func() {
cs.reqBody.Close()
close(reqBodyClosed)
}()
})
}
type stickyErrWriter struct {
@ -537,15 +599,6 @@ func authorityAddr(scheme string, authority string) (addr string) {
return net.JoinHostPort(host, port)
}
var retryBackoffHook func(time.Duration) *time.Timer
func backoffNewTimer(d time.Duration) *time.Timer {
if retryBackoffHook != nil {
return retryBackoffHook(d)
}
return time.NewTimer(d)
}
// RoundTripOpt is like RoundTrip, but takes options.
func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
if !(req.URL.Scheme == "https" || (req.URL.Scheme == "http" && t.AllowHTTP)) {
@ -573,13 +626,27 @@ func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Res
backoff := float64(uint(1) << (uint(retry) - 1))
backoff += backoff * (0.1 * mathrand.Float64())
d := time.Second * time.Duration(backoff)
timer := backoffNewTimer(d)
var tm timer
if t.syncHooks != nil {
tm = t.syncHooks.newTimer(d)
t.syncHooks.blockUntil(func() bool {
select {
case <-tm.C():
case <-req.Context().Done():
default:
return false
}
return true
})
} else {
tm = newTimeTimer(d)
}
select {
case <-timer.C:
case <-tm.C():
t.vlogf("RoundTrip retrying after failure: %v", roundTripErr)
continue
case <-req.Context().Done():
timer.Stop()
tm.Stop()
err = req.Context().Err()
}
}
@ -658,6 +725,9 @@ func canRetryError(err error) bool {
}
func (t *Transport) dialClientConn(ctx context.Context, addr string, singleUse bool) (*ClientConn, error) {
if t.syncHooks != nil {
return t.newClientConn(nil, singleUse, t.syncHooks)
}
host, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
@ -666,7 +736,7 @@ func (t *Transport) dialClientConn(ctx context.Context, addr string, singleUse b
if err != nil {
return nil, err
}
return t.newClientConn(tconn, singleUse)
return t.newClientConn(tconn, singleUse, nil)
}
func (t *Transport) newTLSConfig(host string) *tls.Config {
@ -732,10 +802,10 @@ func (t *Transport) maxEncoderHeaderTableSize() uint32 {
}
func (t *Transport) NewClientConn(c net.Conn) (*ClientConn, error) {
return t.newClientConn(c, t.disableKeepAlives())
return t.newClientConn(c, t.disableKeepAlives(), nil)
}
func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, error) {
func (t *Transport) newClientConn(c net.Conn, singleUse bool, hooks *testSyncHooks) (*ClientConn, error) {
cc := &ClientConn{
t: t,
tconn: c,
@ -750,10 +820,15 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro
wantSettingsAck: true,
pings: make(map[[8]byte]chan struct{}),
reqHeaderMu: make(chan struct{}, 1),
syncHooks: hooks,
}
if hooks != nil {
hooks.newclientconn(cc)
c = cc.tconn
}
if d := t.idleConnTimeout(); d != 0 {
cc.idleTimeout = d
cc.idleTimer = time.AfterFunc(d, cc.onIdleTimeout)
cc.idleTimer = cc.afterFunc(d, cc.onIdleTimeout)
}
if VerboseLogs {
t.vlogf("http2: Transport creating client conn %p to %v", cc, c.RemoteAddr())
@ -818,7 +893,7 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro
return nil, cc.werr
}
go cc.readLoop()
cc.goRun(cc.readLoop)
return cc, nil
}
@ -826,7 +901,7 @@ func (cc *ClientConn) healthCheck() {
pingTimeout := cc.t.pingTimeout()
// We don't need to periodically ping in the health check, because the readLoop of ClientConn will
// trigger the healthCheck again if there is no frame received.
ctx, cancel := context.WithTimeout(context.Background(), pingTimeout)
ctx, cancel := cc.contextWithTimeout(context.Background(), pingTimeout)
defer cancel()
cc.vlogf("http2: Transport sending health check")
err := cc.Ping(ctx)
@ -1056,7 +1131,7 @@ func (cc *ClientConn) Shutdown(ctx context.Context) error {
// Wait for all in-flight streams to complete or connection to close
done := make(chan struct{})
cancelled := false // guarded by cc.mu
go func() {
cc.goRun(func() {
cc.mu.Lock()
defer cc.mu.Unlock()
for {
@ -1068,9 +1143,9 @@ func (cc *ClientConn) Shutdown(ctx context.Context) error {
if cancelled {
break
}
cc.cond.Wait()
cc.condWait()
}
}()
})
shutdownEnterWaitStateHook()
select {
case <-done:
@ -1080,7 +1155,7 @@ func (cc *ClientConn) Shutdown(ctx context.Context) error {
cc.mu.Lock()
// Free the goroutine above
cancelled = true
cc.cond.Broadcast()
cc.condBroadcast()
cc.mu.Unlock()
return ctx.Err()
}
@ -1118,7 +1193,7 @@ func (cc *ClientConn) closeForError(err error) {
for _, cs := range cc.streams {
cs.abortStreamLocked(err)
}
cc.cond.Broadcast()
cc.condBroadcast()
cc.mu.Unlock()
cc.closeConn()
}
@ -1215,6 +1290,10 @@ func (cc *ClientConn) decrStreamReservationsLocked() {
}
func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
return cc.roundTrip(req, nil)
}
func (cc *ClientConn) roundTrip(req *http.Request, streamf func(*clientStream)) (*http.Response, error) {
ctx := req.Context()
cs := &clientStream{
cc: cc,
@ -1229,9 +1308,23 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
respHeaderRecv: make(chan struct{}),
donec: make(chan struct{}),
}
go cs.doRequest(req)
cc.goRun(func() {
cs.doRequest(req)
})
waitDone := func() error {
if cc.syncHooks != nil {
cc.syncHooks.blockUntil(func() bool {
select {
case <-cs.donec:
case <-ctx.Done():
case <-cs.reqCancel:
default:
return false
}
return true
})
}
select {
case <-cs.donec:
return nil
@ -1292,7 +1385,24 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
return err
}
if streamf != nil {
streamf(cs)
}
for {
if cc.syncHooks != nil {
cc.syncHooks.blockUntil(func() bool {
select {
case <-cs.respHeaderRecv:
case <-cs.abort:
case <-ctx.Done():
case <-cs.reqCancel:
default:
return false
}
return true
})
}
select {
case <-cs.respHeaderRecv:
return handleResponseHeaders()
@ -1348,6 +1458,21 @@ func (cs *clientStream) writeRequest(req *http.Request) (err error) {
if cc.reqHeaderMu == nil {
panic("RoundTrip on uninitialized ClientConn") // for tests
}
var newStreamHook func(*clientStream)
if cc.syncHooks != nil {
newStreamHook = cc.syncHooks.newstream
cc.syncHooks.blockUntil(func() bool {
select {
case cc.reqHeaderMu <- struct{}{}:
<-cc.reqHeaderMu
case <-cs.reqCancel:
case <-ctx.Done():
default:
return false
}
return true
})
}
select {
case cc.reqHeaderMu <- struct{}{}:
case <-cs.reqCancel:
@ -1372,6 +1497,10 @@ func (cs *clientStream) writeRequest(req *http.Request) (err error) {
}
cc.mu.Unlock()
if newStreamHook != nil {
newStreamHook(cs)
}
// TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere?
if !cc.t.disableCompression() &&
req.Header.Get("Accept-Encoding") == "" &&
@ -1452,15 +1581,30 @@ func (cs *clientStream) writeRequest(req *http.Request) (err error) {
var respHeaderTimer <-chan time.Time
var respHeaderRecv chan struct{}
if d := cc.responseHeaderTimeout(); d != 0 {
timer := time.NewTimer(d)
timer := cc.newTimer(d)
defer timer.Stop()
respHeaderTimer = timer.C
respHeaderTimer = timer.C()
respHeaderRecv = cs.respHeaderRecv
}
// Wait until the peer half-closes its end of the stream,
// or until the request is aborted (via context, error, or otherwise),
// whichever comes first.
for {
if cc.syncHooks != nil {
cc.syncHooks.blockUntil(func() bool {
select {
case <-cs.peerClosed:
case <-respHeaderTimer:
case <-respHeaderRecv:
case <-cs.abort:
case <-ctx.Done():
case <-cs.reqCancel:
default:
return false
}
return true
})
}
select {
case <-cs.peerClosed:
return nil
@ -1609,7 +1753,7 @@ func (cc *ClientConn) awaitOpenSlotForStreamLocked(cs *clientStream) error {
return nil
}
cc.pendingRequests++
cc.cond.Wait()
cc.condWait()
cc.pendingRequests--
select {
case <-cs.abort:
@ -1871,10 +2015,26 @@ func (cs *clientStream) awaitFlowControl(maxBytes int) (taken int32, err error)
cs.flow.take(take)
return take, nil
}
cc.cond.Wait()
cc.condWait()
}
}
func validateHeaders(hdrs http.Header) string {
for k, vv := range hdrs {
if !httpguts.ValidHeaderFieldName(k) {
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 ""
}
var errNilRequestURL = errors.New("http2: Request.URI is nil")
// requires cc.wmu be held.
@ -1912,19 +2072,14 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail
}
}
// Check for any invalid headers and return an error before we
// 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)
for k, vv := range req.Header {
if !httpguts.ValidHeaderFieldName(k) {
return nil, fmt.Errorf("invalid HTTP header 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 nil, fmt.Errorf("invalid HTTP header value for header %q", k)
}
}
if err := validateHeaders(req.Header); err != "" {
return nil, fmt.Errorf("invalid HTTP header %s", err)
}
if err := validateHeaders(req.Trailer); err != "" {
return nil, fmt.Errorf("invalid HTTP trailer %s", err)
}
enumerateHeaders := func(f func(name, value string)) {
@ -2143,7 +2298,7 @@ func (cc *ClientConn) forgetStreamID(id uint32) {
}
// Wake up writeRequestBody via clientStream.awaitFlowControl and
// wake up RoundTrip if there is a pending request.
cc.cond.Broadcast()
cc.condBroadcast()
closeOnIdle := cc.singleUse || cc.doNotReuse || cc.t.disableKeepAlives() || cc.goAway != nil
if closeOnIdle && cc.streamsReserved == 0 && len(cc.streams) == 0 {
@ -2231,7 +2386,7 @@ func (rl *clientConnReadLoop) cleanup() {
cs.abortStreamLocked(err)
}
}
cc.cond.Broadcast()
cc.condBroadcast()
cc.mu.Unlock()
}
@ -2266,10 +2421,9 @@ func (rl *clientConnReadLoop) run() error {
cc := rl.cc
gotSettings := false
readIdleTimeout := cc.t.ReadIdleTimeout
var t *time.Timer
var t timer
if readIdleTimeout != 0 {
t = time.AfterFunc(readIdleTimeout, cc.healthCheck)
defer t.Stop()
t = cc.afterFunc(readIdleTimeout, cc.healthCheck)
}
for {
f, err := cc.fr.ReadFrame()
@ -2684,7 +2838,7 @@ func (rl *clientConnReadLoop) processData(f *DataFrame) error {
})
return nil
}
if !cs.firstByte {
if !cs.pastHeaders {
cc.logf("protocol error: received DATA before a HEADERS frame")
rl.endStreamError(cs, StreamError{
StreamID: f.StreamID,
@ -2867,7 +3021,7 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
for _, cs := range cc.streams {
cs.flow.add(delta)
}
cc.cond.Broadcast()
cc.condBroadcast()
cc.initialWindowSize = s.Val
case SettingHeaderTableSize:
@ -2922,7 +3076,7 @@ func (rl *clientConnReadLoop) processWindowUpdate(f *WindowUpdateFrame) error {
return ConnectionError(ErrCodeFlowControl)
}
cc.cond.Broadcast()
cc.condBroadcast()
return nil
}
@ -2964,24 +3118,38 @@ func (cc *ClientConn) Ping(ctx context.Context) error {
}
cc.mu.Unlock()
}
errc := make(chan error, 1)
go func() {
var pingError error
errc := make(chan struct{})
cc.goRun(func() {
cc.wmu.Lock()
defer cc.wmu.Unlock()
if err := cc.fr.WritePing(false, p); err != nil {
errc <- err
if pingError = cc.fr.WritePing(false, p); pingError != nil {
close(errc)
return
}
if err := cc.bw.Flush(); err != nil {
errc <- err
if pingError = cc.bw.Flush(); pingError != nil {
close(errc)
return
}
}()
})
if cc.syncHooks != nil {
cc.syncHooks.blockUntil(func() bool {
select {
case <-c:
case <-errc:
case <-ctx.Done():
case <-cc.readerDone:
default:
return false
}
return true
})
}
select {
case <-c:
return nil
case err := <-errc:
return err
case <-errc:
return pingError
case <-ctx.Done():
return ctx.Err()
case <-cc.readerDone:
@ -3150,9 +3318,17 @@ func (rt noDialH2RoundTripper) RoundTrip(req *http.Request) (*http.Response, err
}
func (t *Transport) idleConnTimeout() time.Duration {
// to keep things backwards compatible, we use non-zero values of
// IdleConnTimeout, followed by using the IdleConnTimeout on the underlying
// http1 transport, followed by 0
if t.IdleConnTimeout != 0 {
return t.IdleConnTimeout
}
if t.t1 != nil {
return t.t1.IdleConnTimeout
}
return 0
}

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build aix || darwin || dragonfly || freebsd || openbsd || solaris
//go:build aix || darwin || dragonfly || freebsd || openbsd || solaris || zos
package unix

View File

@ -1520,6 +1520,14 @@ func (m *mmapper) Munmap(data []byte) (err error) {
return nil
}
func Mmap(fd int, offset int64, length int, prot int, flags int) (data []byte, err error) {
return mapper.Mmap(fd, offset, length, prot, flags)
}
func Munmap(b []byte) (err error) {
return mapper.Munmap(b)
}
func Read(fd int, p []byte) (n int, err error) {
n, err = read(fd, p)
if raceenabled {

14
vendor/modules.txt vendored
View File

@ -1,7 +1,7 @@
# github.com/antchfx/htmlquery v1.3.0
# github.com/antchfx/htmlquery v1.3.1
## explicit; go 1.14
github.com/antchfx/htmlquery
# github.com/antchfx/xpath v1.2.5
# github.com/antchfx/xpath v1.3.0
## explicit; go 1.14
github.com/antchfx/xpath
# github.com/goccy/go-json v0.10.2
@ -24,7 +24,7 @@ github.com/golang/snappy
# github.com/json-iterator/go v1.1.12
## explicit; go 1.12
github.com/json-iterator/go
# github.com/labstack/echo/v4 v4.11.4
# github.com/labstack/echo/v4 v4.12.0
## explicit; go 1.18
github.com/labstack/echo/v4
# github.com/labstack/gommon v0.4.2
@ -77,11 +77,11 @@ github.com/valyala/bytebufferpool
# github.com/valyala/fasttemplate v1.2.2
## explicit; go 1.12
github.com/valyala/fasttemplate
# golang.org/x/crypto v0.21.0
# golang.org/x/crypto v0.22.0
## explicit; go 1.18
golang.org/x/crypto/acme
golang.org/x/crypto/acme/autocert
# golang.org/x/net v0.22.0
# golang.org/x/net v0.24.0
## explicit; go 1.18
golang.org/x/net/html
golang.org/x/net/html/atom
@ -91,7 +91,7 @@ golang.org/x/net/http2
golang.org/x/net/http2/h2c
golang.org/x/net/http2/hpack
golang.org/x/net/idna
# golang.org/x/sys v0.18.0
# golang.org/x/sys v0.19.0
## explicit; go 1.18
golang.org/x/sys/unix
# golang.org/x/text v0.14.0
@ -122,7 +122,7 @@ gopkg.in/ini.v1
# xorm.io/builder v0.3.13
## explicit; go 1.11
xorm.io/builder
# xorm.io/xorm v1.3.8
# xorm.io/xorm v1.3.9
## explicit; go 1.18
xorm.io/xorm
xorm.io/xorm/caches

43
vendor/xorm.io/xorm/convert/time.go generated vendored
View File

@ -28,14 +28,19 @@ func String2Time(s string, originalLocation *time.Location, convertedLocation *t
dt = dt.In(convertedLocation)
return &dt, nil
} else if len(s) == 20 && s[10] == 'T' && s[19] == 'Z' {
if strings.HasPrefix(s, "0000-00-00T00:00:00") || strings.HasPrefix(s, "0001-01-01T00:00:00") {
return &time.Time{}, nil
}
dt, err := time.ParseInLocation("2006-01-02T15:04:05", s[:19], originalLocation)
if err != nil {
return nil, err
}
dt = dt.In(convertedLocation)
dt.IsZero()
return &dt, nil
} else if len(s) == 25 && s[10] == 'T' && s[19] == '+' && s[22] == ':' {
if strings.HasPrefix(s, "0000-00-00T00:00:00") || strings.HasPrefix(s, "0001-01-01T00:00:00") {
return &time.Time{}, nil
}
dt, err := time.Parse(time.RFC3339, s)
if err != nil {
return nil, err
@ -43,6 +48,10 @@ func String2Time(s string, originalLocation *time.Location, convertedLocation *t
dt = dt.In(convertedLocation)
return &dt, nil
} else if len(s) >= 21 && s[10] == 'T' && s[19] == '.' {
if strings.HasPrefix(s, "0000-00-00T00:00:00."+strings.Repeat("0", len(s)-20)) ||
strings.HasPrefix(s, "0001-01-01T00:00:00."+strings.Repeat("0", len(s)-20)) {
return &time.Time{}, nil
}
dt, err := time.Parse(time.RFC3339Nano, s)
if err != nil {
return nil, err
@ -50,6 +59,10 @@ func String2Time(s string, originalLocation *time.Location, convertedLocation *t
dt = dt.In(convertedLocation)
return &dt, nil
} else if len(s) >= 21 && s[19] == '.' {
if strings.HasPrefix(s, "0000-00-00T00:00:00."+strings.Repeat("0", len(s)-20)) ||
strings.HasPrefix(s, "0001-01-01T00:00:00."+strings.Repeat("0", len(s)-20)) {
return &time.Time{}, nil
}
layout := "2006-01-02 15:04:05." + strings.Repeat("0", len(s)-20)
dt, err := time.ParseInLocation(layout, s, originalLocation)
if err != nil {
@ -68,20 +81,20 @@ func String2Time(s string, originalLocation *time.Location, convertedLocation *t
dt = dt.In(convertedLocation)
return &dt, nil
} else if len(s) == 8 && s[2] == ':' && s[5] == ':' {
currentDate := time.Now()
dt, err := time.ParseInLocation("15:04:05", s, originalLocation)
if err != nil {
return nil, err
}
// add current date for correct time locations
dt = dt.AddDate(currentDate.Year(), int(currentDate.Month()), currentDate.Day())
dt = dt.In(convertedLocation)
dt = dt.AddDate(2006, 01, 02).In(convertedLocation)
// back to zero year
dt = dt.AddDate(-currentDate.Year(), int(-currentDate.Month()), -currentDate.Day())
dt = dt.AddDate(-2006, -01, -02)
return &dt, nil
} else {
i, err := strconv.ParseInt(s, 10, 64)
if err == nil {
if i == 0 {
return &time.Time{}, nil
}
tm := time.Unix(i, 0).In(convertedLocation)
return &tm, nil
}
@ -108,6 +121,9 @@ func AsTime(src interface{}, dbLoc *time.Location, uiLoc *time.Location) (*time.
if !t.Valid {
return nil, nil
}
if utils.IsTimeZero(t.Time) {
return &time.Time{}, nil
}
z, _ := t.Time.Zone()
if len(z) == 0 || t.Time.Year() == 0 || t.Time.Location().String() != dbLoc.String() {
tm := time.Date(t.Time.Year(), t.Time.Month(), t.Time.Day(), t.Time.Hour(),
@ -117,6 +133,9 @@ func AsTime(src interface{}, dbLoc *time.Location, uiLoc *time.Location) (*time.
tm := t.Time.In(uiLoc)
return &tm, nil
case *time.Time:
if utils.IsTimeZero(*t) {
return &time.Time{}, nil
}
z, _ := t.Zone()
if len(z) == 0 || t.Year() == 0 || t.Location().String() != dbLoc.String() {
tm := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(),
@ -126,6 +145,9 @@ func AsTime(src interface{}, dbLoc *time.Location, uiLoc *time.Location) (*time.
tm := t.In(uiLoc)
return &tm, nil
case time.Time:
if utils.IsTimeZero(t) {
return &time.Time{}, nil
}
z, _ := t.Zone()
if len(z) == 0 || t.Year() == 0 || t.Location().String() != dbLoc.String() {
tm := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(),
@ -135,12 +157,21 @@ func AsTime(src interface{}, dbLoc *time.Location, uiLoc *time.Location) (*time.
tm := t.In(uiLoc)
return &tm, nil
case int:
if t == 0 {
return &time.Time{}, nil
}
tm := time.Unix(int64(t), 0).In(uiLoc)
return &tm, nil
case int64:
if t == 0 {
return &time.Time{}, nil
}
tm := time.Unix(t, 0).In(uiLoc)
return &tm, nil
case *sql.NullInt64:
if t.Int64 == 0 {
return &time.Time{}, nil
}
tm := time.Unix(t.Int64, 0).In(uiLoc)
return &tm, nil
}

View File

@ -618,8 +618,8 @@ func (db *dameng) SQLType(c *schemas.Column) string {
res = t
}
hasLen1 := (c.Length > 0)
hasLen2 := (c.Length2 > 0)
hasLen1 := c.Length > 0
hasLen2 := c.Length2 > 0
if hasLen2 {
res += "(" + strconv.FormatInt(c.Length, 10) + "," + strconv.FormatInt(c.Length2, 10) + ")"

View File

@ -330,15 +330,11 @@ func (db *mssql) SQLType(c *schemas.Column) string {
res += "(MAX)"
}
case schemas.TimeStamp, schemas.DateTime:
if c.Length > 3 {
res = "DATETIME2"
} else {
return schemas.DateTime
}
return "DATETIME2"
case schemas.TimeStampz:
res = "DATETIMEOFFSET"
c.Length = 7
case schemas.MediumInt, schemas.TinyInt, schemas.SmallInt, schemas.UnsignedMediumInt, schemas.UnsignedTinyInt, schemas.UnsignedSmallInt:
case schemas.MediumInt, schemas.SmallInt, schemas.UnsignedMediumInt, schemas.UnsignedTinyInt, schemas.UnsignedSmallInt:
res = schemas.Int
case schemas.Text, schemas.MediumText, schemas.TinyText, schemas.LongText, schemas.Json:
res = db.defaultVarchar + "(MAX)"
@ -381,8 +377,8 @@ func (db *mssql) SQLType(c *schemas.Column) string {
return res
}
hasLen1 := (c.Length > 0)
hasLen2 := (c.Length2 > 0)
hasLen1 := c.Length > 0
hasLen2 := c.Length2 > 0
if hasLen2 {
res += "(" + strconv.FormatInt(c.Length, 10) + "," + strconv.FormatInt(c.Length2, 10) + ")"

View File

@ -326,8 +326,8 @@ func (db *mysql) SQLType(c *schemas.Column) string {
res = t
}
hasLen1 := (c.Length > 0)
hasLen2 := (c.Length2 > 0)
hasLen1 := c.Length > 0
hasLen2 := c.Length2 > 0
if res == schemas.BigInt && !hasLen1 && !hasLen2 {
c.Length = 20

View File

@ -585,8 +585,8 @@ func (db *oracle) SQLType(c *schemas.Column) string {
res = t
}
hasLen1 := (c.Length > 0)
hasLen2 := (c.Length2 > 0)
hasLen1 := c.Length > 0
hasLen2 := c.Length2 > 0
if hasLen2 {
res += "(" + strconv.FormatInt(c.Length, 10) + "," + strconv.FormatInt(c.Length2, 10) + ")"

View File

@ -957,8 +957,8 @@ func (db *postgres) SQLType(c *schemas.Column) string {
// for bool, we don't need length information
return res
}
hasLen1 := (c.Length > 0)
hasLen2 := (c.Length2 > 0)
hasLen1 := c.Length > 0
hasLen2 := c.Length2 > 0
if hasLen2 {
res += "(" + strconv.FormatInt(c.Length, 10) + "," + strconv.FormatInt(c.Length2, 10) + ")"
@ -1185,7 +1185,7 @@ WHERE n.nspname= s.table_schema AND c.relkind = 'r' AND c.relname = $1%s AND f.a
col.IsPrimaryKey = true
}
col.Nullable = (isNullable == "YES")
col.Nullable = isNullable == "YES"
switch strings.ToLower(dataType) {
case "character varying", "string":

View File

@ -7,20 +7,23 @@ package dialects
import (
"strings"
"time"
"xorm.io/xorm/internal/utils"
"xorm.io/xorm/schemas"
)
// FormatColumnTime format column time
func FormatColumnTime(dialect Dialect, dbLocation *time.Location, col *schemas.Column, t time.Time) (interface{}, error) {
if t.IsZero() {
if utils.IsTimeZero(t) {
if col.Nullable {
return nil, nil
}
if col.SQLType.IsNumeric() {
return 0, nil
}
if col.SQLType.Name == schemas.TimeStamp || col.SQLType.Name == schemas.TimeStampz {
t = time.Unix(0, 0)
}
}
tmZone := dbLocation

2
vendor/xorm.io/xorm/engine.go generated vendored
View File

@ -1212,7 +1212,7 @@ func (engine *Engine) Insert(beans ...interface{}) (int64, error) {
func (engine *Engine) InsertOne(bean interface{}) (int64, error) {
session := engine.NewSession()
defer session.Close()
return session.InsertOne(bean)
return session.Insert(bean)
}
// Update records, bean's non-empty fields are updated contents,

View File

@ -50,7 +50,7 @@ var ErrNoColumnName = errors.New("no column name")
func (statement *Statement) writeOrderBy(w *builder.BytesWriter, orderBy orderBy) error {
switch t := orderBy.orderStr.(type) {
case (*builder.Expression):
case *builder.Expression:
if _, err := fmt.Fprint(w.Builder, statement.dialect.Quoter().Replace(t.Content())); err != nil {
return err
}

View File

@ -170,7 +170,7 @@ func (statement *Statement) Reset() {
// SQL adds raw sql statement
func (statement *Statement) SQL(query interface{}, args ...interface{}) *Statement {
switch t := query.(type) {
case (*builder.Builder):
case *builder.Builder:
var err error
statement.RawSQL, statement.RawParams, err = t.ToSQL()
if err != nil {
@ -616,7 +616,7 @@ func (statement *Statement) BuildConds(table *schemas.Table, bean interface{}, i
// MergeConds merge conditions from bean and id
func (statement *Statement) MergeConds(bean interface{}) error {
if !statement.NoAutoCondition && statement.RefTable != nil {
addedTableName := (len(statement.joins) > 0)
addedTableName := len(statement.joins) > 0
autoCond, err := statement.BuildConds(statement.RefTable, bean, true, true, false, true, addedTableName)
if err != nil {
return err
@ -713,11 +713,14 @@ func (statement *Statement) CondDeleted(col *schemas.Column) builder.Cond {
cond := builder.NewCond()
if col.SQLType.IsNumeric() {
cond = builder.Eq{colName: 0}
} else {
// FIXME: mssql: The conversion of a nvarchar data type to a datetime data type resulted in an out-of-range value.
if statement.dialect.URI().DBType != schemas.MSSQL {
cond = builder.Eq{colName: utils.ZeroTime1}
} else if col.SQLType.Name == schemas.TimeStamp || col.SQLType.Name == schemas.TimeStampz {
tmZone := statement.defaultTimeZone
if col.TimeZone != nil {
tmZone = col.TimeZone
}
cond = builder.Eq{colName: time.Unix(0, 0).In(tmZone).Format("2006-01-02 15:04:05.999999999")}
} else {
cond = builder.Eq{colName: utils.ZeroTime1}
}
if col.Nullable {

View File

@ -126,6 +126,9 @@ func (statement *Statement) BuildUpdates(tableValue reflect.Value,
if fieldValue.CanAddr() {
if structConvert, ok := fieldValue.Addr().Interface().(convert.Conversion); ok {
if utils.IsZero(fieldValue.Interface()) {
continue
}
data, err := structConvert.ToDB()
if err != nil {
return nil, nil, err

View File

@ -146,6 +146,6 @@ const (
// IsTimeZero return true if a time is zero
func IsTimeZero(t time.Time) bool {
return t.IsZero() || t.Format("2006-01-02 15:04:05") == ZeroTime0 ||
t.Format("2006-01-02 15:04:05") == ZeroTime1
return t.IsZero() || t.Format("2006-01-02 15:04:05.999999999") == ZeroTime0 ||
t.Format("2006-01-02 15:04:05.999999999") == ZeroTime1
}

View File

@ -149,7 +149,7 @@ func isASCIIUpper(r rune) bool {
func toASCIIUpper(r rune) rune {
if 'a' <= r && r <= 'z' {
r -= ('a' - 'A')
r -= 'a' - 'A'
}
return r
}

View File

@ -471,7 +471,8 @@ func (session *Session) genInsertColumns(bean interface{}) ([]string, []interfac
}
if col.IsDeleted {
arg, err := dialects.FormatColumnTime(session.engine.dialect, session.engine.DatabaseTZ, col, time.Time{})
zeroTime := time.Date(1, 1, 1, 0, 0, 0, 0, session.engine.DatabaseTZ)
arg, err := dialects.FormatColumnTime(session.engine.dialect, session.engine.DatabaseTZ, col, zeroTime)
if err != nil {
return nil, nil, err
}

5
vendor/xorm.io/xorm/sync.go generated vendored
View File

@ -17,6 +17,8 @@ type SyncOptions struct {
IgnoreConstrains bool
// IgnoreIndices will not add or delete indices
IgnoreIndices bool
// IgnoreDropIndices will not delete indices
IgnoreDropIndices bool
}
type SyncResult struct{}
@ -55,6 +57,7 @@ func (session *Session) Sync(beans ...interface{}) error {
WarnIfDatabaseColumnMissed: false,
IgnoreConstrains: false,
IgnoreIndices: false,
IgnoreDropIndices: false,
}, beans...)
return err
}
@ -244,7 +247,7 @@ func (session *Session) SyncWithOptions(opts SyncOptions, beans ...interface{})
for name2, index2 := range oriTable.Indexes {
if _, ok := foundIndexNames[name2]; !ok {
// ignore based on there type
if (index2.Type == schemas.IndexType && opts.IgnoreIndices) ||
if (index2.Type == schemas.IndexType && (opts.IgnoreIndices || opts.IgnoreDropIndices)) ||
(index2.Type == schemas.UniqueType && opts.IgnoreConstrains) {
// make sure we do not add a index with same name later
delete(addedNames, name2)

2
vendor/xorm.io/xorm/tags/tag.go generated vendored
View File

@ -163,7 +163,7 @@ func PKTagHandler(ctx *Context) error {
// NULLTagHandler describes null tag handler
func NULLTagHandler(ctx *Context) error {
ctx.col.Nullable = (strings.ToUpper(ctx.preTag) != "NOT")
ctx.col.Nullable = strings.ToUpper(ctx.preTag) != "NOT"
return nil
}