feat(ecosystem): compliant Praxis/Hexis integration + vendored build
Bring the Nexus/Praxis/Hexis integration in line with MAVEN_ECOSYSTEM_ARCHITECTURE.md: - Praxis over HTTP: drop the in-process praxis.db open (praxisstore/ praxistools) and call praxisd's /api/v1/tools/* API via a new praxisClient. Honors the "no component reads another's DB" invariant (AC#12). PraxisConfig.DBPath -> URL. - Hexis confirmation gate: mutating capabilities (ReadOnly=false) now park a bound pendingHexis confirmation and require a spoken "да" before executing; read-only run immediately (AC#7, no auto attention->action). - Capability safety: >1 verb match is ambiguous -> ask instead of firing the first; ambiguous Nexus resolution asks for clarification (AC#2). - Correlation IDs on Hexis execute, recorded in the cross-service trace. - Bug: importance arrives as JSON float64 over HTTP, not int. - Tests: confirm-gate, decline, read-only, and ambiguity paths. Build: vendor/ bakes in the hexis client (replace-directed at a sibling repo outside the Docker context); Dockerfile builds from vendor and no longer `go mod download`s the unreachable replace paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
Copyright (c) 2023 Anmol Sethi <hi@nhooyr.io>
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
# websocket
|
||||
|
||||
[](https://pkg.go.dev/github.com/coder/websocket)
|
||||
[](https://github.com/coder/websocket/coverage.html)
|
||||
|
||||
websocket is a minimal and idiomatic WebSocket library for Go.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
go get github.com/coder/websocket
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Coder now maintains this project as explained in [this blog post](https://coder.com/blog/websocket).
|
||||
> We're grateful to [nhooyr](https://github.com/nhooyr) for authoring and maintaining this project from
|
||||
> 2019 to 2024.
|
||||
|
||||
## Highlights
|
||||
|
||||
- Minimal and idiomatic API
|
||||
- First class [context.Context](https://blog.golang.org/context) support
|
||||
- Fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite)
|
||||
- [Zero dependencies](https://pkg.go.dev/github.com/coder/websocket?tab=imports)
|
||||
- JSON helpers in the [wsjson](https://pkg.go.dev/github.com/coder/websocket/wsjson) subpackage
|
||||
- Zero alloc reads and writes
|
||||
- Concurrent writes
|
||||
- [Close handshake](https://pkg.go.dev/github.com/coder/websocket#Conn.Close)
|
||||
- [net.Conn](https://pkg.go.dev/github.com/coder/websocket#NetConn) wrapper
|
||||
- [Ping pong](https://pkg.go.dev/github.com/coder/websocket#Conn.Ping) API
|
||||
- [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression
|
||||
- [CloseRead](https://pkg.go.dev/github.com/coder/websocket#Conn.CloseRead) helper for write only connections
|
||||
- Compile to [Wasm](https://pkg.go.dev/github.com/coder/websocket#hdr-Wasm)
|
||||
|
||||
## Roadmap
|
||||
|
||||
See GitHub issues for minor issues but the major future enhancements are:
|
||||
|
||||
- [ ] Perfect examples [#217](https://github.com/nhooyr/websocket/issues/217)
|
||||
- [ ] wstest.Pipe for in memory testing [#340](https://github.com/nhooyr/websocket/issues/340)
|
||||
- [ ] Ping pong heartbeat helper [#267](https://github.com/nhooyr/websocket/issues/267)
|
||||
- [ ] Ping pong instrumentation callbacks [#246](https://github.com/nhooyr/websocket/issues/246)
|
||||
- [ ] Graceful shutdown helpers [#209](https://github.com/nhooyr/websocket/issues/209)
|
||||
- [ ] Assembly for WebSocket masking [#16](https://github.com/nhooyr/websocket/issues/16)
|
||||
- WIP at [#326](https://github.com/nhooyr/websocket/pull/326), about 3x faster
|
||||
- [ ] HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4)
|
||||
- [ ] The holy grail [#402](https://github.com/nhooyr/websocket/issues/402)
|
||||
|
||||
## Examples
|
||||
|
||||
For a production quality example that demonstrates the complete API, see the
|
||||
[echo example](./internal/examples/echo).
|
||||
|
||||
For a full stack example, see the [chat example](./internal/examples/chat).
|
||||
|
||||
### Server
|
||||
|
||||
```go
|
||||
http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
|
||||
c, err := websocket.Accept(w, r, nil)
|
||||
if err != nil {
|
||||
// ...
|
||||
}
|
||||
defer c.CloseNow()
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
var v interface{}
|
||||
err = wsjson.Read(ctx, c, &v)
|
||||
if err != nil {
|
||||
// ...
|
||||
}
|
||||
|
||||
log.Printf("received: %v", v)
|
||||
|
||||
c.Close(websocket.StatusNormalClosure, "")
|
||||
})
|
||||
```
|
||||
|
||||
### Client
|
||||
|
||||
```go
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
c, _, err := websocket.Dial(ctx, "ws://localhost:8080", nil)
|
||||
if err != nil {
|
||||
// ...
|
||||
}
|
||||
defer c.CloseNow()
|
||||
|
||||
err = wsjson.Write(ctx, c, "hi")
|
||||
if err != nil {
|
||||
// ...
|
||||
}
|
||||
|
||||
c.Close(websocket.StatusNormalClosure, "")
|
||||
```
|
||||
|
||||
## Comparison
|
||||
|
||||
### gorilla/websocket
|
||||
|
||||
Advantages of [gorilla/websocket](https://github.com/gorilla/websocket):
|
||||
|
||||
- Mature and widely used
|
||||
- [Prepared writes](https://pkg.go.dev/github.com/gorilla/websocket#PreparedMessage)
|
||||
- Configurable [buffer sizes](https://pkg.go.dev/github.com/gorilla/websocket#hdr-Buffers)
|
||||
- No extra goroutine per connection to support cancellation with context.Context. This costs github.com/coder/websocket 2 KB of memory per connection.
|
||||
- Will be removed soon with [context.AfterFunc](https://github.com/golang/go/issues/57928). See [#411](https://github.com/nhooyr/websocket/issues/411)
|
||||
|
||||
Advantages of github.com/coder/websocket:
|
||||
|
||||
- Minimal and idiomatic API
|
||||
- Compare godoc of [github.com/coder/websocket](https://pkg.go.dev/github.com/coder/websocket) with [gorilla/websocket](https://pkg.go.dev/github.com/gorilla/websocket) side by side.
|
||||
- [net.Conn](https://pkg.go.dev/github.com/coder/websocket#NetConn) wrapper
|
||||
- Zero alloc reads and writes ([gorilla/websocket#535](https://github.com/gorilla/websocket/issues/535))
|
||||
- Full [context.Context](https://blog.golang.org/context) support
|
||||
- Dial uses [net/http.Client](https://golang.org/pkg/net/http/#Client)
|
||||
- Will enable easy HTTP/2 support in the future
|
||||
- Gorilla writes directly to a net.Conn and so duplicates features of net/http.Client.
|
||||
- Concurrent writes
|
||||
- Close handshake ([gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448))
|
||||
- Idiomatic [ping pong](https://pkg.go.dev/github.com/coder/websocket#Conn.Ping) API
|
||||
- Gorilla requires registering a pong callback before sending a Ping
|
||||
- Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432))
|
||||
- Transparent message buffer reuse with [wsjson](https://pkg.go.dev/github.com/coder/websocket/wsjson) subpackage
|
||||
- [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go
|
||||
- Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/).
|
||||
Soon we'll have assembly and be 3x faster [#326](https://github.com/nhooyr/websocket/pull/326)
|
||||
- Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support
|
||||
- Gorilla only supports no context takeover mode
|
||||
- [CloseRead](https://pkg.go.dev/github.com/coder/websocket#Conn.CloseRead) helper for write only connections ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492))
|
||||
|
||||
#### golang.org/x/net/websocket
|
||||
|
||||
[golang.org/x/net/websocket](https://pkg.go.dev/golang.org/x/net/websocket) is deprecated.
|
||||
See [golang/go/issues/18152](https://github.com/golang/go/issues/18152).
|
||||
|
||||
The [net.Conn](https://pkg.go.dev/github.com/coder/websocket#NetConn) can help in transitioning
|
||||
to github.com/coder/websocket.
|
||||
|
||||
#### gobwas/ws
|
||||
|
||||
[gobwas/ws](https://github.com/gobwas/ws) has an extremely flexible API that allows it to be used
|
||||
in an event driven style for performance. See the author's [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb).
|
||||
|
||||
However it is quite bloated. See https://pkg.go.dev/github.com/gobwas/ws
|
||||
|
||||
When writing idiomatic Go, github.com/coder/websocket will be faster and easier to use.
|
||||
|
||||
#### lesismal/nbio
|
||||
|
||||
[lesismal/nbio](https://github.com/lesismal/nbio) is similar to gobwas/ws in that the API is
|
||||
event driven for performance reasons.
|
||||
|
||||
However it is quite bloated. See https://pkg.go.dev/github.com/lesismal/nbio
|
||||
|
||||
When writing idiomatic Go, github.com/coder/websocket will be faster and easier to use.
|
||||
+352
@@ -0,0 +1,352 @@
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/coder/websocket/internal/errd"
|
||||
)
|
||||
|
||||
// AcceptOptions represents Accept's options.
|
||||
type AcceptOptions struct {
|
||||
// Subprotocols lists the WebSocket subprotocols that Accept will negotiate with the client.
|
||||
// The empty subprotocol will always be negotiated as per RFC 6455. If you would like to
|
||||
// reject it, close the connection when c.Subprotocol() == "".
|
||||
Subprotocols []string
|
||||
|
||||
// InsecureSkipVerify is used to disable Accept's origin verification behaviour.
|
||||
//
|
||||
// You probably want to use OriginPatterns instead.
|
||||
InsecureSkipVerify bool
|
||||
|
||||
// OriginPatterns lists the host patterns for authorized origins.
|
||||
// The request host is always authorized.
|
||||
// Use this to enable cross origin WebSockets.
|
||||
//
|
||||
// i.e javascript running on example.com wants to access a WebSocket server at chat.example.com.
|
||||
// In such a case, example.com is the origin and chat.example.com is the request host.
|
||||
// One would set this field to []string{"example.com"} to authorize example.com to connect.
|
||||
//
|
||||
// Each pattern is matched case insensitively against the request origin host
|
||||
// with filepath.Match.
|
||||
// See https://golang.org/pkg/path/filepath/#Match
|
||||
//
|
||||
// Please ensure you understand the ramifications of enabling this.
|
||||
// If used incorrectly your WebSocket server will be open to CSRF attacks.
|
||||
//
|
||||
// Do not use * as a pattern to allow any origin, prefer to use InsecureSkipVerify instead
|
||||
// to bring attention to the danger of such a setting.
|
||||
OriginPatterns []string
|
||||
|
||||
// CompressionMode controls the compression mode.
|
||||
// Defaults to CompressionDisabled.
|
||||
//
|
||||
// See docs on CompressionMode for details.
|
||||
CompressionMode CompressionMode
|
||||
|
||||
// CompressionThreshold controls the minimum size of a message before compression is applied.
|
||||
//
|
||||
// Defaults to 512 bytes for CompressionNoContextTakeover and 128 bytes
|
||||
// for CompressionContextTakeover.
|
||||
CompressionThreshold int
|
||||
}
|
||||
|
||||
func (opts *AcceptOptions) cloneWithDefaults() *AcceptOptions {
|
||||
var o AcceptOptions
|
||||
if opts != nil {
|
||||
o = *opts
|
||||
}
|
||||
return &o
|
||||
}
|
||||
|
||||
// Accept accepts a WebSocket handshake from a client and upgrades the
|
||||
// the connection to a WebSocket.
|
||||
//
|
||||
// Accept will not allow cross origin requests by default.
|
||||
// See the InsecureSkipVerify and OriginPatterns options to allow cross origin requests.
|
||||
//
|
||||
// Accept will write a response to w on all errors.
|
||||
func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) {
|
||||
return accept(w, r, opts)
|
||||
}
|
||||
|
||||
func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Conn, err error) {
|
||||
defer errd.Wrap(&err, "failed to accept WebSocket connection")
|
||||
|
||||
errCode, err := verifyClientRequest(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), errCode)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts = opts.cloneWithDefaults()
|
||||
if !opts.InsecureSkipVerify {
|
||||
err = authenticateOrigin(r, opts.OriginPatterns)
|
||||
if err != nil {
|
||||
if errors.Is(err, filepath.ErrBadPattern) {
|
||||
log.Printf("websocket: %v", err)
|
||||
err = errors.New(http.StatusText(http.StatusForbidden))
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
hj, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
err = errors.New("http.ResponseWriter does not implement http.Hijacker")
|
||||
http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w.Header().Set("Upgrade", "websocket")
|
||||
w.Header().Set("Connection", "Upgrade")
|
||||
|
||||
key := r.Header.Get("Sec-WebSocket-Key")
|
||||
w.Header().Set("Sec-WebSocket-Accept", secWebSocketAccept(key))
|
||||
|
||||
subproto := selectSubprotocol(r, opts.Subprotocols)
|
||||
if subproto != "" {
|
||||
w.Header().Set("Sec-WebSocket-Protocol", subproto)
|
||||
}
|
||||
|
||||
copts, ok := selectDeflate(websocketExtensions(r.Header), opts.CompressionMode)
|
||||
if ok {
|
||||
w.Header().Set("Sec-WebSocket-Extensions", copts.String())
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusSwitchingProtocols)
|
||||
// See https://github.com/nhooyr/websocket/issues/166
|
||||
if ginWriter, ok := w.(interface {
|
||||
WriteHeaderNow()
|
||||
}); ok {
|
||||
ginWriter.WriteHeaderNow()
|
||||
}
|
||||
|
||||
netConn, brw, err := hj.Hijack()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to hijack connection: %w", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// https://github.com/golang/go/issues/32314
|
||||
b, _ := brw.Reader.Peek(brw.Reader.Buffered())
|
||||
brw.Reader.Reset(io.MultiReader(bytes.NewReader(b), netConn))
|
||||
|
||||
return newConn(connConfig{
|
||||
subprotocol: w.Header().Get("Sec-WebSocket-Protocol"),
|
||||
rwc: netConn,
|
||||
client: false,
|
||||
copts: copts,
|
||||
flateThreshold: opts.CompressionThreshold,
|
||||
|
||||
br: brw.Reader,
|
||||
bw: brw.Writer,
|
||||
}), nil
|
||||
}
|
||||
|
||||
func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _ error) {
|
||||
if !r.ProtoAtLeast(1, 1) {
|
||||
return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto)
|
||||
}
|
||||
|
||||
if !headerContainsTokenIgnoreCase(r.Header, "Connection", "Upgrade") {
|
||||
w.Header().Set("Connection", "Upgrade")
|
||||
w.Header().Set("Upgrade", "websocket")
|
||||
return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection"))
|
||||
}
|
||||
|
||||
if !headerContainsTokenIgnoreCase(r.Header, "Upgrade", "websocket") {
|
||||
w.Header().Set("Connection", "Upgrade")
|
||||
w.Header().Set("Upgrade", "websocket")
|
||||
return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade"))
|
||||
}
|
||||
|
||||
if r.Method != "GET" {
|
||||
return http.StatusMethodNotAllowed, fmt.Errorf("WebSocket protocol violation: handshake request method is not GET but %q", r.Method)
|
||||
}
|
||||
|
||||
if r.Header.Get("Sec-WebSocket-Version") != "13" {
|
||||
w.Header().Set("Sec-WebSocket-Version", "13")
|
||||
return http.StatusBadRequest, fmt.Errorf("unsupported WebSocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version"))
|
||||
}
|
||||
|
||||
websocketSecKeys := r.Header.Values("Sec-WebSocket-Key")
|
||||
if len(websocketSecKeys) == 0 {
|
||||
return http.StatusBadRequest, errors.New("WebSocket protocol violation: missing Sec-WebSocket-Key")
|
||||
}
|
||||
|
||||
if len(websocketSecKeys) > 1 {
|
||||
return http.StatusBadRequest, errors.New("WebSocket protocol violation: multiple Sec-WebSocket-Key headers")
|
||||
}
|
||||
|
||||
// The RFC states to remove any leading or trailing whitespace.
|
||||
websocketSecKey := strings.TrimSpace(websocketSecKeys[0])
|
||||
if v, err := base64.StdEncoding.DecodeString(websocketSecKey); err != nil || len(v) != 16 {
|
||||
return http.StatusBadRequest, fmt.Errorf("WebSocket protocol violation: invalid Sec-WebSocket-Key %q, must be a 16 byte base64 encoded string", websocketSecKey)
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func authenticateOrigin(r *http.Request, originHosts []string) error {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
u, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse Origin header %q: %w", origin, err)
|
||||
}
|
||||
|
||||
if strings.EqualFold(r.Host, u.Host) {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, hostPattern := range originHosts {
|
||||
matched, err := match(hostPattern, u.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse filepath pattern %q: %w", hostPattern, err)
|
||||
}
|
||||
if matched {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if u.Host == "" {
|
||||
return fmt.Errorf("request Origin %q is not a valid URL with a host", origin)
|
||||
}
|
||||
return fmt.Errorf("request Origin %q is not authorized for Host %q", u.Host, r.Host)
|
||||
}
|
||||
|
||||
func match(pattern, s string) (bool, error) {
|
||||
return filepath.Match(strings.ToLower(pattern), strings.ToLower(s))
|
||||
}
|
||||
|
||||
func selectSubprotocol(r *http.Request, subprotocols []string) string {
|
||||
cps := headerTokens(r.Header, "Sec-WebSocket-Protocol")
|
||||
for _, sp := range subprotocols {
|
||||
for _, cp := range cps {
|
||||
if strings.EqualFold(sp, cp) {
|
||||
return cp
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func selectDeflate(extensions []websocketExtension, mode CompressionMode) (*compressionOptions, bool) {
|
||||
if mode == CompressionDisabled {
|
||||
return nil, false
|
||||
}
|
||||
for _, ext := range extensions {
|
||||
switch ext.name {
|
||||
// We used to implement x-webkit-deflate-frame too for Safari but Safari has bugs...
|
||||
// See https://github.com/nhooyr/websocket/issues/218
|
||||
case "permessage-deflate":
|
||||
copts, ok := acceptDeflate(ext, mode)
|
||||
if ok {
|
||||
return copts, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func acceptDeflate(ext websocketExtension, mode CompressionMode) (*compressionOptions, bool) {
|
||||
copts := mode.opts()
|
||||
for _, p := range ext.params {
|
||||
switch p {
|
||||
case "client_no_context_takeover":
|
||||
copts.clientNoContextTakeover = true
|
||||
continue
|
||||
case "server_no_context_takeover":
|
||||
copts.serverNoContextTakeover = true
|
||||
continue
|
||||
case "client_max_window_bits",
|
||||
"server_max_window_bits=15":
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(p, "client_max_window_bits=") {
|
||||
// We can't adjust the deflate window, but decoding with a larger window is acceptable.
|
||||
continue
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
return copts, true
|
||||
}
|
||||
|
||||
func headerContainsTokenIgnoreCase(h http.Header, key, token string) bool {
|
||||
for _, t := range headerTokens(h, key) {
|
||||
if strings.EqualFold(t, token) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type websocketExtension struct {
|
||||
name string
|
||||
params []string
|
||||
}
|
||||
|
||||
func websocketExtensions(h http.Header) []websocketExtension {
|
||||
var exts []websocketExtension
|
||||
extStrs := headerTokens(h, "Sec-WebSocket-Extensions")
|
||||
for _, extStr := range extStrs {
|
||||
if extStr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
vals := strings.Split(extStr, ";")
|
||||
for i := range vals {
|
||||
vals[i] = strings.TrimSpace(vals[i])
|
||||
}
|
||||
|
||||
e := websocketExtension{
|
||||
name: vals[0],
|
||||
params: vals[1:],
|
||||
}
|
||||
|
||||
exts = append(exts, e)
|
||||
}
|
||||
return exts
|
||||
}
|
||||
|
||||
func headerTokens(h http.Header, key string) []string {
|
||||
key = textproto.CanonicalMIMEHeaderKey(key)
|
||||
var tokens []string
|
||||
for _, v := range h[key] {
|
||||
v = strings.TrimSpace(v)
|
||||
for _, t := range strings.Split(v, ",") {
|
||||
t = strings.TrimSpace(t)
|
||||
tokens = append(tokens, t)
|
||||
}
|
||||
}
|
||||
return tokens
|
||||
}
|
||||
|
||||
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
|
||||
|
||||
func secWebSocketAccept(secWebSocketKey string) string {
|
||||
h := sha1.New()
|
||||
h.Write([]byte(secWebSocketKey))
|
||||
h.Write(keyGUID)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
+348
@@ -0,0 +1,348 @@
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket/internal/errd"
|
||||
)
|
||||
|
||||
// StatusCode represents a WebSocket status code.
|
||||
// https://tools.ietf.org/html/rfc6455#section-7.4
|
||||
type StatusCode int
|
||||
|
||||
// https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number
|
||||
//
|
||||
// These are only the status codes defined by the protocol.
|
||||
//
|
||||
// You can define custom codes in the 3000-4999 range.
|
||||
// The 3000-3999 range is reserved for use by libraries, frameworks and applications.
|
||||
// The 4000-4999 range is reserved for private use.
|
||||
const (
|
||||
StatusNormalClosure StatusCode = 1000
|
||||
StatusGoingAway StatusCode = 1001
|
||||
StatusProtocolError StatusCode = 1002
|
||||
StatusUnsupportedData StatusCode = 1003
|
||||
|
||||
// 1004 is reserved and so unexported.
|
||||
statusReserved StatusCode = 1004
|
||||
|
||||
// StatusNoStatusRcvd cannot be sent in a close message.
|
||||
// It is reserved for when a close message is received without
|
||||
// a status code.
|
||||
StatusNoStatusRcvd StatusCode = 1005
|
||||
|
||||
// StatusAbnormalClosure is exported for use only with Wasm.
|
||||
// In non Wasm Go, the returned error will indicate whether the
|
||||
// connection was closed abnormally.
|
||||
StatusAbnormalClosure StatusCode = 1006
|
||||
|
||||
StatusInvalidFramePayloadData StatusCode = 1007
|
||||
StatusPolicyViolation StatusCode = 1008
|
||||
StatusMessageTooBig StatusCode = 1009
|
||||
StatusMandatoryExtension StatusCode = 1010
|
||||
StatusInternalError StatusCode = 1011
|
||||
StatusServiceRestart StatusCode = 1012
|
||||
StatusTryAgainLater StatusCode = 1013
|
||||
StatusBadGateway StatusCode = 1014
|
||||
|
||||
// StatusTLSHandshake is only exported for use with Wasm.
|
||||
// In non Wasm Go, the returned error will indicate whether there was
|
||||
// a TLS handshake failure.
|
||||
StatusTLSHandshake StatusCode = 1015
|
||||
)
|
||||
|
||||
// CloseError is returned when the connection is closed with a status and reason.
|
||||
//
|
||||
// Use Go 1.13's errors.As to check for this error.
|
||||
// Also see the CloseStatus helper.
|
||||
type CloseError struct {
|
||||
Code StatusCode
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (ce CloseError) Error() string {
|
||||
return fmt.Sprintf("status = %v and reason = %q", ce.Code, ce.Reason)
|
||||
}
|
||||
|
||||
// CloseStatus is a convenience wrapper around Go 1.13's errors.As to grab
|
||||
// the status code from a CloseError.
|
||||
//
|
||||
// -1 will be returned if the passed error is nil or not a CloseError.
|
||||
func CloseStatus(err error) StatusCode {
|
||||
var ce CloseError
|
||||
if errors.As(err, &ce) {
|
||||
return ce.Code
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Close performs the WebSocket close handshake with the given status code and reason.
|
||||
//
|
||||
// It will write a WebSocket close frame with a timeout of 5s and then wait 5s for
|
||||
// the peer to send a close frame.
|
||||
// All data messages received from the peer during the close handshake will be discarded.
|
||||
//
|
||||
// The connection can only be closed once. Additional calls to Close
|
||||
// are no-ops.
|
||||
//
|
||||
// The maximum length of reason must be 125 bytes. Avoid sending a dynamic reason.
|
||||
//
|
||||
// Close will unblock all goroutines interacting with the connection once
|
||||
// complete.
|
||||
func (c *Conn) Close(code StatusCode, reason string) (err error) {
|
||||
defer errd.Wrap(&err, "failed to close WebSocket")
|
||||
|
||||
if !c.casClosing() {
|
||||
err = c.waitGoroutines()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return net.ErrClosed
|
||||
}
|
||||
defer func() {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
err = nil
|
||||
}
|
||||
}()
|
||||
|
||||
err = c.closeHandshake(code, reason)
|
||||
|
||||
err2 := c.close()
|
||||
if err == nil && err2 != nil {
|
||||
err = err2
|
||||
}
|
||||
|
||||
err2 = c.waitGoroutines()
|
||||
if err == nil && err2 != nil {
|
||||
err = err2
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// CloseNow closes the WebSocket connection without attempting a close handshake.
|
||||
// Use when you do not want the overhead of the close handshake.
|
||||
func (c *Conn) CloseNow() (err error) {
|
||||
defer errd.Wrap(&err, "failed to immediately close WebSocket")
|
||||
|
||||
if !c.casClosing() {
|
||||
err = c.waitGoroutines()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return net.ErrClosed
|
||||
}
|
||||
defer func() {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
err = nil
|
||||
}
|
||||
}()
|
||||
|
||||
err = c.close()
|
||||
|
||||
err2 := c.waitGoroutines()
|
||||
if err == nil && err2 != nil {
|
||||
err = err2
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Conn) closeHandshake(code StatusCode, reason string) error {
|
||||
err := c.writeClose(code, reason)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.waitCloseHandshake()
|
||||
if CloseStatus(err) != code {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) writeClose(code StatusCode, reason string) error {
|
||||
ce := CloseError{
|
||||
Code: code,
|
||||
Reason: reason,
|
||||
}
|
||||
|
||||
var p []byte
|
||||
var err error
|
||||
if ce.Code != StatusNoStatusRcvd {
|
||||
p, err = ce.bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
err = c.writeControl(ctx, opClose, p)
|
||||
// If the connection closed as we're writing we ignore the error as we might
|
||||
// have written the close frame, the peer responded and then someone else read it
|
||||
// and closed the connection.
|
||||
if err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) waitCloseHandshake() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
err := c.readMu.lock(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.readMu.unlock()
|
||||
|
||||
for i := int64(0); i < c.msgReader.payloadLength; i++ {
|
||||
_, err := c.br.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
h, err := c.readLoop(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := int64(0); i < h.payloadLength; i++ {
|
||||
_, err := c.br.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) waitGoroutines() error {
|
||||
t := time.NewTimer(time.Second * 15)
|
||||
defer t.Stop()
|
||||
|
||||
select {
|
||||
case <-c.timeoutLoopDone:
|
||||
case <-t.C:
|
||||
return errors.New("failed to wait for timeoutLoop goroutine to exit")
|
||||
}
|
||||
|
||||
c.closeReadMu.Lock()
|
||||
closeRead := c.closeReadCtx != nil
|
||||
c.closeReadMu.Unlock()
|
||||
if closeRead {
|
||||
select {
|
||||
case <-c.closeReadDone:
|
||||
case <-t.C:
|
||||
return errors.New("failed to wait for close read goroutine to exit")
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-c.closed:
|
||||
case <-t.C:
|
||||
return errors.New("failed to wait for connection to be closed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseClosePayload(p []byte) (CloseError, error) {
|
||||
if len(p) == 0 {
|
||||
return CloseError{
|
||||
Code: StatusNoStatusRcvd,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if len(p) < 2 {
|
||||
return CloseError{}, fmt.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p)
|
||||
}
|
||||
|
||||
ce := CloseError{
|
||||
Code: StatusCode(binary.BigEndian.Uint16(p)),
|
||||
Reason: string(p[2:]),
|
||||
}
|
||||
|
||||
if !validWireCloseCode(ce.Code) {
|
||||
return CloseError{}, fmt.Errorf("invalid status code %v", ce.Code)
|
||||
}
|
||||
|
||||
return ce, nil
|
||||
}
|
||||
|
||||
// See http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number
|
||||
// and https://tools.ietf.org/html/rfc6455#section-7.4.1
|
||||
func validWireCloseCode(code StatusCode) bool {
|
||||
switch code {
|
||||
case statusReserved, StatusNoStatusRcvd, StatusAbnormalClosure, StatusTLSHandshake:
|
||||
return false
|
||||
}
|
||||
|
||||
if code >= StatusNormalClosure && code <= StatusBadGateway {
|
||||
return true
|
||||
}
|
||||
if code >= 3000 && code <= 4999 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (ce CloseError) bytes() ([]byte, error) {
|
||||
p, err := ce.bytesErr()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to marshal close frame: %w", err)
|
||||
ce = CloseError{
|
||||
Code: StatusInternalError,
|
||||
}
|
||||
p, _ = ce.bytesErr()
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
|
||||
const maxCloseReason = maxControlPayload - 2
|
||||
|
||||
func (ce CloseError) bytesErr() ([]byte, error) {
|
||||
if len(ce.Reason) > maxCloseReason {
|
||||
return nil, fmt.Errorf("reason string max is %v but got %q with length %v", maxCloseReason, ce.Reason, len(ce.Reason))
|
||||
}
|
||||
|
||||
if !validWireCloseCode(ce.Code) {
|
||||
return nil, fmt.Errorf("status code %v cannot be set", ce.Code)
|
||||
}
|
||||
|
||||
buf := make([]byte, 2+len(ce.Reason))
|
||||
binary.BigEndian.PutUint16(buf, uint16(ce.Code))
|
||||
copy(buf[2:], ce.Reason)
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func (c *Conn) casClosing() bool {
|
||||
c.closeMu.Lock()
|
||||
defer c.closeMu.Unlock()
|
||||
if !c.closing {
|
||||
c.closing = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Conn) isClosed() bool {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"compress/flate"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// CompressionMode represents the modes available to the permessage-deflate extension.
|
||||
// See https://tools.ietf.org/html/rfc7692
|
||||
//
|
||||
// Works in all modern browsers except Safari which does not implement the permessage-deflate extension.
|
||||
//
|
||||
// Compression is only used if the peer supports the mode selected.
|
||||
type CompressionMode int
|
||||
|
||||
const (
|
||||
// CompressionDisabled disables the negotiation of the permessage-deflate extension.
|
||||
//
|
||||
// This is the default. Do not enable compression without benchmarking for your particular use case first.
|
||||
CompressionDisabled CompressionMode = iota
|
||||
|
||||
// CompressionContextTakeover compresses each message greater than 128 bytes reusing the 32 KB sliding window from
|
||||
// previous messages. i.e compression context across messages is preserved.
|
||||
//
|
||||
// As most WebSocket protocols are text based and repetitive, this compression mode can be very efficient.
|
||||
//
|
||||
// The memory overhead is a fixed 32 KB sliding window, a fixed 1.2 MB flate.Writer and a sync.Pool of 40 KB flate.Reader's
|
||||
// that are used when reading and then returned.
|
||||
//
|
||||
// Thus, it uses more memory than CompressionNoContextTakeover but compresses more efficiently.
|
||||
//
|
||||
// If the peer does not support CompressionContextTakeover then we will fall back to CompressionNoContextTakeover.
|
||||
CompressionContextTakeover
|
||||
|
||||
// CompressionNoContextTakeover compresses each message greater than 512 bytes. Each message is compressed with
|
||||
// a new 1.2 MB flate.Writer pulled from a sync.Pool. Each message is read with a 40 KB flate.Reader pulled from
|
||||
// a sync.Pool.
|
||||
//
|
||||
// This means less efficient compression as the sliding window from previous messages will not be used but the
|
||||
// memory overhead will be lower as there will be no fixed cost for the flate.Writer nor the 32 KB sliding window.
|
||||
// Especially if the connections are long lived and seldom written to.
|
||||
//
|
||||
// Thus, it uses less memory than CompressionContextTakeover but compresses less efficiently.
|
||||
//
|
||||
// If the peer does not support CompressionNoContextTakeover then we will fall back to CompressionDisabled.
|
||||
CompressionNoContextTakeover
|
||||
)
|
||||
|
||||
func (m CompressionMode) opts() *compressionOptions {
|
||||
return &compressionOptions{
|
||||
clientNoContextTakeover: m == CompressionNoContextTakeover,
|
||||
serverNoContextTakeover: m == CompressionNoContextTakeover,
|
||||
}
|
||||
}
|
||||
|
||||
type compressionOptions struct {
|
||||
clientNoContextTakeover bool
|
||||
serverNoContextTakeover bool
|
||||
}
|
||||
|
||||
func (copts *compressionOptions) String() string {
|
||||
s := "permessage-deflate"
|
||||
if copts.clientNoContextTakeover {
|
||||
s += "; client_no_context_takeover"
|
||||
}
|
||||
if copts.serverNoContextTakeover {
|
||||
s += "; server_no_context_takeover"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// These bytes are required to get flate.Reader to return.
|
||||
// They are removed when sending to avoid the overhead as
|
||||
// WebSocket framing tell's when the message has ended but then
|
||||
// we need to add them back otherwise flate.Reader keeps
|
||||
// trying to read more bytes.
|
||||
const deflateMessageTail = "\x00\x00\xff\xff"
|
||||
|
||||
type trimLastFourBytesWriter struct {
|
||||
w io.Writer
|
||||
tail []byte
|
||||
}
|
||||
|
||||
func (tw *trimLastFourBytesWriter) reset() {
|
||||
if tw != nil && tw.tail != nil {
|
||||
tw.tail = tw.tail[:0]
|
||||
}
|
||||
}
|
||||
|
||||
func (tw *trimLastFourBytesWriter) Write(p []byte) (int, error) {
|
||||
if tw.tail == nil {
|
||||
tw.tail = make([]byte, 0, 4)
|
||||
}
|
||||
|
||||
extra := len(tw.tail) + len(p) - 4
|
||||
|
||||
if extra <= 0 {
|
||||
tw.tail = append(tw.tail, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Now we need to write as many extra bytes as we can from the previous tail.
|
||||
if extra > len(tw.tail) {
|
||||
extra = len(tw.tail)
|
||||
}
|
||||
if extra > 0 {
|
||||
_, err := tw.w.Write(tw.tail[:extra])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Shift remaining bytes in tail over.
|
||||
n := copy(tw.tail, tw.tail[extra:])
|
||||
tw.tail = tw.tail[:n]
|
||||
}
|
||||
|
||||
// If p is less than or equal to 4 bytes,
|
||||
// all of it is is part of the tail.
|
||||
if len(p) <= 4 {
|
||||
tw.tail = append(tw.tail, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Otherwise, only the last 4 bytes are.
|
||||
tw.tail = append(tw.tail, p[len(p)-4:]...)
|
||||
|
||||
p = p[:len(p)-4]
|
||||
n, err := tw.w.Write(p)
|
||||
return n + 4, err
|
||||
}
|
||||
|
||||
var flateReaderPool sync.Pool
|
||||
|
||||
func getFlateReader(r io.Reader, dict []byte) io.Reader {
|
||||
fr, ok := flateReaderPool.Get().(io.Reader)
|
||||
if !ok {
|
||||
return flate.NewReaderDict(r, dict)
|
||||
}
|
||||
fr.(flate.Resetter).Reset(r, dict)
|
||||
return fr
|
||||
}
|
||||
|
||||
func putFlateReader(fr io.Reader) {
|
||||
flateReaderPool.Put(fr)
|
||||
}
|
||||
|
||||
var flateWriterPool sync.Pool
|
||||
|
||||
func getFlateWriter(w io.Writer) *flate.Writer {
|
||||
fw, ok := flateWriterPool.Get().(*flate.Writer)
|
||||
if !ok {
|
||||
fw, _ = flate.NewWriter(w, flate.BestSpeed)
|
||||
return fw
|
||||
}
|
||||
fw.Reset(w)
|
||||
return fw
|
||||
}
|
||||
|
||||
func putFlateWriter(w *flate.Writer) {
|
||||
flateWriterPool.Put(w)
|
||||
}
|
||||
|
||||
type slidingWindow struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
var swPoolMu sync.RWMutex
|
||||
var swPool = map[int]*sync.Pool{}
|
||||
|
||||
func slidingWindowPool(n int) *sync.Pool {
|
||||
swPoolMu.RLock()
|
||||
p, ok := swPool[n]
|
||||
swPoolMu.RUnlock()
|
||||
if ok {
|
||||
return p
|
||||
}
|
||||
|
||||
p = &sync.Pool{}
|
||||
|
||||
swPoolMu.Lock()
|
||||
swPool[n] = p
|
||||
swPoolMu.Unlock()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (sw *slidingWindow) init(n int) {
|
||||
if sw.buf != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
n = 32768
|
||||
}
|
||||
|
||||
p := slidingWindowPool(n)
|
||||
sw2, ok := p.Get().(*slidingWindow)
|
||||
if ok {
|
||||
*sw = *sw2
|
||||
} else {
|
||||
sw.buf = make([]byte, 0, n)
|
||||
}
|
||||
}
|
||||
|
||||
func (sw *slidingWindow) close() {
|
||||
sw.buf = sw.buf[:0]
|
||||
swPoolMu.Lock()
|
||||
swPool[cap(sw.buf)].Put(sw)
|
||||
swPoolMu.Unlock()
|
||||
}
|
||||
|
||||
func (sw *slidingWindow) write(p []byte) {
|
||||
if len(p) >= cap(sw.buf) {
|
||||
sw.buf = sw.buf[:cap(sw.buf)]
|
||||
p = p[len(p)-cap(sw.buf):]
|
||||
copy(sw.buf, p)
|
||||
return
|
||||
}
|
||||
|
||||
left := cap(sw.buf) - len(sw.buf)
|
||||
if left < len(p) {
|
||||
// We need to shift spaceNeeded bytes from the end to make room for p at the end.
|
||||
spaceNeeded := len(p) - left
|
||||
copy(sw.buf, sw.buf[spaceNeeded:])
|
||||
sw.buf = sw.buf[:len(sw.buf)-spaceNeeded]
|
||||
}
|
||||
|
||||
sw.buf = append(sw.buf, p...)
|
||||
}
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// MessageType represents the type of a WebSocket message.
|
||||
// See https://tools.ietf.org/html/rfc6455#section-5.6
|
||||
type MessageType int
|
||||
|
||||
// MessageType constants.
|
||||
const (
|
||||
// MessageText is for UTF-8 encoded text messages like JSON.
|
||||
MessageText MessageType = iota + 1
|
||||
// MessageBinary is for binary messages like protobufs.
|
||||
MessageBinary
|
||||
)
|
||||
|
||||
// Conn represents a WebSocket connection.
|
||||
// All methods may be called concurrently except for Reader and Read.
|
||||
//
|
||||
// You must always read from the connection. Otherwise control
|
||||
// frames will not be handled. See Reader and CloseRead.
|
||||
//
|
||||
// Be sure to call Close on the connection when you
|
||||
// are finished with it to release associated resources.
|
||||
//
|
||||
// On any error from any method, the connection is closed
|
||||
// with an appropriate reason.
|
||||
//
|
||||
// This applies to context expirations as well unfortunately.
|
||||
// See https://github.com/nhooyr/websocket/issues/242#issuecomment-633182220
|
||||
type Conn struct {
|
||||
noCopy noCopy
|
||||
|
||||
subprotocol string
|
||||
rwc io.ReadWriteCloser
|
||||
client bool
|
||||
copts *compressionOptions
|
||||
flateThreshold int
|
||||
br *bufio.Reader
|
||||
bw *bufio.Writer
|
||||
|
||||
readTimeout chan context.Context
|
||||
writeTimeout chan context.Context
|
||||
timeoutLoopDone chan struct{}
|
||||
|
||||
// Read state.
|
||||
readMu *mu
|
||||
readHeaderBuf [8]byte
|
||||
readControlBuf [maxControlPayload]byte
|
||||
msgReader *msgReader
|
||||
|
||||
// Write state.
|
||||
msgWriter *msgWriter
|
||||
writeFrameMu *mu
|
||||
writeBuf []byte
|
||||
writeHeaderBuf [8]byte
|
||||
writeHeader header
|
||||
|
||||
closeReadMu sync.Mutex
|
||||
closeReadCtx context.Context
|
||||
closeReadDone chan struct{}
|
||||
|
||||
closed chan struct{}
|
||||
closeMu sync.Mutex
|
||||
closing bool
|
||||
|
||||
pingCounter int32
|
||||
activePingsMu sync.Mutex
|
||||
activePings map[string]chan<- struct{}
|
||||
}
|
||||
|
||||
type connConfig struct {
|
||||
subprotocol string
|
||||
rwc io.ReadWriteCloser
|
||||
client bool
|
||||
copts *compressionOptions
|
||||
flateThreshold int
|
||||
|
||||
br *bufio.Reader
|
||||
bw *bufio.Writer
|
||||
}
|
||||
|
||||
func newConn(cfg connConfig) *Conn {
|
||||
c := &Conn{
|
||||
subprotocol: cfg.subprotocol,
|
||||
rwc: cfg.rwc,
|
||||
client: cfg.client,
|
||||
copts: cfg.copts,
|
||||
flateThreshold: cfg.flateThreshold,
|
||||
|
||||
br: cfg.br,
|
||||
bw: cfg.bw,
|
||||
|
||||
readTimeout: make(chan context.Context),
|
||||
writeTimeout: make(chan context.Context),
|
||||
timeoutLoopDone: make(chan struct{}),
|
||||
|
||||
closed: make(chan struct{}),
|
||||
activePings: make(map[string]chan<- struct{}),
|
||||
}
|
||||
|
||||
c.readMu = newMu(c)
|
||||
c.writeFrameMu = newMu(c)
|
||||
|
||||
c.msgReader = newMsgReader(c)
|
||||
|
||||
c.msgWriter = newMsgWriter(c)
|
||||
if c.client {
|
||||
c.writeBuf = extractBufioWriterBuf(c.bw, c.rwc)
|
||||
}
|
||||
|
||||
if c.flate() && c.flateThreshold == 0 {
|
||||
c.flateThreshold = 128
|
||||
if !c.msgWriter.flateContextTakeover() {
|
||||
c.flateThreshold = 512
|
||||
}
|
||||
}
|
||||
|
||||
runtime.SetFinalizer(c, func(c *Conn) {
|
||||
c.close()
|
||||
})
|
||||
|
||||
go c.timeoutLoop()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Subprotocol returns the negotiated subprotocol.
|
||||
// An empty string means the default protocol.
|
||||
func (c *Conn) Subprotocol() string {
|
||||
return c.subprotocol
|
||||
}
|
||||
|
||||
func (c *Conn) close() error {
|
||||
c.closeMu.Lock()
|
||||
defer c.closeMu.Unlock()
|
||||
|
||||
if c.isClosed() {
|
||||
return net.ErrClosed
|
||||
}
|
||||
runtime.SetFinalizer(c, nil)
|
||||
close(c.closed)
|
||||
|
||||
// Have to close after c.closed is closed to ensure any goroutine that wakes up
|
||||
// from the connection being closed also sees that c.closed is closed and returns
|
||||
// closeErr.
|
||||
err := c.rwc.Close()
|
||||
// With the close of rwc, these become safe to close.
|
||||
c.msgWriter.close()
|
||||
c.msgReader.close()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Conn) timeoutLoop() {
|
||||
defer close(c.timeoutLoopDone)
|
||||
|
||||
readCtx := context.Background()
|
||||
writeCtx := context.Background()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return
|
||||
|
||||
case writeCtx = <-c.writeTimeout:
|
||||
case readCtx = <-c.readTimeout:
|
||||
|
||||
case <-readCtx.Done():
|
||||
c.close()
|
||||
return
|
||||
case <-writeCtx.Done():
|
||||
c.close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) flate() bool {
|
||||
return c.copts != nil
|
||||
}
|
||||
|
||||
// Ping sends a ping to the peer and waits for a pong.
|
||||
// Use this to measure latency or ensure the peer is responsive.
|
||||
// Ping must be called concurrently with Reader as it does
|
||||
// not read from the connection but instead waits for a Reader call
|
||||
// to read the pong.
|
||||
//
|
||||
// TCP Keepalives should suffice for most use cases.
|
||||
func (c *Conn) Ping(ctx context.Context) error {
|
||||
p := atomic.AddInt32(&c.pingCounter, 1)
|
||||
|
||||
err := c.ping(ctx, strconv.Itoa(int(p)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to ping: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) ping(ctx context.Context, p string) error {
|
||||
pong := make(chan struct{}, 1)
|
||||
|
||||
c.activePingsMu.Lock()
|
||||
c.activePings[p] = pong
|
||||
c.activePingsMu.Unlock()
|
||||
|
||||
defer func() {
|
||||
c.activePingsMu.Lock()
|
||||
delete(c.activePings, p)
|
||||
c.activePingsMu.Unlock()
|
||||
}()
|
||||
|
||||
err := c.writeControl(ctx, opPing, []byte(p))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-c.closed:
|
||||
return net.ErrClosed
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("failed to wait for pong: %w", ctx.Err())
|
||||
case <-pong:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type mu struct {
|
||||
c *Conn
|
||||
ch chan struct{}
|
||||
}
|
||||
|
||||
func newMu(c *Conn) *mu {
|
||||
return &mu{
|
||||
c: c,
|
||||
ch: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mu) forceLock() {
|
||||
m.ch <- struct{}{}
|
||||
}
|
||||
|
||||
func (m *mu) tryLock() bool {
|
||||
select {
|
||||
case m.ch <- struct{}{}:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mu) lock(ctx context.Context) error {
|
||||
select {
|
||||
case <-m.c.closed:
|
||||
return net.ErrClosed
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("failed to acquire lock: %w", ctx.Err())
|
||||
case m.ch <- struct{}{}:
|
||||
// To make sure the connection is certainly alive.
|
||||
// As it's possible the send on m.ch was selected
|
||||
// over the receive on closed.
|
||||
select {
|
||||
case <-m.c.closed:
|
||||
// Make sure to release.
|
||||
m.unlock()
|
||||
return net.ErrClosed
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mu) unlock() {
|
||||
select {
|
||||
case <-m.ch:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
type noCopy struct{}
|
||||
|
||||
func (*noCopy) Lock() {}
|
||||
+330
@@ -0,0 +1,330 @@
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket/internal/errd"
|
||||
)
|
||||
|
||||
// DialOptions represents Dial's options.
|
||||
type DialOptions struct {
|
||||
// HTTPClient is used for the connection.
|
||||
// Its Transport must return writable bodies for WebSocket handshakes.
|
||||
// http.Transport does beginning with Go 1.12.
|
||||
HTTPClient *http.Client
|
||||
|
||||
// HTTPHeader specifies the HTTP headers included in the handshake request.
|
||||
HTTPHeader http.Header
|
||||
|
||||
// Host optionally overrides the Host HTTP header to send. If empty, the value
|
||||
// of URL.Host will be used.
|
||||
Host string
|
||||
|
||||
// Subprotocols lists the WebSocket subprotocols to negotiate with the server.
|
||||
Subprotocols []string
|
||||
|
||||
// CompressionMode controls the compression mode.
|
||||
// Defaults to CompressionDisabled.
|
||||
//
|
||||
// See docs on CompressionMode for details.
|
||||
CompressionMode CompressionMode
|
||||
|
||||
// CompressionThreshold controls the minimum size of a message before compression is applied.
|
||||
//
|
||||
// Defaults to 512 bytes for CompressionNoContextTakeover and 128 bytes
|
||||
// for CompressionContextTakeover.
|
||||
CompressionThreshold int
|
||||
}
|
||||
|
||||
func (opts *DialOptions) cloneWithDefaults(ctx context.Context) (context.Context, context.CancelFunc, *DialOptions) {
|
||||
var cancel context.CancelFunc
|
||||
|
||||
var o DialOptions
|
||||
if opts != nil {
|
||||
o = *opts
|
||||
}
|
||||
if o.HTTPClient == nil {
|
||||
o.HTTPClient = http.DefaultClient
|
||||
}
|
||||
if o.HTTPClient.Timeout > 0 {
|
||||
ctx, cancel = context.WithTimeout(ctx, o.HTTPClient.Timeout)
|
||||
|
||||
newClient := *o.HTTPClient
|
||||
newClient.Timeout = 0
|
||||
o.HTTPClient = &newClient
|
||||
}
|
||||
if o.HTTPHeader == nil {
|
||||
o.HTTPHeader = http.Header{}
|
||||
}
|
||||
newClient := *o.HTTPClient
|
||||
oldCheckRedirect := o.HTTPClient.CheckRedirect
|
||||
newClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
switch req.URL.Scheme {
|
||||
case "ws":
|
||||
req.URL.Scheme = "http"
|
||||
case "wss":
|
||||
req.URL.Scheme = "https"
|
||||
}
|
||||
if oldCheckRedirect != nil {
|
||||
return oldCheckRedirect(req, via)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
o.HTTPClient = &newClient
|
||||
|
||||
return ctx, cancel, &o
|
||||
}
|
||||
|
||||
// Dial performs a WebSocket handshake on url.
|
||||
//
|
||||
// The response is the WebSocket handshake response from the server.
|
||||
// You never need to close resp.Body yourself.
|
||||
//
|
||||
// If an error occurs, the returned response may be non nil.
|
||||
// However, you can only read the first 1024 bytes of the body.
|
||||
//
|
||||
// This function requires at least Go 1.12 as it uses a new feature
|
||||
// in net/http to perform WebSocket handshakes.
|
||||
// See docs on the HTTPClient option and https://github.com/golang/go/issues/26937#issuecomment-415855861
|
||||
//
|
||||
// URLs with http/https schemes will work and are interpreted as ws/wss.
|
||||
func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Response, error) {
|
||||
return dial(ctx, u, opts, nil)
|
||||
}
|
||||
|
||||
func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) (_ *Conn, _ *http.Response, err error) {
|
||||
defer errd.Wrap(&err, "failed to WebSocket dial")
|
||||
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel, opts = opts.cloneWithDefaults(ctx)
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
secWebSocketKey, err := secWebSocketKey(rand)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to generate Sec-WebSocket-Key: %w", err)
|
||||
}
|
||||
|
||||
var copts *compressionOptions
|
||||
if opts.CompressionMode != CompressionDisabled {
|
||||
copts = opts.CompressionMode.opts()
|
||||
}
|
||||
|
||||
resp, err := handshakeRequest(ctx, urls, opts, copts, secWebSocketKey)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
respBody := resp.Body
|
||||
resp.Body = nil
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// We read a bit of the body for easier debugging.
|
||||
r := io.LimitReader(respBody, 1024)
|
||||
|
||||
timer := time.AfterFunc(time.Second*3, func() {
|
||||
respBody.Close()
|
||||
})
|
||||
defer timer.Stop()
|
||||
|
||||
b, _ := io.ReadAll(r)
|
||||
respBody.Close()
|
||||
resp.Body = io.NopCloser(bytes.NewReader(b))
|
||||
}
|
||||
}()
|
||||
|
||||
copts, err = verifyServerResponse(opts, copts, secWebSocketKey, resp)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
rwc, ok := respBody.(io.ReadWriteCloser)
|
||||
if !ok {
|
||||
return nil, resp, fmt.Errorf("response body is not a io.ReadWriteCloser: %T", respBody)
|
||||
}
|
||||
|
||||
return newConn(connConfig{
|
||||
subprotocol: resp.Header.Get("Sec-WebSocket-Protocol"),
|
||||
rwc: rwc,
|
||||
client: true,
|
||||
copts: copts,
|
||||
flateThreshold: opts.CompressionThreshold,
|
||||
br: getBufioReader(rwc),
|
||||
bw: getBufioWriter(rwc),
|
||||
}), resp, nil
|
||||
}
|
||||
|
||||
func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, copts *compressionOptions, secWebSocketKey string) (*http.Response, error) {
|
||||
u, err := url.Parse(urls)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse url: %w", err)
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "ws":
|
||||
u.Scheme = "http"
|
||||
case "wss":
|
||||
u.Scheme = "https"
|
||||
case "http", "https":
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected url scheme: %q", u.Scheme)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new http request: %w", err)
|
||||
}
|
||||
if len(opts.Host) > 0 {
|
||||
req.Host = opts.Host
|
||||
}
|
||||
req.Header = opts.HTTPHeader.Clone()
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
req.Header.Set("Upgrade", "websocket")
|
||||
req.Header.Set("Sec-WebSocket-Version", "13")
|
||||
req.Header.Set("Sec-WebSocket-Key", secWebSocketKey)
|
||||
if len(opts.Subprotocols) > 0 {
|
||||
req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ","))
|
||||
}
|
||||
if copts != nil {
|
||||
req.Header.Set("Sec-WebSocket-Extensions", copts.String())
|
||||
}
|
||||
|
||||
resp, err := opts.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send handshake request: %w", err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func secWebSocketKey(rr io.Reader) (string, error) {
|
||||
if rr == nil {
|
||||
rr = rand.Reader
|
||||
}
|
||||
b := make([]byte, 16)
|
||||
_, err := io.ReadFull(rr, b)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read random data from rand.Reader: %w", err)
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
func verifyServerResponse(opts *DialOptions, copts *compressionOptions, secWebSocketKey string, resp *http.Response) (*compressionOptions, error) {
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
return nil, fmt.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode)
|
||||
}
|
||||
|
||||
if !headerContainsTokenIgnoreCase(resp.Header, "Connection", "Upgrade") {
|
||||
return nil, fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection"))
|
||||
}
|
||||
|
||||
if !headerContainsTokenIgnoreCase(resp.Header, "Upgrade", "WebSocket") {
|
||||
return nil, fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade"))
|
||||
}
|
||||
|
||||
if resp.Header.Get("Sec-WebSocket-Accept") != secWebSocketAccept(secWebSocketKey) {
|
||||
return nil, fmt.Errorf("WebSocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q",
|
||||
resp.Header.Get("Sec-WebSocket-Accept"),
|
||||
secWebSocketKey,
|
||||
)
|
||||
}
|
||||
|
||||
err := verifySubprotocol(opts.Subprotocols, resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return verifyServerExtensions(copts, resp.Header)
|
||||
}
|
||||
|
||||
func verifySubprotocol(subprotos []string, resp *http.Response) error {
|
||||
proto := resp.Header.Get("Sec-WebSocket-Protocol")
|
||||
if proto == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, sp2 := range subprotos {
|
||||
if strings.EqualFold(sp2, proto) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("WebSocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto)
|
||||
}
|
||||
|
||||
func verifyServerExtensions(copts *compressionOptions, h http.Header) (*compressionOptions, error) {
|
||||
exts := websocketExtensions(h)
|
||||
if len(exts) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ext := exts[0]
|
||||
if ext.name != "permessage-deflate" || len(exts) > 1 || copts == nil {
|
||||
return nil, fmt.Errorf("WebSocket protcol violation: unsupported extensions from server: %+v", exts[1:])
|
||||
}
|
||||
|
||||
_copts := *copts
|
||||
copts = &_copts
|
||||
|
||||
for _, p := range ext.params {
|
||||
switch p {
|
||||
case "client_no_context_takeover":
|
||||
copts.clientNoContextTakeover = true
|
||||
continue
|
||||
case "server_no_context_takeover":
|
||||
copts.serverNoContextTakeover = true
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(p, "server_max_window_bits=") {
|
||||
// We can't adjust the deflate window, but decoding with a larger window is acceptable.
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p)
|
||||
}
|
||||
|
||||
return copts, nil
|
||||
}
|
||||
|
||||
var bufioReaderPool sync.Pool
|
||||
|
||||
func getBufioReader(r io.Reader) *bufio.Reader {
|
||||
br, ok := bufioReaderPool.Get().(*bufio.Reader)
|
||||
if !ok {
|
||||
return bufio.NewReader(r)
|
||||
}
|
||||
br.Reset(r)
|
||||
return br
|
||||
}
|
||||
|
||||
func putBufioReader(br *bufio.Reader) {
|
||||
bufioReaderPool.Put(br)
|
||||
}
|
||||
|
||||
var bufioWriterPool sync.Pool
|
||||
|
||||
func getBufioWriter(w io.Writer) *bufio.Writer {
|
||||
bw, ok := bufioWriterPool.Get().(*bufio.Writer)
|
||||
if !ok {
|
||||
return bufio.NewWriter(w)
|
||||
}
|
||||
bw.Reset(w)
|
||||
return bw
|
||||
}
|
||||
|
||||
func putBufioWriter(bw *bufio.Writer) {
|
||||
bufioWriterPool.Put(bw)
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
// Package websocket implements the RFC 6455 WebSocket protocol.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc6455
|
||||
//
|
||||
// Use Dial to dial a WebSocket server.
|
||||
//
|
||||
// Use Accept to accept a WebSocket client.
|
||||
//
|
||||
// Conn represents the resulting WebSocket connection.
|
||||
//
|
||||
// The examples are the best way to understand how to correctly use the library.
|
||||
//
|
||||
// The wsjson subpackage contain helpers for JSON and protobuf messages.
|
||||
//
|
||||
// More documentation at https://github.com/coder/websocket.
|
||||
//
|
||||
// # Wasm
|
||||
//
|
||||
// The client side supports compiling to Wasm.
|
||||
// It wraps the WebSocket browser API.
|
||||
//
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
|
||||
//
|
||||
// Some important caveats to be aware of:
|
||||
//
|
||||
// - Accept always errors out
|
||||
// - Conn.Ping is no-op
|
||||
// - Conn.CloseNow is Close(StatusGoingAway, "")
|
||||
// - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op
|
||||
// - *http.Response from Dial is &http.Response{} with a 101 status code on success
|
||||
package websocket // import "github.com/coder/websocket"
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
//go:build !js
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"github.com/coder/websocket/internal/errd"
|
||||
)
|
||||
|
||||
// opcode represents a WebSocket opcode.
|
||||
type opcode int
|
||||
|
||||
// https://tools.ietf.org/html/rfc6455#section-11.8.
|
||||
const (
|
||||
opContinuation opcode = iota
|
||||
opText
|
||||
opBinary
|
||||
// 3 - 7 are reserved for further non-control frames.
|
||||
_
|
||||
_
|
||||
_
|
||||
_
|
||||
_
|
||||
opClose
|
||||
opPing
|
||||
opPong
|
||||
// 11-16 are reserved for further control frames.
|
||||
)
|
||||
|
||||
// header represents a WebSocket frame header.
|
||||
// See https://tools.ietf.org/html/rfc6455#section-5.2.
|
||||
type header struct {
|
||||
fin bool
|
||||
rsv1 bool
|
||||
rsv2 bool
|
||||
rsv3 bool
|
||||
opcode opcode
|
||||
|
||||
payloadLength int64
|
||||
|
||||
masked bool
|
||||
maskKey uint32
|
||||
}
|
||||
|
||||
// readFrameHeader reads a header from the reader.
|
||||
// See https://tools.ietf.org/html/rfc6455#section-5.2.
|
||||
func readFrameHeader(r *bufio.Reader, readBuf []byte) (h header, err error) {
|
||||
defer errd.Wrap(&err, "failed to read frame header")
|
||||
|
||||
b, err := r.ReadByte()
|
||||
if err != nil {
|
||||
return header{}, err
|
||||
}
|
||||
|
||||
h.fin = b&(1<<7) != 0
|
||||
h.rsv1 = b&(1<<6) != 0
|
||||
h.rsv2 = b&(1<<5) != 0
|
||||
h.rsv3 = b&(1<<4) != 0
|
||||
|
||||
h.opcode = opcode(b & 0xf)
|
||||
|
||||
b, err = r.ReadByte()
|
||||
if err != nil {
|
||||
return header{}, err
|
||||
}
|
||||
|
||||
h.masked = b&(1<<7) != 0
|
||||
|
||||
payloadLength := b &^ (1 << 7)
|
||||
switch {
|
||||
case payloadLength < 126:
|
||||
h.payloadLength = int64(payloadLength)
|
||||
case payloadLength == 126:
|
||||
_, err = io.ReadFull(r, readBuf[:2])
|
||||
h.payloadLength = int64(binary.BigEndian.Uint16(readBuf))
|
||||
case payloadLength == 127:
|
||||
_, err = io.ReadFull(r, readBuf)
|
||||
h.payloadLength = int64(binary.BigEndian.Uint64(readBuf))
|
||||
}
|
||||
if err != nil {
|
||||
return header{}, err
|
||||
}
|
||||
|
||||
if h.payloadLength < 0 {
|
||||
return header{}, fmt.Errorf("received negative payload length: %v", h.payloadLength)
|
||||
}
|
||||
|
||||
if h.masked {
|
||||
_, err = io.ReadFull(r, readBuf[:4])
|
||||
if err != nil {
|
||||
return header{}, err
|
||||
}
|
||||
h.maskKey = binary.LittleEndian.Uint32(readBuf)
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// maxControlPayload is the maximum length of a control frame payload.
|
||||
// See https://tools.ietf.org/html/rfc6455#section-5.5.
|
||||
const maxControlPayload = 125
|
||||
|
||||
// writeFrameHeader writes the bytes of the header to w.
|
||||
// See https://tools.ietf.org/html/rfc6455#section-5.2
|
||||
func writeFrameHeader(h header, w *bufio.Writer, buf []byte) (err error) {
|
||||
defer errd.Wrap(&err, "failed to write frame header")
|
||||
|
||||
var b byte
|
||||
if h.fin {
|
||||
b |= 1 << 7
|
||||
}
|
||||
if h.rsv1 {
|
||||
b |= 1 << 6
|
||||
}
|
||||
if h.rsv2 {
|
||||
b |= 1 << 5
|
||||
}
|
||||
if h.rsv3 {
|
||||
b |= 1 << 4
|
||||
}
|
||||
|
||||
b |= byte(h.opcode)
|
||||
|
||||
err = w.WriteByte(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lengthByte := byte(0)
|
||||
if h.masked {
|
||||
lengthByte |= 1 << 7
|
||||
}
|
||||
|
||||
switch {
|
||||
case h.payloadLength > math.MaxUint16:
|
||||
lengthByte |= 127
|
||||
case h.payloadLength > 125:
|
||||
lengthByte |= 126
|
||||
case h.payloadLength >= 0:
|
||||
lengthByte |= byte(h.payloadLength)
|
||||
}
|
||||
err = w.WriteByte(lengthByte)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
case h.payloadLength > math.MaxUint16:
|
||||
binary.BigEndian.PutUint64(buf, uint64(h.payloadLength))
|
||||
_, err = w.Write(buf)
|
||||
case h.payloadLength > 125:
|
||||
binary.BigEndian.PutUint16(buf, uint16(h.payloadLength))
|
||||
_, err = w.Write(buf[:2])
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if h.masked {
|
||||
binary.LittleEndian.PutUint32(buf, h.maskKey)
|
||||
_, err = w.Write(buf[:4])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package bpool
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var bpool sync.Pool
|
||||
|
||||
// Get returns a buffer from the pool or creates a new one if
|
||||
// the pool is empty.
|
||||
func Get() *bytes.Buffer {
|
||||
b := bpool.Get()
|
||||
if b == nil {
|
||||
return &bytes.Buffer{}
|
||||
}
|
||||
return b.(*bytes.Buffer)
|
||||
}
|
||||
|
||||
// Put returns a buffer into the pool.
|
||||
func Put(b *bytes.Buffer) {
|
||||
b.Reset()
|
||||
bpool.Put(b)
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package errd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Wrap wraps err with fmt.Errorf if err is non nil.
|
||||
// Intended for use with defer and a named error return.
|
||||
// Inspired by https://github.com/golang/go/issues/32676.
|
||||
func Wrap(err *error, f string, v ...interface{}) {
|
||||
if *err != nil {
|
||||
*err = fmt.Errorf(f+": %w", append(v, *err)...)
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package util
|
||||
|
||||
// WriterFunc is used to implement one off io.Writers.
|
||||
type WriterFunc func(p []byte) (int, error)
|
||||
|
||||
func (f WriterFunc) Write(p []byte) (int, error) {
|
||||
return f(p)
|
||||
}
|
||||
|
||||
// ReaderFunc is used to implement one off io.Readers.
|
||||
type ReaderFunc func(p []byte) (int, error)
|
||||
|
||||
func (f ReaderFunc) Read(p []byte) (int, error) {
|
||||
return f(p)
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
//go:build js
|
||||
// +build js
|
||||
|
||||
// Package wsjs implements typed access to the browser javascript WebSocket API.
|
||||
//
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
|
||||
package wsjs
|
||||
|
||||
import (
|
||||
"syscall/js"
|
||||
)
|
||||
|
||||
func handleJSError(err *error, onErr func()) {
|
||||
r := recover()
|
||||
|
||||
if jsErr, ok := r.(js.Error); ok {
|
||||
*err = jsErr
|
||||
|
||||
if onErr != nil {
|
||||
onErr()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if r != nil {
|
||||
panic(r)
|
||||
}
|
||||
}
|
||||
|
||||
// New is a wrapper around the javascript WebSocket constructor.
|
||||
func New(url string, protocols []string) (c WebSocket, err error) {
|
||||
defer handleJSError(&err, func() {
|
||||
c = WebSocket{}
|
||||
})
|
||||
|
||||
jsProtocols := make([]interface{}, len(protocols))
|
||||
for i, p := range protocols {
|
||||
jsProtocols[i] = p
|
||||
}
|
||||
|
||||
c = WebSocket{
|
||||
v: js.Global().Get("WebSocket").New(url, jsProtocols),
|
||||
}
|
||||
|
||||
c.setBinaryType("arraybuffer")
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// WebSocket is a wrapper around a javascript WebSocket object.
|
||||
type WebSocket struct {
|
||||
v js.Value
|
||||
}
|
||||
|
||||
func (c WebSocket) setBinaryType(typ string) {
|
||||
c.v.Set("binaryType", string(typ))
|
||||
}
|
||||
|
||||
func (c WebSocket) addEventListener(eventType string, fn func(e js.Value)) func() {
|
||||
f := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
fn(args[0])
|
||||
return nil
|
||||
})
|
||||
c.v.Call("addEventListener", eventType, f)
|
||||
|
||||
return func() {
|
||||
c.v.Call("removeEventListener", eventType, f)
|
||||
f.Release()
|
||||
}
|
||||
}
|
||||
|
||||
// CloseEvent is the type passed to a WebSocket close handler.
|
||||
type CloseEvent struct {
|
||||
Code uint16
|
||||
Reason string
|
||||
WasClean bool
|
||||
}
|
||||
|
||||
// OnClose registers a function to be called when the WebSocket is closed.
|
||||
func (c WebSocket) OnClose(fn func(CloseEvent)) (remove func()) {
|
||||
return c.addEventListener("close", func(e js.Value) {
|
||||
ce := CloseEvent{
|
||||
Code: uint16(e.Get("code").Int()),
|
||||
Reason: e.Get("reason").String(),
|
||||
WasClean: e.Get("wasClean").Bool(),
|
||||
}
|
||||
fn(ce)
|
||||
})
|
||||
}
|
||||
|
||||
// OnError registers a function to be called when there is an error
|
||||
// with the WebSocket.
|
||||
func (c WebSocket) OnError(fn func(e js.Value)) (remove func()) {
|
||||
return c.addEventListener("error", fn)
|
||||
}
|
||||
|
||||
// MessageEvent is the type passed to a message handler.
|
||||
type MessageEvent struct {
|
||||
// string or []byte.
|
||||
Data interface{}
|
||||
|
||||
// There are more fields to the interface but we don't use them.
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent
|
||||
}
|
||||
|
||||
// OnMessage registers a function to be called when the WebSocket receives a message.
|
||||
func (c WebSocket) OnMessage(fn func(m MessageEvent)) (remove func()) {
|
||||
return c.addEventListener("message", func(e js.Value) {
|
||||
var data interface{}
|
||||
|
||||
arrayBuffer := e.Get("data")
|
||||
if arrayBuffer.Type() == js.TypeString {
|
||||
data = arrayBuffer.String()
|
||||
} else {
|
||||
data = extractArrayBuffer(arrayBuffer)
|
||||
}
|
||||
|
||||
me := MessageEvent{
|
||||
Data: data,
|
||||
}
|
||||
fn(me)
|
||||
})
|
||||
}
|
||||
|
||||
// Subprotocol returns the WebSocket subprotocol in use.
|
||||
func (c WebSocket) Subprotocol() string {
|
||||
return c.v.Get("protocol").String()
|
||||
}
|
||||
|
||||
// OnOpen registers a function to be called when the WebSocket is opened.
|
||||
func (c WebSocket) OnOpen(fn func(e js.Value)) (remove func()) {
|
||||
return c.addEventListener("open", fn)
|
||||
}
|
||||
|
||||
// Close closes the WebSocket with the given code and reason.
|
||||
func (c WebSocket) Close(code int, reason string) (err error) {
|
||||
defer handleJSError(&err, nil)
|
||||
c.v.Call("close", code, reason)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendText sends the given string as a text message
|
||||
// on the WebSocket.
|
||||
func (c WebSocket) SendText(v string) (err error) {
|
||||
defer handleJSError(&err, nil)
|
||||
c.v.Call("send", v)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendBytes sends the given message as a binary message
|
||||
// on the WebSocket.
|
||||
func (c WebSocket) SendBytes(v []byte) (err error) {
|
||||
defer handleJSError(&err, nil)
|
||||
c.v.Call("send", uint8Array(v))
|
||||
return err
|
||||
}
|
||||
|
||||
func extractArrayBuffer(arrayBuffer js.Value) []byte {
|
||||
uint8Array := js.Global().Get("Uint8Array").New(arrayBuffer)
|
||||
dst := make([]byte, uint8Array.Length())
|
||||
js.CopyBytesToGo(dst, uint8Array)
|
||||
return dst
|
||||
}
|
||||
|
||||
func uint8Array(src []byte) js.Value {
|
||||
uint8Array := js.Global().Get("Uint8Array").New(len(src))
|
||||
js.CopyBytesToJS(uint8Array, src)
|
||||
return uint8Array
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package xsync
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
// Go allows running a function in another goroutine
|
||||
// and waiting for its error.
|
||||
func Go(fn func() error) <-chan error {
|
||||
errs := make(chan error, 1)
|
||||
go func() {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
select {
|
||||
case errs <- fmt.Errorf("panic in go fn: %v, %s", r, debug.Stack()):
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
errs <- fn()
|
||||
}()
|
||||
|
||||
return errs
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package xsync
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Int64 represents an atomic int64.
|
||||
type Int64 struct {
|
||||
// We do not use atomic.Load/StoreInt64 since it does not
|
||||
// work on 32 bit computers but we need 64 bit integers.
|
||||
i atomic.Value
|
||||
}
|
||||
|
||||
// Load loads the int64.
|
||||
func (v *Int64) Load() int64 {
|
||||
i, _ := v.i.Load().(int64)
|
||||
return i
|
||||
}
|
||||
|
||||
// Store stores the int64.
|
||||
func (v *Int64) Store(i int64) {
|
||||
v.i.Store(i)
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
cd -- "$(dirname "$0")"
|
||||
|
||||
echo "=== fmt.sh"
|
||||
./ci/fmt.sh
|
||||
echo "=== lint.sh"
|
||||
./ci/lint.sh
|
||||
echo "=== test.sh"
|
||||
./ci/test.sh "$@"
|
||||
echo "=== bench.sh"
|
||||
./ci/bench.sh
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math/bits"
|
||||
)
|
||||
|
||||
// maskGo applies the WebSocket masking algorithm to p
|
||||
// with the given key.
|
||||
// See https://tools.ietf.org/html/rfc6455#section-5.3
|
||||
//
|
||||
// The returned value is the correctly rotated key to
|
||||
// to continue to mask/unmask the message.
|
||||
//
|
||||
// It is optimized for LittleEndian and expects the key
|
||||
// to be in little endian.
|
||||
//
|
||||
// See https://github.com/golang/go/issues/31586
|
||||
func maskGo(b []byte, key uint32) uint32 {
|
||||
if len(b) >= 8 {
|
||||
key64 := uint64(key)<<32 | uint64(key)
|
||||
|
||||
// At some point in the future we can clean these unrolled loops up.
|
||||
// See https://github.com/golang/go/issues/31586#issuecomment-487436401
|
||||
|
||||
// Then we xor until b is less than 128 bytes.
|
||||
for len(b) >= 128 {
|
||||
v := binary.LittleEndian.Uint64(b)
|
||||
binary.LittleEndian.PutUint64(b, v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[8:16])
|
||||
binary.LittleEndian.PutUint64(b[8:16], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[16:24])
|
||||
binary.LittleEndian.PutUint64(b[16:24], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[24:32])
|
||||
binary.LittleEndian.PutUint64(b[24:32], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[32:40])
|
||||
binary.LittleEndian.PutUint64(b[32:40], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[40:48])
|
||||
binary.LittleEndian.PutUint64(b[40:48], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[48:56])
|
||||
binary.LittleEndian.PutUint64(b[48:56], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[56:64])
|
||||
binary.LittleEndian.PutUint64(b[56:64], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[64:72])
|
||||
binary.LittleEndian.PutUint64(b[64:72], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[72:80])
|
||||
binary.LittleEndian.PutUint64(b[72:80], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[80:88])
|
||||
binary.LittleEndian.PutUint64(b[80:88], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[88:96])
|
||||
binary.LittleEndian.PutUint64(b[88:96], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[96:104])
|
||||
binary.LittleEndian.PutUint64(b[96:104], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[104:112])
|
||||
binary.LittleEndian.PutUint64(b[104:112], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[112:120])
|
||||
binary.LittleEndian.PutUint64(b[112:120], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[120:128])
|
||||
binary.LittleEndian.PutUint64(b[120:128], v^key64)
|
||||
b = b[128:]
|
||||
}
|
||||
|
||||
// Then we xor until b is less than 64 bytes.
|
||||
for len(b) >= 64 {
|
||||
v := binary.LittleEndian.Uint64(b)
|
||||
binary.LittleEndian.PutUint64(b, v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[8:16])
|
||||
binary.LittleEndian.PutUint64(b[8:16], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[16:24])
|
||||
binary.LittleEndian.PutUint64(b[16:24], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[24:32])
|
||||
binary.LittleEndian.PutUint64(b[24:32], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[32:40])
|
||||
binary.LittleEndian.PutUint64(b[32:40], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[40:48])
|
||||
binary.LittleEndian.PutUint64(b[40:48], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[48:56])
|
||||
binary.LittleEndian.PutUint64(b[48:56], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[56:64])
|
||||
binary.LittleEndian.PutUint64(b[56:64], v^key64)
|
||||
b = b[64:]
|
||||
}
|
||||
|
||||
// Then we xor until b is less than 32 bytes.
|
||||
for len(b) >= 32 {
|
||||
v := binary.LittleEndian.Uint64(b)
|
||||
binary.LittleEndian.PutUint64(b, v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[8:16])
|
||||
binary.LittleEndian.PutUint64(b[8:16], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[16:24])
|
||||
binary.LittleEndian.PutUint64(b[16:24], v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[24:32])
|
||||
binary.LittleEndian.PutUint64(b[24:32], v^key64)
|
||||
b = b[32:]
|
||||
}
|
||||
|
||||
// Then we xor until b is less than 16 bytes.
|
||||
for len(b) >= 16 {
|
||||
v := binary.LittleEndian.Uint64(b)
|
||||
binary.LittleEndian.PutUint64(b, v^key64)
|
||||
v = binary.LittleEndian.Uint64(b[8:16])
|
||||
binary.LittleEndian.PutUint64(b[8:16], v^key64)
|
||||
b = b[16:]
|
||||
}
|
||||
|
||||
// Then we xor until b is less than 8 bytes.
|
||||
for len(b) >= 8 {
|
||||
v := binary.LittleEndian.Uint64(b)
|
||||
binary.LittleEndian.PutUint64(b, v^key64)
|
||||
b = b[8:]
|
||||
}
|
||||
}
|
||||
|
||||
// Then we xor until b is less than 4 bytes.
|
||||
for len(b) >= 4 {
|
||||
v := binary.LittleEndian.Uint32(b)
|
||||
binary.LittleEndian.PutUint32(b, v^key)
|
||||
b = b[4:]
|
||||
}
|
||||
|
||||
// xor remaining bytes.
|
||||
for i := range b {
|
||||
b[i] ^= byte(key)
|
||||
key = bits.RotateLeft32(key, -8)
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
#include "textflag.h"
|
||||
|
||||
// func maskAsm(b *byte, len int, key uint32)
|
||||
TEXT ·maskAsm(SB), NOSPLIT, $0-28
|
||||
// AX = b
|
||||
// CX = len (left length)
|
||||
// SI = key (uint32)
|
||||
// DI = uint64(SI) | uint64(SI)<<32
|
||||
MOVQ b+0(FP), AX
|
||||
MOVQ len+8(FP), CX
|
||||
MOVL key+16(FP), SI
|
||||
|
||||
// calculate the DI
|
||||
// DI = SI<<32 | SI
|
||||
MOVL SI, DI
|
||||
MOVQ DI, DX
|
||||
SHLQ $32, DI
|
||||
ORQ DX, DI
|
||||
|
||||
CMPQ CX, $15
|
||||
JLE less_than_16
|
||||
CMPQ CX, $63
|
||||
JLE less_than_64
|
||||
CMPQ CX, $128
|
||||
JLE sse
|
||||
TESTQ $31, AX
|
||||
JNZ unaligned
|
||||
|
||||
unaligned_loop_1byte:
|
||||
XORB SI, (AX)
|
||||
INCQ AX
|
||||
DECQ CX
|
||||
ROLL $24, SI
|
||||
TESTQ $7, AX
|
||||
JNZ unaligned_loop_1byte
|
||||
|
||||
// calculate DI again since SI was modified
|
||||
// DI = SI<<32 | SI
|
||||
MOVL SI, DI
|
||||
MOVQ DI, DX
|
||||
SHLQ $32, DI
|
||||
ORQ DX, DI
|
||||
|
||||
TESTQ $31, AX
|
||||
JZ sse
|
||||
|
||||
unaligned:
|
||||
TESTQ $7, AX // AND $7 & len, if not zero jump to loop_1b.
|
||||
JNZ unaligned_loop_1byte
|
||||
|
||||
unaligned_loop:
|
||||
// we don't need to check the CX since we know it's above 128
|
||||
XORQ DI, (AX)
|
||||
ADDQ $8, AX
|
||||
SUBQ $8, CX
|
||||
TESTQ $31, AX
|
||||
JNZ unaligned_loop
|
||||
JMP sse
|
||||
|
||||
sse:
|
||||
CMPQ CX, $0x40
|
||||
JL less_than_64
|
||||
MOVQ DI, X0
|
||||
PUNPCKLQDQ X0, X0
|
||||
|
||||
sse_loop:
|
||||
MOVOU 0*16(AX), X1
|
||||
MOVOU 1*16(AX), X2
|
||||
MOVOU 2*16(AX), X3
|
||||
MOVOU 3*16(AX), X4
|
||||
PXOR X0, X1
|
||||
PXOR X0, X2
|
||||
PXOR X0, X3
|
||||
PXOR X0, X4
|
||||
MOVOU X1, 0*16(AX)
|
||||
MOVOU X2, 1*16(AX)
|
||||
MOVOU X3, 2*16(AX)
|
||||
MOVOU X4, 3*16(AX)
|
||||
ADDQ $0x40, AX
|
||||
SUBQ $0x40, CX
|
||||
CMPQ CX, $0x40
|
||||
JAE sse_loop
|
||||
|
||||
less_than_64:
|
||||
TESTQ $32, CX
|
||||
JZ less_than_32
|
||||
XORQ DI, (AX)
|
||||
XORQ DI, 8(AX)
|
||||
XORQ DI, 16(AX)
|
||||
XORQ DI, 24(AX)
|
||||
ADDQ $32, AX
|
||||
|
||||
less_than_32:
|
||||
TESTQ $16, CX
|
||||
JZ less_than_16
|
||||
XORQ DI, (AX)
|
||||
XORQ DI, 8(AX)
|
||||
ADDQ $16, AX
|
||||
|
||||
less_than_16:
|
||||
TESTQ $8, CX
|
||||
JZ less_than_8
|
||||
XORQ DI, (AX)
|
||||
ADDQ $8, AX
|
||||
|
||||
less_than_8:
|
||||
TESTQ $4, CX
|
||||
JZ less_than_4
|
||||
XORL SI, (AX)
|
||||
ADDQ $4, AX
|
||||
|
||||
less_than_4:
|
||||
TESTQ $2, CX
|
||||
JZ less_than_2
|
||||
XORW SI, (AX)
|
||||
ROLL $16, SI
|
||||
ADDQ $2, AX
|
||||
|
||||
less_than_2:
|
||||
TESTQ $1, CX
|
||||
JZ done
|
||||
XORB SI, (AX)
|
||||
ROLL $24, SI
|
||||
|
||||
done:
|
||||
MOVL SI, ret+24(FP)
|
||||
RET
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
#include "textflag.h"
|
||||
|
||||
// func maskAsm(b *byte, len int, key uint32)
|
||||
TEXT ·maskAsm(SB), NOSPLIT, $0-28
|
||||
// R0 = b
|
||||
// R1 = len
|
||||
// R3 = key (uint32)
|
||||
// R2 = uint64(key)<<32 | uint64(key)
|
||||
MOVD b_ptr+0(FP), R0
|
||||
MOVD b_len+8(FP), R1
|
||||
MOVWU key+16(FP), R3
|
||||
MOVD R3, R2
|
||||
ORR R2<<32, R2, R2
|
||||
VDUP R2, V0.D2
|
||||
CMP $64, R1
|
||||
BLT less_than_64
|
||||
|
||||
loop_64:
|
||||
VLD1 (R0), [V1.B16, V2.B16, V3.B16, V4.B16]
|
||||
VEOR V1.B16, V0.B16, V1.B16
|
||||
VEOR V2.B16, V0.B16, V2.B16
|
||||
VEOR V3.B16, V0.B16, V3.B16
|
||||
VEOR V4.B16, V0.B16, V4.B16
|
||||
VST1.P [V1.B16, V2.B16, V3.B16, V4.B16], 64(R0)
|
||||
SUBS $64, R1
|
||||
CMP $64, R1
|
||||
BGE loop_64
|
||||
|
||||
less_than_64:
|
||||
CBZ R1, end
|
||||
TBZ $5, R1, less_than_32
|
||||
VLD1 (R0), [V1.B16, V2.B16]
|
||||
VEOR V1.B16, V0.B16, V1.B16
|
||||
VEOR V2.B16, V0.B16, V2.B16
|
||||
VST1.P [V1.B16, V2.B16], 32(R0)
|
||||
|
||||
less_than_32:
|
||||
TBZ $4, R1, less_than_16
|
||||
LDP (R0), (R11, R12)
|
||||
EOR R11, R2, R11
|
||||
EOR R12, R2, R12
|
||||
STP.P (R11, R12), 16(R0)
|
||||
|
||||
less_than_16:
|
||||
TBZ $3, R1, less_than_8
|
||||
MOVD (R0), R11
|
||||
EOR R2, R11, R11
|
||||
MOVD.P R11, 8(R0)
|
||||
|
||||
less_than_8:
|
||||
TBZ $2, R1, less_than_4
|
||||
MOVWU (R0), R11
|
||||
EORW R2, R11, R11
|
||||
MOVWU.P R11, 4(R0)
|
||||
|
||||
less_than_4:
|
||||
TBZ $1, R1, less_than_2
|
||||
MOVHU (R0), R11
|
||||
EORW R3, R11, R11
|
||||
MOVHU.P R11, 2(R0)
|
||||
RORW $16, R3
|
||||
|
||||
less_than_2:
|
||||
TBZ $0, R1, end
|
||||
MOVBU (R0), R11
|
||||
EORW R3, R11, R11
|
||||
MOVBU.P R11, 1(R0)
|
||||
RORW $8, R3
|
||||
|
||||
end:
|
||||
MOVWU R3, ret+24(FP)
|
||||
RET
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
//go:build amd64 || arm64
|
||||
|
||||
package websocket
|
||||
|
||||
func mask(b []byte, key uint32) uint32 {
|
||||
// TODO: Will enable in v1.9.0.
|
||||
return maskGo(b, key)
|
||||
/*
|
||||
if len(b) > 0 {
|
||||
return maskAsm(&b[0], len(b), key)
|
||||
}
|
||||
return key
|
||||
*/
|
||||
}
|
||||
|
||||
// @nhooyr: I am not confident that the amd64 or the arm64 implementations of this
|
||||
// function are perfect. There are almost certainly missing optimizations or
|
||||
// opportunities for simplification. I'm confident there are no bugs though.
|
||||
// For example, the arm64 implementation doesn't align memory like the amd64.
|
||||
// Or the amd64 implementation could use AVX512 instead of just AVX2.
|
||||
// The AVX2 code I had to disable anyway as it wasn't performing as expected.
|
||||
// See https://github.com/nhooyr/websocket/pull/326#issuecomment-1771138049
|
||||
//
|
||||
//go:noescape
|
||||
//lint:ignore U1000 disabled till v1.9.0
|
||||
func maskAsm(b *byte, len int, key uint32) uint32
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
//go:build !amd64 && !arm64 && !js
|
||||
|
||||
package websocket
|
||||
|
||||
func mask(b []byte, key uint32) uint32 {
|
||||
return maskGo(b, key)
|
||||
}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NetConn converts a *websocket.Conn into a net.Conn.
|
||||
//
|
||||
// It's for tunneling arbitrary protocols over WebSockets.
|
||||
// Few users of the library will need this but it's tricky to implement
|
||||
// correctly and so provided in the library.
|
||||
// See https://github.com/nhooyr/websocket/issues/100.
|
||||
//
|
||||
// Every Write to the net.Conn will correspond to a message write of
|
||||
// the given type on *websocket.Conn.
|
||||
//
|
||||
// The passed ctx bounds the lifetime of the net.Conn. If cancelled,
|
||||
// all reads and writes on the net.Conn will be cancelled.
|
||||
//
|
||||
// If a message is read that is not of the correct type, the connection
|
||||
// will be closed with StatusUnsupportedData and an error will be returned.
|
||||
//
|
||||
// Close will close the *websocket.Conn with StatusNormalClosure.
|
||||
//
|
||||
// When a deadline is hit and there is an active read or write goroutine, the
|
||||
// connection will be closed. This is different from most net.Conn implementations
|
||||
// where only the reading/writing goroutines are interrupted but the connection
|
||||
// is kept alive.
|
||||
//
|
||||
// The Addr methods will return the real addresses for connections obtained
|
||||
// from websocket.Accept. But for connections obtained from websocket.Dial, a mock net.Addr
|
||||
// will be returned that gives "websocket" for Network() and "websocket/unknown-addr" for
|
||||
// String(). This is because websocket.Dial only exposes a io.ReadWriteCloser instead of the
|
||||
// full net.Conn to us.
|
||||
//
|
||||
// When running as WASM, the Addr methods will always return the mock address described above.
|
||||
//
|
||||
// A received StatusNormalClosure or StatusGoingAway close frame will be translated to
|
||||
// io.EOF when reading.
|
||||
//
|
||||
// Furthermore, the ReadLimit is set to -1 to disable it.
|
||||
func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn {
|
||||
c.SetReadLimit(-1)
|
||||
|
||||
nc := &netConn{
|
||||
c: c,
|
||||
msgType: msgType,
|
||||
readMu: newMu(c),
|
||||
writeMu: newMu(c),
|
||||
}
|
||||
|
||||
nc.writeCtx, nc.writeCancel = context.WithCancel(ctx)
|
||||
nc.readCtx, nc.readCancel = context.WithCancel(ctx)
|
||||
|
||||
nc.writeTimer = time.AfterFunc(math.MaxInt64, func() {
|
||||
if !nc.writeMu.tryLock() {
|
||||
// If the lock cannot be acquired, then there is an
|
||||
// active write goroutine and so we should cancel the context.
|
||||
nc.writeCancel()
|
||||
return
|
||||
}
|
||||
defer nc.writeMu.unlock()
|
||||
|
||||
// Prevents future writes from writing until the deadline is reset.
|
||||
atomic.StoreInt64(&nc.writeExpired, 1)
|
||||
})
|
||||
if !nc.writeTimer.Stop() {
|
||||
<-nc.writeTimer.C
|
||||
}
|
||||
|
||||
nc.readTimer = time.AfterFunc(math.MaxInt64, func() {
|
||||
if !nc.readMu.tryLock() {
|
||||
// If the lock cannot be acquired, then there is an
|
||||
// active read goroutine and so we should cancel the context.
|
||||
nc.readCancel()
|
||||
return
|
||||
}
|
||||
defer nc.readMu.unlock()
|
||||
|
||||
// Prevents future reads from reading until the deadline is reset.
|
||||
atomic.StoreInt64(&nc.readExpired, 1)
|
||||
})
|
||||
if !nc.readTimer.Stop() {
|
||||
<-nc.readTimer.C
|
||||
}
|
||||
|
||||
return nc
|
||||
}
|
||||
|
||||
type netConn struct {
|
||||
// These must be first to be aligned on 32 bit platforms.
|
||||
// https://github.com/nhooyr/websocket/pull/438
|
||||
readExpired int64
|
||||
writeExpired int64
|
||||
|
||||
c *Conn
|
||||
msgType MessageType
|
||||
|
||||
writeTimer *time.Timer
|
||||
writeMu *mu
|
||||
writeCtx context.Context
|
||||
writeCancel context.CancelFunc
|
||||
|
||||
readTimer *time.Timer
|
||||
readMu *mu
|
||||
readCtx context.Context
|
||||
readCancel context.CancelFunc
|
||||
readEOFed bool
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
var _ net.Conn = &netConn{}
|
||||
|
||||
func (nc *netConn) Close() error {
|
||||
nc.writeTimer.Stop()
|
||||
nc.writeCancel()
|
||||
nc.readTimer.Stop()
|
||||
nc.readCancel()
|
||||
return nc.c.Close(StatusNormalClosure, "")
|
||||
}
|
||||
|
||||
func (nc *netConn) Write(p []byte) (int, error) {
|
||||
nc.writeMu.forceLock()
|
||||
defer nc.writeMu.unlock()
|
||||
|
||||
if atomic.LoadInt64(&nc.writeExpired) == 1 {
|
||||
return 0, fmt.Errorf("failed to write: %w", context.DeadlineExceeded)
|
||||
}
|
||||
|
||||
err := nc.c.Write(nc.writeCtx, nc.msgType, p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (nc *netConn) Read(p []byte) (int, error) {
|
||||
nc.readMu.forceLock()
|
||||
defer nc.readMu.unlock()
|
||||
|
||||
for {
|
||||
n, err := nc.read(p)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (nc *netConn) read(p []byte) (int, error) {
|
||||
if atomic.LoadInt64(&nc.readExpired) == 1 {
|
||||
return 0, fmt.Errorf("failed to read: %w", context.DeadlineExceeded)
|
||||
}
|
||||
|
||||
if nc.readEOFed {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
if nc.reader == nil {
|
||||
typ, r, err := nc.c.Reader(nc.readCtx)
|
||||
if err != nil {
|
||||
switch CloseStatus(err) {
|
||||
case StatusNormalClosure, StatusGoingAway:
|
||||
nc.readEOFed = true
|
||||
return 0, io.EOF
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
if typ != nc.msgType {
|
||||
err := fmt.Errorf("unexpected frame type read (expected %v): %v", nc.msgType, typ)
|
||||
nc.c.Close(StatusUnsupportedData, err.Error())
|
||||
return 0, err
|
||||
}
|
||||
nc.reader = r
|
||||
}
|
||||
|
||||
n, err := nc.reader.Read(p)
|
||||
if err == io.EOF {
|
||||
nc.reader = nil
|
||||
err = nil
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
type websocketAddr struct {
|
||||
}
|
||||
|
||||
func (a websocketAddr) Network() string {
|
||||
return "websocket"
|
||||
}
|
||||
|
||||
func (a websocketAddr) String() string {
|
||||
return "websocket/unknown-addr"
|
||||
}
|
||||
|
||||
func (nc *netConn) SetDeadline(t time.Time) error {
|
||||
nc.SetWriteDeadline(t)
|
||||
nc.SetReadDeadline(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nc *netConn) SetWriteDeadline(t time.Time) error {
|
||||
atomic.StoreInt64(&nc.writeExpired, 0)
|
||||
if t.IsZero() {
|
||||
nc.writeTimer.Stop()
|
||||
} else {
|
||||
dur := time.Until(t)
|
||||
if dur <= 0 {
|
||||
dur = 1
|
||||
}
|
||||
nc.writeTimer.Reset(dur)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nc *netConn) SetReadDeadline(t time.Time) error {
|
||||
atomic.StoreInt64(&nc.readExpired, 0)
|
||||
if t.IsZero() {
|
||||
nc.readTimer.Stop()
|
||||
} else {
|
||||
dur := time.Until(t)
|
||||
if dur <= 0 {
|
||||
dur = 1
|
||||
}
|
||||
nc.readTimer.Reset(dur)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package websocket
|
||||
|
||||
import "net"
|
||||
|
||||
func (nc *netConn) RemoteAddr() net.Addr {
|
||||
return websocketAddr{}
|
||||
}
|
||||
|
||||
func (nc *netConn) LocalAddr() net.Addr {
|
||||
return websocketAddr{}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
package websocket
|
||||
|
||||
import "net"
|
||||
|
||||
func (nc *netConn) RemoteAddr() net.Addr {
|
||||
if unc, ok := nc.c.rwc.(net.Conn); ok {
|
||||
return unc.RemoteAddr()
|
||||
}
|
||||
return websocketAddr{}
|
||||
}
|
||||
|
||||
func (nc *netConn) LocalAddr() net.Addr {
|
||||
if unc, ok := nc.c.rwc.(net.Conn); ok {
|
||||
return unc.LocalAddr()
|
||||
}
|
||||
return websocketAddr{}
|
||||
}
|
||||
+506
@@ -0,0 +1,506 @@
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket/internal/errd"
|
||||
"github.com/coder/websocket/internal/util"
|
||||
"github.com/coder/websocket/internal/xsync"
|
||||
)
|
||||
|
||||
// Reader reads from the connection until there is a WebSocket
|
||||
// data message to be read. It will handle ping, pong and close frames as appropriate.
|
||||
//
|
||||
// It returns the type of the message and an io.Reader to read it.
|
||||
// The passed context will also bound the reader.
|
||||
// Ensure you read to EOF otherwise the connection will hang.
|
||||
//
|
||||
// Call CloseRead if you do not expect any data messages from the peer.
|
||||
//
|
||||
// Only one Reader may be open at a time.
|
||||
//
|
||||
// If you need a separate timeout on the Reader call and the Read itself,
|
||||
// use time.AfterFunc to cancel the context passed in.
|
||||
// See https://github.com/nhooyr/websocket/issues/87#issue-451703332
|
||||
// Most users should not need this.
|
||||
func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) {
|
||||
return c.reader(ctx)
|
||||
}
|
||||
|
||||
// Read is a convenience method around Reader to read a single message
|
||||
// from the connection.
|
||||
func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) {
|
||||
typ, r, err := c.Reader(ctx)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(r)
|
||||
return typ, b, err
|
||||
}
|
||||
|
||||
// CloseRead starts a goroutine to read from the connection until it is closed
|
||||
// or a data message is received.
|
||||
//
|
||||
// Once CloseRead is called you cannot read any messages from the connection.
|
||||
// The returned context will be cancelled when the connection is closed.
|
||||
//
|
||||
// If a data message is received, the connection will be closed with StatusPolicyViolation.
|
||||
//
|
||||
// Call CloseRead when you do not expect to read any more messages.
|
||||
// Since it actively reads from the connection, it will ensure that ping, pong and close
|
||||
// frames are responded to. This means c.Ping and c.Close will still work as expected.
|
||||
//
|
||||
// This function is idempotent.
|
||||
func (c *Conn) CloseRead(ctx context.Context) context.Context {
|
||||
c.closeReadMu.Lock()
|
||||
ctx2 := c.closeReadCtx
|
||||
if ctx2 != nil {
|
||||
c.closeReadMu.Unlock()
|
||||
return ctx2
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
c.closeReadCtx = ctx
|
||||
c.closeReadDone = make(chan struct{})
|
||||
c.closeReadMu.Unlock()
|
||||
|
||||
go func() {
|
||||
defer close(c.closeReadDone)
|
||||
defer cancel()
|
||||
defer c.close()
|
||||
_, _, err := c.Reader(ctx)
|
||||
if err == nil {
|
||||
c.Close(StatusPolicyViolation, "unexpected data message")
|
||||
}
|
||||
}()
|
||||
return ctx
|
||||
}
|
||||
|
||||
// SetReadLimit sets the max number of bytes to read for a single message.
|
||||
// It applies to the Reader and Read methods.
|
||||
//
|
||||
// By default, the connection has a message read limit of 32768 bytes.
|
||||
//
|
||||
// When the limit is hit, the connection will be closed with StatusMessageTooBig.
|
||||
//
|
||||
// Set to -1 to disable.
|
||||
func (c *Conn) SetReadLimit(n int64) {
|
||||
if n >= 0 {
|
||||
// We read one more byte than the limit in case
|
||||
// there is a fin frame that needs to be read.
|
||||
n++
|
||||
}
|
||||
|
||||
c.msgReader.limitReader.limit.Store(n)
|
||||
}
|
||||
|
||||
const defaultReadLimit = 32768
|
||||
|
||||
func newMsgReader(c *Conn) *msgReader {
|
||||
mr := &msgReader{
|
||||
c: c,
|
||||
fin: true,
|
||||
}
|
||||
mr.readFunc = mr.read
|
||||
|
||||
mr.limitReader = newLimitReader(c, mr.readFunc, defaultReadLimit+1)
|
||||
return mr
|
||||
}
|
||||
|
||||
func (mr *msgReader) resetFlate() {
|
||||
if mr.flateContextTakeover() {
|
||||
if mr.dict == nil {
|
||||
mr.dict = &slidingWindow{}
|
||||
}
|
||||
mr.dict.init(32768)
|
||||
}
|
||||
if mr.flateBufio == nil {
|
||||
mr.flateBufio = getBufioReader(mr.readFunc)
|
||||
}
|
||||
|
||||
if mr.flateContextTakeover() {
|
||||
mr.flateReader = getFlateReader(mr.flateBufio, mr.dict.buf)
|
||||
} else {
|
||||
mr.flateReader = getFlateReader(mr.flateBufio, nil)
|
||||
}
|
||||
mr.limitReader.r = mr.flateReader
|
||||
mr.flateTail.Reset(deflateMessageTail)
|
||||
}
|
||||
|
||||
func (mr *msgReader) putFlateReader() {
|
||||
if mr.flateReader != nil {
|
||||
putFlateReader(mr.flateReader)
|
||||
mr.flateReader = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (mr *msgReader) close() {
|
||||
mr.c.readMu.forceLock()
|
||||
mr.putFlateReader()
|
||||
if mr.dict != nil {
|
||||
mr.dict.close()
|
||||
mr.dict = nil
|
||||
}
|
||||
if mr.flateBufio != nil {
|
||||
putBufioReader(mr.flateBufio)
|
||||
}
|
||||
|
||||
if mr.c.client {
|
||||
putBufioReader(mr.c.br)
|
||||
mr.c.br = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (mr *msgReader) flateContextTakeover() bool {
|
||||
if mr.c.client {
|
||||
return !mr.c.copts.serverNoContextTakeover
|
||||
}
|
||||
return !mr.c.copts.clientNoContextTakeover
|
||||
}
|
||||
|
||||
func (c *Conn) readRSV1Illegal(h header) bool {
|
||||
// If compression is disabled, rsv1 is illegal.
|
||||
if !c.flate() {
|
||||
return true
|
||||
}
|
||||
// rsv1 is only allowed on data frames beginning messages.
|
||||
if h.opcode != opText && h.opcode != opBinary {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Conn) readLoop(ctx context.Context) (header, error) {
|
||||
for {
|
||||
h, err := c.readFrameHeader(ctx)
|
||||
if err != nil {
|
||||
return header{}, err
|
||||
}
|
||||
|
||||
if h.rsv1 && c.readRSV1Illegal(h) || h.rsv2 || h.rsv3 {
|
||||
err := fmt.Errorf("received header with unexpected rsv bits set: %v:%v:%v", h.rsv1, h.rsv2, h.rsv3)
|
||||
c.writeError(StatusProtocolError, err)
|
||||
return header{}, err
|
||||
}
|
||||
|
||||
if !c.client && !h.masked {
|
||||
return header{}, errors.New("received unmasked frame from client")
|
||||
}
|
||||
|
||||
switch h.opcode {
|
||||
case opClose, opPing, opPong:
|
||||
err = c.handleControl(ctx, h)
|
||||
if err != nil {
|
||||
// Pass through CloseErrors when receiving a close frame.
|
||||
if h.opcode == opClose && CloseStatus(err) != -1 {
|
||||
return header{}, err
|
||||
}
|
||||
return header{}, fmt.Errorf("failed to handle control frame %v: %w", h.opcode, err)
|
||||
}
|
||||
case opContinuation, opText, opBinary:
|
||||
return h, nil
|
||||
default:
|
||||
err := fmt.Errorf("received unknown opcode %v", h.opcode)
|
||||
c.writeError(StatusProtocolError, err)
|
||||
return header{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) readFrameHeader(ctx context.Context) (header, error) {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return header{}, net.ErrClosed
|
||||
case c.readTimeout <- ctx:
|
||||
}
|
||||
|
||||
h, err := readFrameHeader(c.br, c.readHeaderBuf[:])
|
||||
if err != nil {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return header{}, net.ErrClosed
|
||||
case <-ctx.Done():
|
||||
return header{}, ctx.Err()
|
||||
default:
|
||||
return header{}, err
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-c.closed:
|
||||
return header{}, net.ErrClosed
|
||||
case c.readTimeout <- context.Background():
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return 0, net.ErrClosed
|
||||
case c.readTimeout <- ctx:
|
||||
}
|
||||
|
||||
n, err := io.ReadFull(c.br, p)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return n, net.ErrClosed
|
||||
case <-ctx.Done():
|
||||
return n, ctx.Err()
|
||||
default:
|
||||
return n, fmt.Errorf("failed to read frame payload: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-c.closed:
|
||||
return n, net.ErrClosed
|
||||
case c.readTimeout <- context.Background():
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *Conn) handleControl(ctx context.Context, h header) (err error) {
|
||||
if h.payloadLength < 0 || h.payloadLength > maxControlPayload {
|
||||
err := fmt.Errorf("received control frame payload with invalid length: %d", h.payloadLength)
|
||||
c.writeError(StatusProtocolError, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !h.fin {
|
||||
err := errors.New("received fragmented control frame")
|
||||
c.writeError(StatusProtocolError, err)
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
b := c.readControlBuf[:h.payloadLength]
|
||||
_, err = c.readFramePayload(ctx, b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if h.masked {
|
||||
mask(b, h.maskKey)
|
||||
}
|
||||
|
||||
switch h.opcode {
|
||||
case opPing:
|
||||
return c.writeControl(ctx, opPong, b)
|
||||
case opPong:
|
||||
c.activePingsMu.Lock()
|
||||
pong, ok := c.activePings[string(b)]
|
||||
c.activePingsMu.Unlock()
|
||||
if ok {
|
||||
select {
|
||||
case pong <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// opClose
|
||||
|
||||
ce, err := parseClosePayload(b)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("received invalid close payload: %w", err)
|
||||
c.writeError(StatusProtocolError, err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = fmt.Errorf("received close frame: %w", ce)
|
||||
c.writeClose(ce.Code, ce.Reason)
|
||||
c.readMu.unlock()
|
||||
c.close()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err error) {
|
||||
defer errd.Wrap(&err, "failed to get reader")
|
||||
|
||||
err = c.readMu.lock(ctx)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
defer c.readMu.unlock()
|
||||
|
||||
if !c.msgReader.fin {
|
||||
return 0, nil, errors.New("previous message not read to completion")
|
||||
}
|
||||
|
||||
h, err := c.readLoop(ctx)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
if h.opcode == opContinuation {
|
||||
err := errors.New("received continuation frame without text or binary frame")
|
||||
c.writeError(StatusProtocolError, err)
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
c.msgReader.reset(ctx, h)
|
||||
|
||||
return MessageType(h.opcode), c.msgReader, nil
|
||||
}
|
||||
|
||||
type msgReader struct {
|
||||
c *Conn
|
||||
|
||||
ctx context.Context
|
||||
flate bool
|
||||
flateReader io.Reader
|
||||
flateBufio *bufio.Reader
|
||||
flateTail strings.Reader
|
||||
limitReader *limitReader
|
||||
dict *slidingWindow
|
||||
|
||||
fin bool
|
||||
payloadLength int64
|
||||
maskKey uint32
|
||||
|
||||
// util.ReaderFunc(mr.Read) to avoid continuous allocations.
|
||||
readFunc util.ReaderFunc
|
||||
}
|
||||
|
||||
func (mr *msgReader) reset(ctx context.Context, h header) {
|
||||
mr.ctx = ctx
|
||||
mr.flate = h.rsv1
|
||||
mr.limitReader.reset(mr.readFunc)
|
||||
|
||||
if mr.flate {
|
||||
mr.resetFlate()
|
||||
}
|
||||
|
||||
mr.setFrame(h)
|
||||
}
|
||||
|
||||
func (mr *msgReader) setFrame(h header) {
|
||||
mr.fin = h.fin
|
||||
mr.payloadLength = h.payloadLength
|
||||
mr.maskKey = h.maskKey
|
||||
}
|
||||
|
||||
func (mr *msgReader) Read(p []byte) (n int, err error) {
|
||||
err = mr.c.readMu.lock(mr.ctx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to read: %w", err)
|
||||
}
|
||||
defer mr.c.readMu.unlock()
|
||||
|
||||
n, err = mr.limitReader.Read(p)
|
||||
if mr.flate && mr.flateContextTakeover() {
|
||||
p = p[:n]
|
||||
mr.dict.write(p)
|
||||
}
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) && mr.fin && mr.flate {
|
||||
mr.putFlateReader()
|
||||
return n, io.EOF
|
||||
}
|
||||
if err != nil {
|
||||
return n, fmt.Errorf("failed to read: %w", err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (mr *msgReader) read(p []byte) (int, error) {
|
||||
for {
|
||||
if mr.payloadLength == 0 {
|
||||
if mr.fin {
|
||||
if mr.flate {
|
||||
return mr.flateTail.Read(p)
|
||||
}
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
h, err := mr.c.readLoop(mr.ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if h.opcode != opContinuation {
|
||||
err := errors.New("received new data message without finishing the previous message")
|
||||
mr.c.writeError(StatusProtocolError, err)
|
||||
return 0, err
|
||||
}
|
||||
mr.setFrame(h)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if int64(len(p)) > mr.payloadLength {
|
||||
p = p[:mr.payloadLength]
|
||||
}
|
||||
|
||||
n, err := mr.c.readFramePayload(mr.ctx, p)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
mr.payloadLength -= int64(n)
|
||||
|
||||
if !mr.c.client {
|
||||
mr.maskKey = mask(p, mr.maskKey)
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
|
||||
type limitReader struct {
|
||||
c *Conn
|
||||
r io.Reader
|
||||
limit xsync.Int64
|
||||
n int64
|
||||
}
|
||||
|
||||
func newLimitReader(c *Conn, r io.Reader, limit int64) *limitReader {
|
||||
lr := &limitReader{
|
||||
c: c,
|
||||
}
|
||||
lr.limit.Store(limit)
|
||||
lr.reset(r)
|
||||
return lr
|
||||
}
|
||||
|
||||
func (lr *limitReader) reset(r io.Reader) {
|
||||
lr.n = lr.limit.Load()
|
||||
lr.r = r
|
||||
}
|
||||
|
||||
func (lr *limitReader) Read(p []byte) (int, error) {
|
||||
if lr.n < 0 {
|
||||
return lr.r.Read(p)
|
||||
}
|
||||
|
||||
if lr.n == 0 {
|
||||
err := fmt.Errorf("read limited at %v bytes", lr.limit.Load())
|
||||
lr.c.writeError(StatusMessageTooBig, err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if int64(len(p)) > lr.n {
|
||||
p = p[:lr.n]
|
||||
}
|
||||
n, err := lr.r.Read(p)
|
||||
lr.n -= int64(n)
|
||||
if lr.n < 0 {
|
||||
lr.n = 0
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
// Code generated by "stringer -type=opcode,MessageType,StatusCode -output=stringer.go"; DO NOT EDIT.
|
||||
|
||||
package websocket
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[opContinuation-0]
|
||||
_ = x[opText-1]
|
||||
_ = x[opBinary-2]
|
||||
_ = x[opClose-8]
|
||||
_ = x[opPing-9]
|
||||
_ = x[opPong-10]
|
||||
}
|
||||
|
||||
const (
|
||||
_opcode_name_0 = "opContinuationopTextopBinary"
|
||||
_opcode_name_1 = "opCloseopPingopPong"
|
||||
)
|
||||
|
||||
var (
|
||||
_opcode_index_0 = [...]uint8{0, 14, 20, 28}
|
||||
_opcode_index_1 = [...]uint8{0, 7, 13, 19}
|
||||
)
|
||||
|
||||
func (i opcode) String() string {
|
||||
switch {
|
||||
case 0 <= i && i <= 2:
|
||||
return _opcode_name_0[_opcode_index_0[i]:_opcode_index_0[i+1]]
|
||||
case 8 <= i && i <= 10:
|
||||
i -= 8
|
||||
return _opcode_name_1[_opcode_index_1[i]:_opcode_index_1[i+1]]
|
||||
default:
|
||||
return "opcode(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[MessageText-1]
|
||||
_ = x[MessageBinary-2]
|
||||
}
|
||||
|
||||
const _MessageType_name = "MessageTextMessageBinary"
|
||||
|
||||
var _MessageType_index = [...]uint8{0, 11, 24}
|
||||
|
||||
func (i MessageType) String() string {
|
||||
i -= 1
|
||||
if i < 0 || i >= MessageType(len(_MessageType_index)-1) {
|
||||
return "MessageType(" + strconv.FormatInt(int64(i+1), 10) + ")"
|
||||
}
|
||||
return _MessageType_name[_MessageType_index[i]:_MessageType_index[i+1]]
|
||||
}
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[StatusNormalClosure-1000]
|
||||
_ = x[StatusGoingAway-1001]
|
||||
_ = x[StatusProtocolError-1002]
|
||||
_ = x[StatusUnsupportedData-1003]
|
||||
_ = x[statusReserved-1004]
|
||||
_ = x[StatusNoStatusRcvd-1005]
|
||||
_ = x[StatusAbnormalClosure-1006]
|
||||
_ = x[StatusInvalidFramePayloadData-1007]
|
||||
_ = x[StatusPolicyViolation-1008]
|
||||
_ = x[StatusMessageTooBig-1009]
|
||||
_ = x[StatusMandatoryExtension-1010]
|
||||
_ = x[StatusInternalError-1011]
|
||||
_ = x[StatusServiceRestart-1012]
|
||||
_ = x[StatusTryAgainLater-1013]
|
||||
_ = x[StatusBadGateway-1014]
|
||||
_ = x[StatusTLSHandshake-1015]
|
||||
}
|
||||
|
||||
const _StatusCode_name = "StatusNormalClosureStatusGoingAwayStatusProtocolErrorStatusUnsupportedDatastatusReservedStatusNoStatusRcvdStatusAbnormalClosureStatusInvalidFramePayloadDataStatusPolicyViolationStatusMessageTooBigStatusMandatoryExtensionStatusInternalErrorStatusServiceRestartStatusTryAgainLaterStatusBadGatewayStatusTLSHandshake"
|
||||
|
||||
var _StatusCode_index = [...]uint16{0, 19, 34, 53, 74, 88, 106, 127, 156, 177, 196, 220, 239, 259, 278, 294, 312}
|
||||
|
||||
func (i StatusCode) String() string {
|
||||
i -= 1000
|
||||
if i < 0 || i >= StatusCode(len(_StatusCode_index)-1) {
|
||||
return "StatusCode(" + strconv.FormatInt(int64(i+1000), 10) + ")"
|
||||
}
|
||||
return _StatusCode_name[_StatusCode_index[i]:_StatusCode_index[i+1]]
|
||||
}
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"compress/flate"
|
||||
|
||||
"github.com/coder/websocket/internal/errd"
|
||||
"github.com/coder/websocket/internal/util"
|
||||
)
|
||||
|
||||
// Writer returns a writer bounded by the context that will write
|
||||
// a WebSocket message of type dataType to the connection.
|
||||
//
|
||||
// You must close the writer once you have written the entire message.
|
||||
//
|
||||
// Only one writer can be open at a time, multiple calls will block until the previous writer
|
||||
// is closed.
|
||||
func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) {
|
||||
w, err := c.writer(ctx, typ)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get writer: %w", err)
|
||||
}
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Write writes a message to the connection.
|
||||
//
|
||||
// See the Writer method if you want to stream a message.
|
||||
//
|
||||
// If compression is disabled or the compression threshold is not met, then it
|
||||
// will write the message in a single frame.
|
||||
func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error {
|
||||
_, err := c.write(ctx, typ, p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write msg: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type msgWriter struct {
|
||||
c *Conn
|
||||
|
||||
mu *mu
|
||||
writeMu *mu
|
||||
closed bool
|
||||
|
||||
ctx context.Context
|
||||
opcode opcode
|
||||
flate bool
|
||||
|
||||
trimWriter *trimLastFourBytesWriter
|
||||
flateWriter *flate.Writer
|
||||
}
|
||||
|
||||
func newMsgWriter(c *Conn) *msgWriter {
|
||||
mw := &msgWriter{
|
||||
c: c,
|
||||
mu: newMu(c),
|
||||
writeMu: newMu(c),
|
||||
}
|
||||
return mw
|
||||
}
|
||||
|
||||
func (mw *msgWriter) ensureFlate() {
|
||||
if mw.trimWriter == nil {
|
||||
mw.trimWriter = &trimLastFourBytesWriter{
|
||||
w: util.WriterFunc(mw.write),
|
||||
}
|
||||
}
|
||||
|
||||
if mw.flateWriter == nil {
|
||||
mw.flateWriter = getFlateWriter(mw.trimWriter)
|
||||
}
|
||||
mw.flate = true
|
||||
}
|
||||
|
||||
func (mw *msgWriter) flateContextTakeover() bool {
|
||||
if mw.c.client {
|
||||
return !mw.c.copts.clientNoContextTakeover
|
||||
}
|
||||
return !mw.c.copts.serverNoContextTakeover
|
||||
}
|
||||
|
||||
func (c *Conn) writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) {
|
||||
err := c.msgWriter.reset(ctx, typ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.msgWriter, nil
|
||||
}
|
||||
|
||||
func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error) {
|
||||
mw, err := c.writer(ctx, typ)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if !c.flate() {
|
||||
defer c.msgWriter.mu.unlock()
|
||||
return c.writeFrame(ctx, true, false, c.msgWriter.opcode, p)
|
||||
}
|
||||
|
||||
n, err := mw.Write(p)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
err = mw.Close()
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (mw *msgWriter) reset(ctx context.Context, typ MessageType) error {
|
||||
err := mw.mu.lock(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mw.ctx = ctx
|
||||
mw.opcode = opcode(typ)
|
||||
mw.flate = false
|
||||
mw.closed = false
|
||||
|
||||
mw.trimWriter.reset()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mw *msgWriter) putFlateWriter() {
|
||||
if mw.flateWriter != nil {
|
||||
putFlateWriter(mw.flateWriter)
|
||||
mw.flateWriter = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes the given bytes to the WebSocket connection.
|
||||
func (mw *msgWriter) Write(p []byte) (_ int, err error) {
|
||||
err = mw.writeMu.lock(mw.ctx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to write: %w", err)
|
||||
}
|
||||
defer mw.writeMu.unlock()
|
||||
|
||||
if mw.closed {
|
||||
return 0, errors.New("cannot use closed writer")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to write: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if mw.c.flate() {
|
||||
// Only enables flate if the length crosses the
|
||||
// threshold on the first frame
|
||||
if mw.opcode != opContinuation && len(p) >= mw.c.flateThreshold {
|
||||
mw.ensureFlate()
|
||||
}
|
||||
}
|
||||
|
||||
if mw.flate {
|
||||
return mw.flateWriter.Write(p)
|
||||
}
|
||||
|
||||
return mw.write(p)
|
||||
}
|
||||
|
||||
func (mw *msgWriter) write(p []byte) (int, error) {
|
||||
n, err := mw.c.writeFrame(mw.ctx, false, mw.flate, mw.opcode, p)
|
||||
if err != nil {
|
||||
return n, fmt.Errorf("failed to write data frame: %w", err)
|
||||
}
|
||||
mw.opcode = opContinuation
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Close flushes the frame to the connection.
|
||||
func (mw *msgWriter) Close() (err error) {
|
||||
defer errd.Wrap(&err, "failed to close writer")
|
||||
|
||||
err = mw.writeMu.lock(mw.ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer mw.writeMu.unlock()
|
||||
|
||||
if mw.closed {
|
||||
return errors.New("writer already closed")
|
||||
}
|
||||
mw.closed = true
|
||||
|
||||
if mw.flate {
|
||||
err = mw.flateWriter.Flush()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to flush flate: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = mw.c.writeFrame(mw.ctx, true, mw.flate, mw.opcode, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write fin frame: %w", err)
|
||||
}
|
||||
|
||||
if mw.flate && !mw.flateContextTakeover() {
|
||||
mw.putFlateWriter()
|
||||
}
|
||||
mw.mu.unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mw *msgWriter) close() {
|
||||
if mw.c.client {
|
||||
mw.c.writeFrameMu.forceLock()
|
||||
putBufioWriter(mw.c.bw)
|
||||
}
|
||||
|
||||
mw.writeMu.forceLock()
|
||||
mw.putFlateWriter()
|
||||
}
|
||||
|
||||
func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
_, err := c.writeFrame(ctx, true, false, opcode, p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write control frame %v: %w", opcode, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeFrame handles all writes to the connection.
|
||||
func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (_ int, err error) {
|
||||
err = c.writeFrameMu.lock(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer c.writeFrameMu.unlock()
|
||||
|
||||
select {
|
||||
case <-c.closed:
|
||||
return 0, net.ErrClosed
|
||||
case c.writeTimeout <- ctx:
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
select {
|
||||
case <-c.closed:
|
||||
err = net.ErrClosed
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
default:
|
||||
}
|
||||
err = fmt.Errorf("failed to write frame: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
c.writeHeader.fin = fin
|
||||
c.writeHeader.opcode = opcode
|
||||
c.writeHeader.payloadLength = int64(len(p))
|
||||
|
||||
if c.client {
|
||||
c.writeHeader.masked = true
|
||||
_, err = io.ReadFull(rand.Reader, c.writeHeaderBuf[:4])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to generate masking key: %w", err)
|
||||
}
|
||||
c.writeHeader.maskKey = binary.LittleEndian.Uint32(c.writeHeaderBuf[:])
|
||||
}
|
||||
|
||||
c.writeHeader.rsv1 = false
|
||||
if flate && (opcode == opText || opcode == opBinary) {
|
||||
c.writeHeader.rsv1 = true
|
||||
}
|
||||
|
||||
err = writeFrameHeader(c.writeHeader, c.bw, c.writeHeaderBuf[:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n, err := c.writeFramePayload(p)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
if c.writeHeader.fin {
|
||||
err = c.bw.Flush()
|
||||
if err != nil {
|
||||
return n, fmt.Errorf("failed to flush: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-c.closed:
|
||||
if opcode == opClose {
|
||||
return n, nil
|
||||
}
|
||||
return n, net.ErrClosed
|
||||
case c.writeTimeout <- context.Background():
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *Conn) writeFramePayload(p []byte) (n int, err error) {
|
||||
defer errd.Wrap(&err, "failed to write frame payload")
|
||||
|
||||
if !c.writeHeader.masked {
|
||||
return c.bw.Write(p)
|
||||
}
|
||||
|
||||
maskKey := c.writeHeader.maskKey
|
||||
for len(p) > 0 {
|
||||
// If the buffer is full, we need to flush.
|
||||
if c.bw.Available() == 0 {
|
||||
err = c.bw.Flush()
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
// Start of next write in the buffer.
|
||||
i := c.bw.Buffered()
|
||||
|
||||
j := len(p)
|
||||
if j > c.bw.Available() {
|
||||
j = c.bw.Available()
|
||||
}
|
||||
|
||||
_, err := c.bw.Write(p[:j])
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
maskKey = mask(c.writeBuf[i:c.bw.Buffered()], maskKey)
|
||||
|
||||
p = p[j:]
|
||||
n += j
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// extractBufioWriterBuf grabs the []byte backing a *bufio.Writer
|
||||
// and returns it.
|
||||
func extractBufioWriterBuf(bw *bufio.Writer, w io.Writer) []byte {
|
||||
var writeBuf []byte
|
||||
bw.Reset(util.WriterFunc(func(p2 []byte) (int, error) {
|
||||
writeBuf = p2[:cap(p2)]
|
||||
return len(p2), nil
|
||||
}))
|
||||
|
||||
bw.WriteByte(0)
|
||||
bw.Flush()
|
||||
|
||||
bw.Reset(w)
|
||||
|
||||
return writeBuf
|
||||
}
|
||||
|
||||
func (c *Conn) writeError(code StatusCode, err error) {
|
||||
c.writeClose(code, err.Error())
|
||||
}
|
||||
+598
@@ -0,0 +1,598 @@
|
||||
package websocket // import "github.com/coder/websocket"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall/js"
|
||||
|
||||
"github.com/coder/websocket/internal/bpool"
|
||||
"github.com/coder/websocket/internal/wsjs"
|
||||
"github.com/coder/websocket/internal/xsync"
|
||||
)
|
||||
|
||||
// opcode represents a WebSocket opcode.
|
||||
type opcode int
|
||||
|
||||
// https://tools.ietf.org/html/rfc6455#section-11.8.
|
||||
const (
|
||||
opContinuation opcode = iota
|
||||
opText
|
||||
opBinary
|
||||
// 3 - 7 are reserved for further non-control frames.
|
||||
_
|
||||
_
|
||||
_
|
||||
_
|
||||
_
|
||||
opClose
|
||||
opPing
|
||||
opPong
|
||||
// 11-16 are reserved for further control frames.
|
||||
)
|
||||
|
||||
// Conn provides a wrapper around the browser WebSocket API.
|
||||
type Conn struct {
|
||||
noCopy noCopy
|
||||
ws wsjs.WebSocket
|
||||
|
||||
// read limit for a message in bytes.
|
||||
msgReadLimit xsync.Int64
|
||||
|
||||
closeReadMu sync.Mutex
|
||||
closeReadCtx context.Context
|
||||
|
||||
closingMu sync.Mutex
|
||||
closeOnce sync.Once
|
||||
closed chan struct{}
|
||||
closeErrOnce sync.Once
|
||||
closeErr error
|
||||
closeWasClean bool
|
||||
|
||||
releaseOnClose func()
|
||||
releaseOnError func()
|
||||
releaseOnMessage func()
|
||||
|
||||
readSignal chan struct{}
|
||||
readBufMu sync.Mutex
|
||||
readBuf []wsjs.MessageEvent
|
||||
}
|
||||
|
||||
func (c *Conn) close(err error, wasClean bool) {
|
||||
c.closeOnce.Do(func() {
|
||||
runtime.SetFinalizer(c, nil)
|
||||
|
||||
if !wasClean {
|
||||
err = fmt.Errorf("unclean connection close: %w", err)
|
||||
}
|
||||
c.setCloseErr(err)
|
||||
c.closeWasClean = wasClean
|
||||
close(c.closed)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Conn) init() {
|
||||
c.closed = make(chan struct{})
|
||||
c.readSignal = make(chan struct{}, 1)
|
||||
|
||||
c.msgReadLimit.Store(32768)
|
||||
|
||||
c.releaseOnClose = c.ws.OnClose(func(e wsjs.CloseEvent) {
|
||||
err := CloseError{
|
||||
Code: StatusCode(e.Code),
|
||||
Reason: e.Reason,
|
||||
}
|
||||
// We do not know if we sent or received this close as
|
||||
// its possible the browser triggered it without us
|
||||
// explicitly sending it.
|
||||
c.close(err, e.WasClean)
|
||||
|
||||
c.releaseOnClose()
|
||||
c.releaseOnError()
|
||||
c.releaseOnMessage()
|
||||
})
|
||||
|
||||
c.releaseOnError = c.ws.OnError(func(v js.Value) {
|
||||
c.setCloseErr(errors.New(v.Get("message").String()))
|
||||
c.closeWithInternal()
|
||||
})
|
||||
|
||||
c.releaseOnMessage = c.ws.OnMessage(func(e wsjs.MessageEvent) {
|
||||
c.readBufMu.Lock()
|
||||
defer c.readBufMu.Unlock()
|
||||
|
||||
c.readBuf = append(c.readBuf, e)
|
||||
|
||||
// Lets the read goroutine know there is definitely something in readBuf.
|
||||
select {
|
||||
case c.readSignal <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
})
|
||||
|
||||
runtime.SetFinalizer(c, func(c *Conn) {
|
||||
c.setCloseErr(errors.New("connection garbage collected"))
|
||||
c.closeWithInternal()
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Conn) closeWithInternal() {
|
||||
c.Close(StatusInternalError, "something went wrong")
|
||||
}
|
||||
|
||||
// Read attempts to read a message from the connection.
|
||||
// The maximum time spent waiting is bounded by the context.
|
||||
func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) {
|
||||
c.closeReadMu.Lock()
|
||||
closedRead := c.closeReadCtx != nil
|
||||
c.closeReadMu.Unlock()
|
||||
if closedRead {
|
||||
return 0, nil, errors.New("WebSocket connection read closed")
|
||||
}
|
||||
|
||||
typ, p, err := c.read(ctx)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("failed to read: %w", err)
|
||||
}
|
||||
readLimit := c.msgReadLimit.Load()
|
||||
if readLimit >= 0 && int64(len(p)) > readLimit {
|
||||
err := fmt.Errorf("read limited at %v bytes", c.msgReadLimit.Load())
|
||||
c.Close(StatusMessageTooBig, err.Error())
|
||||
return 0, nil, err
|
||||
}
|
||||
return typ, p, nil
|
||||
}
|
||||
|
||||
func (c *Conn) read(ctx context.Context) (MessageType, []byte, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.Close(StatusPolicyViolation, "read timed out")
|
||||
return 0, nil, ctx.Err()
|
||||
case <-c.readSignal:
|
||||
case <-c.closed:
|
||||
return 0, nil, net.ErrClosed
|
||||
}
|
||||
|
||||
c.readBufMu.Lock()
|
||||
defer c.readBufMu.Unlock()
|
||||
|
||||
me := c.readBuf[0]
|
||||
// We copy the messages forward and decrease the size
|
||||
// of the slice to avoid reallocating.
|
||||
copy(c.readBuf, c.readBuf[1:])
|
||||
c.readBuf = c.readBuf[:len(c.readBuf)-1]
|
||||
|
||||
if len(c.readBuf) > 0 {
|
||||
// Next time we read, we'll grab the message.
|
||||
select {
|
||||
case c.readSignal <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
switch p := me.Data.(type) {
|
||||
case string:
|
||||
return MessageText, []byte(p), nil
|
||||
case []byte:
|
||||
return MessageBinary, p, nil
|
||||
default:
|
||||
panic("websocket: unexpected data type from wsjs OnMessage: " + reflect.TypeOf(me.Data).String())
|
||||
}
|
||||
}
|
||||
|
||||
// Ping is mocked out for Wasm.
|
||||
func (c *Conn) Ping(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write writes a message of the given type to the connection.
|
||||
// Always non blocking.
|
||||
func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error {
|
||||
err := c.write(ctx, typ, p)
|
||||
if err != nil {
|
||||
// Have to ensure the WebSocket is closed after a write error
|
||||
// to match the Go API. It can only error if the message type
|
||||
// is unexpected or the passed bytes contain invalid UTF-8 for
|
||||
// MessageText.
|
||||
err := fmt.Errorf("failed to write: %w", err)
|
||||
c.setCloseErr(err)
|
||||
c.closeWithInternal()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error {
|
||||
if c.isClosed() {
|
||||
return net.ErrClosed
|
||||
}
|
||||
switch typ {
|
||||
case MessageBinary:
|
||||
return c.ws.SendBytes(p)
|
||||
case MessageText:
|
||||
return c.ws.SendText(string(p))
|
||||
default:
|
||||
return fmt.Errorf("unexpected message type: %v", typ)
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the WebSocket with the given code and reason.
|
||||
// It will wait until the peer responds with a close frame
|
||||
// or the connection is closed.
|
||||
// It thus performs the full WebSocket close handshake.
|
||||
func (c *Conn) Close(code StatusCode, reason string) error {
|
||||
err := c.exportedClose(code, reason)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to close WebSocket: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseNow closes the WebSocket connection without attempting a close handshake.
|
||||
// Use when you do not want the overhead of the close handshake.
|
||||
//
|
||||
// note: No different from Close(StatusGoingAway, "") in WASM as there is no way to close
|
||||
// a WebSocket without the close handshake.
|
||||
func (c *Conn) CloseNow() error {
|
||||
return c.Close(StatusGoingAway, "")
|
||||
}
|
||||
|
||||
func (c *Conn) exportedClose(code StatusCode, reason string) error {
|
||||
c.closingMu.Lock()
|
||||
defer c.closingMu.Unlock()
|
||||
|
||||
if c.isClosed() {
|
||||
return net.ErrClosed
|
||||
}
|
||||
|
||||
ce := fmt.Errorf("sent close: %w", CloseError{
|
||||
Code: code,
|
||||
Reason: reason,
|
||||
})
|
||||
|
||||
c.setCloseErr(ce)
|
||||
err := c.ws.Close(int(code), reason)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
<-c.closed
|
||||
if !c.closeWasClean {
|
||||
return c.closeErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Subprotocol returns the negotiated subprotocol.
|
||||
// An empty string means the default protocol.
|
||||
func (c *Conn) Subprotocol() string {
|
||||
return c.ws.Subprotocol()
|
||||
}
|
||||
|
||||
// DialOptions represents the options available to pass to Dial.
|
||||
type DialOptions struct {
|
||||
// Subprotocols lists the subprotocols to negotiate with the server.
|
||||
Subprotocols []string
|
||||
}
|
||||
|
||||
// Dial creates a new WebSocket connection to the given url with the given options.
|
||||
// The passed context bounds the maximum time spent waiting for the connection to open.
|
||||
// The returned *http.Response is always nil or a mock. It's only in the signature
|
||||
// to match the core API.
|
||||
func Dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Response, error) {
|
||||
c, resp, err := dial(ctx, url, opts)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to WebSocket dial %q: %w", url, err)
|
||||
}
|
||||
return c, resp, nil
|
||||
}
|
||||
|
||||
func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Response, error) {
|
||||
if opts == nil {
|
||||
opts = &DialOptions{}
|
||||
}
|
||||
|
||||
url = strings.Replace(url, "http://", "ws://", 1)
|
||||
url = strings.Replace(url, "https://", "wss://", 1)
|
||||
|
||||
ws, err := wsjs.New(url, opts.Subprotocols)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
c := &Conn{
|
||||
ws: ws,
|
||||
}
|
||||
c.init()
|
||||
|
||||
opench := make(chan struct{})
|
||||
releaseOpen := ws.OnOpen(func(e js.Value) {
|
||||
close(opench)
|
||||
})
|
||||
defer releaseOpen()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.Close(StatusPolicyViolation, "dial timed out")
|
||||
return nil, nil, ctx.Err()
|
||||
case <-opench:
|
||||
return c, &http.Response{
|
||||
StatusCode: http.StatusSwitchingProtocols,
|
||||
}, nil
|
||||
case <-c.closed:
|
||||
return nil, nil, net.ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
// Reader attempts to read a message from the connection.
|
||||
// The maximum time spent waiting is bounded by the context.
|
||||
func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) {
|
||||
typ, p, err := c.Read(ctx)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return typ, bytes.NewReader(p), nil
|
||||
}
|
||||
|
||||
// Writer returns a writer to write a WebSocket data message to the connection.
|
||||
// It buffers the entire message in memory and then sends it when the writer
|
||||
// is closed.
|
||||
func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) {
|
||||
return &writer{
|
||||
c: c,
|
||||
ctx: ctx,
|
||||
typ: typ,
|
||||
b: bpool.Get(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type writer struct {
|
||||
closed bool
|
||||
|
||||
c *Conn
|
||||
ctx context.Context
|
||||
typ MessageType
|
||||
|
||||
b *bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *writer) Write(p []byte) (int, error) {
|
||||
if w.closed {
|
||||
return 0, errors.New("cannot write to closed writer")
|
||||
}
|
||||
n, err := w.b.Write(p)
|
||||
if err != nil {
|
||||
return n, fmt.Errorf("failed to write message: %w", err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (w *writer) Close() error {
|
||||
if w.closed {
|
||||
return errors.New("cannot close closed writer")
|
||||
}
|
||||
w.closed = true
|
||||
defer bpool.Put(w.b)
|
||||
|
||||
err := w.c.Write(w.ctx, w.typ, w.b.Bytes())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to close writer: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseRead implements *Conn.CloseRead for wasm.
|
||||
func (c *Conn) CloseRead(ctx context.Context) context.Context {
|
||||
c.closeReadMu.Lock()
|
||||
ctx2 := c.closeReadCtx
|
||||
if ctx2 != nil {
|
||||
c.closeReadMu.Unlock()
|
||||
return ctx2
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
c.closeReadCtx = ctx
|
||||
c.closeReadMu.Unlock()
|
||||
|
||||
go func() {
|
||||
defer cancel()
|
||||
defer c.CloseNow()
|
||||
_, _, err := c.read(ctx)
|
||||
if err != nil {
|
||||
c.Close(StatusPolicyViolation, "unexpected data message")
|
||||
}
|
||||
}()
|
||||
return ctx
|
||||
}
|
||||
|
||||
// SetReadLimit implements *Conn.SetReadLimit for wasm.
|
||||
func (c *Conn) SetReadLimit(n int64) {
|
||||
c.msgReadLimit.Store(n)
|
||||
}
|
||||
|
||||
func (c *Conn) setCloseErr(err error) {
|
||||
c.closeErrOnce.Do(func() {
|
||||
c.closeErr = fmt.Errorf("WebSocket closed: %w", err)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Conn) isClosed() bool {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// AcceptOptions represents Accept's options.
|
||||
type AcceptOptions struct {
|
||||
Subprotocols []string
|
||||
InsecureSkipVerify bool
|
||||
OriginPatterns []string
|
||||
CompressionMode CompressionMode
|
||||
CompressionThreshold int
|
||||
}
|
||||
|
||||
// Accept is stubbed out for Wasm.
|
||||
func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) {
|
||||
return nil, errors.New("unimplemented")
|
||||
}
|
||||
|
||||
// StatusCode represents a WebSocket status code.
|
||||
// https://tools.ietf.org/html/rfc6455#section-7.4
|
||||
type StatusCode int
|
||||
|
||||
// https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number
|
||||
//
|
||||
// These are only the status codes defined by the protocol.
|
||||
//
|
||||
// You can define custom codes in the 3000-4999 range.
|
||||
// The 3000-3999 range is reserved for use by libraries, frameworks and applications.
|
||||
// The 4000-4999 range is reserved for private use.
|
||||
const (
|
||||
StatusNormalClosure StatusCode = 1000
|
||||
StatusGoingAway StatusCode = 1001
|
||||
StatusProtocolError StatusCode = 1002
|
||||
StatusUnsupportedData StatusCode = 1003
|
||||
|
||||
// 1004 is reserved and so unexported.
|
||||
statusReserved StatusCode = 1004
|
||||
|
||||
// StatusNoStatusRcvd cannot be sent in a close message.
|
||||
// It is reserved for when a close message is received without
|
||||
// a status code.
|
||||
StatusNoStatusRcvd StatusCode = 1005
|
||||
|
||||
// StatusAbnormalClosure is exported for use only with Wasm.
|
||||
// In non Wasm Go, the returned error will indicate whether the
|
||||
// connection was closed abnormally.
|
||||
StatusAbnormalClosure StatusCode = 1006
|
||||
|
||||
StatusInvalidFramePayloadData StatusCode = 1007
|
||||
StatusPolicyViolation StatusCode = 1008
|
||||
StatusMessageTooBig StatusCode = 1009
|
||||
StatusMandatoryExtension StatusCode = 1010
|
||||
StatusInternalError StatusCode = 1011
|
||||
StatusServiceRestart StatusCode = 1012
|
||||
StatusTryAgainLater StatusCode = 1013
|
||||
StatusBadGateway StatusCode = 1014
|
||||
|
||||
// StatusTLSHandshake is only exported for use with Wasm.
|
||||
// In non Wasm Go, the returned error will indicate whether there was
|
||||
// a TLS handshake failure.
|
||||
StatusTLSHandshake StatusCode = 1015
|
||||
)
|
||||
|
||||
// CloseError is returned when the connection is closed with a status and reason.
|
||||
//
|
||||
// Use Go 1.13's errors.As to check for this error.
|
||||
// Also see the CloseStatus helper.
|
||||
type CloseError struct {
|
||||
Code StatusCode
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (ce CloseError) Error() string {
|
||||
return fmt.Sprintf("status = %v and reason = %q", ce.Code, ce.Reason)
|
||||
}
|
||||
|
||||
// CloseStatus is a convenience wrapper around Go 1.13's errors.As to grab
|
||||
// the status code from a CloseError.
|
||||
//
|
||||
// -1 will be returned if the passed error is nil or not a CloseError.
|
||||
func CloseStatus(err error) StatusCode {
|
||||
var ce CloseError
|
||||
if errors.As(err, &ce) {
|
||||
return ce.Code
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// CompressionMode represents the modes available to the deflate extension.
|
||||
// See https://tools.ietf.org/html/rfc7692
|
||||
// Works in all browsers except Safari which does not implement the deflate extension.
|
||||
type CompressionMode int
|
||||
|
||||
const (
|
||||
// CompressionNoContextTakeover grabs a new flate.Reader and flate.Writer as needed
|
||||
// for every message. This applies to both server and client side.
|
||||
//
|
||||
// This means less efficient compression as the sliding window from previous messages
|
||||
// will not be used but the memory overhead will be lower if the connections
|
||||
// are long lived and seldom used.
|
||||
//
|
||||
// The message will only be compressed if greater than 512 bytes.
|
||||
CompressionNoContextTakeover CompressionMode = iota
|
||||
|
||||
// CompressionContextTakeover uses a flate.Reader and flate.Writer per connection.
|
||||
// This enables reusing the sliding window from previous messages.
|
||||
// As most WebSocket protocols are repetitive, this can be very efficient.
|
||||
// It carries an overhead of 8 kB for every connection compared to CompressionNoContextTakeover.
|
||||
//
|
||||
// If the peer negotiates NoContextTakeover on the client or server side, it will be
|
||||
// used instead as this is required by the RFC.
|
||||
CompressionContextTakeover
|
||||
|
||||
// CompressionDisabled disables the deflate extension.
|
||||
//
|
||||
// Use this if you are using a predominantly binary protocol with very
|
||||
// little duplication in between messages or CPU and memory are more
|
||||
// important than bandwidth.
|
||||
CompressionDisabled
|
||||
)
|
||||
|
||||
// MessageType represents the type of a WebSocket message.
|
||||
// See https://tools.ietf.org/html/rfc6455#section-5.6
|
||||
type MessageType int
|
||||
|
||||
// MessageType constants.
|
||||
const (
|
||||
// MessageText is for UTF-8 encoded text messages like JSON.
|
||||
MessageText MessageType = iota + 1
|
||||
// MessageBinary is for binary messages like protobufs.
|
||||
MessageBinary
|
||||
)
|
||||
|
||||
type mu struct {
|
||||
c *Conn
|
||||
ch chan struct{}
|
||||
}
|
||||
|
||||
func newMu(c *Conn) *mu {
|
||||
return &mu{
|
||||
c: c,
|
||||
ch: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mu) forceLock() {
|
||||
m.ch <- struct{}{}
|
||||
}
|
||||
|
||||
func (m *mu) tryLock() bool {
|
||||
select {
|
||||
case m.ch <- struct{}{}:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mu) unlock() {
|
||||
select {
|
||||
case <-m.ch:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
type noCopy struct{}
|
||||
|
||||
func (*noCopy) Lock() {}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
sudo: false
|
||||
language: go
|
||||
go_import_path: github.com/dustin/go-humanize
|
||||
go:
|
||||
- 1.13.x
|
||||
- 1.14.x
|
||||
- 1.15.x
|
||||
- 1.16.x
|
||||
- stable
|
||||
- master
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: master
|
||||
fast_finish: true
|
||||
install:
|
||||
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step).
|
||||
script:
|
||||
- diff -u <(echo -n) <(gofmt -d -s .)
|
||||
- go vet .
|
||||
- go install -v -race ./...
|
||||
- go test -v -race ./...
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
<http://www.opensource.org/licenses/mit-license.php>
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
# Humane Units [](https://travis-ci.org/dustin/go-humanize) [](https://godoc.org/github.com/dustin/go-humanize)
|
||||
|
||||
Just a few functions for helping humanize times and sizes.
|
||||
|
||||
`go get` it as `github.com/dustin/go-humanize`, import it as
|
||||
`"github.com/dustin/go-humanize"`, use it as `humanize`.
|
||||
|
||||
See [godoc](https://pkg.go.dev/github.com/dustin/go-humanize) for
|
||||
complete documentation.
|
||||
|
||||
## Sizes
|
||||
|
||||
This lets you take numbers like `82854982` and convert them to useful
|
||||
strings like, `83 MB` or `79 MiB` (whichever you prefer).
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("That file is %s.", humanize.Bytes(82854982)) // That file is 83 MB.
|
||||
```
|
||||
|
||||
## Times
|
||||
|
||||
This lets you take a `time.Time` and spit it out in relative terms.
|
||||
For example, `12 seconds ago` or `3 days from now`.
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("This was touched %s.", humanize.Time(someTimeInstance)) // This was touched 7 hours ago.
|
||||
```
|
||||
|
||||
Thanks to Kyle Lemons for the time implementation from an IRC
|
||||
conversation one day. It's pretty neat.
|
||||
|
||||
## Ordinals
|
||||
|
||||
From a [mailing list discussion][odisc] where a user wanted to be able
|
||||
to label ordinals.
|
||||
|
||||
0 -> 0th
|
||||
1 -> 1st
|
||||
2 -> 2nd
|
||||
3 -> 3rd
|
||||
4 -> 4th
|
||||
[...]
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193)) // You are my 193rd best friend.
|
||||
```
|
||||
|
||||
## Commas
|
||||
|
||||
Want to shove commas into numbers? Be my guest.
|
||||
|
||||
0 -> 0
|
||||
100 -> 100
|
||||
1000 -> 1,000
|
||||
1000000000 -> 1,000,000,000
|
||||
-100000 -> -100,000
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) // You owe $6,582,491.
|
||||
```
|
||||
|
||||
## Ftoa
|
||||
|
||||
Nicer float64 formatter that removes trailing zeros.
|
||||
|
||||
```go
|
||||
fmt.Printf("%f", 2.24) // 2.240000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24
|
||||
fmt.Printf("%f", 2.0) // 2.000000
|
||||
fmt.Printf("%s", humanize.Ftoa(2.0)) // 2
|
||||
```
|
||||
|
||||
## SI notation
|
||||
|
||||
Format numbers with [SI notation][sinotation].
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
humanize.SI(0.00000000223, "M") // 2.23 nM
|
||||
```
|
||||
|
||||
## English-specific functions
|
||||
|
||||
The following functions are in the `humanize/english` subpackage.
|
||||
|
||||
### Plurals
|
||||
|
||||
Simple English pluralization
|
||||
|
||||
```go
|
||||
english.PluralWord(1, "object", "") // object
|
||||
english.PluralWord(42, "object", "") // objects
|
||||
english.PluralWord(2, "bus", "") // buses
|
||||
english.PluralWord(99, "locus", "loci") // loci
|
||||
|
||||
english.Plural(1, "object", "") // 1 object
|
||||
english.Plural(42, "object", "") // 42 objects
|
||||
english.Plural(2, "bus", "") // 2 buses
|
||||
english.Plural(99, "locus", "loci") // 99 loci
|
||||
```
|
||||
|
||||
### Word series
|
||||
|
||||
Format comma-separated words lists with conjuctions:
|
||||
|
||||
```go
|
||||
english.WordSeries([]string{"foo"}, "and") // foo
|
||||
english.WordSeries([]string{"foo", "bar"}, "and") // foo and bar
|
||||
english.WordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar and baz
|
||||
|
||||
english.OxfordWordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar, and baz
|
||||
```
|
||||
|
||||
[odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion
|
||||
[sinotation]: http://en.wikipedia.org/wiki/Metric_prefix
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// order of magnitude (to a max order)
|
||||
func oomm(n, b *big.Int, maxmag int) (float64, int) {
|
||||
mag := 0
|
||||
m := &big.Int{}
|
||||
for n.Cmp(b) >= 0 {
|
||||
n.DivMod(n, b, m)
|
||||
mag++
|
||||
if mag == maxmag && maxmag >= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
||||
}
|
||||
|
||||
// total order of magnitude
|
||||
// (same as above, but with no upper limit)
|
||||
func oom(n, b *big.Int) (float64, int) {
|
||||
mag := 0
|
||||
m := &big.Int{}
|
||||
for n.Cmp(b) >= 0 {
|
||||
n.DivMod(n, b, m)
|
||||
mag++
|
||||
}
|
||||
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
bigIECExp = big.NewInt(1024)
|
||||
|
||||
// BigByte is one byte in bit.Ints
|
||||
BigByte = big.NewInt(1)
|
||||
// BigKiByte is 1,024 bytes in bit.Ints
|
||||
BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp)
|
||||
// BigMiByte is 1,024 k bytes in bit.Ints
|
||||
BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp)
|
||||
// BigGiByte is 1,024 m bytes in bit.Ints
|
||||
BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp)
|
||||
// BigTiByte is 1,024 g bytes in bit.Ints
|
||||
BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp)
|
||||
// BigPiByte is 1,024 t bytes in bit.Ints
|
||||
BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp)
|
||||
// BigEiByte is 1,024 p bytes in bit.Ints
|
||||
BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp)
|
||||
// BigZiByte is 1,024 e bytes in bit.Ints
|
||||
BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp)
|
||||
// BigYiByte is 1,024 z bytes in bit.Ints
|
||||
BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp)
|
||||
// BigRiByte is 1,024 y bytes in bit.Ints
|
||||
BigRiByte = (&big.Int{}).Mul(BigYiByte, bigIECExp)
|
||||
// BigQiByte is 1,024 r bytes in bit.Ints
|
||||
BigQiByte = (&big.Int{}).Mul(BigRiByte, bigIECExp)
|
||||
)
|
||||
|
||||
var (
|
||||
bigSIExp = big.NewInt(1000)
|
||||
|
||||
// BigSIByte is one SI byte in big.Ints
|
||||
BigSIByte = big.NewInt(1)
|
||||
// BigKByte is 1,000 SI bytes in big.Ints
|
||||
BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp)
|
||||
// BigMByte is 1,000 SI k bytes in big.Ints
|
||||
BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp)
|
||||
// BigGByte is 1,000 SI m bytes in big.Ints
|
||||
BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp)
|
||||
// BigTByte is 1,000 SI g bytes in big.Ints
|
||||
BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp)
|
||||
// BigPByte is 1,000 SI t bytes in big.Ints
|
||||
BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp)
|
||||
// BigEByte is 1,000 SI p bytes in big.Ints
|
||||
BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp)
|
||||
// BigZByte is 1,000 SI e bytes in big.Ints
|
||||
BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp)
|
||||
// BigYByte is 1,000 SI z bytes in big.Ints
|
||||
BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp)
|
||||
// BigRByte is 1,000 SI y bytes in big.Ints
|
||||
BigRByte = (&big.Int{}).Mul(BigYByte, bigSIExp)
|
||||
// BigQByte is 1,000 SI r bytes in big.Ints
|
||||
BigQByte = (&big.Int{}).Mul(BigRByte, bigSIExp)
|
||||
)
|
||||
|
||||
var bigBytesSizeTable = map[string]*big.Int{
|
||||
"b": BigByte,
|
||||
"kib": BigKiByte,
|
||||
"kb": BigKByte,
|
||||
"mib": BigMiByte,
|
||||
"mb": BigMByte,
|
||||
"gib": BigGiByte,
|
||||
"gb": BigGByte,
|
||||
"tib": BigTiByte,
|
||||
"tb": BigTByte,
|
||||
"pib": BigPiByte,
|
||||
"pb": BigPByte,
|
||||
"eib": BigEiByte,
|
||||
"eb": BigEByte,
|
||||
"zib": BigZiByte,
|
||||
"zb": BigZByte,
|
||||
"yib": BigYiByte,
|
||||
"yb": BigYByte,
|
||||
"rib": BigRiByte,
|
||||
"rb": BigRByte,
|
||||
"qib": BigQiByte,
|
||||
"qb": BigQByte,
|
||||
// Without suffix
|
||||
"": BigByte,
|
||||
"ki": BigKiByte,
|
||||
"k": BigKByte,
|
||||
"mi": BigMiByte,
|
||||
"m": BigMByte,
|
||||
"gi": BigGiByte,
|
||||
"g": BigGByte,
|
||||
"ti": BigTiByte,
|
||||
"t": BigTByte,
|
||||
"pi": BigPiByte,
|
||||
"p": BigPByte,
|
||||
"ei": BigEiByte,
|
||||
"e": BigEByte,
|
||||
"z": BigZByte,
|
||||
"zi": BigZiByte,
|
||||
"y": BigYByte,
|
||||
"yi": BigYiByte,
|
||||
"r": BigRByte,
|
||||
"ri": BigRiByte,
|
||||
"q": BigQByte,
|
||||
"qi": BigQiByte,
|
||||
}
|
||||
|
||||
var ten = big.NewInt(10)
|
||||
|
||||
func humanateBigBytes(s, base *big.Int, sizes []string) string {
|
||||
if s.Cmp(ten) < 0 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
c := (&big.Int{}).Set(s)
|
||||
val, mag := oomm(c, base, len(sizes)-1)
|
||||
suffix := sizes[mag]
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
|
||||
}
|
||||
|
||||
// BigBytes produces a human readable representation of an SI size.
|
||||
//
|
||||
// See also: ParseBigBytes.
|
||||
//
|
||||
// BigBytes(82854982) -> 83 MB
|
||||
func BigBytes(s *big.Int) string {
|
||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "RB", "QB"}
|
||||
return humanateBigBytes(s, bigSIExp, sizes)
|
||||
}
|
||||
|
||||
// BigIBytes produces a human readable representation of an IEC size.
|
||||
//
|
||||
// See also: ParseBigBytes.
|
||||
//
|
||||
// BigIBytes(82854982) -> 79 MiB
|
||||
func BigIBytes(s *big.Int) string {
|
||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB"}
|
||||
return humanateBigBytes(s, bigIECExp, sizes)
|
||||
}
|
||||
|
||||
// ParseBigBytes parses a string representation of bytes into the number
|
||||
// of bytes it represents.
|
||||
//
|
||||
// See also: BigBytes, BigIBytes.
|
||||
//
|
||||
// ParseBigBytes("42 MB") -> 42000000, nil
|
||||
// ParseBigBytes("42 mib") -> 44040192, nil
|
||||
func ParseBigBytes(s string) (*big.Int, error) {
|
||||
lastDigit := 0
|
||||
hasComma := false
|
||||
for _, r := range s {
|
||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||
break
|
||||
}
|
||||
if r == ',' {
|
||||
hasComma = true
|
||||
}
|
||||
lastDigit++
|
||||
}
|
||||
|
||||
num := s[:lastDigit]
|
||||
if hasComma {
|
||||
num = strings.Replace(num, ",", "", -1)
|
||||
}
|
||||
|
||||
val := &big.Rat{}
|
||||
_, err := fmt.Sscanf(num, "%f", val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||
if m, ok := bigBytesSizeTable[extra]; ok {
|
||||
mv := (&big.Rat{}).SetInt(m)
|
||||
val.Mul(val, mv)
|
||||
rv := &big.Int{}
|
||||
rv.Div(val.Num(), val.Denom())
|
||||
return rv, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unhandled size name: %v", extra)
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// IEC Sizes.
|
||||
// kibis of bits
|
||||
const (
|
||||
Byte = 1 << (iota * 10)
|
||||
KiByte
|
||||
MiByte
|
||||
GiByte
|
||||
TiByte
|
||||
PiByte
|
||||
EiByte
|
||||
)
|
||||
|
||||
// SI Sizes.
|
||||
const (
|
||||
IByte = 1
|
||||
KByte = IByte * 1000
|
||||
MByte = KByte * 1000
|
||||
GByte = MByte * 1000
|
||||
TByte = GByte * 1000
|
||||
PByte = TByte * 1000
|
||||
EByte = PByte * 1000
|
||||
)
|
||||
|
||||
var bytesSizeTable = map[string]uint64{
|
||||
"b": Byte,
|
||||
"kib": KiByte,
|
||||
"kb": KByte,
|
||||
"mib": MiByte,
|
||||
"mb": MByte,
|
||||
"gib": GiByte,
|
||||
"gb": GByte,
|
||||
"tib": TiByte,
|
||||
"tb": TByte,
|
||||
"pib": PiByte,
|
||||
"pb": PByte,
|
||||
"eib": EiByte,
|
||||
"eb": EByte,
|
||||
// Without suffix
|
||||
"": Byte,
|
||||
"ki": KiByte,
|
||||
"k": KByte,
|
||||
"mi": MiByte,
|
||||
"m": MByte,
|
||||
"gi": GiByte,
|
||||
"g": GByte,
|
||||
"ti": TiByte,
|
||||
"t": TByte,
|
||||
"pi": PiByte,
|
||||
"p": PByte,
|
||||
"ei": EiByte,
|
||||
"e": EByte,
|
||||
}
|
||||
|
||||
func logn(n, b float64) float64 {
|
||||
return math.Log(n) / math.Log(b)
|
||||
}
|
||||
|
||||
func humanateBytes(s uint64, base float64, sizes []string) string {
|
||||
if s < 10 {
|
||||
return fmt.Sprintf("%d B", s)
|
||||
}
|
||||
e := math.Floor(logn(float64(s), base))
|
||||
suffix := sizes[int(e)]
|
||||
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
|
||||
f := "%.0f %s"
|
||||
if val < 10 {
|
||||
f = "%.1f %s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(f, val, suffix)
|
||||
}
|
||||
|
||||
// Bytes produces a human readable representation of an SI size.
|
||||
//
|
||||
// See also: ParseBytes.
|
||||
//
|
||||
// Bytes(82854982) -> 83 MB
|
||||
func Bytes(s uint64) string {
|
||||
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
|
||||
return humanateBytes(s, 1000, sizes)
|
||||
}
|
||||
|
||||
// IBytes produces a human readable representation of an IEC size.
|
||||
//
|
||||
// See also: ParseBytes.
|
||||
//
|
||||
// IBytes(82854982) -> 79 MiB
|
||||
func IBytes(s uint64) string {
|
||||
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
|
||||
return humanateBytes(s, 1024, sizes)
|
||||
}
|
||||
|
||||
// ParseBytes parses a string representation of bytes into the number
|
||||
// of bytes it represents.
|
||||
//
|
||||
// See Also: Bytes, IBytes.
|
||||
//
|
||||
// ParseBytes("42 MB") -> 42000000, nil
|
||||
// ParseBytes("42 mib") -> 44040192, nil
|
||||
func ParseBytes(s string) (uint64, error) {
|
||||
lastDigit := 0
|
||||
hasComma := false
|
||||
for _, r := range s {
|
||||
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
|
||||
break
|
||||
}
|
||||
if r == ',' {
|
||||
hasComma = true
|
||||
}
|
||||
lastDigit++
|
||||
}
|
||||
|
||||
num := s[:lastDigit]
|
||||
if hasComma {
|
||||
num = strings.Replace(num, ",", "", -1)
|
||||
}
|
||||
|
||||
f, err := strconv.ParseFloat(num, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
|
||||
if m, ok := bytesSizeTable[extra]; ok {
|
||||
f *= float64(m)
|
||||
if f >= math.MaxUint64 {
|
||||
return 0, fmt.Errorf("too large: %v", s)
|
||||
}
|
||||
return uint64(f), nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("unhandled size name: %v", extra)
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Comma produces a string form of the given number in base 10 with
|
||||
// commas after every three orders of magnitude.
|
||||
//
|
||||
// e.g. Comma(834142) -> 834,142
|
||||
func Comma(v int64) string {
|
||||
sign := ""
|
||||
|
||||
// Min int64 can't be negated to a usable value, so it has to be special cased.
|
||||
if v == math.MinInt64 {
|
||||
return "-9,223,372,036,854,775,808"
|
||||
}
|
||||
|
||||
if v < 0 {
|
||||
sign = "-"
|
||||
v = 0 - v
|
||||
}
|
||||
|
||||
parts := []string{"", "", "", "", "", "", ""}
|
||||
j := len(parts) - 1
|
||||
|
||||
for v > 999 {
|
||||
parts[j] = strconv.FormatInt(v%1000, 10)
|
||||
switch len(parts[j]) {
|
||||
case 2:
|
||||
parts[j] = "0" + parts[j]
|
||||
case 1:
|
||||
parts[j] = "00" + parts[j]
|
||||
}
|
||||
v = v / 1000
|
||||
j--
|
||||
}
|
||||
parts[j] = strconv.Itoa(int(v))
|
||||
return sign + strings.Join(parts[j:], ",")
|
||||
}
|
||||
|
||||
// Commaf produces a string form of the given number in base 10 with
|
||||
// commas after every three orders of magnitude.
|
||||
//
|
||||
// e.g. Commaf(834142.32) -> 834,142.32
|
||||
func Commaf(v float64) string {
|
||||
buf := &bytes.Buffer{}
|
||||
if v < 0 {
|
||||
buf.Write([]byte{'-'})
|
||||
v = 0 - v
|
||||
}
|
||||
|
||||
comma := []byte{','}
|
||||
|
||||
parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".")
|
||||
pos := 0
|
||||
if len(parts[0])%3 != 0 {
|
||||
pos += len(parts[0]) % 3
|
||||
buf.WriteString(parts[0][:pos])
|
||||
buf.Write(comma)
|
||||
}
|
||||
for ; pos < len(parts[0]); pos += 3 {
|
||||
buf.WriteString(parts[0][pos : pos+3])
|
||||
buf.Write(comma)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
|
||||
if len(parts) > 1 {
|
||||
buf.Write([]byte{'.'})
|
||||
buf.WriteString(parts[1])
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// CommafWithDigits works like the Commaf but limits the resulting
|
||||
// string to the given number of decimal places.
|
||||
//
|
||||
// e.g. CommafWithDigits(834142.32, 1) -> 834,142.3
|
||||
func CommafWithDigits(f float64, decimals int) string {
|
||||
return stripTrailingDigits(Commaf(f), decimals)
|
||||
}
|
||||
|
||||
// BigComma produces a string form of the given big.Int in base 10
|
||||
// with commas after every three orders of magnitude.
|
||||
func BigComma(b *big.Int) string {
|
||||
sign := ""
|
||||
if b.Sign() < 0 {
|
||||
sign = "-"
|
||||
b.Abs(b)
|
||||
}
|
||||
|
||||
athousand := big.NewInt(1000)
|
||||
c := (&big.Int{}).Set(b)
|
||||
_, m := oom(c, athousand)
|
||||
parts := make([]string, m+1)
|
||||
j := len(parts) - 1
|
||||
|
||||
mod := &big.Int{}
|
||||
for b.Cmp(athousand) >= 0 {
|
||||
b.DivMod(b, athousand, mod)
|
||||
parts[j] = strconv.FormatInt(mod.Int64(), 10)
|
||||
switch len(parts[j]) {
|
||||
case 2:
|
||||
parts[j] = "0" + parts[j]
|
||||
case 1:
|
||||
parts[j] = "00" + parts[j]
|
||||
}
|
||||
j--
|
||||
}
|
||||
parts[j] = strconv.Itoa(int(b.Int64()))
|
||||
return sign + strings.Join(parts[j:], ",")
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
//go:build go1.6
|
||||
// +build go1.6
|
||||
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BigCommaf produces a string form of the given big.Float in base 10
|
||||
// with commas after every three orders of magnitude.
|
||||
func BigCommaf(v *big.Float) string {
|
||||
buf := &bytes.Buffer{}
|
||||
if v.Sign() < 0 {
|
||||
buf.Write([]byte{'-'})
|
||||
v.Abs(v)
|
||||
}
|
||||
|
||||
comma := []byte{','}
|
||||
|
||||
parts := strings.Split(v.Text('f', -1), ".")
|
||||
pos := 0
|
||||
if len(parts[0])%3 != 0 {
|
||||
pos += len(parts[0]) % 3
|
||||
buf.WriteString(parts[0][:pos])
|
||||
buf.Write(comma)
|
||||
}
|
||||
for ; pos < len(parts[0]); pos += 3 {
|
||||
buf.WriteString(parts[0][pos : pos+3])
|
||||
buf.Write(comma)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
|
||||
if len(parts) > 1 {
|
||||
buf.Write([]byte{'.'})
|
||||
buf.WriteString(parts[1])
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func stripTrailingZeros(s string) string {
|
||||
if !strings.ContainsRune(s, '.') {
|
||||
return s
|
||||
}
|
||||
offset := len(s) - 1
|
||||
for offset > 0 {
|
||||
if s[offset] == '.' {
|
||||
offset--
|
||||
break
|
||||
}
|
||||
if s[offset] != '0' {
|
||||
break
|
||||
}
|
||||
offset--
|
||||
}
|
||||
return s[:offset+1]
|
||||
}
|
||||
|
||||
func stripTrailingDigits(s string, digits int) string {
|
||||
if i := strings.Index(s, "."); i >= 0 {
|
||||
if digits <= 0 {
|
||||
return s[:i]
|
||||
}
|
||||
i++
|
||||
if i+digits >= len(s) {
|
||||
return s
|
||||
}
|
||||
return s[:i+digits]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Ftoa converts a float to a string with no trailing zeros.
|
||||
func Ftoa(num float64) string {
|
||||
return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64))
|
||||
}
|
||||
|
||||
// FtoaWithDigits converts a float to a string but limits the resulting string
|
||||
// to the given number of decimal places, and no trailing zeros.
|
||||
func FtoaWithDigits(num float64, digits int) string {
|
||||
return stripTrailingZeros(stripTrailingDigits(strconv.FormatFloat(num, 'f', 6, 64), digits))
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Package humanize converts boring ugly numbers to human-friendly strings and back.
|
||||
|
||||
Durations can be turned into strings such as "3 days ago", numbers
|
||||
representing sizes like 82854982 into useful strings like, "83 MB" or
|
||||
"79 MiB" (whichever you prefer).
|
||||
*/
|
||||
package humanize
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
package humanize
|
||||
|
||||
/*
|
||||
Slightly adapted from the source to fit go-humanize.
|
||||
|
||||
Author: https://github.com/gorhill
|
||||
Source: https://gist.github.com/gorhill/5285193
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
renderFloatPrecisionMultipliers = [...]float64{
|
||||
1,
|
||||
10,
|
||||
100,
|
||||
1000,
|
||||
10000,
|
||||
100000,
|
||||
1000000,
|
||||
10000000,
|
||||
100000000,
|
||||
1000000000,
|
||||
}
|
||||
|
||||
renderFloatPrecisionRounders = [...]float64{
|
||||
0.5,
|
||||
0.05,
|
||||
0.005,
|
||||
0.0005,
|
||||
0.00005,
|
||||
0.000005,
|
||||
0.0000005,
|
||||
0.00000005,
|
||||
0.000000005,
|
||||
0.0000000005,
|
||||
}
|
||||
)
|
||||
|
||||
// FormatFloat produces a formatted number as string based on the following user-specified criteria:
|
||||
// * thousands separator
|
||||
// * decimal separator
|
||||
// * decimal precision
|
||||
//
|
||||
// Usage: s := RenderFloat(format, n)
|
||||
// The format parameter tells how to render the number n.
|
||||
//
|
||||
// See examples: http://play.golang.org/p/LXc1Ddm1lJ
|
||||
//
|
||||
// Examples of format strings, given n = 12345.6789:
|
||||
// "#,###.##" => "12,345.67"
|
||||
// "#,###." => "12,345"
|
||||
// "#,###" => "12345,678"
|
||||
// "#\u202F###,##" => "12 345,68"
|
||||
// "#.###,###### => 12.345,678900
|
||||
// "" (aka default format) => 12,345.67
|
||||
//
|
||||
// The highest precision allowed is 9 digits after the decimal symbol.
|
||||
// There is also a version for integer number, FormatInteger(),
|
||||
// which is convenient for calls within template.
|
||||
func FormatFloat(format string, n float64) string {
|
||||
// Special cases:
|
||||
// NaN = "NaN"
|
||||
// +Inf = "+Infinity"
|
||||
// -Inf = "-Infinity"
|
||||
if math.IsNaN(n) {
|
||||
return "NaN"
|
||||
}
|
||||
if n > math.MaxFloat64 {
|
||||
return "Infinity"
|
||||
}
|
||||
if n < (0.0 - math.MaxFloat64) {
|
||||
return "-Infinity"
|
||||
}
|
||||
|
||||
// default format
|
||||
precision := 2
|
||||
decimalStr := "."
|
||||
thousandStr := ","
|
||||
positiveStr := ""
|
||||
negativeStr := "-"
|
||||
|
||||
if len(format) > 0 {
|
||||
format := []rune(format)
|
||||
|
||||
// If there is an explicit format directive,
|
||||
// then default values are these:
|
||||
precision = 9
|
||||
thousandStr = ""
|
||||
|
||||
// collect indices of meaningful formatting directives
|
||||
formatIndx := []int{}
|
||||
for i, char := range format {
|
||||
if char != '#' && char != '0' {
|
||||
formatIndx = append(formatIndx, i)
|
||||
}
|
||||
}
|
||||
|
||||
if len(formatIndx) > 0 {
|
||||
// Directive at index 0:
|
||||
// Must be a '+'
|
||||
// Raise an error if not the case
|
||||
// index: 0123456789
|
||||
// +0.000,000
|
||||
// +000,000.0
|
||||
// +0000.00
|
||||
// +0000
|
||||
if formatIndx[0] == 0 {
|
||||
if format[formatIndx[0]] != '+' {
|
||||
panic("RenderFloat(): invalid positive sign directive")
|
||||
}
|
||||
positiveStr = "+"
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// Two directives:
|
||||
// First is thousands separator
|
||||
// Raise an error if not followed by 3-digit
|
||||
// 0123456789
|
||||
// 0.000,000
|
||||
// 000,000.00
|
||||
if len(formatIndx) == 2 {
|
||||
if (formatIndx[1] - formatIndx[0]) != 4 {
|
||||
panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers")
|
||||
}
|
||||
thousandStr = string(format[formatIndx[0]])
|
||||
formatIndx = formatIndx[1:]
|
||||
}
|
||||
|
||||
// One directive:
|
||||
// Directive is decimal separator
|
||||
// The number of digit-specifier following the separator indicates wanted precision
|
||||
// 0123456789
|
||||
// 0.00
|
||||
// 000,0000
|
||||
if len(formatIndx) == 1 {
|
||||
decimalStr = string(format[formatIndx[0]])
|
||||
precision = len(format) - formatIndx[0] - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate sign part
|
||||
var signStr string
|
||||
if n >= 0.000000001 {
|
||||
signStr = positiveStr
|
||||
} else if n <= -0.000000001 {
|
||||
signStr = negativeStr
|
||||
n = -n
|
||||
} else {
|
||||
signStr = ""
|
||||
n = 0.0
|
||||
}
|
||||
|
||||
// split number into integer and fractional parts
|
||||
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])
|
||||
|
||||
// generate integer part string
|
||||
intStr := strconv.FormatInt(int64(intf), 10)
|
||||
|
||||
// add thousand separator if required
|
||||
if len(thousandStr) > 0 {
|
||||
for i := len(intStr); i > 3; {
|
||||
i -= 3
|
||||
intStr = intStr[:i] + thousandStr + intStr[i:]
|
||||
}
|
||||
}
|
||||
|
||||
// no fractional part, we can leave now
|
||||
if precision == 0 {
|
||||
return signStr + intStr
|
||||
}
|
||||
|
||||
// generate fractional part
|
||||
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
|
||||
// may need padding
|
||||
if len(fracStr) < precision {
|
||||
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
|
||||
}
|
||||
|
||||
return signStr + intStr + decimalStr + fracStr
|
||||
}
|
||||
|
||||
// FormatInteger produces a formatted number as string.
|
||||
// See FormatFloat.
|
||||
func FormatInteger(format string, n int) string {
|
||||
return FormatFloat(format, float64(n))
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package humanize
|
||||
|
||||
import "strconv"
|
||||
|
||||
// Ordinal gives you the input number in a rank/ordinal format.
|
||||
//
|
||||
// Ordinal(3) -> 3rd
|
||||
func Ordinal(x int) string {
|
||||
suffix := "th"
|
||||
switch x % 10 {
|
||||
case 1:
|
||||
if x%100 != 11 {
|
||||
suffix = "st"
|
||||
}
|
||||
case 2:
|
||||
if x%100 != 12 {
|
||||
suffix = "nd"
|
||||
}
|
||||
case 3:
|
||||
if x%100 != 13 {
|
||||
suffix = "rd"
|
||||
}
|
||||
}
|
||||
return strconv.Itoa(x) + suffix
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var siPrefixTable = map[float64]string{
|
||||
-30: "q", // quecto
|
||||
-27: "r", // ronto
|
||||
-24: "y", // yocto
|
||||
-21: "z", // zepto
|
||||
-18: "a", // atto
|
||||
-15: "f", // femto
|
||||
-12: "p", // pico
|
||||
-9: "n", // nano
|
||||
-6: "µ", // micro
|
||||
-3: "m", // milli
|
||||
0: "",
|
||||
3: "k", // kilo
|
||||
6: "M", // mega
|
||||
9: "G", // giga
|
||||
12: "T", // tera
|
||||
15: "P", // peta
|
||||
18: "E", // exa
|
||||
21: "Z", // zetta
|
||||
24: "Y", // yotta
|
||||
27: "R", // ronna
|
||||
30: "Q", // quetta
|
||||
}
|
||||
|
||||
var revSIPrefixTable = revfmap(siPrefixTable)
|
||||
|
||||
// revfmap reverses the map and precomputes the power multiplier
|
||||
func revfmap(in map[float64]string) map[string]float64 {
|
||||
rv := map[string]float64{}
|
||||
for k, v := range in {
|
||||
rv[v] = math.Pow(10, k)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
||||
var riParseRegex *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
ri := `^([\-0-9.]+)\s?([`
|
||||
for _, v := range siPrefixTable {
|
||||
ri += v
|
||||
}
|
||||
ri += `]?)(.*)`
|
||||
|
||||
riParseRegex = regexp.MustCompile(ri)
|
||||
}
|
||||
|
||||
// ComputeSI finds the most appropriate SI prefix for the given number
|
||||
// and returns the prefix along with the value adjusted to be within
|
||||
// that prefix.
|
||||
//
|
||||
// See also: SI, ParseSI.
|
||||
//
|
||||
// e.g. ComputeSI(2.2345e-12) -> (2.2345, "p")
|
||||
func ComputeSI(input float64) (float64, string) {
|
||||
if input == 0 {
|
||||
return 0, ""
|
||||
}
|
||||
mag := math.Abs(input)
|
||||
exponent := math.Floor(logn(mag, 10))
|
||||
exponent = math.Floor(exponent/3) * 3
|
||||
|
||||
value := mag / math.Pow(10, exponent)
|
||||
|
||||
// Handle special case where value is exactly 1000.0
|
||||
// Should return 1 M instead of 1000 k
|
||||
if value == 1000.0 {
|
||||
exponent += 3
|
||||
value = mag / math.Pow(10, exponent)
|
||||
}
|
||||
|
||||
value = math.Copysign(value, input)
|
||||
|
||||
prefix := siPrefixTable[exponent]
|
||||
return value, prefix
|
||||
}
|
||||
|
||||
// SI returns a string with default formatting.
|
||||
//
|
||||
// SI uses Ftoa to format float value, removing trailing zeros.
|
||||
//
|
||||
// See also: ComputeSI, ParseSI.
|
||||
//
|
||||
// e.g. SI(1000000, "B") -> 1 MB
|
||||
// e.g. SI(2.2345e-12, "F") -> 2.2345 pF
|
||||
func SI(input float64, unit string) string {
|
||||
value, prefix := ComputeSI(input)
|
||||
return Ftoa(value) + " " + prefix + unit
|
||||
}
|
||||
|
||||
// SIWithDigits works like SI but limits the resulting string to the
|
||||
// given number of decimal places.
|
||||
//
|
||||
// e.g. SIWithDigits(1000000, 0, "B") -> 1 MB
|
||||
// e.g. SIWithDigits(2.2345e-12, 2, "F") -> 2.23 pF
|
||||
func SIWithDigits(input float64, decimals int, unit string) string {
|
||||
value, prefix := ComputeSI(input)
|
||||
return FtoaWithDigits(value, decimals) + " " + prefix + unit
|
||||
}
|
||||
|
||||
var errInvalid = errors.New("invalid input")
|
||||
|
||||
// ParseSI parses an SI string back into the number and unit.
|
||||
//
|
||||
// See also: SI, ComputeSI.
|
||||
//
|
||||
// e.g. ParseSI("2.2345 pF") -> (2.2345e-12, "F", nil)
|
||||
func ParseSI(input string) (float64, string, error) {
|
||||
found := riParseRegex.FindStringSubmatch(input)
|
||||
if len(found) != 4 {
|
||||
return 0, "", errInvalid
|
||||
}
|
||||
mag := revSIPrefixTable[found[2]]
|
||||
unit := found[3]
|
||||
|
||||
base, err := strconv.ParseFloat(found[1], 64)
|
||||
return base * mag, unit, err
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package humanize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Seconds-based time units
|
||||
const (
|
||||
Day = 24 * time.Hour
|
||||
Week = 7 * Day
|
||||
Month = 30 * Day
|
||||
Year = 12 * Month
|
||||
LongTime = 37 * Year
|
||||
)
|
||||
|
||||
// Time formats a time into a relative string.
|
||||
//
|
||||
// Time(someT) -> "3 weeks ago"
|
||||
func Time(then time.Time) string {
|
||||
return RelTime(then, time.Now(), "ago", "from now")
|
||||
}
|
||||
|
||||
// A RelTimeMagnitude struct contains a relative time point at which
|
||||
// the relative format of time will switch to a new format string. A
|
||||
// slice of these in ascending order by their "D" field is passed to
|
||||
// CustomRelTime to format durations.
|
||||
//
|
||||
// The Format field is a string that may contain a "%s" which will be
|
||||
// replaced with the appropriate signed label (e.g. "ago" or "from
|
||||
// now") and a "%d" that will be replaced by the quantity.
|
||||
//
|
||||
// The DivBy field is the amount of time the time difference must be
|
||||
// divided by in order to display correctly.
|
||||
//
|
||||
// e.g. if D is 2*time.Minute and you want to display "%d minutes %s"
|
||||
// DivBy should be time.Minute so whatever the duration is will be
|
||||
// expressed in minutes.
|
||||
type RelTimeMagnitude struct {
|
||||
D time.Duration
|
||||
Format string
|
||||
DivBy time.Duration
|
||||
}
|
||||
|
||||
var defaultMagnitudes = []RelTimeMagnitude{
|
||||
{time.Second, "now", time.Second},
|
||||
{2 * time.Second, "1 second %s", 1},
|
||||
{time.Minute, "%d seconds %s", time.Second},
|
||||
{2 * time.Minute, "1 minute %s", 1},
|
||||
{time.Hour, "%d minutes %s", time.Minute},
|
||||
{2 * time.Hour, "1 hour %s", 1},
|
||||
{Day, "%d hours %s", time.Hour},
|
||||
{2 * Day, "1 day %s", 1},
|
||||
{Week, "%d days %s", Day},
|
||||
{2 * Week, "1 week %s", 1},
|
||||
{Month, "%d weeks %s", Week},
|
||||
{2 * Month, "1 month %s", 1},
|
||||
{Year, "%d months %s", Month},
|
||||
{18 * Month, "1 year %s", 1},
|
||||
{2 * Year, "2 years %s", 1},
|
||||
{LongTime, "%d years %s", Year},
|
||||
{math.MaxInt64, "a long while %s", 1},
|
||||
}
|
||||
|
||||
// RelTime formats a time into a relative string.
|
||||
//
|
||||
// It takes two times and two labels. In addition to the generic time
|
||||
// delta string (e.g. 5 minutes), the labels are used applied so that
|
||||
// the label corresponding to the smaller time is applied.
|
||||
//
|
||||
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
|
||||
func RelTime(a, b time.Time, albl, blbl string) string {
|
||||
return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
|
||||
}
|
||||
|
||||
// CustomRelTime formats a time into a relative string.
|
||||
//
|
||||
// It takes two times two labels and a table of relative time formats.
|
||||
// In addition to the generic time delta string (e.g. 5 minutes), the
|
||||
// labels are used applied so that the label corresponding to the
|
||||
// smaller time is applied.
|
||||
func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string {
|
||||
lbl := albl
|
||||
diff := b.Sub(a)
|
||||
|
||||
if a.After(b) {
|
||||
lbl = blbl
|
||||
diff = a.Sub(b)
|
||||
}
|
||||
|
||||
n := sort.Search(len(magnitudes), func(i int) bool {
|
||||
return magnitudes[i].D > diff
|
||||
})
|
||||
|
||||
if n >= len(magnitudes) {
|
||||
n = len(magnitudes) - 1
|
||||
}
|
||||
mag := magnitudes[n]
|
||||
args := []interface{}{}
|
||||
escaped := false
|
||||
for _, ch := range mag.Format {
|
||||
if escaped {
|
||||
switch ch {
|
||||
case 's':
|
||||
args = append(args, lbl)
|
||||
case 'd':
|
||||
args = append(args, diff/mag.DivBy)
|
||||
}
|
||||
escaped = false
|
||||
} else {
|
||||
escaped = ch == '%'
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf(mag.Format, args...)
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# Changelog
|
||||
|
||||
## [1.6.0](https://github.com/google/uuid/compare/v1.5.0...v1.6.0) (2024-01-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Max UUID constant ([#149](https://github.com/google/uuid/issues/149)) ([c58770e](https://github.com/google/uuid/commit/c58770eb495f55fe2ced6284f93c5158a62e53e3))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix typo in version 7 uuid documentation ([#153](https://github.com/google/uuid/issues/153)) ([016b199](https://github.com/google/uuid/commit/016b199544692f745ffc8867b914129ecb47ef06))
|
||||
* Monotonicity in UUIDv7 ([#150](https://github.com/google/uuid/issues/150)) ([a2b2b32](https://github.com/google/uuid/commit/a2b2b32373ff0b1a312b7fdf6d38a977099698a6))
|
||||
|
||||
## [1.5.0](https://github.com/google/uuid/compare/v1.4.0...v1.5.0) (2023-12-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Validate UUID without creating new UUID ([#141](https://github.com/google/uuid/issues/141)) ([9ee7366](https://github.com/google/uuid/commit/9ee7366e66c9ad96bab89139418a713dc584ae29))
|
||||
|
||||
## [1.4.0](https://github.com/google/uuid/compare/v1.3.1...v1.4.0) (2023-10-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* UUIDs slice type with Strings() convenience method ([#133](https://github.com/google/uuid/issues/133)) ([cd5fbbd](https://github.com/google/uuid/commit/cd5fbbdd02f3e3467ac18940e07e062be1f864b4))
|
||||
|
||||
### Fixes
|
||||
|
||||
* Clarify that Parse's job is to parse but not necessarily validate strings. (Documents current behavior)
|
||||
|
||||
## [1.3.1](https://github.com/google/uuid/compare/v1.3.0...v1.3.1) (2023-08-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Use .EqualFold() to parse urn prefixed UUIDs ([#118](https://github.com/google/uuid/issues/118)) ([574e687](https://github.com/google/uuid/commit/574e6874943741fb99d41764c705173ada5293f0))
|
||||
|
||||
## Changelog
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
# How to contribute
|
||||
|
||||
We definitely welcome patches and contribution to this project!
|
||||
|
||||
### Tips
|
||||
|
||||
Commits must be formatted according to the [Conventional Commits Specification](https://www.conventionalcommits.org).
|
||||
|
||||
Always try to include a test case! If it is not possible or not necessary,
|
||||
please explain why in the pull request description.
|
||||
|
||||
### Releasing
|
||||
|
||||
Commits that would precipitate a SemVer change, as described in the Conventional
|
||||
Commits Specification, will trigger [`release-please`](https://github.com/google-github-actions/release-please-action)
|
||||
to create a release candidate pull request. Once submitted, `release-please`
|
||||
will create a release.
|
||||
|
||||
For tips on how to work with `release-please`, see its documentation.
|
||||
|
||||
### Legal requirements
|
||||
|
||||
In order to protect both you and ourselves, you will need to sign the
|
||||
[Contributor License Agreement](https://cla.developers.google.com/clas).
|
||||
|
||||
You may have already signed it for other Google projects.
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
Paul Borman <borman@google.com>
|
||||
bmatsuo
|
||||
shawnps
|
||||
theory
|
||||
jboverfelt
|
||||
dsymonds
|
||||
cd1
|
||||
wallclockbuilder
|
||||
dansouza
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2009,2014 Google Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# uuid
|
||||
The uuid package generates and inspects UUIDs based on
|
||||
[RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122)
|
||||
and DCE 1.1: Authentication and Security Services.
|
||||
|
||||
This package is based on the github.com/pborman/uuid package (previously named
|
||||
code.google.com/p/go-uuid). It differs from these earlier packages in that
|
||||
a UUID is a 16 byte array rather than a byte slice. One loss due to this
|
||||
change is the ability to represent an invalid UUID (vs a NIL UUID).
|
||||
|
||||
###### Install
|
||||
```sh
|
||||
go get github.com/google/uuid
|
||||
```
|
||||
|
||||
###### Documentation
|
||||
[](https://pkg.go.dev/github.com/google/uuid)
|
||||
|
||||
Full `go doc` style documentation for the package can be viewed online without
|
||||
installing this package by using the GoDoc site here:
|
||||
http://pkg.go.dev/github.com/google/uuid
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
// Copyright 2016 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// A Domain represents a Version 2 domain
|
||||
type Domain byte
|
||||
|
||||
// Domain constants for DCE Security (Version 2) UUIDs.
|
||||
const (
|
||||
Person = Domain(0)
|
||||
Group = Domain(1)
|
||||
Org = Domain(2)
|
||||
)
|
||||
|
||||
// NewDCESecurity returns a DCE Security (Version 2) UUID.
|
||||
//
|
||||
// The domain should be one of Person, Group or Org.
|
||||
// On a POSIX system the id should be the users UID for the Person
|
||||
// domain and the users GID for the Group. The meaning of id for
|
||||
// the domain Org or on non-POSIX systems is site defined.
|
||||
//
|
||||
// For a given domain/id pair the same token may be returned for up to
|
||||
// 7 minutes and 10 seconds.
|
||||
func NewDCESecurity(domain Domain, id uint32) (UUID, error) {
|
||||
uuid, err := NewUUID()
|
||||
if err == nil {
|
||||
uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2
|
||||
uuid[9] = byte(domain)
|
||||
binary.BigEndian.PutUint32(uuid[0:], id)
|
||||
}
|
||||
return uuid, err
|
||||
}
|
||||
|
||||
// NewDCEPerson returns a DCE Security (Version 2) UUID in the person
|
||||
// domain with the id returned by os.Getuid.
|
||||
//
|
||||
// NewDCESecurity(Person, uint32(os.Getuid()))
|
||||
func NewDCEPerson() (UUID, error) {
|
||||
return NewDCESecurity(Person, uint32(os.Getuid()))
|
||||
}
|
||||
|
||||
// NewDCEGroup returns a DCE Security (Version 2) UUID in the group
|
||||
// domain with the id returned by os.Getgid.
|
||||
//
|
||||
// NewDCESecurity(Group, uint32(os.Getgid()))
|
||||
func NewDCEGroup() (UUID, error) {
|
||||
return NewDCESecurity(Group, uint32(os.Getgid()))
|
||||
}
|
||||
|
||||
// Domain returns the domain for a Version 2 UUID. Domains are only defined
|
||||
// for Version 2 UUIDs.
|
||||
func (uuid UUID) Domain() Domain {
|
||||
return Domain(uuid[9])
|
||||
}
|
||||
|
||||
// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2
|
||||
// UUIDs.
|
||||
func (uuid UUID) ID() uint32 {
|
||||
return binary.BigEndian.Uint32(uuid[0:4])
|
||||
}
|
||||
|
||||
func (d Domain) String() string {
|
||||
switch d {
|
||||
case Person:
|
||||
return "Person"
|
||||
case Group:
|
||||
return "Group"
|
||||
case Org:
|
||||
return "Org"
|
||||
}
|
||||
return fmt.Sprintf("Domain%d", int(d))
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// Copyright 2016 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package uuid generates and inspects UUIDs.
|
||||
//
|
||||
// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security
|
||||
// Services.
|
||||
//
|
||||
// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to
|
||||
// maps or compared directly.
|
||||
package uuid
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Copyright 2016 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"hash"
|
||||
)
|
||||
|
||||
// Well known namespace IDs and UUIDs
|
||||
var (
|
||||
NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8"))
|
||||
NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8"))
|
||||
NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8"))
|
||||
NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8"))
|
||||
Nil UUID // empty UUID, all zeros
|
||||
|
||||
// The Max UUID is special form of UUID that is specified to have all 128 bits set to 1.
|
||||
Max = UUID{
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
}
|
||||
)
|
||||
|
||||
// NewHash returns a new UUID derived from the hash of space concatenated with
|
||||
// data generated by h. The hash should be at least 16 byte in length. The
|
||||
// first 16 bytes of the hash are used to form the UUID. The version of the
|
||||
// UUID will be the lower 4 bits of version. NewHash is used to implement
|
||||
// NewMD5 and NewSHA1.
|
||||
func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID {
|
||||
h.Reset()
|
||||
h.Write(space[:]) //nolint:errcheck
|
||||
h.Write(data) //nolint:errcheck
|
||||
s := h.Sum(nil)
|
||||
var uuid UUID
|
||||
copy(uuid[:], s)
|
||||
uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4)
|
||||
uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant
|
||||
return uuid
|
||||
}
|
||||
|
||||
// NewMD5 returns a new MD5 (Version 3) UUID based on the
|
||||
// supplied name space and data. It is the same as calling:
|
||||
//
|
||||
// NewHash(md5.New(), space, data, 3)
|
||||
func NewMD5(space UUID, data []byte) UUID {
|
||||
return NewHash(md5.New(), space, data, 3)
|
||||
}
|
||||
|
||||
// NewSHA1 returns a new SHA1 (Version 5) UUID based on the
|
||||
// supplied name space and data. It is the same as calling:
|
||||
//
|
||||
// NewHash(sha1.New(), space, data, 5)
|
||||
func NewSHA1(space UUID, data []byte) UUID {
|
||||
return NewHash(sha1.New(), space, data, 5)
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright 2016 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import "fmt"
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler.
|
||||
func (uuid UUID) MarshalText() ([]byte, error) {
|
||||
var js [36]byte
|
||||
encodeHex(js[:], uuid)
|
||||
return js[:], nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||
func (uuid *UUID) UnmarshalText(data []byte) error {
|
||||
id, err := ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*uuid = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalBinary implements encoding.BinaryMarshaler.
|
||||
func (uuid UUID) MarshalBinary() ([]byte, error) {
|
||||
return uuid[:], nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
|
||||
func (uuid *UUID) UnmarshalBinary(data []byte) error {
|
||||
if len(data) != 16 {
|
||||
return fmt.Errorf("invalid UUID (got %d bytes)", len(data))
|
||||
}
|
||||
copy(uuid[:], data)
|
||||
return nil
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
// Copyright 2016 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
nodeMu sync.Mutex
|
||||
ifname string // name of interface being used
|
||||
nodeID [6]byte // hardware for version 1 UUIDs
|
||||
zeroID [6]byte // nodeID with only 0's
|
||||
)
|
||||
|
||||
// NodeInterface returns the name of the interface from which the NodeID was
|
||||
// derived. The interface "user" is returned if the NodeID was set by
|
||||
// SetNodeID.
|
||||
func NodeInterface() string {
|
||||
defer nodeMu.Unlock()
|
||||
nodeMu.Lock()
|
||||
return ifname
|
||||
}
|
||||
|
||||
// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs.
|
||||
// If name is "" then the first usable interface found will be used or a random
|
||||
// Node ID will be generated. If a named interface cannot be found then false
|
||||
// is returned.
|
||||
//
|
||||
// SetNodeInterface never fails when name is "".
|
||||
func SetNodeInterface(name string) bool {
|
||||
defer nodeMu.Unlock()
|
||||
nodeMu.Lock()
|
||||
return setNodeInterface(name)
|
||||
}
|
||||
|
||||
func setNodeInterface(name string) bool {
|
||||
iname, addr := getHardwareInterface(name) // null implementation for js
|
||||
if iname != "" && addr != nil {
|
||||
ifname = iname
|
||||
copy(nodeID[:], addr)
|
||||
return true
|
||||
}
|
||||
|
||||
// We found no interfaces with a valid hardware address. If name
|
||||
// does not specify a specific interface generate a random Node ID
|
||||
// (section 4.1.6)
|
||||
if name == "" {
|
||||
ifname = "random"
|
||||
randomBits(nodeID[:])
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NodeID returns a slice of a copy of the current Node ID, setting the Node ID
|
||||
// if not already set.
|
||||
func NodeID() []byte {
|
||||
defer nodeMu.Unlock()
|
||||
nodeMu.Lock()
|
||||
if nodeID == zeroID {
|
||||
setNodeInterface("")
|
||||
}
|
||||
nid := nodeID
|
||||
return nid[:]
|
||||
}
|
||||
|
||||
// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes
|
||||
// of id are used. If id is less than 6 bytes then false is returned and the
|
||||
// Node ID is not set.
|
||||
func SetNodeID(id []byte) bool {
|
||||
if len(id) < 6 {
|
||||
return false
|
||||
}
|
||||
defer nodeMu.Unlock()
|
||||
nodeMu.Lock()
|
||||
copy(nodeID[:], id)
|
||||
ifname = "user"
|
||||
return true
|
||||
}
|
||||
|
||||
// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is
|
||||
// not valid. The NodeID is only well defined for version 1 and 2 UUIDs.
|
||||
func (uuid UUID) NodeID() []byte {
|
||||
var node [6]byte
|
||||
copy(node[:], uuid[10:])
|
||||
return node[:]
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// Copyright 2017 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build js
|
||||
|
||||
package uuid
|
||||
|
||||
// getHardwareInterface returns nil values for the JS version of the code.
|
||||
// This removes the "net" dependency, because it is not used in the browser.
|
||||
// Using the "net" library inflates the size of the transpiled JS code by 673k bytes.
|
||||
func getHardwareInterface(name string) (string, []byte) { return "", nil }
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// Copyright 2017 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !js
|
||||
|
||||
package uuid
|
||||
|
||||
import "net"
|
||||
|
||||
var interfaces []net.Interface // cached list of interfaces
|
||||
|
||||
// getHardwareInterface returns the name and hardware address of interface name.
|
||||
// If name is "" then the name and hardware address of one of the system's
|
||||
// interfaces is returned. If no interfaces are found (name does not exist or
|
||||
// there are no interfaces) then "", nil is returned.
|
||||
//
|
||||
// Only addresses of at least 6 bytes are returned.
|
||||
func getHardwareInterface(name string) (string, []byte) {
|
||||
if interfaces == nil {
|
||||
var err error
|
||||
interfaces, err = net.Interfaces()
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
for _, ifs := range interfaces {
|
||||
if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) {
|
||||
return ifs.Name, ifs.HardwareAddr
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
// Copyright 2021 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var jsonNull = []byte("null")
|
||||
|
||||
// NullUUID represents a UUID that may be null.
|
||||
// NullUUID implements the SQL driver.Scanner interface so
|
||||
// it can be used as a scan destination:
|
||||
//
|
||||
// var u uuid.NullUUID
|
||||
// err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&u)
|
||||
// ...
|
||||
// if u.Valid {
|
||||
// // use u.UUID
|
||||
// } else {
|
||||
// // NULL value
|
||||
// }
|
||||
//
|
||||
type NullUUID struct {
|
||||
UUID UUID
|
||||
Valid bool // Valid is true if UUID is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the SQL driver.Scanner interface.
|
||||
func (nu *NullUUID) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
nu.UUID, nu.Valid = Nil, false
|
||||
return nil
|
||||
}
|
||||
|
||||
err := nu.UUID.Scan(value)
|
||||
if err != nil {
|
||||
nu.Valid = false
|
||||
return err
|
||||
}
|
||||
|
||||
nu.Valid = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (nu NullUUID) Value() (driver.Value, error) {
|
||||
if !nu.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
// Delegate to UUID Value function
|
||||
return nu.UUID.Value()
|
||||
}
|
||||
|
||||
// MarshalBinary implements encoding.BinaryMarshaler.
|
||||
func (nu NullUUID) MarshalBinary() ([]byte, error) {
|
||||
if nu.Valid {
|
||||
return nu.UUID[:], nil
|
||||
}
|
||||
|
||||
return []byte(nil), nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
|
||||
func (nu *NullUUID) UnmarshalBinary(data []byte) error {
|
||||
if len(data) != 16 {
|
||||
return fmt.Errorf("invalid UUID (got %d bytes)", len(data))
|
||||
}
|
||||
copy(nu.UUID[:], data)
|
||||
nu.Valid = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler.
|
||||
func (nu NullUUID) MarshalText() ([]byte, error) {
|
||||
if nu.Valid {
|
||||
return nu.UUID.MarshalText()
|
||||
}
|
||||
|
||||
return jsonNull, nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||
func (nu *NullUUID) UnmarshalText(data []byte) error {
|
||||
id, err := ParseBytes(data)
|
||||
if err != nil {
|
||||
nu.Valid = false
|
||||
return err
|
||||
}
|
||||
nu.UUID = id
|
||||
nu.Valid = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler.
|
||||
func (nu NullUUID) MarshalJSON() ([]byte, error) {
|
||||
if nu.Valid {
|
||||
return json.Marshal(nu.UUID)
|
||||
}
|
||||
|
||||
return jsonNull, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (nu *NullUUID) UnmarshalJSON(data []byte) error {
|
||||
if bytes.Equal(data, jsonNull) {
|
||||
*nu = NullUUID{}
|
||||
return nil // valid null UUID
|
||||
}
|
||||
err := json.Unmarshal(data, &nu.UUID)
|
||||
nu.Valid = err == nil
|
||||
return err
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Copyright 2016 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Scan implements sql.Scanner so UUIDs can be read from databases transparently.
|
||||
// Currently, database types that map to string and []byte are supported. Please
|
||||
// consult database-specific driver documentation for matching types.
|
||||
func (uuid *UUID) Scan(src interface{}) error {
|
||||
switch src := src.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
|
||||
case string:
|
||||
// if an empty UUID comes from a table, we return a null UUID
|
||||
if src == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// see Parse for required string format
|
||||
u, err := Parse(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Scan: %v", err)
|
||||
}
|
||||
|
||||
*uuid = u
|
||||
|
||||
case []byte:
|
||||
// if an empty UUID comes from a table, we return a null UUID
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// assumes a simple slice of bytes if 16 bytes
|
||||
// otherwise attempts to parse
|
||||
if len(src) != 16 {
|
||||
return uuid.Scan(string(src))
|
||||
}
|
||||
copy((*uuid)[:], src)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("Scan: unable to scan type %T into UUID", src)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements sql.Valuer so that UUIDs can be written to databases
|
||||
// transparently. Currently, UUIDs map to strings. Please consult
|
||||
// database-specific driver documentation for matching types.
|
||||
func (uuid UUID) Value() (driver.Value, error) {
|
||||
return uuid.String(), nil
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
// Copyright 2016 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A Time represents a time as the number of 100's of nanoseconds since 15 Oct
|
||||
// 1582.
|
||||
type Time int64
|
||||
|
||||
const (
|
||||
lillian = 2299160 // Julian day of 15 Oct 1582
|
||||
unix = 2440587 // Julian day of 1 Jan 1970
|
||||
epoch = unix - lillian // Days between epochs
|
||||
g1582 = epoch * 86400 // seconds between epochs
|
||||
g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs
|
||||
)
|
||||
|
||||
var (
|
||||
timeMu sync.Mutex
|
||||
lasttime uint64 // last time we returned
|
||||
clockSeq uint16 // clock sequence for this run
|
||||
|
||||
timeNow = time.Now // for testing
|
||||
)
|
||||
|
||||
// UnixTime converts t the number of seconds and nanoseconds using the Unix
|
||||
// epoch of 1 Jan 1970.
|
||||
func (t Time) UnixTime() (sec, nsec int64) {
|
||||
sec = int64(t - g1582ns100)
|
||||
nsec = (sec % 10000000) * 100
|
||||
sec /= 10000000
|
||||
return sec, nsec
|
||||
}
|
||||
|
||||
// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and
|
||||
// clock sequence as well as adjusting the clock sequence as needed. An error
|
||||
// is returned if the current time cannot be determined.
|
||||
func GetTime() (Time, uint16, error) {
|
||||
defer timeMu.Unlock()
|
||||
timeMu.Lock()
|
||||
return getTime()
|
||||
}
|
||||
|
||||
func getTime() (Time, uint16, error) {
|
||||
t := timeNow()
|
||||
|
||||
// If we don't have a clock sequence already, set one.
|
||||
if clockSeq == 0 {
|
||||
setClockSequence(-1)
|
||||
}
|
||||
now := uint64(t.UnixNano()/100) + g1582ns100
|
||||
|
||||
// If time has gone backwards with this clock sequence then we
|
||||
// increment the clock sequence
|
||||
if now <= lasttime {
|
||||
clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000
|
||||
}
|
||||
lasttime = now
|
||||
return Time(now), clockSeq, nil
|
||||
}
|
||||
|
||||
// ClockSequence returns the current clock sequence, generating one if not
|
||||
// already set. The clock sequence is only used for Version 1 UUIDs.
|
||||
//
|
||||
// The uuid package does not use global static storage for the clock sequence or
|
||||
// the last time a UUID was generated. Unless SetClockSequence is used, a new
|
||||
// random clock sequence is generated the first time a clock sequence is
|
||||
// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1)
|
||||
func ClockSequence() int {
|
||||
defer timeMu.Unlock()
|
||||
timeMu.Lock()
|
||||
return clockSequence()
|
||||
}
|
||||
|
||||
func clockSequence() int {
|
||||
if clockSeq == 0 {
|
||||
setClockSequence(-1)
|
||||
}
|
||||
return int(clockSeq & 0x3fff)
|
||||
}
|
||||
|
||||
// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to
|
||||
// -1 causes a new sequence to be generated.
|
||||
func SetClockSequence(seq int) {
|
||||
defer timeMu.Unlock()
|
||||
timeMu.Lock()
|
||||
setClockSequence(seq)
|
||||
}
|
||||
|
||||
func setClockSequence(seq int) {
|
||||
if seq == -1 {
|
||||
var b [2]byte
|
||||
randomBits(b[:]) // clock sequence
|
||||
seq = int(b[0])<<8 | int(b[1])
|
||||
}
|
||||
oldSeq := clockSeq
|
||||
clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant
|
||||
if oldSeq != clockSeq {
|
||||
lasttime = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in
|
||||
// uuid. The time is only defined for version 1, 2, 6 and 7 UUIDs.
|
||||
func (uuid UUID) Time() Time {
|
||||
var t Time
|
||||
switch uuid.Version() {
|
||||
case 6:
|
||||
time := binary.BigEndian.Uint64(uuid[:8]) // Ignore uuid[6] version b0110
|
||||
t = Time(time)
|
||||
case 7:
|
||||
time := binary.BigEndian.Uint64(uuid[:8])
|
||||
t = Time((time>>16)*10000 + g1582ns100)
|
||||
default: // forward compatible
|
||||
time := int64(binary.BigEndian.Uint32(uuid[0:4]))
|
||||
time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32
|
||||
time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48
|
||||
t = Time(time)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// ClockSequence returns the clock sequence encoded in uuid.
|
||||
// The clock sequence is only well defined for version 1 and 2 UUIDs.
|
||||
func (uuid UUID) ClockSequence() int {
|
||||
return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
// Copyright 2016 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// randomBits completely fills slice b with random data.
|
||||
func randomBits(b []byte) {
|
||||
if _, err := io.ReadFull(rander, b); err != nil {
|
||||
panic(err.Error()) // rand should never fail
|
||||
}
|
||||
}
|
||||
|
||||
// xvalues returns the value of a byte as a hexadecimal digit or 255.
|
||||
var xvalues = [256]byte{
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255,
|
||||
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
}
|
||||
|
||||
// xtob converts hex characters x1 and x2 into a byte.
|
||||
func xtob(x1, x2 byte) (byte, bool) {
|
||||
b1 := xvalues[x1]
|
||||
b2 := xvalues[x2]
|
||||
return (b1 << 4) | b2, b1 != 255 && b2 != 255
|
||||
}
|
||||
+365
@@ -0,0 +1,365 @@
|
||||
// Copyright 2018 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC
|
||||
// 4122.
|
||||
type UUID [16]byte
|
||||
|
||||
// A Version represents a UUID's version.
|
||||
type Version byte
|
||||
|
||||
// A Variant represents a UUID's variant.
|
||||
type Variant byte
|
||||
|
||||
// Constants returned by Variant.
|
||||
const (
|
||||
Invalid = Variant(iota) // Invalid UUID
|
||||
RFC4122 // The variant specified in RFC4122
|
||||
Reserved // Reserved, NCS backward compatibility.
|
||||
Microsoft // Reserved, Microsoft Corporation backward compatibility.
|
||||
Future // Reserved for future definition.
|
||||
)
|
||||
|
||||
const randPoolSize = 16 * 16
|
||||
|
||||
var (
|
||||
rander = rand.Reader // random function
|
||||
poolEnabled = false
|
||||
poolMu sync.Mutex
|
||||
poolPos = randPoolSize // protected with poolMu
|
||||
pool [randPoolSize]byte // protected with poolMu
|
||||
)
|
||||
|
||||
type invalidLengthError struct{ len int }
|
||||
|
||||
func (err invalidLengthError) Error() string {
|
||||
return fmt.Sprintf("invalid UUID length: %d", err.len)
|
||||
}
|
||||
|
||||
// IsInvalidLengthError is matcher function for custom error invalidLengthError
|
||||
func IsInvalidLengthError(err error) bool {
|
||||
_, ok := err.(invalidLengthError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Parse decodes s into a UUID or returns an error if it cannot be parsed. Both
|
||||
// the standard UUID forms defined in RFC 4122
|
||||
// (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and
|
||||
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) are decoded. In addition,
|
||||
// Parse accepts non-standard strings such as the raw hex encoding
|
||||
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx and 38 byte "Microsoft style" encodings,
|
||||
// e.g. {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}. Only the middle 36 bytes are
|
||||
// examined in the latter case. Parse should not be used to validate strings as
|
||||
// it parses non-standard encodings as indicated above.
|
||||
func Parse(s string) (UUID, error) {
|
||||
var uuid UUID
|
||||
switch len(s) {
|
||||
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
case 36:
|
||||
|
||||
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
case 36 + 9:
|
||||
if !strings.EqualFold(s[:9], "urn:uuid:") {
|
||||
return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9])
|
||||
}
|
||||
s = s[9:]
|
||||
|
||||
// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
|
||||
case 36 + 2:
|
||||
s = s[1:]
|
||||
|
||||
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
case 32:
|
||||
var ok bool
|
||||
for i := range uuid {
|
||||
uuid[i], ok = xtob(s[i*2], s[i*2+1])
|
||||
if !ok {
|
||||
return uuid, errors.New("invalid UUID format")
|
||||
}
|
||||
}
|
||||
return uuid, nil
|
||||
default:
|
||||
return uuid, invalidLengthError{len(s)}
|
||||
}
|
||||
// s is now at least 36 bytes long
|
||||
// it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
|
||||
return uuid, errors.New("invalid UUID format")
|
||||
}
|
||||
for i, x := range [16]int{
|
||||
0, 2, 4, 6,
|
||||
9, 11,
|
||||
14, 16,
|
||||
19, 21,
|
||||
24, 26, 28, 30, 32, 34,
|
||||
} {
|
||||
v, ok := xtob(s[x], s[x+1])
|
||||
if !ok {
|
||||
return uuid, errors.New("invalid UUID format")
|
||||
}
|
||||
uuid[i] = v
|
||||
}
|
||||
return uuid, nil
|
||||
}
|
||||
|
||||
// ParseBytes is like Parse, except it parses a byte slice instead of a string.
|
||||
func ParseBytes(b []byte) (UUID, error) {
|
||||
var uuid UUID
|
||||
switch len(b) {
|
||||
case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
if !bytes.EqualFold(b[:9], []byte("urn:uuid:")) {
|
||||
return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9])
|
||||
}
|
||||
b = b[9:]
|
||||
case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
|
||||
b = b[1:]
|
||||
case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
var ok bool
|
||||
for i := 0; i < 32; i += 2 {
|
||||
uuid[i/2], ok = xtob(b[i], b[i+1])
|
||||
if !ok {
|
||||
return uuid, errors.New("invalid UUID format")
|
||||
}
|
||||
}
|
||||
return uuid, nil
|
||||
default:
|
||||
return uuid, invalidLengthError{len(b)}
|
||||
}
|
||||
// s is now at least 36 bytes long
|
||||
// it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' {
|
||||
return uuid, errors.New("invalid UUID format")
|
||||
}
|
||||
for i, x := range [16]int{
|
||||
0, 2, 4, 6,
|
||||
9, 11,
|
||||
14, 16,
|
||||
19, 21,
|
||||
24, 26, 28, 30, 32, 34,
|
||||
} {
|
||||
v, ok := xtob(b[x], b[x+1])
|
||||
if !ok {
|
||||
return uuid, errors.New("invalid UUID format")
|
||||
}
|
||||
uuid[i] = v
|
||||
}
|
||||
return uuid, nil
|
||||
}
|
||||
|
||||
// MustParse is like Parse but panics if the string cannot be parsed.
|
||||
// It simplifies safe initialization of global variables holding compiled UUIDs.
|
||||
func MustParse(s string) UUID {
|
||||
uuid, err := Parse(s)
|
||||
if err != nil {
|
||||
panic(`uuid: Parse(` + s + `): ` + err.Error())
|
||||
}
|
||||
return uuid
|
||||
}
|
||||
|
||||
// FromBytes creates a new UUID from a byte slice. Returns an error if the slice
|
||||
// does not have a length of 16. The bytes are copied from the slice.
|
||||
func FromBytes(b []byte) (uuid UUID, err error) {
|
||||
err = uuid.UnmarshalBinary(b)
|
||||
return uuid, err
|
||||
}
|
||||
|
||||
// Must returns uuid if err is nil and panics otherwise.
|
||||
func Must(uuid UUID, err error) UUID {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return uuid
|
||||
}
|
||||
|
||||
// Validate returns an error if s is not a properly formatted UUID in one of the following formats:
|
||||
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
|
||||
// It returns an error if the format is invalid, otherwise nil.
|
||||
func Validate(s string) error {
|
||||
switch len(s) {
|
||||
// Standard UUID format
|
||||
case 36:
|
||||
|
||||
// UUID with "urn:uuid:" prefix
|
||||
case 36 + 9:
|
||||
if !strings.EqualFold(s[:9], "urn:uuid:") {
|
||||
return fmt.Errorf("invalid urn prefix: %q", s[:9])
|
||||
}
|
||||
s = s[9:]
|
||||
|
||||
// UUID enclosed in braces
|
||||
case 36 + 2:
|
||||
if s[0] != '{' || s[len(s)-1] != '}' {
|
||||
return fmt.Errorf("invalid bracketed UUID format")
|
||||
}
|
||||
s = s[1 : len(s)-1]
|
||||
|
||||
// UUID without hyphens
|
||||
case 32:
|
||||
for i := 0; i < len(s); i += 2 {
|
||||
_, ok := xtob(s[i], s[i+1])
|
||||
if !ok {
|
||||
return errors.New("invalid UUID format")
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return invalidLengthError{len(s)}
|
||||
}
|
||||
|
||||
// Check for standard UUID format
|
||||
if len(s) == 36 {
|
||||
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
|
||||
return errors.New("invalid UUID format")
|
||||
}
|
||||
for _, x := range []int{0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34} {
|
||||
if _, ok := xtob(s[x], s[x+1]); !ok {
|
||||
return errors.New("invalid UUID format")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
// , or "" if uuid is invalid.
|
||||
func (uuid UUID) String() string {
|
||||
var buf [36]byte
|
||||
encodeHex(buf[:], uuid)
|
||||
return string(buf[:])
|
||||
}
|
||||
|
||||
// URN returns the RFC 2141 URN form of uuid,
|
||||
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid.
|
||||
func (uuid UUID) URN() string {
|
||||
var buf [36 + 9]byte
|
||||
copy(buf[:], "urn:uuid:")
|
||||
encodeHex(buf[9:], uuid)
|
||||
return string(buf[:])
|
||||
}
|
||||
|
||||
func encodeHex(dst []byte, uuid UUID) {
|
||||
hex.Encode(dst, uuid[:4])
|
||||
dst[8] = '-'
|
||||
hex.Encode(dst[9:13], uuid[4:6])
|
||||
dst[13] = '-'
|
||||
hex.Encode(dst[14:18], uuid[6:8])
|
||||
dst[18] = '-'
|
||||
hex.Encode(dst[19:23], uuid[8:10])
|
||||
dst[23] = '-'
|
||||
hex.Encode(dst[24:], uuid[10:])
|
||||
}
|
||||
|
||||
// Variant returns the variant encoded in uuid.
|
||||
func (uuid UUID) Variant() Variant {
|
||||
switch {
|
||||
case (uuid[8] & 0xc0) == 0x80:
|
||||
return RFC4122
|
||||
case (uuid[8] & 0xe0) == 0xc0:
|
||||
return Microsoft
|
||||
case (uuid[8] & 0xe0) == 0xe0:
|
||||
return Future
|
||||
default:
|
||||
return Reserved
|
||||
}
|
||||
}
|
||||
|
||||
// Version returns the version of uuid.
|
||||
func (uuid UUID) Version() Version {
|
||||
return Version(uuid[6] >> 4)
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
if v > 15 {
|
||||
return fmt.Sprintf("BAD_VERSION_%d", v)
|
||||
}
|
||||
return fmt.Sprintf("VERSION_%d", v)
|
||||
}
|
||||
|
||||
func (v Variant) String() string {
|
||||
switch v {
|
||||
case RFC4122:
|
||||
return "RFC4122"
|
||||
case Reserved:
|
||||
return "Reserved"
|
||||
case Microsoft:
|
||||
return "Microsoft"
|
||||
case Future:
|
||||
return "Future"
|
||||
case Invalid:
|
||||
return "Invalid"
|
||||
}
|
||||
return fmt.Sprintf("BadVariant%d", int(v))
|
||||
}
|
||||
|
||||
// SetRand sets the random number generator to r, which implements io.Reader.
|
||||
// If r.Read returns an error when the package requests random data then
|
||||
// a panic will be issued.
|
||||
//
|
||||
// Calling SetRand with nil sets the random number generator to the default
|
||||
// generator.
|
||||
func SetRand(r io.Reader) {
|
||||
if r == nil {
|
||||
rander = rand.Reader
|
||||
return
|
||||
}
|
||||
rander = r
|
||||
}
|
||||
|
||||
// EnableRandPool enables internal randomness pool used for Random
|
||||
// (Version 4) UUID generation. The pool contains random bytes read from
|
||||
// the random number generator on demand in batches. Enabling the pool
|
||||
// may improve the UUID generation throughput significantly.
|
||||
//
|
||||
// Since the pool is stored on the Go heap, this feature may be a bad fit
|
||||
// for security sensitive applications.
|
||||
//
|
||||
// Both EnableRandPool and DisableRandPool are not thread-safe and should
|
||||
// only be called when there is no possibility that New or any other
|
||||
// UUID Version 4 generation function will be called concurrently.
|
||||
func EnableRandPool() {
|
||||
poolEnabled = true
|
||||
}
|
||||
|
||||
// DisableRandPool disables the randomness pool if it was previously
|
||||
// enabled with EnableRandPool.
|
||||
//
|
||||
// Both EnableRandPool and DisableRandPool are not thread-safe and should
|
||||
// only be called when there is no possibility that New or any other
|
||||
// UUID Version 4 generation function will be called concurrently.
|
||||
func DisableRandPool() {
|
||||
poolEnabled = false
|
||||
defer poolMu.Unlock()
|
||||
poolMu.Lock()
|
||||
poolPos = randPoolSize
|
||||
}
|
||||
|
||||
// UUIDs is a slice of UUID types.
|
||||
type UUIDs []UUID
|
||||
|
||||
// Strings returns a string slice containing the string form of each UUID in uuids.
|
||||
func (uuids UUIDs) Strings() []string {
|
||||
var uuidStrs = make([]string, len(uuids))
|
||||
for i, uuid := range uuids {
|
||||
uuidStrs[i] = uuid.String()
|
||||
}
|
||||
return uuidStrs
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
// Copyright 2016 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// NewUUID returns a Version 1 UUID based on the current NodeID and clock
|
||||
// sequence, and the current time. If the NodeID has not been set by SetNodeID
|
||||
// or SetNodeInterface then it will be set automatically. If the NodeID cannot
|
||||
// be set NewUUID returns nil. If clock sequence has not been set by
|
||||
// SetClockSequence then it will be set automatically. If GetTime fails to
|
||||
// return the current NewUUID returns nil and an error.
|
||||
//
|
||||
// In most cases, New should be used.
|
||||
func NewUUID() (UUID, error) {
|
||||
var uuid UUID
|
||||
now, seq, err := GetTime()
|
||||
if err != nil {
|
||||
return uuid, err
|
||||
}
|
||||
|
||||
timeLow := uint32(now & 0xffffffff)
|
||||
timeMid := uint16((now >> 32) & 0xffff)
|
||||
timeHi := uint16((now >> 48) & 0x0fff)
|
||||
timeHi |= 0x1000 // Version 1
|
||||
|
||||
binary.BigEndian.PutUint32(uuid[0:], timeLow)
|
||||
binary.BigEndian.PutUint16(uuid[4:], timeMid)
|
||||
binary.BigEndian.PutUint16(uuid[6:], timeHi)
|
||||
binary.BigEndian.PutUint16(uuid[8:], seq)
|
||||
|
||||
nodeMu.Lock()
|
||||
if nodeID == zeroID {
|
||||
setNodeInterface("")
|
||||
}
|
||||
copy(uuid[10:], nodeID[:])
|
||||
nodeMu.Unlock()
|
||||
|
||||
return uuid, nil
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
// Copyright 2016 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import "io"
|
||||
|
||||
// New creates a new random UUID or panics. New is equivalent to
|
||||
// the expression
|
||||
//
|
||||
// uuid.Must(uuid.NewRandom())
|
||||
func New() UUID {
|
||||
return Must(NewRandom())
|
||||
}
|
||||
|
||||
// NewString creates a new random UUID and returns it as a string or panics.
|
||||
// NewString is equivalent to the expression
|
||||
//
|
||||
// uuid.New().String()
|
||||
func NewString() string {
|
||||
return Must(NewRandom()).String()
|
||||
}
|
||||
|
||||
// NewRandom returns a Random (Version 4) UUID.
|
||||
//
|
||||
// The strength of the UUIDs is based on the strength of the crypto/rand
|
||||
// package.
|
||||
//
|
||||
// Uses the randomness pool if it was enabled with EnableRandPool.
|
||||
//
|
||||
// A note about uniqueness derived from the UUID Wikipedia entry:
|
||||
//
|
||||
// Randomly generated UUIDs have 122 random bits. One's annual risk of being
|
||||
// hit by a meteorite is estimated to be one chance in 17 billion, that
|
||||
// means the probability is about 0.00000000006 (6 × 10−11),
|
||||
// equivalent to the odds of creating a few tens of trillions of UUIDs in a
|
||||
// year and having one duplicate.
|
||||
func NewRandom() (UUID, error) {
|
||||
if !poolEnabled {
|
||||
return NewRandomFromReader(rander)
|
||||
}
|
||||
return newRandomFromPool()
|
||||
}
|
||||
|
||||
// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader.
|
||||
func NewRandomFromReader(r io.Reader) (UUID, error) {
|
||||
var uuid UUID
|
||||
_, err := io.ReadFull(r, uuid[:])
|
||||
if err != nil {
|
||||
return Nil, err
|
||||
}
|
||||
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
|
||||
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
|
||||
return uuid, nil
|
||||
}
|
||||
|
||||
func newRandomFromPool() (UUID, error) {
|
||||
var uuid UUID
|
||||
poolMu.Lock()
|
||||
if poolPos == randPoolSize {
|
||||
_, err := io.ReadFull(rander, pool[:])
|
||||
if err != nil {
|
||||
poolMu.Unlock()
|
||||
return Nil, err
|
||||
}
|
||||
poolPos = 0
|
||||
}
|
||||
copy(uuid[:], pool[poolPos:(poolPos+16)])
|
||||
poolPos += 16
|
||||
poolMu.Unlock()
|
||||
|
||||
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
|
||||
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
|
||||
return uuid, nil
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
// Copyright 2023 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// UUID version 6 is a field-compatible version of UUIDv1, reordered for improved DB locality.
|
||||
// It is expected that UUIDv6 will primarily be used in contexts where there are existing v1 UUIDs.
|
||||
// Systems that do not involve legacy UUIDv1 SHOULD consider using UUIDv7 instead.
|
||||
//
|
||||
// see https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03#uuidv6
|
||||
//
|
||||
// NewV6 returns a Version 6 UUID based on the current NodeID and clock
|
||||
// sequence, and the current time. If the NodeID has not been set by SetNodeID
|
||||
// or SetNodeInterface then it will be set automatically. If the NodeID cannot
|
||||
// be set NewV6 set NodeID is random bits automatically . If clock sequence has not been set by
|
||||
// SetClockSequence then it will be set automatically. If GetTime fails to
|
||||
// return the current NewV6 returns Nil and an error.
|
||||
func NewV6() (UUID, error) {
|
||||
var uuid UUID
|
||||
now, seq, err := GetTime()
|
||||
if err != nil {
|
||||
return uuid, err
|
||||
}
|
||||
|
||||
/*
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| time_high |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| time_mid | time_low_and_version |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|clk_seq_hi_res | clk_seq_low | node (0-1) |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| node (2-5) |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
*/
|
||||
|
||||
binary.BigEndian.PutUint64(uuid[0:], uint64(now))
|
||||
binary.BigEndian.PutUint16(uuid[8:], seq)
|
||||
|
||||
uuid[6] = 0x60 | (uuid[6] & 0x0F)
|
||||
uuid[8] = 0x80 | (uuid[8] & 0x3F)
|
||||
|
||||
nodeMu.Lock()
|
||||
if nodeID == zeroID {
|
||||
setNodeInterface("")
|
||||
}
|
||||
copy(uuid[10:], nodeID[:])
|
||||
nodeMu.Unlock()
|
||||
|
||||
return uuid, nil
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
// Copyright 2023 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// UUID version 7 features a time-ordered value field derived from the widely
|
||||
// implemented and well known Unix Epoch timestamp source,
|
||||
// the number of milliseconds seconds since midnight 1 Jan 1970 UTC, leap seconds excluded.
|
||||
// As well as improved entropy characteristics over versions 1 or 6.
|
||||
//
|
||||
// see https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03#name-uuid-version-7
|
||||
//
|
||||
// Implementations SHOULD utilize UUID version 7 over UUID version 1 and 6 if possible.
|
||||
//
|
||||
// NewV7 returns a Version 7 UUID based on the current time(Unix Epoch).
|
||||
// Uses the randomness pool if it was enabled with EnableRandPool.
|
||||
// On error, NewV7 returns Nil and an error
|
||||
func NewV7() (UUID, error) {
|
||||
uuid, err := NewRandom()
|
||||
if err != nil {
|
||||
return uuid, err
|
||||
}
|
||||
makeV7(uuid[:])
|
||||
return uuid, nil
|
||||
}
|
||||
|
||||
// NewV7FromReader returns a Version 7 UUID based on the current time(Unix Epoch).
|
||||
// it use NewRandomFromReader fill random bits.
|
||||
// On error, NewV7FromReader returns Nil and an error.
|
||||
func NewV7FromReader(r io.Reader) (UUID, error) {
|
||||
uuid, err := NewRandomFromReader(r)
|
||||
if err != nil {
|
||||
return uuid, err
|
||||
}
|
||||
|
||||
makeV7(uuid[:])
|
||||
return uuid, nil
|
||||
}
|
||||
|
||||
// makeV7 fill 48 bits time (uuid[0] - uuid[5]), set version b0111 (uuid[6])
|
||||
// uuid[8] already has the right version number (Variant is 10)
|
||||
// see function NewV7 and NewV7FromReader
|
||||
func makeV7(uuid []byte) {
|
||||
/*
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| unix_ts_ms |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| unix_ts_ms | ver | rand_a (12 bit seq) |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|var| rand_b |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| rand_b |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
*/
|
||||
_ = uuid[15] // bounds check
|
||||
|
||||
t, s := getV7Time()
|
||||
|
||||
uuid[0] = byte(t >> 40)
|
||||
uuid[1] = byte(t >> 32)
|
||||
uuid[2] = byte(t >> 24)
|
||||
uuid[3] = byte(t >> 16)
|
||||
uuid[4] = byte(t >> 8)
|
||||
uuid[5] = byte(t)
|
||||
|
||||
uuid[6] = 0x70 | (0x0F & byte(s>>8))
|
||||
uuid[7] = byte(s)
|
||||
}
|
||||
|
||||
// lastV7time is the last time we returned stored as:
|
||||
//
|
||||
// 52 bits of time in milliseconds since epoch
|
||||
// 12 bits of (fractional nanoseconds) >> 8
|
||||
var lastV7time int64
|
||||
|
||||
const nanoPerMilli = 1000000
|
||||
|
||||
// getV7Time returns the time in milliseconds and nanoseconds / 256.
|
||||
// The returned (milli << 12 + seq) is guarenteed to be greater than
|
||||
// (milli << 12 + seq) returned by any previous call to getV7Time.
|
||||
func getV7Time() (milli, seq int64) {
|
||||
timeMu.Lock()
|
||||
defer timeMu.Unlock()
|
||||
|
||||
nano := timeNow().UnixNano()
|
||||
milli = nano / nanoPerMilli
|
||||
// Sequence number is between 0 and 3906 (nanoPerMilli>>8)
|
||||
seq = (nano - milli*nanoPerMilli) >> 8
|
||||
now := milli<<12 + seq
|
||||
if now <= lastV7time {
|
||||
now = lastV7time + 1
|
||||
milli = now >> 12
|
||||
seq = now & 0xfff
|
||||
}
|
||||
lastV7time = now
|
||||
return milli, seq
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL string) *Client {
|
||||
return &Client{
|
||||
baseURL: baseURL,
|
||||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) do(ctx context.Context, method, path string, body, result any) error {
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
reqBody = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, reqBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("do request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
var errResp struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&errResp)
|
||||
if errResp.Error != "" {
|
||||
return fmt.Errorf("%s: %s", http.StatusText(resp.StatusCode), errResp.Error)
|
||||
}
|
||||
return fmt.Errorf("%s", http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
return json.NewDecoder(resp.Body).Decode(result)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Capability struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
TargetTypes []string `json:"target_types"`
|
||||
TargetEntityID string `json:"target_entity_id,omitempty"`
|
||||
Provider string `json:"provider"`
|
||||
Operation string `json:"operation"`
|
||||
Risk string `json:"risk,omitempty"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
ExpectedSideEffects string `json:"expected_side_effects,omitempty"`
|
||||
}
|
||||
|
||||
type Execution struct {
|
||||
ID string `json:"id"`
|
||||
CapabilityID string `json:"capability_id"`
|
||||
TargetEntityID string `json:"target_entity_id"`
|
||||
Status string `json:"status"`
|
||||
Result map[string]any `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
RequestedBy map[string]string `json:"requested_by,omitempty"`
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
||||
}
|
||||
|
||||
type ExecuteRequest struct {
|
||||
CapabilityID string `json:"capability_id"`
|
||||
TargetEntityID string `json:"target_entity_id"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
RequestedBy map[string]string `json:"requested_by,omitempty"`
|
||||
Origin map[string]string `json:"origin,omitempty"`
|
||||
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
}
|
||||
|
||||
type CreateCapabilityRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
TargetTypes []string `json:"target_types"`
|
||||
TargetEntityID string `json:"target_entity_id,omitempty"`
|
||||
Provider string `json:"provider"`
|
||||
Operation string `json:"operation"`
|
||||
Risk string `json:"risk,omitempty"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
ExpectedSideEffects string `json:"expected_side_effects,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) Capabilities(ctx context.Context, entityID string) ([]Capability, error) {
|
||||
var result []Capability
|
||||
path := "/api/v1/capabilities"
|
||||
if entityID != "" {
|
||||
path += "?entity_id=" + entityID
|
||||
}
|
||||
if err := c.do(ctx, http.MethodGet, path, nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetCapability(ctx context.Context, id string) (*Capability, error) {
|
||||
var result Capability
|
||||
if err := c.do(ctx, http.MethodGet, "/api/v1/capabilities/"+id, nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *Client) CreateCapability(ctx context.Context, req CreateCapabilityRequest) (*Capability, error) {
|
||||
var result Capability
|
||||
if err := c.do(ctx, http.MethodPost, "/api/v1/capabilities", req, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *Client) Execute(ctx context.Context, req ExecuteRequest) (*Execution, error) {
|
||||
var result Execution
|
||||
if err := c.do(ctx, http.MethodPost, "/api/v1/execute", req, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetExecution(ctx context.Context, id string) (*Execution, error) {
|
||||
var result Execution
|
||||
if err := c.do(ctx, http.MethodGet, "/api/v1/executions/"+id, nil, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *Client) Health(ctx context.Context) error {
|
||||
return c.do(ctx, http.MethodGet, "/health", nil, nil)
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
Copyright (c) Yasuhiro MATSUMOTO <mattn.jp@gmail.com>
|
||||
|
||||
MIT License (Expat)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
# go-isatty
|
||||
|
||||
[](http://godoc.org/github.com/mattn/go-isatty)
|
||||
[](https://codecov.io/gh/mattn/go-isatty)
|
||||
[](https://coveralls.io/github/mattn/go-isatty?branch=master)
|
||||
[](https://goreportcard.com/report/mattn/go-isatty)
|
||||
|
||||
isatty for golang
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mattn/go-isatty"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
fmt.Println("Is Terminal")
|
||||
} else if isatty.IsCygwinTerminal(os.Stdout.Fd()) {
|
||||
fmt.Println("Is Cygwin/MSYS2 Terminal")
|
||||
} else {
|
||||
fmt.Println("Is Not Terminal")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
$ go get github.com/mattn/go-isatty
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Author
|
||||
|
||||
Yasuhiro Matsumoto (a.k.a mattn)
|
||||
|
||||
## Thanks
|
||||
|
||||
* k-takata: base idea for IsCygwinTerminal
|
||||
|
||||
https://github.com/k-takata/go-iscygpty
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// Package isatty implements interface to isatty
|
||||
package isatty
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
echo "" > coverage.txt
|
||||
|
||||
for d in $(go list ./... | grep -v vendor); do
|
||||
go test -race -coverprofile=profile.out -covermode=atomic "$d"
|
||||
if [ -f profile.out ]; then
|
||||
cat profile.out >> coverage.txt
|
||||
rm profile.out
|
||||
fi
|
||||
done
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
//go:build (darwin || freebsd || openbsd || netbsd || dragonfly || hurd) && !appengine && !tinygo
|
||||
// +build darwin freebsd openbsd netbsd dragonfly hurd
|
||||
// +build !appengine
|
||||
// +build !tinygo
|
||||
|
||||
package isatty
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
// IsTerminal return true if the file descriptor is terminal.
|
||||
func IsTerminal(fd uintptr) bool {
|
||||
_, err := unix.IoctlGetTermios(int(fd), unix.TIOCGETA)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2
|
||||
// terminal. This is also always false on this environment.
|
||||
func IsCygwinTerminal(fd uintptr) bool {
|
||||
return false
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
//go:build (appengine || js || nacl || tinygo || wasm) && !windows
|
||||
// +build appengine js nacl tinygo wasm
|
||||
// +build !windows
|
||||
|
||||
package isatty
|
||||
|
||||
// IsTerminal returns true if the file descriptor is terminal which
|
||||
// is always false on js and appengine classic which is a sandboxed PaaS.
|
||||
func IsTerminal(fd uintptr) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2
|
||||
// terminal. This is also always false on this environment.
|
||||
func IsCygwinTerminal(fd uintptr) bool {
|
||||
return false
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
//go:build plan9
|
||||
// +build plan9
|
||||
|
||||
package isatty
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||
func IsTerminal(fd uintptr) bool {
|
||||
path, err := syscall.Fd2path(int(fd))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return path == "/dev/cons" || path == "/mnt/term/dev/cons"
|
||||
}
|
||||
|
||||
// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2
|
||||
// terminal. This is also always false on this environment.
|
||||
func IsCygwinTerminal(fd uintptr) bool {
|
||||
return false
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
//go:build solaris && !appengine
|
||||
// +build solaris,!appengine
|
||||
|
||||
package isatty
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||
// see: https://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libc/port/gen/isatty.c
|
||||
func IsTerminal(fd uintptr) bool {
|
||||
_, err := unix.IoctlGetTermio(int(fd), unix.TCGETA)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2
|
||||
// terminal. This is also always false on this environment.
|
||||
func IsCygwinTerminal(fd uintptr) bool {
|
||||
return false
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
//go:build (linux || aix || zos) && !appengine && !tinygo
|
||||
// +build linux aix zos
|
||||
// +build !appengine
|
||||
// +build !tinygo
|
||||
|
||||
package isatty
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
// IsTerminal return true if the file descriptor is terminal.
|
||||
func IsTerminal(fd uintptr) bool {
|
||||
_, err := unix.IoctlGetTermios(int(fd), unix.TCGETS)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2
|
||||
// terminal. This is also always false on this environment.
|
||||
func IsCygwinTerminal(fd uintptr) bool {
|
||||
return false
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
//go:build windows && !appengine
|
||||
// +build windows,!appengine
|
||||
|
||||
package isatty
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
objectNameInfo uintptr = 1
|
||||
fileNameInfo = 2
|
||||
fileTypePipe = 3
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
ntdll = syscall.NewLazyDLL("ntdll.dll")
|
||||
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
|
||||
procGetFileInformationByHandleEx = kernel32.NewProc("GetFileInformationByHandleEx")
|
||||
procGetFileType = kernel32.NewProc("GetFileType")
|
||||
procNtQueryObject = ntdll.NewProc("NtQueryObject")
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Check if GetFileInformationByHandleEx is available.
|
||||
if procGetFileInformationByHandleEx.Find() != nil {
|
||||
procGetFileInformationByHandleEx = nil
|
||||
}
|
||||
}
|
||||
|
||||
// IsTerminal return true if the file descriptor is terminal.
|
||||
func IsTerminal(fd uintptr) bool {
|
||||
var st uint32
|
||||
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0)
|
||||
return r != 0 && e == 0
|
||||
}
|
||||
|
||||
// Check pipe name is used for cygwin/msys2 pty.
|
||||
// Cygwin/MSYS2 PTY has a name like:
|
||||
// \{cygwin,msys}-XXXXXXXXXXXXXXXX-ptyN-{from,to}-master
|
||||
func isCygwinPipeName(name string) bool {
|
||||
token := strings.Split(name, "-")
|
||||
if len(token) < 5 {
|
||||
return false
|
||||
}
|
||||
|
||||
if token[0] != `\msys` &&
|
||||
token[0] != `\cygwin` &&
|
||||
token[0] != `\Device\NamedPipe\msys` &&
|
||||
token[0] != `\Device\NamedPipe\cygwin` {
|
||||
return false
|
||||
}
|
||||
|
||||
if token[1] == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(token[2], "pty") {
|
||||
return false
|
||||
}
|
||||
|
||||
if token[3] != `from` && token[3] != `to` {
|
||||
return false
|
||||
}
|
||||
|
||||
if token[4] != "master" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// getFileNameByHandle use the undocomented ntdll NtQueryObject to get file full name from file handler
|
||||
// since GetFileInformationByHandleEx is not available under windows Vista and still some old fashion
|
||||
// guys are using Windows XP, this is a workaround for those guys, it will also work on system from
|
||||
// Windows vista to 10
|
||||
// see https://stackoverflow.com/a/18792477 for details
|
||||
func getFileNameByHandle(fd uintptr) (string, error) {
|
||||
if procNtQueryObject == nil {
|
||||
return "", errors.New("ntdll.dll: NtQueryObject not supported")
|
||||
}
|
||||
|
||||
var buf [4 + syscall.MAX_PATH]uint16
|
||||
var result int
|
||||
r, _, e := syscall.Syscall6(procNtQueryObject.Addr(), 5,
|
||||
fd, objectNameInfo, uintptr(unsafe.Pointer(&buf)), uintptr(2*len(buf)), uintptr(unsafe.Pointer(&result)), 0)
|
||||
if r != 0 {
|
||||
return "", e
|
||||
}
|
||||
return string(utf16.Decode(buf[4 : 4+buf[0]/2])), nil
|
||||
}
|
||||
|
||||
// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2
|
||||
// terminal.
|
||||
func IsCygwinTerminal(fd uintptr) bool {
|
||||
if procGetFileInformationByHandleEx == nil {
|
||||
name, err := getFileNameByHandle(fd)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return isCygwinPipeName(name)
|
||||
}
|
||||
|
||||
// Cygwin/msys's pty is a pipe.
|
||||
ft, _, e := syscall.Syscall(procGetFileType.Addr(), 1, fd, 0, 0)
|
||||
if ft != fileTypePipe || e != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
var buf [2 + syscall.MAX_PATH]uint16
|
||||
r, _, e := syscall.Syscall6(procGetFileInformationByHandleEx.Addr(),
|
||||
4, fd, fileNameInfo, uintptr(unsafe.Pointer(&buf)),
|
||||
uintptr(len(buf)*2), 0, 0)
|
||||
if r == 0 || e != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
l := *(*uint32)(unsafe.Pointer(&buf))
|
||||
return isCygwinPipeName(string(utf16.Decode(buf[2 : 2+l/2])))
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Nuno Cruces
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# `strftime`/`strptime` compatible time formatting and parsing for Go
|
||||
|
||||
[](https://pkg.go.dev/github.com/ncruces/go-strftime)
|
||||
[](https://goreportcard.com/report/github.com/ncruces/go-strftime)
|
||||
[](https://raw.githack.com/wiki/ncruces/go-strftime/coverage.html)
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package strftime
|
||||
|
||||
import "unicode/utf8"
|
||||
|
||||
type parser struct {
|
||||
format func(spec, flag byte) error
|
||||
literal func(byte) error
|
||||
}
|
||||
|
||||
func (p *parser) parse(fmt string) error {
|
||||
const (
|
||||
initial = iota
|
||||
percent
|
||||
flagged
|
||||
modified
|
||||
)
|
||||
|
||||
var flag, modifier byte
|
||||
var err error
|
||||
state := initial
|
||||
start := 0
|
||||
for i, b := range []byte(fmt) {
|
||||
switch state {
|
||||
default:
|
||||
if b == '%' {
|
||||
state = percent
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
err = p.literal(b)
|
||||
|
||||
case percent:
|
||||
if b == '-' || b == ':' {
|
||||
state = flagged
|
||||
flag = b
|
||||
continue
|
||||
}
|
||||
if b == 'E' || b == 'O' {
|
||||
state = modified
|
||||
modifier = b
|
||||
flag = 0
|
||||
continue
|
||||
}
|
||||
err = p.format(b, 0)
|
||||
state = initial
|
||||
|
||||
case flagged:
|
||||
if b == 'E' || b == 'O' {
|
||||
state = modified
|
||||
modifier = b
|
||||
continue
|
||||
}
|
||||
err = p.format(b, flag)
|
||||
state = initial
|
||||
|
||||
case modified:
|
||||
if okModifier(modifier, b) {
|
||||
err = p.format(b, flag)
|
||||
} else {
|
||||
err = p.literals(fmt[start : i+1])
|
||||
}
|
||||
state = initial
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err, ok := err.(formatError); ok {
|
||||
err.setDirective(fmt, start, i)
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if state != initial {
|
||||
return p.literals(fmt[start:])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) literals(literal string) error {
|
||||
for _, b := range []byte(literal) {
|
||||
if err := p.literal(b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type literalErr string
|
||||
|
||||
func (e literalErr) Error() string {
|
||||
return "strftime: unsupported literal: " + string(e)
|
||||
}
|
||||
|
||||
type formatError struct {
|
||||
message string
|
||||
directive string
|
||||
}
|
||||
|
||||
func (e formatError) Error() string {
|
||||
return "strftime: unsupported directive: " + e.directive + " " + e.message
|
||||
}
|
||||
|
||||
func (e *formatError) setDirective(str string, i, j int) {
|
||||
_, n := utf8.DecodeRuneInString(str[j:])
|
||||
e.directive = str[i : j+n]
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
Package strftime provides strftime/strptime compatible time formatting and parsing.
|
||||
|
||||
The following formatting specifiers are available:
|
||||
|
||||
Date (Year, Month, Day):
|
||||
%Y - Year with century (can be negative, 4 digits at least)
|
||||
-0001, 0000, 1995, 2009, 14292, etc.
|
||||
%C - year / 100 (round down, 20 in 2009)
|
||||
%y - year % 100 (00..99)
|
||||
|
||||
%m - Month of the year, zero-padded (01..12)
|
||||
%-m no-padded (1..12)
|
||||
%B - Full month name (January)
|
||||
%b - Abbreviated month name (Jan)
|
||||
%h - Equivalent to %b
|
||||
|
||||
%d - Day of the month, zero-padded (01..31)
|
||||
%-d no-padded (1..31)
|
||||
%e - Day of the month, blank-padded ( 1..31)
|
||||
|
||||
%j - Day of the year (001..366)
|
||||
%-j no-padded (1..366)
|
||||
|
||||
Time (Hour, Minute, Second, Subsecond):
|
||||
%H - Hour of the day, 24-hour clock, zero-padded (00..23)
|
||||
%-H no-padded (0..23)
|
||||
%k - Hour of the day, 24-hour clock, blank-padded ( 0..23)
|
||||
%I - Hour of the day, 12-hour clock, zero-padded (01..12)
|
||||
%-I no-padded (1..12)
|
||||
%l - Hour of the day, 12-hour clock, blank-padded ( 1..12)
|
||||
%P - Meridian indicator, lowercase (am or pm)
|
||||
%p - Meridian indicator, uppercase (AM or PM)
|
||||
|
||||
%M - Minute of the hour (00..59)
|
||||
%-M no-padded (0..59)
|
||||
|
||||
%S - Second of the minute (00..60)
|
||||
%-S no-padded (0..60)
|
||||
|
||||
%L - Millisecond of the second (000..999)
|
||||
%f - Microsecond of the second (000000..999999)
|
||||
%N - Nanosecond of the second (000000000..999999999)
|
||||
|
||||
Time zone:
|
||||
%z - Time zone as hour and minute offset from UTC (e.g. +0900)
|
||||
%:z - hour and minute offset from UTC with a colon (e.g. +09:00)
|
||||
%Z - Time zone abbreviation (e.g. MST)
|
||||
|
||||
Weekday:
|
||||
%A - Full weekday name (Sunday)
|
||||
%a - Abbreviated weekday name (Sun)
|
||||
%u - Day of the week (Monday is 1, 1..7)
|
||||
%w - Day of the week (Sunday is 0, 0..6)
|
||||
|
||||
ISO 8601 week-based year and week number:
|
||||
Week 1 of YYYY starts with a Monday and includes YYYY-01-04.
|
||||
The days in the year before the first week are in the last week of
|
||||
the previous year.
|
||||
%G - Week-based year
|
||||
%g - Last 2 digits of the week-based year (00..99)
|
||||
%V - Week number of the week-based year (01..53)
|
||||
%-V no-padded (1..53)
|
||||
|
||||
Week number:
|
||||
Week 1 of YYYY starts with a Sunday or Monday (according to %U or %W).
|
||||
The days in the year before the first week are in week 0.
|
||||
%U - Week number of the year. The week starts with Sunday. (00..53)
|
||||
%-U no-padded (0..53)
|
||||
%W - Week number of the year. The week starts with Monday. (00..53)
|
||||
%-W no-padded (0..53)
|
||||
|
||||
Seconds since the Unix Epoch:
|
||||
%s - Number of seconds since 1970-01-01 00:00:00 UTC.
|
||||
%Q - Number of milliseconds since 1970-01-01 00:00:00 UTC.
|
||||
|
||||
Literal string:
|
||||
%n - Newline character (\n)
|
||||
%t - Tab character (\t)
|
||||
%% - Literal % character
|
||||
|
||||
Combination:
|
||||
%c - date and time (%a %b %e %T %Y)
|
||||
%D - Date (%m/%d/%y)
|
||||
%F - ISO 8601 date format (%Y-%m-%d)
|
||||
%v - VMS date (%e-%b-%Y)
|
||||
%x - Same as %D
|
||||
%X - Same as %T
|
||||
%r - 12-hour time (%I:%M:%S %p)
|
||||
%R - 24-hour time (%H:%M)
|
||||
%T - 24-hour time (%H:%M:%S)
|
||||
%+ - date(1) (%a %b %e %H:%M:%S %Z %Y)
|
||||
|
||||
The modifiers “E” and “O” are ignored.
|
||||
*/
|
||||
package strftime
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
package strftime
|
||||
|
||||
import "strings"
|
||||
|
||||
// https://strftime.org/
|
||||
func goLayout(spec, flag byte, parsing bool) string {
|
||||
switch spec {
|
||||
default:
|
||||
return ""
|
||||
|
||||
case 'B':
|
||||
return "January"
|
||||
case 'b', 'h':
|
||||
return "Jan"
|
||||
case 'm':
|
||||
if flag == '-' || parsing {
|
||||
return "1"
|
||||
}
|
||||
return "01"
|
||||
case 'A':
|
||||
return "Monday"
|
||||
case 'a':
|
||||
return "Mon"
|
||||
case 'e':
|
||||
return "_2"
|
||||
case 'd':
|
||||
if flag == '-' || parsing {
|
||||
return "2"
|
||||
}
|
||||
return "02"
|
||||
case 'j':
|
||||
if flag == '-' {
|
||||
if parsing {
|
||||
return "__2"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return "002"
|
||||
case 'I':
|
||||
if flag == '-' || parsing {
|
||||
return "3"
|
||||
}
|
||||
return "03"
|
||||
case 'H':
|
||||
if flag == '-' && !parsing {
|
||||
return ""
|
||||
}
|
||||
return "15"
|
||||
case 'M':
|
||||
if flag == '-' || parsing {
|
||||
return "4"
|
||||
}
|
||||
return "04"
|
||||
case 'S':
|
||||
if flag == '-' || parsing {
|
||||
return "5"
|
||||
}
|
||||
return "05"
|
||||
case 'y':
|
||||
return "06"
|
||||
case 'Y':
|
||||
return "2006"
|
||||
case 'p':
|
||||
return "PM"
|
||||
case 'P':
|
||||
return "pm"
|
||||
case 'Z':
|
||||
return "MST"
|
||||
case 'z':
|
||||
if flag == ':' {
|
||||
if parsing {
|
||||
return "Z07:00"
|
||||
}
|
||||
return "-07:00"
|
||||
}
|
||||
if parsing {
|
||||
return "Z0700"
|
||||
}
|
||||
return "-0700"
|
||||
|
||||
case '+':
|
||||
if parsing {
|
||||
return "Mon Jan _2 15:4:5 MST 2006"
|
||||
}
|
||||
return "Mon Jan _2 15:04:05 MST 2006"
|
||||
case 'c':
|
||||
if parsing {
|
||||
return "Mon Jan _2 15:4:5 2006"
|
||||
}
|
||||
return "Mon Jan _2 15:04:05 2006"
|
||||
case 'v':
|
||||
return "_2-Jan-2006"
|
||||
case 'F':
|
||||
if parsing {
|
||||
return "2006-1-2"
|
||||
}
|
||||
return "2006-01-02"
|
||||
case 'D', 'x':
|
||||
if parsing {
|
||||
return "1/2/06"
|
||||
}
|
||||
return "01/02/06"
|
||||
case 'r':
|
||||
if parsing {
|
||||
return "3:4:5 PM"
|
||||
}
|
||||
return "03:04:05 PM"
|
||||
case 'T', 'X':
|
||||
if parsing {
|
||||
return "15:4:5"
|
||||
}
|
||||
return "15:04:05"
|
||||
case 'R':
|
||||
if parsing {
|
||||
return "15:4"
|
||||
}
|
||||
return "15:04"
|
||||
|
||||
case '%':
|
||||
return "%"
|
||||
case 't':
|
||||
return "\t"
|
||||
case 'n':
|
||||
return "\n"
|
||||
}
|
||||
}
|
||||
|
||||
// https://nsdateformatter.com/
|
||||
func uts35Pattern(spec, flag byte) string {
|
||||
switch spec {
|
||||
default:
|
||||
return ""
|
||||
|
||||
case 'B':
|
||||
return "MMMM"
|
||||
case 'b', 'h':
|
||||
return "MMM"
|
||||
case 'm':
|
||||
if flag == '-' {
|
||||
return "M"
|
||||
}
|
||||
return "MM"
|
||||
case 'A':
|
||||
return "EEEE"
|
||||
case 'a':
|
||||
return "E"
|
||||
case 'd':
|
||||
if flag == '-' {
|
||||
return "d"
|
||||
}
|
||||
return "dd"
|
||||
case 'j':
|
||||
if flag == '-' {
|
||||
return "D"
|
||||
}
|
||||
return "DDD"
|
||||
case 'I':
|
||||
if flag == '-' {
|
||||
return "h"
|
||||
}
|
||||
return "hh"
|
||||
case 'H':
|
||||
if flag == '-' {
|
||||
return "H"
|
||||
}
|
||||
return "HH"
|
||||
case 'M':
|
||||
if flag == '-' {
|
||||
return "m"
|
||||
}
|
||||
return "mm"
|
||||
case 'S':
|
||||
if flag == '-' {
|
||||
return "s"
|
||||
}
|
||||
return "ss"
|
||||
case 'y':
|
||||
return "yy"
|
||||
case 'Y':
|
||||
return "yyyy"
|
||||
case 'g':
|
||||
return "YY"
|
||||
case 'G':
|
||||
return "YYYY"
|
||||
case 'V':
|
||||
if flag == '-' {
|
||||
return "w"
|
||||
}
|
||||
return "ww"
|
||||
case 'p':
|
||||
return "a"
|
||||
case 'Z':
|
||||
return "zzz"
|
||||
case 'z':
|
||||
if flag == ':' {
|
||||
return "xxx"
|
||||
}
|
||||
return "xx"
|
||||
case 'L':
|
||||
return "SSS"
|
||||
case 'f':
|
||||
return "SSSSSS"
|
||||
case 'N':
|
||||
return "SSSSSSSSS"
|
||||
|
||||
case '+':
|
||||
return "E MMM d HH:mm:ss zzz yyyy"
|
||||
case 'c':
|
||||
return "E MMM d HH:mm:ss yyyy"
|
||||
case 'v':
|
||||
return "d-MMM-yyyy"
|
||||
case 'F':
|
||||
return "yyyy-MM-dd"
|
||||
case 'D', 'x':
|
||||
return "MM/dd/yy"
|
||||
case 'r':
|
||||
return "hh:mm:ss a"
|
||||
case 'T', 'X':
|
||||
return "HH:mm:ss"
|
||||
case 'R':
|
||||
return "HH:mm"
|
||||
|
||||
case '%':
|
||||
return "%"
|
||||
case 't':
|
||||
return "\t"
|
||||
case 'n':
|
||||
return "\n"
|
||||
}
|
||||
}
|
||||
|
||||
// http://man.he.net/man3/strftime
|
||||
func okModifier(mod, spec byte) bool {
|
||||
if mod == 'E' {
|
||||
return strings.Contains("cCxXyY", string(spec))
|
||||
}
|
||||
if mod == 'O' {
|
||||
return strings.Contains("deHImMSuUVwWy", string(spec))
|
||||
}
|
||||
return false
|
||||
}
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
package strftime
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Format returns a textual representation of the time value
|
||||
// formatted according to the strftime format specification.
|
||||
func Format(fmt string, t time.Time) string {
|
||||
buf := buffer(fmt)
|
||||
return string(AppendFormat(buf, fmt, t))
|
||||
}
|
||||
|
||||
// AppendFormat is like Format, but appends the textual representation
|
||||
// to dst and returns the extended buffer.
|
||||
func AppendFormat(dst []byte, fmt string, t time.Time) []byte {
|
||||
var parser parser
|
||||
|
||||
parser.literal = func(b byte) error {
|
||||
dst = append(dst, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
parser.format = func(spec, flag byte) error {
|
||||
switch spec {
|
||||
case 'A':
|
||||
dst = append(dst, t.Weekday().String()...)
|
||||
return nil
|
||||
case 'a':
|
||||
dst = append(dst, t.Weekday().String()[:3]...)
|
||||
return nil
|
||||
case 'B':
|
||||
dst = append(dst, t.Month().String()...)
|
||||
return nil
|
||||
case 'b', 'h':
|
||||
dst = append(dst, t.Month().String()[:3]...)
|
||||
return nil
|
||||
case 'm':
|
||||
dst = appendInt2(dst, int(t.Month()), flag)
|
||||
return nil
|
||||
case 'd':
|
||||
dst = appendInt2(dst, int(t.Day()), flag)
|
||||
return nil
|
||||
case 'e':
|
||||
dst = appendInt2(dst, int(t.Day()), ' ')
|
||||
return nil
|
||||
case 'I':
|
||||
dst = append12Hour(dst, t, flag)
|
||||
return nil
|
||||
case 'l':
|
||||
dst = append12Hour(dst, t, ' ')
|
||||
return nil
|
||||
case 'H':
|
||||
dst = appendInt2(dst, t.Hour(), flag)
|
||||
return nil
|
||||
case 'k':
|
||||
dst = appendInt2(dst, t.Hour(), ' ')
|
||||
return nil
|
||||
case 'M':
|
||||
dst = appendInt2(dst, t.Minute(), flag)
|
||||
return nil
|
||||
case 'S':
|
||||
dst = appendInt2(dst, t.Second(), flag)
|
||||
return nil
|
||||
case 'L':
|
||||
dst = append(dst, t.Format(".000")[1:]...)
|
||||
return nil
|
||||
case 'f':
|
||||
dst = append(dst, t.Format(".000000")[1:]...)
|
||||
return nil
|
||||
case 'N':
|
||||
dst = append(dst, t.Format(".000000000")[1:]...)
|
||||
return nil
|
||||
case 'y':
|
||||
dst = t.AppendFormat(dst, "06")
|
||||
return nil
|
||||
case 'Y':
|
||||
dst = t.AppendFormat(dst, "2006")
|
||||
return nil
|
||||
case 'C':
|
||||
dst = t.AppendFormat(dst, "2006")
|
||||
dst = dst[:len(dst)-2]
|
||||
return nil
|
||||
case 'U':
|
||||
dst = appendWeekNumber(dst, t, flag, true)
|
||||
return nil
|
||||
case 'W':
|
||||
dst = appendWeekNumber(dst, t, flag, false)
|
||||
return nil
|
||||
case 'V':
|
||||
_, w := t.ISOWeek()
|
||||
dst = appendInt2(dst, w, flag)
|
||||
return nil
|
||||
case 'g':
|
||||
y, _ := t.ISOWeek()
|
||||
dst = year(y).AppendFormat(dst, "06")
|
||||
return nil
|
||||
case 'G':
|
||||
y, _ := t.ISOWeek()
|
||||
dst = year(y).AppendFormat(dst, "2006")
|
||||
return nil
|
||||
case 's':
|
||||
dst = strconv.AppendInt(dst, t.Unix(), 10)
|
||||
return nil
|
||||
case 'Q':
|
||||
dst = strconv.AppendInt(dst, t.UnixMilli(), 10)
|
||||
return nil
|
||||
case 'w':
|
||||
w := t.Weekday()
|
||||
dst = appendInt1(dst, int(w))
|
||||
return nil
|
||||
case 'u':
|
||||
if w := t.Weekday(); w == 0 {
|
||||
dst = append(dst, '7')
|
||||
} else {
|
||||
dst = appendInt1(dst, int(w))
|
||||
}
|
||||
return nil
|
||||
case 'j':
|
||||
if flag == '-' {
|
||||
dst = strconv.AppendInt(dst, int64(t.YearDay()), 10)
|
||||
} else {
|
||||
dst = t.AppendFormat(dst, "002")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if layout := goLayout(spec, flag, false); layout != "" {
|
||||
dst = t.AppendFormat(dst, layout)
|
||||
return nil
|
||||
}
|
||||
|
||||
dst = append(dst, '%')
|
||||
if flag != 0 {
|
||||
dst = append(dst, flag)
|
||||
}
|
||||
dst = append(dst, spec)
|
||||
return nil
|
||||
}
|
||||
|
||||
parser.parse(fmt)
|
||||
return dst
|
||||
}
|
||||
|
||||
// Parse converts a textual representation of time to the time value it represents
|
||||
// according to the strptime format specification.
|
||||
//
|
||||
// The following specifiers are not supported for parsing:
|
||||
//
|
||||
// %g %k %l %s %u %w %C %G %Q %U %V %W
|
||||
//
|
||||
// You must also avoid digits and these letter sequences
|
||||
// in fmt literals:
|
||||
//
|
||||
// Jan Mon MST PM pm
|
||||
func Parse(fmt, value string) (time.Time, error) {
|
||||
pattern, err := layout(fmt, true)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return time.Parse(pattern, value)
|
||||
}
|
||||
|
||||
// Layout converts a strftime format specification
|
||||
// to a Go time pattern specification.
|
||||
//
|
||||
// The following specifiers are not supported by Go patterns:
|
||||
//
|
||||
// %f %g %k %l %s %u %w %C %G %L %N %Q %U %V %W
|
||||
//
|
||||
// You must also avoid digits and these letter sequences
|
||||
// in fmt literals:
|
||||
//
|
||||
// Jan Mon MST PM pm
|
||||
func Layout(fmt string) (string, error) {
|
||||
return layout(fmt, false)
|
||||
}
|
||||
|
||||
func layout(fmt string, parsing bool) (string, error) {
|
||||
dst := buffer(fmt)
|
||||
var parser parser
|
||||
|
||||
parser.literal = func(b byte) error {
|
||||
if '0' <= b && b <= '9' {
|
||||
return literalErr(b)
|
||||
}
|
||||
dst = append(dst, b)
|
||||
if b == 'M' || b == 'T' || b == 'm' || b == 'n' {
|
||||
switch {
|
||||
case bytes.HasSuffix(dst, []byte("Jan")):
|
||||
return literalErr("Jan")
|
||||
case bytes.HasSuffix(dst, []byte("Mon")):
|
||||
return literalErr("Mon")
|
||||
case bytes.HasSuffix(dst, []byte("MST")):
|
||||
return literalErr("MST")
|
||||
case bytes.HasSuffix(dst, []byte("PM")):
|
||||
return literalErr("PM")
|
||||
case bytes.HasSuffix(dst, []byte("pm")):
|
||||
return literalErr("pm")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
parser.format = func(spec, flag byte) error {
|
||||
if layout := goLayout(spec, flag, parsing); layout != "" {
|
||||
dst = append(dst, layout...)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch spec {
|
||||
default:
|
||||
return formatError{}
|
||||
|
||||
case 'L', 'f', 'N':
|
||||
if bytes.HasSuffix(dst, []byte(".")) || bytes.HasSuffix(dst, []byte(",")) {
|
||||
switch spec {
|
||||
default:
|
||||
dst = append(dst, "000"...)
|
||||
case 'f':
|
||||
dst = append(dst, "000000"...)
|
||||
case 'N':
|
||||
dst = append(dst, "000000000"...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return formatError{message: "must follow '.' or ','"}
|
||||
}
|
||||
}
|
||||
|
||||
if err := parser.parse(fmt); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(dst), nil
|
||||
}
|
||||
|
||||
// UTS35 converts a strftime format specification
|
||||
// to a Unicode Technical Standard #35 Date Format Pattern.
|
||||
//
|
||||
// The following specifiers are not supported by UTS35:
|
||||
//
|
||||
// %e %k %l %u %w %C %P %U %W
|
||||
func UTS35(fmt string) (string, error) {
|
||||
const quote = '\''
|
||||
var quoted bool
|
||||
dst := buffer(fmt)
|
||||
|
||||
var parser parser
|
||||
|
||||
parser.literal = func(b byte) error {
|
||||
if b == quote {
|
||||
dst = append(dst, quote, quote)
|
||||
return nil
|
||||
}
|
||||
if !quoted && ('a' <= b && b <= 'z' || 'A' <= b && b <= 'Z') {
|
||||
dst = append(dst, quote)
|
||||
quoted = true
|
||||
}
|
||||
dst = append(dst, b)
|
||||
return nil
|
||||
}
|
||||
|
||||
parser.format = func(spec, flag byte) error {
|
||||
if quoted {
|
||||
dst = append(dst, quote)
|
||||
quoted = false
|
||||
}
|
||||
if pattern := uts35Pattern(spec, flag); pattern != "" {
|
||||
dst = append(dst, pattern...)
|
||||
return nil
|
||||
}
|
||||
return formatError{}
|
||||
}
|
||||
|
||||
if err := parser.parse(fmt); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if quoted {
|
||||
dst = append(dst, quote)
|
||||
}
|
||||
return string(dst), nil
|
||||
}
|
||||
|
||||
func buffer(format string) (buf []byte) {
|
||||
const bufSize = 64
|
||||
max := len(format) + 10
|
||||
if max < bufSize {
|
||||
var b [bufSize]byte
|
||||
buf = b[:0]
|
||||
} else {
|
||||
buf = make([]byte, 0, max)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func year(y int) time.Time {
|
||||
return time.Date(y, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
func appendWeekNumber(dst []byte, t time.Time, flag byte, sunday bool) []byte {
|
||||
offset := int(t.Weekday())
|
||||
if sunday {
|
||||
offset = 6 - offset
|
||||
} else if offset != 0 {
|
||||
offset = 7 - offset
|
||||
}
|
||||
return appendInt2(dst, (t.YearDay()+offset)/7, flag)
|
||||
}
|
||||
|
||||
func append12Hour(dst []byte, t time.Time, flag byte) []byte {
|
||||
h := t.Hour()
|
||||
if h == 0 {
|
||||
h = 12
|
||||
} else if h > 12 {
|
||||
h -= 12
|
||||
}
|
||||
return appendInt2(dst, h, flag)
|
||||
}
|
||||
|
||||
func appendInt1(dst []byte, i int) []byte {
|
||||
return append(dst, byte('0'+i))
|
||||
}
|
||||
|
||||
func appendInt2(dst []byte, i int, flag byte) []byte {
|
||||
if flag == 0 || i >= 10 {
|
||||
return append(dst, smallsString[i*2:i*2+2]...)
|
||||
}
|
||||
if flag == ' ' {
|
||||
dst = append(dst, flag)
|
||||
}
|
||||
return appendInt1(dst, i)
|
||||
}
|
||||
|
||||
const smallsString = "" +
|
||||
"00010203040506070809" +
|
||||
"10111213141516171819" +
|
||||
"20212223242526272829" +
|
||||
"30313233343536373839" +
|
||||
"40414243444546474849" +
|
||||
"50515253545556575859" +
|
||||
"60616263646566676869" +
|
||||
"70717273747576777879" +
|
||||
"80818283848586878889" +
|
||||
"90919293949596979899"
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
This library is a toy proof-of-concept implementation of the
|
||||
well-known Schonhage-Strassen method for multiplying integers.
|
||||
It is not expected to have a real life usecase outside number
|
||||
theory computations, nor is it expected to be used in any production
|
||||
system.
|
||||
|
||||
If you are using it in your project, you may want to carefully
|
||||
examine the actual requirement or problem you are trying to solve.
|
||||
|
||||
# Comparison with the standard library and GMP
|
||||
|
||||
Benchmarking math/big vs. bigfft
|
||||
|
||||
Number size old ns/op new ns/op delta
|
||||
1kb 1599 1640 +2.56%
|
||||
10kb 61533 62170 +1.04%
|
||||
50kb 833693 831051 -0.32%
|
||||
100kb 2567995 2693864 +4.90%
|
||||
1Mb 105237800 28446400 -72.97%
|
||||
5Mb 1272947000 168554600 -86.76%
|
||||
10Mb 3834354000 405120200 -89.43%
|
||||
20Mb 11514488000 845081600 -92.66%
|
||||
50Mb 49199945000 2893950000 -94.12%
|
||||
100Mb 147599836000 5921594000 -95.99%
|
||||
|
||||
Benchmarking GMP vs bigfft
|
||||
|
||||
Number size GMP ns/op Go ns/op delta
|
||||
1kb 536 1500 +179.85%
|
||||
10kb 26669 50777 +90.40%
|
||||
50kb 252270 658534 +161.04%
|
||||
100kb 686813 2127534 +209.77%
|
||||
1Mb 12100000 22391830 +85.06%
|
||||
5Mb 111731843 133550600 +19.53%
|
||||
10Mb 212314000 318595800 +50.06%
|
||||
20Mb 490196000 671512800 +36.99%
|
||||
50Mb 1280000000 2451476000 +91.52%
|
||||
100Mb 2673000000 5228991000 +95.62%
|
||||
|
||||
Benchmarks were run on a Core 2 Quad Q8200 (2.33GHz).
|
||||
FFT is enabled when input numbers are over 200kbits.
|
||||
|
||||
Scanning large decimal number from strings.
|
||||
(math/big [n^2 complexity] vs bigfft [n^1.6 complexity], Core i5-4590)
|
||||
|
||||
Digits old ns/op new ns/op delta
|
||||
1e3 9995 10876 +8.81%
|
||||
1e4 175356 243806 +39.03%
|
||||
1e5 9427422 6780545 -28.08%
|
||||
1e6 1776707489 144867502 -91.85%
|
||||
2e6 6865499995 346540778 -94.95%
|
||||
5e6 42641034189 1069878799 -97.49%
|
||||
10e6 151975273589 2693328580 -98.23%
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// Copyright 2010 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 bigfft
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
type Word = big.Word
|
||||
|
||||
//go:linkname addVV math/big.addVV
|
||||
func addVV(z, x, y []Word) (c Word)
|
||||
|
||||
//go:linkname subVV math/big.subVV
|
||||
func subVV(z, x, y []Word) (c Word)
|
||||
|
||||
//go:linkname addVW math/big.addVW
|
||||
func addVW(z, x []Word, y Word) (c Word)
|
||||
|
||||
//go:linkname subVW math/big.subVW
|
||||
func subVW(z, x []Word, y Word) (c Word)
|
||||
|
||||
//go:linkname shlVU math/big.shlVU
|
||||
func shlVU(z, x []Word, s uint) (c Word)
|
||||
|
||||
//go:linkname mulAddVWW math/big.mulAddVWW
|
||||
func mulAddVWW(z, x []Word, y, r Word) (c Word)
|
||||
|
||||
//go:linkname addMulVVW math/big.addMulVVW
|
||||
func addMulVVW(z, x []Word, y Word) (c Word)
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
package bigfft
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// Arithmetic modulo 2^n+1.
|
||||
|
||||
// A fermat of length w+1 represents a number modulo 2^(w*_W) + 1. The last
|
||||
// word is zero or one. A number has at most two representatives satisfying the
|
||||
// 0-1 last word constraint.
|
||||
type fermat nat
|
||||
|
||||
func (n fermat) String() string { return nat(n).String() }
|
||||
|
||||
func (z fermat) norm() {
|
||||
n := len(z) - 1
|
||||
c := z[n]
|
||||
if c == 0 {
|
||||
return
|
||||
}
|
||||
if z[0] >= c {
|
||||
z[n] = 0
|
||||
z[0] -= c
|
||||
return
|
||||
}
|
||||
// z[0] < z[n].
|
||||
subVW(z, z, c) // Substract c
|
||||
if c > 1 {
|
||||
z[n] -= c - 1
|
||||
c = 1
|
||||
}
|
||||
// Add back c.
|
||||
if z[n] == 1 {
|
||||
z[n] = 0
|
||||
return
|
||||
} else {
|
||||
addVW(z, z, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Shift computes (x << k) mod (2^n+1).
|
||||
func (z fermat) Shift(x fermat, k int) {
|
||||
if len(z) != len(x) {
|
||||
panic("len(z) != len(x) in Shift")
|
||||
}
|
||||
n := len(x) - 1
|
||||
// Shift by n*_W is taking the opposite.
|
||||
k %= 2 * n * _W
|
||||
if k < 0 {
|
||||
k += 2 * n * _W
|
||||
}
|
||||
neg := false
|
||||
if k >= n*_W {
|
||||
k -= n * _W
|
||||
neg = true
|
||||
}
|
||||
|
||||
kw, kb := k/_W, k%_W
|
||||
|
||||
z[n] = 1 // Add (-1)
|
||||
if !neg {
|
||||
for i := 0; i < kw; i++ {
|
||||
z[i] = 0
|
||||
}
|
||||
// Shift left by kw words.
|
||||
// x = a·2^(n-k) + b
|
||||
// x<<k = (b<<k) - a
|
||||
copy(z[kw:], x[:n-kw])
|
||||
b := subVV(z[:kw+1], z[:kw+1], x[n-kw:])
|
||||
if z[kw+1] > 0 {
|
||||
z[kw+1] -= b
|
||||
} else {
|
||||
subVW(z[kw+1:], z[kw+1:], b)
|
||||
}
|
||||
} else {
|
||||
for i := kw + 1; i < n; i++ {
|
||||
z[i] = 0
|
||||
}
|
||||
// Shift left and negate, by kw words.
|
||||
copy(z[:kw+1], x[n-kw:n+1]) // z_low = x_high
|
||||
b := subVV(z[kw:n], z[kw:n], x[:n-kw]) // z_high -= x_low
|
||||
z[n] -= b
|
||||
}
|
||||
// Add back 1.
|
||||
if z[n] > 0 {
|
||||
z[n]--
|
||||
} else if z[0] < ^big.Word(0) {
|
||||
z[0]++
|
||||
} else {
|
||||
addVW(z, z, 1)
|
||||
}
|
||||
// Shift left by kb bits
|
||||
shlVU(z, z, uint(kb))
|
||||
z.norm()
|
||||
}
|
||||
|
||||
// ShiftHalf shifts x by k/2 bits the left. Shifting by 1/2 bit
|
||||
// is multiplication by sqrt(2) mod 2^n+1 which is 2^(3n/4) - 2^(n/4).
|
||||
// A temporary buffer must be provided in tmp.
|
||||
func (z fermat) ShiftHalf(x fermat, k int, tmp fermat) {
|
||||
n := len(z) - 1
|
||||
if k%2 == 0 {
|
||||
z.Shift(x, k/2)
|
||||
return
|
||||
}
|
||||
u := (k - 1) / 2
|
||||
a := u + (3*_W/4)*n
|
||||
b := u + (_W/4)*n
|
||||
z.Shift(x, a)
|
||||
tmp.Shift(x, b)
|
||||
z.Sub(z, tmp)
|
||||
}
|
||||
|
||||
// Add computes addition mod 2^n+1.
|
||||
func (z fermat) Add(x, y fermat) fermat {
|
||||
if len(z) != len(x) {
|
||||
panic("Add: len(z) != len(x)")
|
||||
}
|
||||
addVV(z, x, y) // there cannot be a carry here.
|
||||
z.norm()
|
||||
return z
|
||||
}
|
||||
|
||||
// Sub computes substraction mod 2^n+1.
|
||||
func (z fermat) Sub(x, y fermat) fermat {
|
||||
if len(z) != len(x) {
|
||||
panic("Add: len(z) != len(x)")
|
||||
}
|
||||
n := len(y) - 1
|
||||
b := subVV(z[:n], x[:n], y[:n])
|
||||
b += y[n]
|
||||
// If b > 0, we need to subtract b<<n, which is the same as adding b.
|
||||
z[n] = x[n]
|
||||
if z[0] <= ^big.Word(0)-b {
|
||||
z[0] += b
|
||||
} else {
|
||||
addVW(z, z, b)
|
||||
}
|
||||
z.norm()
|
||||
return z
|
||||
}
|
||||
|
||||
func (z fermat) Mul(x, y fermat) fermat {
|
||||
if len(x) != len(y) {
|
||||
panic("Mul: len(x) != len(y)")
|
||||
}
|
||||
n := len(x) - 1
|
||||
if n < 30 {
|
||||
z = z[:2*n+2]
|
||||
basicMul(z, x, y)
|
||||
z = z[:2*n+1]
|
||||
} else {
|
||||
var xi, yi, zi big.Int
|
||||
xi.SetBits(x)
|
||||
yi.SetBits(y)
|
||||
zi.SetBits(z)
|
||||
zb := zi.Mul(&xi, &yi).Bits()
|
||||
if len(zb) <= n {
|
||||
// Short product.
|
||||
copy(z, zb)
|
||||
for i := len(zb); i < len(z); i++ {
|
||||
z[i] = 0
|
||||
}
|
||||
return z
|
||||
}
|
||||
z = zb
|
||||
}
|
||||
// len(z) is at most 2n+1.
|
||||
if len(z) > 2*n+1 {
|
||||
panic("len(z) > 2n+1")
|
||||
}
|
||||
// We now have
|
||||
// z = z[:n] + 1<<(n*W) * z[n:2n+1]
|
||||
// which normalizes to:
|
||||
// z = z[:n] - z[n:2n] + z[2n]
|
||||
c1 := big.Word(0)
|
||||
if len(z) > 2*n {
|
||||
c1 = addVW(z[:n], z[:n], z[2*n])
|
||||
}
|
||||
c2 := big.Word(0)
|
||||
if len(z) >= 2*n {
|
||||
c2 = subVV(z[:n], z[:n], z[n:2*n])
|
||||
} else {
|
||||
m := len(z) - n
|
||||
c2 = subVV(z[:m], z[:m], z[n:])
|
||||
c2 = subVW(z[m:n], z[m:n], c2)
|
||||
}
|
||||
// Restore carries.
|
||||
// Substracting z[n] -= c2 is the same
|
||||
// as z[0] += c2
|
||||
z = z[:n+1]
|
||||
z[n] = c1
|
||||
c := addVW(z, z, c2)
|
||||
if c != 0 {
|
||||
panic("impossible")
|
||||
}
|
||||
z.norm()
|
||||
return z
|
||||
}
|
||||
|
||||
// copied from math/big
|
||||
//
|
||||
// basicMul multiplies x and y and leaves the result in z.
|
||||
// The (non-normalized) result is placed in z[0 : len(x) + len(y)].
|
||||
func basicMul(z, x, y fermat) {
|
||||
// initialize z
|
||||
for i := 0; i < len(z); i++ {
|
||||
z[i] = 0
|
||||
}
|
||||
for i, d := range y {
|
||||
if d != 0 {
|
||||
z[len(x)+i] = addMulVVW(z[i:i+len(x)], x, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
// Package bigfft implements multiplication of big.Int using FFT.
|
||||
//
|
||||
// The implementation is based on the Schönhage-Strassen method
|
||||
// using integer FFT modulo 2^n+1.
|
||||
package bigfft
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const _W = int(unsafe.Sizeof(big.Word(0)) * 8)
|
||||
|
||||
type nat []big.Word
|
||||
|
||||
func (n nat) String() string {
|
||||
v := new(big.Int)
|
||||
v.SetBits(n)
|
||||
return v.String()
|
||||
}
|
||||
|
||||
// fftThreshold is the size (in words) above which FFT is used over
|
||||
// Karatsuba from math/big.
|
||||
//
|
||||
// TestCalibrate seems to indicate a threshold of 60kbits on 32-bit
|
||||
// arches and 110kbits on 64-bit arches.
|
||||
var fftThreshold = 1800
|
||||
|
||||
// Mul computes the product x*y and returns z.
|
||||
// It can be used instead of the Mul method of
|
||||
// *big.Int from math/big package.
|
||||
func Mul(x, y *big.Int) *big.Int {
|
||||
xwords := len(x.Bits())
|
||||
ywords := len(y.Bits())
|
||||
if xwords > fftThreshold && ywords > fftThreshold {
|
||||
return mulFFT(x, y)
|
||||
}
|
||||
return new(big.Int).Mul(x, y)
|
||||
}
|
||||
|
||||
func mulFFT(x, y *big.Int) *big.Int {
|
||||
var xb, yb nat = x.Bits(), y.Bits()
|
||||
zb := fftmul(xb, yb)
|
||||
z := new(big.Int)
|
||||
z.SetBits(zb)
|
||||
if x.Sign()*y.Sign() < 0 {
|
||||
z.Neg(z)
|
||||
}
|
||||
return z
|
||||
}
|
||||
|
||||
// A FFT size of K=1<<k is adequate when K is about 2*sqrt(N) where
|
||||
// N = x.Bitlen() + y.Bitlen().
|
||||
|
||||
func fftmul(x, y nat) nat {
|
||||
k, m := fftSize(x, y)
|
||||
xp := polyFromNat(x, k, m)
|
||||
yp := polyFromNat(y, k, m)
|
||||
rp := xp.Mul(&yp)
|
||||
return rp.Int()
|
||||
}
|
||||
|
||||
// fftSizeThreshold[i] is the maximal size (in bits) where we should use
|
||||
// fft size i.
|
||||
var fftSizeThreshold = [...]int64{0, 0, 0,
|
||||
4 << 10, 8 << 10, 16 << 10, // 5
|
||||
32 << 10, 64 << 10, 1 << 18, 1 << 20, 3 << 20, // 10
|
||||
8 << 20, 30 << 20, 100 << 20, 300 << 20, 600 << 20,
|
||||
}
|
||||
|
||||
// returns the FFT length k, m the number of words per chunk
|
||||
// such that m << k is larger than the number of words
|
||||
// in x*y.
|
||||
func fftSize(x, y nat) (k uint, m int) {
|
||||
words := len(x) + len(y)
|
||||
bits := int64(words) * int64(_W)
|
||||
k = uint(len(fftSizeThreshold))
|
||||
for i := range fftSizeThreshold {
|
||||
if fftSizeThreshold[i] > bits {
|
||||
k = uint(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
// The 1<<k chunks of m words must have N bits so that
|
||||
// 2^N-1 is larger than x*y. That is, m<<k > words
|
||||
m = words>>k + 1
|
||||
return
|
||||
}
|
||||
|
||||
// valueSize returns the length (in words) to use for polynomial
|
||||
// coefficients, to compute a correct product of polynomials P*Q
|
||||
// where deg(P*Q) < K (== 1<<k) and where coefficients of P and Q are
|
||||
// less than b^m (== 1 << (m*_W)).
|
||||
// The chosen length (in bits) must be a multiple of 1 << (k-extra).
|
||||
func valueSize(k uint, m int, extra uint) int {
|
||||
// The coefficients of P*Q are less than b^(2m)*K
|
||||
// so we need W * valueSize >= 2*m*W+K
|
||||
n := 2*m*_W + int(k) // necessary bits
|
||||
K := 1 << (k - extra)
|
||||
if K < _W {
|
||||
K = _W
|
||||
}
|
||||
n = ((n / K) + 1) * K // round to a multiple of K
|
||||
return n / _W
|
||||
}
|
||||
|
||||
// poly represents an integer via a polynomial in Z[x]/(x^K+1)
|
||||
// where K is the FFT length and b^m is the computation basis 1<<(m*_W).
|
||||
// If P = a[0] + a[1] x + ... a[n] x^(K-1), the associated natural number
|
||||
// is P(b^m).
|
||||
type poly struct {
|
||||
k uint // k is such that K = 1<<k.
|
||||
m int // the m such that P(b^m) is the original number.
|
||||
a []nat // a slice of at most K m-word coefficients.
|
||||
}
|
||||
|
||||
// polyFromNat slices the number x into a polynomial
|
||||
// with 1<<k coefficients made of m words.
|
||||
func polyFromNat(x nat, k uint, m int) poly {
|
||||
p := poly{k: k, m: m}
|
||||
length := len(x)/m + 1
|
||||
p.a = make([]nat, length)
|
||||
for i := range p.a {
|
||||
if len(x) < m {
|
||||
p.a[i] = make(nat, m)
|
||||
copy(p.a[i], x)
|
||||
break
|
||||
}
|
||||
p.a[i] = x[:m]
|
||||
x = x[m:]
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// Int evaluates back a poly to its integer value.
|
||||
func (p *poly) Int() nat {
|
||||
length := len(p.a)*p.m + 1
|
||||
if na := len(p.a); na > 0 {
|
||||
length += len(p.a[na-1])
|
||||
}
|
||||
n := make(nat, length)
|
||||
m := p.m
|
||||
np := n
|
||||
for i := range p.a {
|
||||
l := len(p.a[i])
|
||||
c := addVV(np[:l], np[:l], p.a[i])
|
||||
if np[l] < ^big.Word(0) {
|
||||
np[l] += c
|
||||
} else {
|
||||
addVW(np[l:], np[l:], c)
|
||||
}
|
||||
np = np[m:]
|
||||
}
|
||||
n = trim(n)
|
||||
return n
|
||||
}
|
||||
|
||||
func trim(n nat) nat {
|
||||
for i := range n {
|
||||
if n[len(n)-1-i] != 0 {
|
||||
return n[:len(n)-i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mul multiplies p and q modulo X^K-1, where K = 1<<p.k.
|
||||
// The product is done via a Fourier transform.
|
||||
func (p *poly) Mul(q *poly) poly {
|
||||
// extra=2 because:
|
||||
// * some power of 2 is a K-th root of unity when n is a multiple of K/2.
|
||||
// * 2 itself is a square (see fermat.ShiftHalf)
|
||||
n := valueSize(p.k, p.m, 2)
|
||||
|
||||
pv, qv := p.Transform(n), q.Transform(n)
|
||||
rv := pv.Mul(&qv)
|
||||
r := rv.InvTransform()
|
||||
r.m = p.m
|
||||
return r
|
||||
}
|
||||
|
||||
// A polValues represents the value of a poly at the powers of a
|
||||
// K-th root of unity θ=2^(l/2) in Z/(b^n+1)Z, where b^n = 2^(K/4*l).
|
||||
type polValues struct {
|
||||
k uint // k is such that K = 1<<k.
|
||||
n int // the length of coefficients, n*_W a multiple of K/4.
|
||||
values []fermat // a slice of K (n+1)-word values
|
||||
}
|
||||
|
||||
// Transform evaluates p at θ^i for i = 0...K-1, where
|
||||
// θ is a K-th primitive root of unity in Z/(b^n+1)Z.
|
||||
func (p *poly) Transform(n int) polValues {
|
||||
k := p.k
|
||||
inputbits := make([]big.Word, (n+1)<<k)
|
||||
input := make([]fermat, 1<<k)
|
||||
// Now computed q(ω^i) for i = 0 ... K-1
|
||||
valbits := make([]big.Word, (n+1)<<k)
|
||||
values := make([]fermat, 1<<k)
|
||||
for i := range values {
|
||||
input[i] = inputbits[i*(n+1) : (i+1)*(n+1)]
|
||||
if i < len(p.a) {
|
||||
copy(input[i], p.a[i])
|
||||
}
|
||||
values[i] = fermat(valbits[i*(n+1) : (i+1)*(n+1)])
|
||||
}
|
||||
fourier(values, input, false, n, k)
|
||||
return polValues{k, n, values}
|
||||
}
|
||||
|
||||
// InvTransform reconstructs p (modulo X^K - 1) from its
|
||||
// values at θ^i for i = 0..K-1.
|
||||
func (v *polValues) InvTransform() poly {
|
||||
k, n := v.k, v.n
|
||||
|
||||
// Perform an inverse Fourier transform to recover p.
|
||||
pbits := make([]big.Word, (n+1)<<k)
|
||||
p := make([]fermat, 1<<k)
|
||||
for i := range p {
|
||||
p[i] = fermat(pbits[i*(n+1) : (i+1)*(n+1)])
|
||||
}
|
||||
fourier(p, v.values, true, n, k)
|
||||
// Divide by K, and untwist q to recover p.
|
||||
u := make(fermat, n+1)
|
||||
a := make([]nat, 1<<k)
|
||||
for i := range p {
|
||||
u.Shift(p[i], -int(k))
|
||||
copy(p[i], u)
|
||||
a[i] = nat(p[i])
|
||||
}
|
||||
return poly{k: k, m: 0, a: a}
|
||||
}
|
||||
|
||||
// NTransform evaluates p at θω^i for i = 0...K-1, where
|
||||
// θ is a (2K)-th primitive root of unity in Z/(b^n+1)Z
|
||||
// and ω = θ².
|
||||
func (p *poly) NTransform(n int) polValues {
|
||||
k := p.k
|
||||
if len(p.a) >= 1<<k {
|
||||
panic("Transform: len(p.a) >= 1<<k")
|
||||
}
|
||||
// θ is represented as a shift.
|
||||
θshift := (n * _W) >> k
|
||||
// p(x) = a_0 + a_1 x + ... + a_{K-1} x^(K-1)
|
||||
// p(θx) = q(x) where
|
||||
// q(x) = a_0 + θa_1 x + ... + θ^(K-1) a_{K-1} x^(K-1)
|
||||
//
|
||||
// Twist p by θ to obtain q.
|
||||
tbits := make([]big.Word, (n+1)<<k)
|
||||
twisted := make([]fermat, 1<<k)
|
||||
src := make(fermat, n+1)
|
||||
for i := range twisted {
|
||||
twisted[i] = fermat(tbits[i*(n+1) : (i+1)*(n+1)])
|
||||
if i < len(p.a) {
|
||||
for i := range src {
|
||||
src[i] = 0
|
||||
}
|
||||
copy(src, p.a[i])
|
||||
twisted[i].Shift(src, θshift*i)
|
||||
}
|
||||
}
|
||||
|
||||
// Now computed q(ω^i) for i = 0 ... K-1
|
||||
valbits := make([]big.Word, (n+1)<<k)
|
||||
values := make([]fermat, 1<<k)
|
||||
for i := range values {
|
||||
values[i] = fermat(valbits[i*(n+1) : (i+1)*(n+1)])
|
||||
}
|
||||
fourier(values, twisted, false, n, k)
|
||||
return polValues{k, n, values}
|
||||
}
|
||||
|
||||
// InvTransform reconstructs a polynomial from its values at
|
||||
// roots of x^K+1. The m field of the returned polynomial
|
||||
// is unspecified.
|
||||
func (v *polValues) InvNTransform() poly {
|
||||
k := v.k
|
||||
n := v.n
|
||||
θshift := (n * _W) >> k
|
||||
|
||||
// Perform an inverse Fourier transform to recover q.
|
||||
qbits := make([]big.Word, (n+1)<<k)
|
||||
q := make([]fermat, 1<<k)
|
||||
for i := range q {
|
||||
q[i] = fermat(qbits[i*(n+1) : (i+1)*(n+1)])
|
||||
}
|
||||
fourier(q, v.values, true, n, k)
|
||||
|
||||
// Divide by K, and untwist q to recover p.
|
||||
u := make(fermat, n+1)
|
||||
a := make([]nat, 1<<k)
|
||||
for i := range q {
|
||||
u.Shift(q[i], -int(k)-i*θshift)
|
||||
copy(q[i], u)
|
||||
a[i] = nat(q[i])
|
||||
}
|
||||
return poly{k: k, m: 0, a: a}
|
||||
}
|
||||
|
||||
// fourier performs an unnormalized Fourier transform
|
||||
// of src, a length 1<<k vector of numbers modulo b^n+1
|
||||
// where b = 1<<_W.
|
||||
func fourier(dst []fermat, src []fermat, backward bool, n int, k uint) {
|
||||
var rec func(dst, src []fermat, size uint)
|
||||
tmp := make(fermat, n+1) // pre-allocate temporary variables.
|
||||
tmp2 := make(fermat, n+1) // pre-allocate temporary variables.
|
||||
|
||||
// The recursion function of the FFT.
|
||||
// The root of unity used in the transform is ω=1<<(ω2shift/2).
|
||||
// The source array may use shifted indices (i.e. the i-th
|
||||
// element is src[i << idxShift]).
|
||||
rec = func(dst, src []fermat, size uint) {
|
||||
idxShift := k - size
|
||||
ω2shift := (4 * n * _W) >> size
|
||||
if backward {
|
||||
ω2shift = -ω2shift
|
||||
}
|
||||
|
||||
// Easy cases.
|
||||
if len(src[0]) != n+1 || len(dst[0]) != n+1 {
|
||||
panic("len(src[0]) != n+1 || len(dst[0]) != n+1")
|
||||
}
|
||||
switch size {
|
||||
case 0:
|
||||
copy(dst[0], src[0])
|
||||
return
|
||||
case 1:
|
||||
dst[0].Add(src[0], src[1<<idxShift]) // dst[0] = src[0] + src[1]
|
||||
dst[1].Sub(src[0], src[1<<idxShift]) // dst[1] = src[0] - src[1]
|
||||
return
|
||||
}
|
||||
|
||||
// Let P(x) = src[0] + src[1<<idxShift] * x + ... + src[K-1 << idxShift] * x^(K-1)
|
||||
// The P(x) = Q1(x²) + x*Q2(x²)
|
||||
// where Q1's coefficients are src with indices shifted by 1
|
||||
// where Q2's coefficients are src[1<<idxShift:] with indices shifted by 1
|
||||
|
||||
// Split destination vectors in halves.
|
||||
dst1 := dst[:1<<(size-1)]
|
||||
dst2 := dst[1<<(size-1):]
|
||||
// Transform Q1 and Q2 in the halves.
|
||||
rec(dst1, src, size-1)
|
||||
rec(dst2, src[1<<idxShift:], size-1)
|
||||
|
||||
// Reconstruct P's transform from transforms of Q1 and Q2.
|
||||
// dst[i] is dst1[i] + ω^i * dst2[i]
|
||||
// dst[i + 1<<(k-1)] is dst1[i] + ω^(i+K/2) * dst2[i]
|
||||
//
|
||||
for i := range dst1 {
|
||||
tmp.ShiftHalf(dst2[i], i*ω2shift, tmp2) // ω^i * dst2[i]
|
||||
dst2[i].Sub(dst1[i], tmp)
|
||||
dst1[i].Add(dst1[i], tmp)
|
||||
}
|
||||
}
|
||||
rec(dst, src, k)
|
||||
}
|
||||
|
||||
// Mul returns the pointwise product of p and q.
|
||||
func (p *polValues) Mul(q *polValues) (r polValues) {
|
||||
n := p.n
|
||||
r.k, r.n = p.k, p.n
|
||||
r.values = make([]fermat, len(p.values))
|
||||
bits := make([]big.Word, len(p.values)*(n+1))
|
||||
buf := make(fermat, 8*n)
|
||||
for i := range r.values {
|
||||
r.values[i] = bits[i*(n+1) : (i+1)*(n+1)]
|
||||
z := buf.Mul(p.values[i], q.values[i])
|
||||
copy(r.values[i], z)
|
||||
}
|
||||
return
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package bigfft
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// FromDecimalString converts the base 10 string
|
||||
// representation of a natural (non-negative) number
|
||||
// into a *big.Int.
|
||||
// Its asymptotic complexity is less than quadratic.
|
||||
func FromDecimalString(s string) *big.Int {
|
||||
var sc scanner
|
||||
z := new(big.Int)
|
||||
sc.scan(z, s)
|
||||
return z
|
||||
}
|
||||
|
||||
type scanner struct {
|
||||
// powers[i] is 10^(2^i * quadraticScanThreshold).
|
||||
powers []*big.Int
|
||||
}
|
||||
|
||||
func (s *scanner) chunkSize(size int) (int, *big.Int) {
|
||||
if size <= quadraticScanThreshold {
|
||||
panic("size < quadraticScanThreshold")
|
||||
}
|
||||
pow := uint(0)
|
||||
for n := size; n > quadraticScanThreshold; n /= 2 {
|
||||
pow++
|
||||
}
|
||||
// threshold * 2^(pow-1) <= size < threshold * 2^pow
|
||||
return quadraticScanThreshold << (pow - 1), s.power(pow - 1)
|
||||
}
|
||||
|
||||
func (s *scanner) power(k uint) *big.Int {
|
||||
for i := len(s.powers); i <= int(k); i++ {
|
||||
z := new(big.Int)
|
||||
if i == 0 {
|
||||
if quadraticScanThreshold%14 != 0 {
|
||||
panic("quadraticScanThreshold % 14 != 0")
|
||||
}
|
||||
z.Exp(big.NewInt(1e14), big.NewInt(quadraticScanThreshold/14), nil)
|
||||
} else {
|
||||
z.Mul(s.powers[i-1], s.powers[i-1])
|
||||
}
|
||||
s.powers = append(s.powers, z)
|
||||
}
|
||||
return s.powers[k]
|
||||
}
|
||||
|
||||
func (s *scanner) scan(z *big.Int, str string) {
|
||||
if len(str) <= quadraticScanThreshold {
|
||||
z.SetString(str, 10)
|
||||
return
|
||||
}
|
||||
sz, pow := s.chunkSize(len(str))
|
||||
// Scan the left half.
|
||||
s.scan(z, str[:len(str)-sz])
|
||||
// FIXME: reuse temporaries.
|
||||
left := Mul(z, pow)
|
||||
// Scan the right half
|
||||
s.scan(z, str[len(str)-sz:])
|
||||
z.Add(z, left)
|
||||
}
|
||||
|
||||
// quadraticScanThreshold is the number of digits
|
||||
// below which big.Int.SetString is more efficient
|
||||
// than subquadratic algorithms.
|
||||
// 1232 digits fit in 4096 bits.
|
||||
const quadraticScanThreshold = 1232
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
+1
@@ -0,0 +1 @@
|
||||
language: go
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
Copyright (C) 2012 Rob Figueiredo
|
||||
All Rights Reserved.
|
||||
|
||||
MIT LICENSE
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
[](http://godoc.org/github.com/robfig/cron)
|
||||
[](https://travis-ci.org/robfig/cron)
|
||||
|
||||
# cron
|
||||
|
||||
Cron V3 has been released!
|
||||
|
||||
To download the specific tagged release, run:
|
||||
|
||||
go get github.com/robfig/cron/v3@v3.0.0
|
||||
|
||||
Import it in your program as:
|
||||
|
||||
import "github.com/robfig/cron/v3"
|
||||
|
||||
It requires Go 1.11 or later due to usage of Go Modules.
|
||||
|
||||
Refer to the documentation here:
|
||||
http://godoc.org/github.com/robfig/cron
|
||||
|
||||
The rest of this document describes the the advances in v3 and a list of
|
||||
breaking changes for users that wish to upgrade from an earlier version.
|
||||
|
||||
## Upgrading to v3 (June 2019)
|
||||
|
||||
cron v3 is a major upgrade to the library that addresses all outstanding bugs,
|
||||
feature requests, and rough edges. It is based on a merge of master which
|
||||
contains various fixes to issues found over the years and the v2 branch which
|
||||
contains some backwards-incompatible features like the ability to remove cron
|
||||
jobs. In addition, v3 adds support for Go Modules, cleans up rough edges like
|
||||
the timezone support, and fixes a number of bugs.
|
||||
|
||||
New features:
|
||||
|
||||
- Support for Go modules. Callers must now import this library as
|
||||
`github.com/robfig/cron/v3`, instead of `gopkg.in/...`
|
||||
|
||||
- Fixed bugs:
|
||||
- 0f01e6b parser: fix combining of Dow and Dom (#70)
|
||||
- dbf3220 adjust times when rolling the clock forward to handle non-existent midnight (#157)
|
||||
- eeecf15 spec_test.go: ensure an error is returned on 0 increment (#144)
|
||||
- 70971dc cron.Entries(): update request for snapshot to include a reply channel (#97)
|
||||
- 1cba5e6 cron: fix: removing a job causes the next scheduled job to run too late (#206)
|
||||
|
||||
- Standard cron spec parsing by default (first field is "minute"), with an easy
|
||||
way to opt into the seconds field (quartz-compatible). Although, note that the
|
||||
year field (optional in Quartz) is not supported.
|
||||
|
||||
- Extensible, key/value logging via an interface that complies with
|
||||
the https://github.com/go-logr/logr project.
|
||||
|
||||
- The new Chain & JobWrapper types allow you to install "interceptors" to add
|
||||
cross-cutting behavior like the following:
|
||||
- Recover any panics from jobs
|
||||
- Delay a job's execution if the previous run hasn't completed yet
|
||||
- Skip a job's execution if the previous run hasn't completed yet
|
||||
- Log each job's invocations
|
||||
- Notification when jobs are completed
|
||||
|
||||
It is backwards incompatible with both v1 and v2. These updates are required:
|
||||
|
||||
- The v1 branch accepted an optional seconds field at the beginning of the cron
|
||||
spec. This is non-standard and has led to a lot of confusion. The new default
|
||||
parser conforms to the standard as described by [the Cron wikipedia page].
|
||||
|
||||
UPDATING: To retain the old behavior, construct your Cron with a custom
|
||||
parser:
|
||||
|
||||
// Seconds field, required
|
||||
cron.New(cron.WithSeconds())
|
||||
|
||||
// Seconds field, optional
|
||||
cron.New(
|
||||
cron.WithParser(
|
||||
cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor))
|
||||
|
||||
- The Cron type now accepts functional options on construction rather than the
|
||||
previous ad-hoc behavior modification mechanisms (setting a field, calling a setter).
|
||||
|
||||
UPDATING: Code that sets Cron.ErrorLogger or calls Cron.SetLocation must be
|
||||
updated to provide those values on construction.
|
||||
|
||||
- CRON_TZ is now the recommended way to specify the timezone of a single
|
||||
schedule, which is sanctioned by the specification. The legacy "TZ=" prefix
|
||||
will continue to be supported since it is unambiguous and easy to do so.
|
||||
|
||||
UPDATING: No update is required.
|
||||
|
||||
- By default, cron will no longer recover panics in jobs that it runs.
|
||||
Recovering can be surprising (see issue #192) and seems to be at odds with
|
||||
typical behavior of libraries. Relatedly, the `cron.WithPanicLogger` option
|
||||
has been removed to accommodate the more general JobWrapper type.
|
||||
|
||||
UPDATING: To opt into panic recovery and configure the panic logger:
|
||||
|
||||
cron.New(cron.WithChain(
|
||||
cron.Recover(logger), // or use cron.DefaultLogger
|
||||
))
|
||||
|
||||
- In adding support for https://github.com/go-logr/logr, `cron.WithVerboseLogger` was
|
||||
removed, since it is duplicative with the leveled logging.
|
||||
|
||||
UPDATING: Callers should use `WithLogger` and specify a logger that does not
|
||||
discard `Info` logs. For convenience, one is provided that wraps `*log.Logger`:
|
||||
|
||||
cron.New(
|
||||
cron.WithLogger(cron.VerbosePrintfLogger(logger)))
|
||||
|
||||
|
||||
### Background - Cron spec format
|
||||
|
||||
There are two cron spec formats in common usage:
|
||||
|
||||
- The "standard" cron format, described on [the Cron wikipedia page] and used by
|
||||
the cron Linux system utility.
|
||||
|
||||
- The cron format used by [the Quartz Scheduler], commonly used for scheduled
|
||||
jobs in Java software
|
||||
|
||||
[the Cron wikipedia page]: https://en.wikipedia.org/wiki/Cron
|
||||
[the Quartz Scheduler]: http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/tutorial-lesson-06.html
|
||||
|
||||
The original version of this package included an optional "seconds" field, which
|
||||
made it incompatible with both of these formats. Now, the "standard" format is
|
||||
the default format accepted, and the Quartz format is opt-in.
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// JobWrapper decorates the given Job with some behavior.
|
||||
type JobWrapper func(Job) Job
|
||||
|
||||
// Chain is a sequence of JobWrappers that decorates submitted jobs with
|
||||
// cross-cutting behaviors like logging or synchronization.
|
||||
type Chain struct {
|
||||
wrappers []JobWrapper
|
||||
}
|
||||
|
||||
// NewChain returns a Chain consisting of the given JobWrappers.
|
||||
func NewChain(c ...JobWrapper) Chain {
|
||||
return Chain{c}
|
||||
}
|
||||
|
||||
// Then decorates the given job with all JobWrappers in the chain.
|
||||
//
|
||||
// This:
|
||||
// NewChain(m1, m2, m3).Then(job)
|
||||
// is equivalent to:
|
||||
// m1(m2(m3(job)))
|
||||
func (c Chain) Then(j Job) Job {
|
||||
for i := range c.wrappers {
|
||||
j = c.wrappers[len(c.wrappers)-i-1](j)
|
||||
}
|
||||
return j
|
||||
}
|
||||
|
||||
// Recover panics in wrapped jobs and log them with the provided logger.
|
||||
func Recover(logger Logger) JobWrapper {
|
||||
return func(j Job) Job {
|
||||
return FuncJob(func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
const size = 64 << 10
|
||||
buf := make([]byte, size)
|
||||
buf = buf[:runtime.Stack(buf, false)]
|
||||
err, ok := r.(error)
|
||||
if !ok {
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
logger.Error(err, "panic", "stack", "...\n"+string(buf))
|
||||
}
|
||||
}()
|
||||
j.Run()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// DelayIfStillRunning serializes jobs, delaying subsequent runs until the
|
||||
// previous one is complete. Jobs running after a delay of more than a minute
|
||||
// have the delay logged at Info.
|
||||
func DelayIfStillRunning(logger Logger) JobWrapper {
|
||||
return func(j Job) Job {
|
||||
var mu sync.Mutex
|
||||
return FuncJob(func() {
|
||||
start := time.Now()
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if dur := time.Since(start); dur > time.Minute {
|
||||
logger.Info("delay", "duration", dur)
|
||||
}
|
||||
j.Run()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// SkipIfStillRunning skips an invocation of the Job if a previous invocation is
|
||||
// still running. It logs skips to the given logger at Info level.
|
||||
func SkipIfStillRunning(logger Logger) JobWrapper {
|
||||
return func(j Job) Job {
|
||||
var ch = make(chan struct{}, 1)
|
||||
ch <- struct{}{}
|
||||
return FuncJob(func() {
|
||||
select {
|
||||
case v := <-ch:
|
||||
j.Run()
|
||||
ch <- v
|
||||
default:
|
||||
logger.Info("skip")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package cron
|
||||
|
||||
import "time"
|
||||
|
||||
// ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes".
|
||||
// It does not support jobs more frequent than once a second.
|
||||
type ConstantDelaySchedule struct {
|
||||
Delay time.Duration
|
||||
}
|
||||
|
||||
// Every returns a crontab Schedule that activates once every duration.
|
||||
// Delays of less than a second are not supported (will round up to 1 second).
|
||||
// Any fields less than a Second are truncated.
|
||||
func Every(duration time.Duration) ConstantDelaySchedule {
|
||||
if duration < time.Second {
|
||||
duration = time.Second
|
||||
}
|
||||
return ConstantDelaySchedule{
|
||||
Delay: duration - time.Duration(duration.Nanoseconds())%time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// Next returns the next time this should be run.
|
||||
// This rounds so that the next activation time will be on the second.
|
||||
func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time {
|
||||
return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond)
|
||||
}
|
||||
+355
@@ -0,0 +1,355 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cron keeps track of any number of entries, invoking the associated func as
|
||||
// specified by the schedule. It may be started, stopped, and the entries may
|
||||
// be inspected while running.
|
||||
type Cron struct {
|
||||
entries []*Entry
|
||||
chain Chain
|
||||
stop chan struct{}
|
||||
add chan *Entry
|
||||
remove chan EntryID
|
||||
snapshot chan chan []Entry
|
||||
running bool
|
||||
logger Logger
|
||||
runningMu sync.Mutex
|
||||
location *time.Location
|
||||
parser ScheduleParser
|
||||
nextID EntryID
|
||||
jobWaiter sync.WaitGroup
|
||||
}
|
||||
|
||||
// ScheduleParser is an interface for schedule spec parsers that return a Schedule
|
||||
type ScheduleParser interface {
|
||||
Parse(spec string) (Schedule, error)
|
||||
}
|
||||
|
||||
// Job is an interface for submitted cron jobs.
|
||||
type Job interface {
|
||||
Run()
|
||||
}
|
||||
|
||||
// Schedule describes a job's duty cycle.
|
||||
type Schedule interface {
|
||||
// Next returns the next activation time, later than the given time.
|
||||
// Next is invoked initially, and then each time the job is run.
|
||||
Next(time.Time) time.Time
|
||||
}
|
||||
|
||||
// EntryID identifies an entry within a Cron instance
|
||||
type EntryID int
|
||||
|
||||
// Entry consists of a schedule and the func to execute on that schedule.
|
||||
type Entry struct {
|
||||
// ID is the cron-assigned ID of this entry, which may be used to look up a
|
||||
// snapshot or remove it.
|
||||
ID EntryID
|
||||
|
||||
// Schedule on which this job should be run.
|
||||
Schedule Schedule
|
||||
|
||||
// Next time the job will run, or the zero time if Cron has not been
|
||||
// started or this entry's schedule is unsatisfiable
|
||||
Next time.Time
|
||||
|
||||
// Prev is the last time this job was run, or the zero time if never.
|
||||
Prev time.Time
|
||||
|
||||
// WrappedJob is the thing to run when the Schedule is activated.
|
||||
WrappedJob Job
|
||||
|
||||
// Job is the thing that was submitted to cron.
|
||||
// It is kept around so that user code that needs to get at the job later,
|
||||
// e.g. via Entries() can do so.
|
||||
Job Job
|
||||
}
|
||||
|
||||
// Valid returns true if this is not the zero entry.
|
||||
func (e Entry) Valid() bool { return e.ID != 0 }
|
||||
|
||||
// byTime is a wrapper for sorting the entry array by time
|
||||
// (with zero time at the end).
|
||||
type byTime []*Entry
|
||||
|
||||
func (s byTime) Len() int { return len(s) }
|
||||
func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s byTime) Less(i, j int) bool {
|
||||
// Two zero times should return false.
|
||||
// Otherwise, zero is "greater" than any other time.
|
||||
// (To sort it at the end of the list.)
|
||||
if s[i].Next.IsZero() {
|
||||
return false
|
||||
}
|
||||
if s[j].Next.IsZero() {
|
||||
return true
|
||||
}
|
||||
return s[i].Next.Before(s[j].Next)
|
||||
}
|
||||
|
||||
// New returns a new Cron job runner, modified by the given options.
|
||||
//
|
||||
// Available Settings
|
||||
//
|
||||
// Time Zone
|
||||
// Description: The time zone in which schedules are interpreted
|
||||
// Default: time.Local
|
||||
//
|
||||
// Parser
|
||||
// Description: Parser converts cron spec strings into cron.Schedules.
|
||||
// Default: Accepts this spec: https://en.wikipedia.org/wiki/Cron
|
||||
//
|
||||
// Chain
|
||||
// Description: Wrap submitted jobs to customize behavior.
|
||||
// Default: A chain that recovers panics and logs them to stderr.
|
||||
//
|
||||
// See "cron.With*" to modify the default behavior.
|
||||
func New(opts ...Option) *Cron {
|
||||
c := &Cron{
|
||||
entries: nil,
|
||||
chain: NewChain(),
|
||||
add: make(chan *Entry),
|
||||
stop: make(chan struct{}),
|
||||
snapshot: make(chan chan []Entry),
|
||||
remove: make(chan EntryID),
|
||||
running: false,
|
||||
runningMu: sync.Mutex{},
|
||||
logger: DefaultLogger,
|
||||
location: time.Local,
|
||||
parser: standardParser,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// FuncJob is a wrapper that turns a func() into a cron.Job
|
||||
type FuncJob func()
|
||||
|
||||
func (f FuncJob) Run() { f() }
|
||||
|
||||
// AddFunc adds a func to the Cron to be run on the given schedule.
|
||||
// The spec is parsed using the time zone of this Cron instance as the default.
|
||||
// An opaque ID is returned that can be used to later remove it.
|
||||
func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error) {
|
||||
return c.AddJob(spec, FuncJob(cmd))
|
||||
}
|
||||
|
||||
// AddJob adds a Job to the Cron to be run on the given schedule.
|
||||
// The spec is parsed using the time zone of this Cron instance as the default.
|
||||
// An opaque ID is returned that can be used to later remove it.
|
||||
func (c *Cron) AddJob(spec string, cmd Job) (EntryID, error) {
|
||||
schedule, err := c.parser.Parse(spec)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return c.Schedule(schedule, cmd), nil
|
||||
}
|
||||
|
||||
// Schedule adds a Job to the Cron to be run on the given schedule.
|
||||
// The job is wrapped with the configured Chain.
|
||||
func (c *Cron) Schedule(schedule Schedule, cmd Job) EntryID {
|
||||
c.runningMu.Lock()
|
||||
defer c.runningMu.Unlock()
|
||||
c.nextID++
|
||||
entry := &Entry{
|
||||
ID: c.nextID,
|
||||
Schedule: schedule,
|
||||
WrappedJob: c.chain.Then(cmd),
|
||||
Job: cmd,
|
||||
}
|
||||
if !c.running {
|
||||
c.entries = append(c.entries, entry)
|
||||
} else {
|
||||
c.add <- entry
|
||||
}
|
||||
return entry.ID
|
||||
}
|
||||
|
||||
// Entries returns a snapshot of the cron entries.
|
||||
func (c *Cron) Entries() []Entry {
|
||||
c.runningMu.Lock()
|
||||
defer c.runningMu.Unlock()
|
||||
if c.running {
|
||||
replyChan := make(chan []Entry, 1)
|
||||
c.snapshot <- replyChan
|
||||
return <-replyChan
|
||||
}
|
||||
return c.entrySnapshot()
|
||||
}
|
||||
|
||||
// Location gets the time zone location
|
||||
func (c *Cron) Location() *time.Location {
|
||||
return c.location
|
||||
}
|
||||
|
||||
// Entry returns a snapshot of the given entry, or nil if it couldn't be found.
|
||||
func (c *Cron) Entry(id EntryID) Entry {
|
||||
for _, entry := range c.Entries() {
|
||||
if id == entry.ID {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
return Entry{}
|
||||
}
|
||||
|
||||
// Remove an entry from being run in the future.
|
||||
func (c *Cron) Remove(id EntryID) {
|
||||
c.runningMu.Lock()
|
||||
defer c.runningMu.Unlock()
|
||||
if c.running {
|
||||
c.remove <- id
|
||||
} else {
|
||||
c.removeEntry(id)
|
||||
}
|
||||
}
|
||||
|
||||
// Start the cron scheduler in its own goroutine, or no-op if already started.
|
||||
func (c *Cron) Start() {
|
||||
c.runningMu.Lock()
|
||||
defer c.runningMu.Unlock()
|
||||
if c.running {
|
||||
return
|
||||
}
|
||||
c.running = true
|
||||
go c.run()
|
||||
}
|
||||
|
||||
// Run the cron scheduler, or no-op if already running.
|
||||
func (c *Cron) Run() {
|
||||
c.runningMu.Lock()
|
||||
if c.running {
|
||||
c.runningMu.Unlock()
|
||||
return
|
||||
}
|
||||
c.running = true
|
||||
c.runningMu.Unlock()
|
||||
c.run()
|
||||
}
|
||||
|
||||
// run the scheduler.. this is private just due to the need to synchronize
|
||||
// access to the 'running' state variable.
|
||||
func (c *Cron) run() {
|
||||
c.logger.Info("start")
|
||||
|
||||
// Figure out the next activation times for each entry.
|
||||
now := c.now()
|
||||
for _, entry := range c.entries {
|
||||
entry.Next = entry.Schedule.Next(now)
|
||||
c.logger.Info("schedule", "now", now, "entry", entry.ID, "next", entry.Next)
|
||||
}
|
||||
|
||||
for {
|
||||
// Determine the next entry to run.
|
||||
sort.Sort(byTime(c.entries))
|
||||
|
||||
var timer *time.Timer
|
||||
if len(c.entries) == 0 || c.entries[0].Next.IsZero() {
|
||||
// If there are no entries yet, just sleep - it still handles new entries
|
||||
// and stop requests.
|
||||
timer = time.NewTimer(100000 * time.Hour)
|
||||
} else {
|
||||
timer = time.NewTimer(c.entries[0].Next.Sub(now))
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case now = <-timer.C:
|
||||
now = now.In(c.location)
|
||||
c.logger.Info("wake", "now", now)
|
||||
|
||||
// Run every entry whose next time was less than now
|
||||
for _, e := range c.entries {
|
||||
if e.Next.After(now) || e.Next.IsZero() {
|
||||
break
|
||||
}
|
||||
c.startJob(e.WrappedJob)
|
||||
e.Prev = e.Next
|
||||
e.Next = e.Schedule.Next(now)
|
||||
c.logger.Info("run", "now", now, "entry", e.ID, "next", e.Next)
|
||||
}
|
||||
|
||||
case newEntry := <-c.add:
|
||||
timer.Stop()
|
||||
now = c.now()
|
||||
newEntry.Next = newEntry.Schedule.Next(now)
|
||||
c.entries = append(c.entries, newEntry)
|
||||
c.logger.Info("added", "now", now, "entry", newEntry.ID, "next", newEntry.Next)
|
||||
|
||||
case replyChan := <-c.snapshot:
|
||||
replyChan <- c.entrySnapshot()
|
||||
continue
|
||||
|
||||
case <-c.stop:
|
||||
timer.Stop()
|
||||
c.logger.Info("stop")
|
||||
return
|
||||
|
||||
case id := <-c.remove:
|
||||
timer.Stop()
|
||||
now = c.now()
|
||||
c.removeEntry(id)
|
||||
c.logger.Info("removed", "entry", id)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startJob runs the given job in a new goroutine.
|
||||
func (c *Cron) startJob(j Job) {
|
||||
c.jobWaiter.Add(1)
|
||||
go func() {
|
||||
defer c.jobWaiter.Done()
|
||||
j.Run()
|
||||
}()
|
||||
}
|
||||
|
||||
// now returns current time in c location
|
||||
func (c *Cron) now() time.Time {
|
||||
return time.Now().In(c.location)
|
||||
}
|
||||
|
||||
// Stop stops the cron scheduler if it is running; otherwise it does nothing.
|
||||
// A context is returned so the caller can wait for running jobs to complete.
|
||||
func (c *Cron) Stop() context.Context {
|
||||
c.runningMu.Lock()
|
||||
defer c.runningMu.Unlock()
|
||||
if c.running {
|
||||
c.stop <- struct{}{}
|
||||
c.running = false
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
c.jobWaiter.Wait()
|
||||
cancel()
|
||||
}()
|
||||
return ctx
|
||||
}
|
||||
|
||||
// entrySnapshot returns a copy of the current cron entry list.
|
||||
func (c *Cron) entrySnapshot() []Entry {
|
||||
var entries = make([]Entry, len(c.entries))
|
||||
for i, e := range c.entries {
|
||||
entries[i] = *e
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func (c *Cron) removeEntry(id EntryID) {
|
||||
var entries []*Entry
|
||||
for _, e := range c.entries {
|
||||
if e.ID != id {
|
||||
entries = append(entries, e)
|
||||
}
|
||||
}
|
||||
c.entries = entries
|
||||
}
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
Package cron implements a cron spec parser and job runner.
|
||||
|
||||
Installation
|
||||
|
||||
To download the specific tagged release, run:
|
||||
|
||||
go get github.com/robfig/cron/v3@v3.0.0
|
||||
|
||||
Import it in your program as:
|
||||
|
||||
import "github.com/robfig/cron/v3"
|
||||
|
||||
It requires Go 1.11 or later due to usage of Go Modules.
|
||||
|
||||
Usage
|
||||
|
||||
Callers may register Funcs to be invoked on a given schedule. Cron will run
|
||||
them in their own goroutines.
|
||||
|
||||
c := cron.New()
|
||||
c.AddFunc("30 * * * *", func() { fmt.Println("Every hour on the half hour") })
|
||||
c.AddFunc("30 3-6,20-23 * * *", func() { fmt.Println(".. in the range 3-6am, 8-11pm") })
|
||||
c.AddFunc("CRON_TZ=Asia/Tokyo 30 04 * * *", func() { fmt.Println("Runs at 04:30 Tokyo time every day") })
|
||||
c.AddFunc("@hourly", func() { fmt.Println("Every hour, starting an hour from now") })
|
||||
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty, starting an hour thirty from now") })
|
||||
c.Start()
|
||||
..
|
||||
// Funcs are invoked in their own goroutine, asynchronously.
|
||||
...
|
||||
// Funcs may also be added to a running Cron
|
||||
c.AddFunc("@daily", func() { fmt.Println("Every day") })
|
||||
..
|
||||
// Inspect the cron job entries' next and previous run times.
|
||||
inspect(c.Entries())
|
||||
..
|
||||
c.Stop() // Stop the scheduler (does not stop any jobs already running).
|
||||
|
||||
CRON Expression Format
|
||||
|
||||
A cron expression represents a set of times, using 5 space-separated fields.
|
||||
|
||||
Field name | Mandatory? | Allowed values | Allowed special characters
|
||||
---------- | ---------- | -------------- | --------------------------
|
||||
Minutes | Yes | 0-59 | * / , -
|
||||
Hours | Yes | 0-23 | * / , -
|
||||
Day of month | Yes | 1-31 | * / , - ?
|
||||
Month | Yes | 1-12 or JAN-DEC | * / , -
|
||||
Day of week | Yes | 0-6 or SUN-SAT | * / , - ?
|
||||
|
||||
Month and Day-of-week field values are case insensitive. "SUN", "Sun", and
|
||||
"sun" are equally accepted.
|
||||
|
||||
The specific interpretation of the format is based on the Cron Wikipedia page:
|
||||
https://en.wikipedia.org/wiki/Cron
|
||||
|
||||
Alternative Formats
|
||||
|
||||
Alternative Cron expression formats support other fields like seconds. You can
|
||||
implement that by creating a custom Parser as follows.
|
||||
|
||||
cron.New(
|
||||
cron.WithParser(
|
||||
cron.NewParser(
|
||||
cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)))
|
||||
|
||||
Since adding Seconds is the most common modification to the standard cron spec,
|
||||
cron provides a builtin function to do that, which is equivalent to the custom
|
||||
parser you saw earlier, except that its seconds field is REQUIRED:
|
||||
|
||||
cron.New(cron.WithSeconds())
|
||||
|
||||
That emulates Quartz, the most popular alternative Cron schedule format:
|
||||
http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger.html
|
||||
|
||||
Special Characters
|
||||
|
||||
Asterisk ( * )
|
||||
|
||||
The asterisk indicates that the cron expression will match for all values of the
|
||||
field; e.g., using an asterisk in the 5th field (month) would indicate every
|
||||
month.
|
||||
|
||||
Slash ( / )
|
||||
|
||||
Slashes are used to describe increments of ranges. For example 3-59/15 in the
|
||||
1st field (minutes) would indicate the 3rd minute of the hour and every 15
|
||||
minutes thereafter. The form "*\/..." is equivalent to the form "first-last/...",
|
||||
that is, an increment over the largest possible range of the field. The form
|
||||
"N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the
|
||||
increment until the end of that specific range. It does not wrap around.
|
||||
|
||||
Comma ( , )
|
||||
|
||||
Commas are used to separate items of a list. For example, using "MON,WED,FRI" in
|
||||
the 5th field (day of week) would mean Mondays, Wednesdays and Fridays.
|
||||
|
||||
Hyphen ( - )
|
||||
|
||||
Hyphens are used to define ranges. For example, 9-17 would indicate every
|
||||
hour between 9am and 5pm inclusive.
|
||||
|
||||
Question mark ( ? )
|
||||
|
||||
Question mark may be used instead of '*' for leaving either day-of-month or
|
||||
day-of-week blank.
|
||||
|
||||
Predefined schedules
|
||||
|
||||
You may use one of several pre-defined schedules in place of a cron expression.
|
||||
|
||||
Entry | Description | Equivalent To
|
||||
----- | ----------- | -------------
|
||||
@yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 1 1 *
|
||||
@monthly | Run once a month, midnight, first of month | 0 0 1 * *
|
||||
@weekly | Run once a week, midnight between Sat/Sun | 0 0 * * 0
|
||||
@daily (or @midnight) | Run once a day, midnight | 0 0 * * *
|
||||
@hourly | Run once an hour, beginning of hour | 0 * * * *
|
||||
|
||||
Intervals
|
||||
|
||||
You may also schedule a job to execute at fixed intervals, starting at the time it's added
|
||||
or cron is run. This is supported by formatting the cron spec like this:
|
||||
|
||||
@every <duration>
|
||||
|
||||
where "duration" is a string accepted by time.ParseDuration
|
||||
(http://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
For example, "@every 1h30m10s" would indicate a schedule that activates after
|
||||
1 hour, 30 minutes, 10 seconds, and then every interval after that.
|
||||
|
||||
Note: The interval does not take the job runtime into account. For example,
|
||||
if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes,
|
||||
it will have only 2 minutes of idle time between each run.
|
||||
|
||||
Time zones
|
||||
|
||||
By default, all interpretation and scheduling is done in the machine's local
|
||||
time zone (time.Local). You can specify a different time zone on construction:
|
||||
|
||||
cron.New(
|
||||
cron.WithLocation(time.UTC))
|
||||
|
||||
Individual cron schedules may also override the time zone they are to be
|
||||
interpreted in by providing an additional space-separated field at the beginning
|
||||
of the cron spec, of the form "CRON_TZ=Asia/Tokyo".
|
||||
|
||||
For example:
|
||||
|
||||
# Runs at 6am in time.Local
|
||||
cron.New().AddFunc("0 6 * * ?", ...)
|
||||
|
||||
# Runs at 6am in America/New_York
|
||||
nyc, _ := time.LoadLocation("America/New_York")
|
||||
c := cron.New(cron.WithLocation(nyc))
|
||||
c.AddFunc("0 6 * * ?", ...)
|
||||
|
||||
# Runs at 6am in Asia/Tokyo
|
||||
cron.New().AddFunc("CRON_TZ=Asia/Tokyo 0 6 * * ?", ...)
|
||||
|
||||
# Runs at 6am in Asia/Tokyo
|
||||
c := cron.New(cron.WithLocation(nyc))
|
||||
c.SetLocation("America/New_York")
|
||||
c.AddFunc("CRON_TZ=Asia/Tokyo 0 6 * * ?", ...)
|
||||
|
||||
The prefix "TZ=(TIME ZONE)" is also supported for legacy compatibility.
|
||||
|
||||
Be aware that jobs scheduled during daylight-savings leap-ahead transitions will
|
||||
not be run!
|
||||
|
||||
Job Wrappers
|
||||
|
||||
A Cron runner may be configured with a chain of job wrappers to add
|
||||
cross-cutting functionality to all submitted jobs. For example, they may be used
|
||||
to achieve the following effects:
|
||||
|
||||
- Recover any panics from jobs (activated by default)
|
||||
- Delay a job's execution if the previous run hasn't completed yet
|
||||
- Skip a job's execution if the previous run hasn't completed yet
|
||||
- Log each job's invocations
|
||||
|
||||
Install wrappers for all jobs added to a cron using the `cron.WithChain` option:
|
||||
|
||||
cron.New(cron.WithChain(
|
||||
cron.SkipIfStillRunning(logger),
|
||||
))
|
||||
|
||||
Install wrappers for individual jobs by explicitly wrapping them:
|
||||
|
||||
job = cron.NewChain(
|
||||
cron.SkipIfStillRunning(logger),
|
||||
).Then(job)
|
||||
|
||||
Thread safety
|
||||
|
||||
Since the Cron service runs concurrently with the calling code, some amount of
|
||||
care must be taken to ensure proper synchronization.
|
||||
|
||||
All cron methods are designed to be correctly synchronized as long as the caller
|
||||
ensures that invocations have a clear happens-before ordering between them.
|
||||
|
||||
Logging
|
||||
|
||||
Cron defines a Logger interface that is a subset of the one defined in
|
||||
github.com/go-logr/logr. It has two logging levels (Info and Error), and
|
||||
parameters are key/value pairs. This makes it possible for cron logging to plug
|
||||
into structured logging systems. An adapter, [Verbose]PrintfLogger, is provided
|
||||
to wrap the standard library *log.Logger.
|
||||
|
||||
For additional insight into Cron operations, verbose logging may be activated
|
||||
which will record job runs, scheduling decisions, and added or removed jobs.
|
||||
Activate it with a one-off logger as follows:
|
||||
|
||||
cron.New(
|
||||
cron.WithLogger(
|
||||
cron.VerbosePrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))))
|
||||
|
||||
|
||||
Implementation
|
||||
|
||||
Cron entries are stored in an array, sorted by their next activation time. Cron
|
||||
sleeps until the next job is due to be run.
|
||||
|
||||
Upon waking:
|
||||
- it runs each entry that is active on that second
|
||||
- it calculates the next run times for the jobs that were run
|
||||
- it re-sorts the array of entries by next activation time.
|
||||
- it goes to sleep until the soonest job.
|
||||
*/
|
||||
package cron
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DefaultLogger is used by Cron if none is specified.
|
||||
var DefaultLogger Logger = PrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))
|
||||
|
||||
// DiscardLogger can be used by callers to discard all log messages.
|
||||
var DiscardLogger Logger = PrintfLogger(log.New(ioutil.Discard, "", 0))
|
||||
|
||||
// Logger is the interface used in this package for logging, so that any backend
|
||||
// can be plugged in. It is a subset of the github.com/go-logr/logr interface.
|
||||
type Logger interface {
|
||||
// Info logs routine messages about cron's operation.
|
||||
Info(msg string, keysAndValues ...interface{})
|
||||
// Error logs an error condition.
|
||||
Error(err error, msg string, keysAndValues ...interface{})
|
||||
}
|
||||
|
||||
// PrintfLogger wraps a Printf-based logger (such as the standard library "log")
|
||||
// into an implementation of the Logger interface which logs errors only.
|
||||
func PrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger {
|
||||
return printfLogger{l, false}
|
||||
}
|
||||
|
||||
// VerbosePrintfLogger wraps a Printf-based logger (such as the standard library
|
||||
// "log") into an implementation of the Logger interface which logs everything.
|
||||
func VerbosePrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger {
|
||||
return printfLogger{l, true}
|
||||
}
|
||||
|
||||
type printfLogger struct {
|
||||
logger interface{ Printf(string, ...interface{}) }
|
||||
logInfo bool
|
||||
}
|
||||
|
||||
func (pl printfLogger) Info(msg string, keysAndValues ...interface{}) {
|
||||
if pl.logInfo {
|
||||
keysAndValues = formatTimes(keysAndValues)
|
||||
pl.logger.Printf(
|
||||
formatString(len(keysAndValues)),
|
||||
append([]interface{}{msg}, keysAndValues...)...)
|
||||
}
|
||||
}
|
||||
|
||||
func (pl printfLogger) Error(err error, msg string, keysAndValues ...interface{}) {
|
||||
keysAndValues = formatTimes(keysAndValues)
|
||||
pl.logger.Printf(
|
||||
formatString(len(keysAndValues)+2),
|
||||
append([]interface{}{msg, "error", err}, keysAndValues...)...)
|
||||
}
|
||||
|
||||
// formatString returns a logfmt-like format string for the number of
|
||||
// key/values.
|
||||
func formatString(numKeysAndValues int) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("%s")
|
||||
if numKeysAndValues > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
for i := 0; i < numKeysAndValues/2; i++ {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString("%v=%v")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatTimes formats any time.Time values as RFC3339.
|
||||
func formatTimes(keysAndValues []interface{}) []interface{} {
|
||||
var formattedArgs []interface{}
|
||||
for _, arg := range keysAndValues {
|
||||
if t, ok := arg.(time.Time); ok {
|
||||
arg = t.Format(time.RFC3339)
|
||||
}
|
||||
formattedArgs = append(formattedArgs, arg)
|
||||
}
|
||||
return formattedArgs
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Option represents a modification to the default behavior of a Cron.
|
||||
type Option func(*Cron)
|
||||
|
||||
// WithLocation overrides the timezone of the cron instance.
|
||||
func WithLocation(loc *time.Location) Option {
|
||||
return func(c *Cron) {
|
||||
c.location = loc
|
||||
}
|
||||
}
|
||||
|
||||
// WithSeconds overrides the parser used for interpreting job schedules to
|
||||
// include a seconds field as the first one.
|
||||
func WithSeconds() Option {
|
||||
return WithParser(NewParser(
|
||||
Second | Minute | Hour | Dom | Month | Dow | Descriptor,
|
||||
))
|
||||
}
|
||||
|
||||
// WithParser overrides the parser used for interpreting job schedules.
|
||||
func WithParser(p ScheduleParser) Option {
|
||||
return func(c *Cron) {
|
||||
c.parser = p
|
||||
}
|
||||
}
|
||||
|
||||
// WithChain specifies Job wrappers to apply to all jobs added to this cron.
|
||||
// Refer to the Chain* functions in this package for provided wrappers.
|
||||
func WithChain(wrappers ...JobWrapper) Option {
|
||||
return func(c *Cron) {
|
||||
c.chain = NewChain(wrappers...)
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger uses the provided logger.
|
||||
func WithLogger(logger Logger) Option {
|
||||
return func(c *Cron) {
|
||||
c.logger = logger
|
||||
}
|
||||
}
|
||||
+434
@@ -0,0 +1,434 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Configuration options for creating a parser. Most options specify which
|
||||
// fields should be included, while others enable features. If a field is not
|
||||
// included the parser will assume a default value. These options do not change
|
||||
// the order fields are parse in.
|
||||
type ParseOption int
|
||||
|
||||
const (
|
||||
Second ParseOption = 1 << iota // Seconds field, default 0
|
||||
SecondOptional // Optional seconds field, default 0
|
||||
Minute // Minutes field, default 0
|
||||
Hour // Hours field, default 0
|
||||
Dom // Day of month field, default *
|
||||
Month // Month field, default *
|
||||
Dow // Day of week field, default *
|
||||
DowOptional // Optional day of week field, default *
|
||||
Descriptor // Allow descriptors such as @monthly, @weekly, etc.
|
||||
)
|
||||
|
||||
var places = []ParseOption{
|
||||
Second,
|
||||
Minute,
|
||||
Hour,
|
||||
Dom,
|
||||
Month,
|
||||
Dow,
|
||||
}
|
||||
|
||||
var defaults = []string{
|
||||
"0",
|
||||
"0",
|
||||
"0",
|
||||
"*",
|
||||
"*",
|
||||
"*",
|
||||
}
|
||||
|
||||
// A custom Parser that can be configured.
|
||||
type Parser struct {
|
||||
options ParseOption
|
||||
}
|
||||
|
||||
// NewParser creates a Parser with custom options.
|
||||
//
|
||||
// It panics if more than one Optional is given, since it would be impossible to
|
||||
// correctly infer which optional is provided or missing in general.
|
||||
//
|
||||
// Examples
|
||||
//
|
||||
// // Standard parser without descriptors
|
||||
// specParser := NewParser(Minute | Hour | Dom | Month | Dow)
|
||||
// sched, err := specParser.Parse("0 0 15 */3 *")
|
||||
//
|
||||
// // Same as above, just excludes time fields
|
||||
// subsParser := NewParser(Dom | Month | Dow)
|
||||
// sched, err := specParser.Parse("15 */3 *")
|
||||
//
|
||||
// // Same as above, just makes Dow optional
|
||||
// subsParser := NewParser(Dom | Month | DowOptional)
|
||||
// sched, err := specParser.Parse("15 */3")
|
||||
//
|
||||
func NewParser(options ParseOption) Parser {
|
||||
optionals := 0
|
||||
if options&DowOptional > 0 {
|
||||
optionals++
|
||||
}
|
||||
if options&SecondOptional > 0 {
|
||||
optionals++
|
||||
}
|
||||
if optionals > 1 {
|
||||
panic("multiple optionals may not be configured")
|
||||
}
|
||||
return Parser{options}
|
||||
}
|
||||
|
||||
// Parse returns a new crontab schedule representing the given spec.
|
||||
// It returns a descriptive error if the spec is not valid.
|
||||
// It accepts crontab specs and features configured by NewParser.
|
||||
func (p Parser) Parse(spec string) (Schedule, error) {
|
||||
if len(spec) == 0 {
|
||||
return nil, fmt.Errorf("empty spec string")
|
||||
}
|
||||
|
||||
// Extract timezone if present
|
||||
var loc = time.Local
|
||||
if strings.HasPrefix(spec, "TZ=") || strings.HasPrefix(spec, "CRON_TZ=") {
|
||||
var err error
|
||||
i := strings.Index(spec, " ")
|
||||
eq := strings.Index(spec, "=")
|
||||
if loc, err = time.LoadLocation(spec[eq+1 : i]); err != nil {
|
||||
return nil, fmt.Errorf("provided bad location %s: %v", spec[eq+1:i], err)
|
||||
}
|
||||
spec = strings.TrimSpace(spec[i:])
|
||||
}
|
||||
|
||||
// Handle named schedules (descriptors), if configured
|
||||
if strings.HasPrefix(spec, "@") {
|
||||
if p.options&Descriptor == 0 {
|
||||
return nil, fmt.Errorf("parser does not accept descriptors: %v", spec)
|
||||
}
|
||||
return parseDescriptor(spec, loc)
|
||||
}
|
||||
|
||||
// Split on whitespace.
|
||||
fields := strings.Fields(spec)
|
||||
|
||||
// Validate & fill in any omitted or optional fields
|
||||
var err error
|
||||
fields, err = normalizeFields(fields, p.options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
field := func(field string, r bounds) uint64 {
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
var bits uint64
|
||||
bits, err = getField(field, r)
|
||||
return bits
|
||||
}
|
||||
|
||||
var (
|
||||
second = field(fields[0], seconds)
|
||||
minute = field(fields[1], minutes)
|
||||
hour = field(fields[2], hours)
|
||||
dayofmonth = field(fields[3], dom)
|
||||
month = field(fields[4], months)
|
||||
dayofweek = field(fields[5], dow)
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SpecSchedule{
|
||||
Second: second,
|
||||
Minute: minute,
|
||||
Hour: hour,
|
||||
Dom: dayofmonth,
|
||||
Month: month,
|
||||
Dow: dayofweek,
|
||||
Location: loc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// normalizeFields takes a subset set of the time fields and returns the full set
|
||||
// with defaults (zeroes) populated for unset fields.
|
||||
//
|
||||
// As part of performing this function, it also validates that the provided
|
||||
// fields are compatible with the configured options.
|
||||
func normalizeFields(fields []string, options ParseOption) ([]string, error) {
|
||||
// Validate optionals & add their field to options
|
||||
optionals := 0
|
||||
if options&SecondOptional > 0 {
|
||||
options |= Second
|
||||
optionals++
|
||||
}
|
||||
if options&DowOptional > 0 {
|
||||
options |= Dow
|
||||
optionals++
|
||||
}
|
||||
if optionals > 1 {
|
||||
return nil, fmt.Errorf("multiple optionals may not be configured")
|
||||
}
|
||||
|
||||
// Figure out how many fields we need
|
||||
max := 0
|
||||
for _, place := range places {
|
||||
if options&place > 0 {
|
||||
max++
|
||||
}
|
||||
}
|
||||
min := max - optionals
|
||||
|
||||
// Validate number of fields
|
||||
if count := len(fields); count < min || count > max {
|
||||
if min == max {
|
||||
return nil, fmt.Errorf("expected exactly %d fields, found %d: %s", min, count, fields)
|
||||
}
|
||||
return nil, fmt.Errorf("expected %d to %d fields, found %d: %s", min, max, count, fields)
|
||||
}
|
||||
|
||||
// Populate the optional field if not provided
|
||||
if min < max && len(fields) == min {
|
||||
switch {
|
||||
case options&DowOptional > 0:
|
||||
fields = append(fields, defaults[5]) // TODO: improve access to default
|
||||
case options&SecondOptional > 0:
|
||||
fields = append([]string{defaults[0]}, fields...)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown optional field")
|
||||
}
|
||||
}
|
||||
|
||||
// Populate all fields not part of options with their defaults
|
||||
n := 0
|
||||
expandedFields := make([]string, len(places))
|
||||
copy(expandedFields, defaults)
|
||||
for i, place := range places {
|
||||
if options&place > 0 {
|
||||
expandedFields[i] = fields[n]
|
||||
n++
|
||||
}
|
||||
}
|
||||
return expandedFields, nil
|
||||
}
|
||||
|
||||
var standardParser = NewParser(
|
||||
Minute | Hour | Dom | Month | Dow | Descriptor,
|
||||
)
|
||||
|
||||
// ParseStandard returns a new crontab schedule representing the given
|
||||
// standardSpec (https://en.wikipedia.org/wiki/Cron). It requires 5 entries
|
||||
// representing: minute, hour, day of month, month and day of week, in that
|
||||
// order. It returns a descriptive error if the spec is not valid.
|
||||
//
|
||||
// It accepts
|
||||
// - Standard crontab specs, e.g. "* * * * ?"
|
||||
// - Descriptors, e.g. "@midnight", "@every 1h30m"
|
||||
func ParseStandard(standardSpec string) (Schedule, error) {
|
||||
return standardParser.Parse(standardSpec)
|
||||
}
|
||||
|
||||
// getField returns an Int with the bits set representing all of the times that
|
||||
// the field represents or error parsing field value. A "field" is a comma-separated
|
||||
// list of "ranges".
|
||||
func getField(field string, r bounds) (uint64, error) {
|
||||
var bits uint64
|
||||
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
|
||||
for _, expr := range ranges {
|
||||
bit, err := getRange(expr, r)
|
||||
if err != nil {
|
||||
return bits, err
|
||||
}
|
||||
bits |= bit
|
||||
}
|
||||
return bits, nil
|
||||
}
|
||||
|
||||
// getRange returns the bits indicated by the given expression:
|
||||
// number | number "-" number [ "/" number ]
|
||||
// or error parsing range.
|
||||
func getRange(expr string, r bounds) (uint64, error) {
|
||||
var (
|
||||
start, end, step uint
|
||||
rangeAndStep = strings.Split(expr, "/")
|
||||
lowAndHigh = strings.Split(rangeAndStep[0], "-")
|
||||
singleDigit = len(lowAndHigh) == 1
|
||||
err error
|
||||
)
|
||||
|
||||
var extra uint64
|
||||
if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
|
||||
start = r.min
|
||||
end = r.max
|
||||
extra = starBit
|
||||
} else {
|
||||
start, err = parseIntOrName(lowAndHigh[0], r.names)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch len(lowAndHigh) {
|
||||
case 1:
|
||||
end = start
|
||||
case 2:
|
||||
end, err = parseIntOrName(lowAndHigh[1], r.names)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
default:
|
||||
return 0, fmt.Errorf("too many hyphens: %s", expr)
|
||||
}
|
||||
}
|
||||
|
||||
switch len(rangeAndStep) {
|
||||
case 1:
|
||||
step = 1
|
||||
case 2:
|
||||
step, err = mustParseInt(rangeAndStep[1])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Special handling: "N/step" means "N-max/step".
|
||||
if singleDigit {
|
||||
end = r.max
|
||||
}
|
||||
if step > 1 {
|
||||
extra = 0
|
||||
}
|
||||
default:
|
||||
return 0, fmt.Errorf("too many slashes: %s", expr)
|
||||
}
|
||||
|
||||
if start < r.min {
|
||||
return 0, fmt.Errorf("beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
|
||||
}
|
||||
if end > r.max {
|
||||
return 0, fmt.Errorf("end of range (%d) above maximum (%d): %s", end, r.max, expr)
|
||||
}
|
||||
if start > end {
|
||||
return 0, fmt.Errorf("beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
|
||||
}
|
||||
if step == 0 {
|
||||
return 0, fmt.Errorf("step of range should be a positive number: %s", expr)
|
||||
}
|
||||
|
||||
return getBits(start, end, step) | extra, nil
|
||||
}
|
||||
|
||||
// parseIntOrName returns the (possibly-named) integer contained in expr.
|
||||
func parseIntOrName(expr string, names map[string]uint) (uint, error) {
|
||||
if names != nil {
|
||||
if namedInt, ok := names[strings.ToLower(expr)]; ok {
|
||||
return namedInt, nil
|
||||
}
|
||||
}
|
||||
return mustParseInt(expr)
|
||||
}
|
||||
|
||||
// mustParseInt parses the given expression as an int or returns an error.
|
||||
func mustParseInt(expr string) (uint, error) {
|
||||
num, err := strconv.Atoi(expr)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse int from %s: %s", expr, err)
|
||||
}
|
||||
if num < 0 {
|
||||
return 0, fmt.Errorf("negative number (%d) not allowed: %s", num, expr)
|
||||
}
|
||||
|
||||
return uint(num), nil
|
||||
}
|
||||
|
||||
// getBits sets all bits in the range [min, max], modulo the given step size.
|
||||
func getBits(min, max, step uint) uint64 {
|
||||
var bits uint64
|
||||
|
||||
// If step is 1, use shifts.
|
||||
if step == 1 {
|
||||
return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
|
||||
}
|
||||
|
||||
// Else, use a simple loop.
|
||||
for i := min; i <= max; i += step {
|
||||
bits |= 1 << i
|
||||
}
|
||||
return bits
|
||||
}
|
||||
|
||||
// all returns all bits within the given bounds. (plus the star bit)
|
||||
func all(r bounds) uint64 {
|
||||
return getBits(r.min, r.max, 1) | starBit
|
||||
}
|
||||
|
||||
// parseDescriptor returns a predefined schedule for the expression, or error if none matches.
|
||||
func parseDescriptor(descriptor string, loc *time.Location) (Schedule, error) {
|
||||
switch descriptor {
|
||||
case "@yearly", "@annually":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: 1 << hours.min,
|
||||
Dom: 1 << dom.min,
|
||||
Month: 1 << months.min,
|
||||
Dow: all(dow),
|
||||
Location: loc,
|
||||
}, nil
|
||||
|
||||
case "@monthly":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: 1 << hours.min,
|
||||
Dom: 1 << dom.min,
|
||||
Month: all(months),
|
||||
Dow: all(dow),
|
||||
Location: loc,
|
||||
}, nil
|
||||
|
||||
case "@weekly":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: 1 << hours.min,
|
||||
Dom: all(dom),
|
||||
Month: all(months),
|
||||
Dow: 1 << dow.min,
|
||||
Location: loc,
|
||||
}, nil
|
||||
|
||||
case "@daily", "@midnight":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: 1 << hours.min,
|
||||
Dom: all(dom),
|
||||
Month: all(months),
|
||||
Dow: all(dow),
|
||||
Location: loc,
|
||||
}, nil
|
||||
|
||||
case "@hourly":
|
||||
return &SpecSchedule{
|
||||
Second: 1 << seconds.min,
|
||||
Minute: 1 << minutes.min,
|
||||
Hour: all(hours),
|
||||
Dom: all(dom),
|
||||
Month: all(months),
|
||||
Dow: all(dow),
|
||||
Location: loc,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
const every = "@every "
|
||||
if strings.HasPrefix(descriptor, every) {
|
||||
duration, err := time.ParseDuration(descriptor[len(every):])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse duration %s: %s", descriptor, err)
|
||||
}
|
||||
return Every(duration), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unrecognized descriptor: %s", descriptor)
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
package cron
|
||||
|
||||
import "time"
|
||||
|
||||
// SpecSchedule specifies a duty cycle (to the second granularity), based on a
|
||||
// traditional crontab specification. It is computed initially and stored as bit sets.
|
||||
type SpecSchedule struct {
|
||||
Second, Minute, Hour, Dom, Month, Dow uint64
|
||||
|
||||
// Override location for this schedule.
|
||||
Location *time.Location
|
||||
}
|
||||
|
||||
// bounds provides a range of acceptable values (plus a map of name to value).
|
||||
type bounds struct {
|
||||
min, max uint
|
||||
names map[string]uint
|
||||
}
|
||||
|
||||
// The bounds for each field.
|
||||
var (
|
||||
seconds = bounds{0, 59, nil}
|
||||
minutes = bounds{0, 59, nil}
|
||||
hours = bounds{0, 23, nil}
|
||||
dom = bounds{1, 31, nil}
|
||||
months = bounds{1, 12, map[string]uint{
|
||||
"jan": 1,
|
||||
"feb": 2,
|
||||
"mar": 3,
|
||||
"apr": 4,
|
||||
"may": 5,
|
||||
"jun": 6,
|
||||
"jul": 7,
|
||||
"aug": 8,
|
||||
"sep": 9,
|
||||
"oct": 10,
|
||||
"nov": 11,
|
||||
"dec": 12,
|
||||
}}
|
||||
dow = bounds{0, 6, map[string]uint{
|
||||
"sun": 0,
|
||||
"mon": 1,
|
||||
"tue": 2,
|
||||
"wed": 3,
|
||||
"thu": 4,
|
||||
"fri": 5,
|
||||
"sat": 6,
|
||||
}}
|
||||
)
|
||||
|
||||
const (
|
||||
// Set the top bit if a star was included in the expression.
|
||||
starBit = 1 << 63
|
||||
)
|
||||
|
||||
// Next returns the next time this schedule is activated, greater than the given
|
||||
// time. If no time can be found to satisfy the schedule, return the zero time.
|
||||
func (s *SpecSchedule) Next(t time.Time) time.Time {
|
||||
// General approach
|
||||
//
|
||||
// For Month, Day, Hour, Minute, Second:
|
||||
// Check if the time value matches. If yes, continue to the next field.
|
||||
// If the field doesn't match the schedule, then increment the field until it matches.
|
||||
// While incrementing the field, a wrap-around brings it back to the beginning
|
||||
// of the field list (since it is necessary to re-verify previous field
|
||||
// values)
|
||||
|
||||
// Convert the given time into the schedule's timezone, if one is specified.
|
||||
// Save the original timezone so we can convert back after we find a time.
|
||||
// Note that schedules without a time zone specified (time.Local) are treated
|
||||
// as local to the time provided.
|
||||
origLocation := t.Location()
|
||||
loc := s.Location
|
||||
if loc == time.Local {
|
||||
loc = t.Location()
|
||||
}
|
||||
if s.Location != time.Local {
|
||||
t = t.In(s.Location)
|
||||
}
|
||||
|
||||
// Start at the earliest possible time (the upcoming second).
|
||||
t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)
|
||||
|
||||
// This flag indicates whether a field has been incremented.
|
||||
added := false
|
||||
|
||||
// If no time is found within five years, return zero.
|
||||
yearLimit := t.Year() + 5
|
||||
|
||||
WRAP:
|
||||
if t.Year() > yearLimit {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// Find the first applicable month.
|
||||
// If it's this month, then do nothing.
|
||||
for 1<<uint(t.Month())&s.Month == 0 {
|
||||
// If we have to add a month, reset the other parts to 0.
|
||||
if !added {
|
||||
added = true
|
||||
// Otherwise, set the date at the beginning (since the current time is irrelevant).
|
||||
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc)
|
||||
}
|
||||
t = t.AddDate(0, 1, 0)
|
||||
|
||||
// Wrapped around.
|
||||
if t.Month() == time.January {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
// Now get a day in that month.
|
||||
//
|
||||
// NOTE: This causes issues for daylight savings regimes where midnight does
|
||||
// not exist. For example: Sao Paulo has DST that transforms midnight on
|
||||
// 11/3 into 1am. Handle that by noticing when the Hour ends up != 0.
|
||||
for !dayMatches(s, t) {
|
||||
if !added {
|
||||
added = true
|
||||
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)
|
||||
}
|
||||
t = t.AddDate(0, 0, 1)
|
||||
// Notice if the hour is no longer midnight due to DST.
|
||||
// Add an hour if it's 23, subtract an hour if it's 1.
|
||||
if t.Hour() != 0 {
|
||||
if t.Hour() > 12 {
|
||||
t = t.Add(time.Duration(24-t.Hour()) * time.Hour)
|
||||
} else {
|
||||
t = t.Add(time.Duration(-t.Hour()) * time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
if t.Day() == 1 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
for 1<<uint(t.Hour())&s.Hour == 0 {
|
||||
if !added {
|
||||
added = true
|
||||
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc)
|
||||
}
|
||||
t = t.Add(1 * time.Hour)
|
||||
|
||||
if t.Hour() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
for 1<<uint(t.Minute())&s.Minute == 0 {
|
||||
if !added {
|
||||
added = true
|
||||
t = t.Truncate(time.Minute)
|
||||
}
|
||||
t = t.Add(1 * time.Minute)
|
||||
|
||||
if t.Minute() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
for 1<<uint(t.Second())&s.Second == 0 {
|
||||
if !added {
|
||||
added = true
|
||||
t = t.Truncate(time.Second)
|
||||
}
|
||||
t = t.Add(1 * time.Second)
|
||||
|
||||
if t.Second() == 0 {
|
||||
goto WRAP
|
||||
}
|
||||
}
|
||||
|
||||
return t.In(origLocation)
|
||||
}
|
||||
|
||||
// dayMatches returns true if the schedule's day-of-week and day-of-month
|
||||
// restrictions are satisfied by the given time.
|
||||
func dayMatches(s *SpecSchedule, t time.Time) bool {
|
||||
var (
|
||||
domMatch bool = 1<<uint(t.Day())&s.Dom > 0
|
||||
dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
|
||||
)
|
||||
if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
|
||||
return domMatch && dowMatch
|
||||
}
|
||||
return domMatch || dowMatch
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
onnxruntime_c_api.h linguist-vendored
|
||||
onnxruntime_ep_c_api.h linguist-vendored
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user