From 7a0bc33ad1fde1a4e40e11a739a3fed9180c7c4f Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Sun, 22 Mar 2026 18:20:46 -0500 Subject: [PATCH 01/15] Add tests for disc, dmsghttp, dmsgctrl, dmsgcurl, dmsgpty, dmsgserver and update dependency graph Improve test coverage across core packages: - pkg/disc: 24% -> 85.6% (client lifecycle, HTTP client, entry validation) - pkg/dmsghttp: 23.8% -> 65.5% (transport, GetServers, ListenAndServe) - pkg/dmsgctrl: 49.3% -> 84.9% (ServeListener, ping/pong, concurrency) - pkg/dmsgcurl: 16.2% -> 44.2% (URL parsing, progress writer, CancellableCopy) - pkg/dmsgpty: 43.1% -> 47.5% (whitelist, RPC utils, config) - pkg/dmsgserver: 0% -> 88.2% (config generation, flush) Also update README to use `go run github.com/loov/goda@latest` and regenerate the dependency graph SVG. --- README.md | 2 +- docs/dmsg-goda-graph.svg | 948 ++++++++++++++------------- pkg/disc/client_test.go | 980 ++++++++++++++++++++++++++++ pkg/dmsgctrl/serve_listener_test.go | 354 ++++++++++ pkg/dmsgcurl/url_test.go | 209 ++++++ pkg/dmsghttp/util_test.go | 507 ++++++++++++++ pkg/dmsgpty/whitelist_test.go | 475 ++++++++++++++ pkg/dmsgserver/config_test.go | 61 ++ 8 files changed, 3064 insertions(+), 472 deletions(-) create mode 100644 pkg/disc/client_test.go create mode 100644 pkg/dmsgctrl/serve_listener_test.go create mode 100644 pkg/dmsgcurl/url_test.go create mode 100644 pkg/dmsghttp/util_test.go create mode 100644 pkg/dmsgpty/whitelist_test.go create mode 100644 pkg/dmsgserver/config_test.go diff --git a/README.md b/README.md index f4534b820..cbecadde8 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ The connection between a `dmsg.Client` and `dmsg.Server` is called a `dmsg.Sessi made with [goda](https://github.com/loov/goda) ``` -goda graph github.com/skycoin/dmsg/... | dot -Tsvg -o docs/dmsg-goda-graph.svg +go run github.com/loov/goda@latest graph github.com/skycoin/dmsg/... | dot -Tsvg -o docs/dmsg-goda-graph.svg ``` ![Dependency Graph](docs/dmsg-goda-graph.svg "github.com/skycoin/dmsg Dependency Graph") diff --git a/docs/dmsg-goda-graph.svg b/docs/dmsg-goda-graph.svg index 21c222abb..effe5d44e 100644 --- a/docs/dmsg-goda-graph.svg +++ b/docs/dmsg-goda-graph.svg @@ -1,21 +1,21 @@ - - - + + G - + github.com/skycoin/dmsg - -github.com/skycoin/dmsg -12 / 253B + +github.com/skycoin/dmsg +12 / 253B @@ -23,25 +23,25 @@ github.com/skycoin/dmsg/cmd/dmsg/commands - -github.com/skycoin/dmsg/cmd/dmsg/commands -145 / 4.0KB + +github.com/skycoin/dmsg/cmd/dmsg/commands +145 / 4.0KB github.com/skycoin/dmsg:e->github.com/skycoin/dmsg/cmd/dmsg/commands - - + + github.com/skycoin/dmsg/cmd/conf - -github.com/skycoin/dmsg/cmd/conf -12 / 261B + +github.com/skycoin/dmsg/cmd/conf +12 / 261B @@ -49,41 +49,41 @@ github.com/skycoin/dmsg/cmd/conf/commands - -github.com/skycoin/dmsg/cmd/conf/commands -29 / 0.8KB + +github.com/skycoin/dmsg/cmd/conf/commands +29 / 0.8KB github.com/skycoin/dmsg/cmd/conf:e->github.com/skycoin/dmsg/cmd/conf/commands - - + + github.com/skycoin/dmsg/pkg/dmsg - - -github.com/skycoin/dmsg/pkg/dmsg -2510 / 72.7KB + + +github.com/skycoin/dmsg/pkg/dmsg +2578 / 75.3KB github.com/skycoin/dmsg/cmd/conf/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/cmd/dial - -github.com/skycoin/dmsg/cmd/dial -12 / 261B + +github.com/skycoin/dmsg/cmd/dial +12 / 261B @@ -91,337 +91,337 @@ github.com/skycoin/dmsg/cmd/dial/commands - -github.com/skycoin/dmsg/cmd/dial/commands -170 / 6.1KB + +github.com/skycoin/dmsg/cmd/dial/commands +170 / 6.1KB github.com/skycoin/dmsg/cmd/dial:e->github.com/skycoin/dmsg/cmd/dial/commands - - + + github.com/skycoin/dmsg/internal/cli - - -github.com/skycoin/dmsg/internal/cli -439 / 17.2KB + + +github.com/skycoin/dmsg/internal/cli +474 / 18.6KB github.com/skycoin/dmsg/cmd/dial/commands:e->github.com/skycoin/dmsg/internal/cli - - + + github.com/skycoin/dmsg/internal/flags - -github.com/skycoin/dmsg/internal/flags -45 / 1.7KB + +github.com/skycoin/dmsg/internal/flags +45 / 1.7KB github.com/skycoin/dmsg/cmd/dial/commands:e->github.com/skycoin/dmsg/internal/flags - - + + github.com/skycoin/dmsg/pkg/disc - - -github.com/skycoin/dmsg/pkg/disc -749 / 23.9KB + + +github.com/skycoin/dmsg/pkg/disc +856 / 27.1KB github.com/skycoin/dmsg/cmd/dial/commands:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/cmd/dial/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/cmd/dmsg - -github.com/skycoin/dmsg/cmd/dmsg -12 / 262B + +github.com/skycoin/dmsg/cmd/dmsg +12 / 262B github.com/skycoin/dmsg/cmd/dmsg:e->github.com/skycoin/dmsg/cmd/dmsg/commands - - + + github.com/skycoin/dmsg/cmd/dmsg-discovery - -github.com/skycoin/dmsg/cmd/dmsg-discovery -12 / 291B + +github.com/skycoin/dmsg/cmd/dmsg-discovery +12 / 291B github.com/skycoin/dmsg/cmd/dmsg-discovery/commands - - -github.com/skycoin/dmsg/cmd/dmsg-discovery/commands -293 / 10.3KB + + +github.com/skycoin/dmsg/cmd/dmsg-discovery/commands +468 / 16.3KB github.com/skycoin/dmsg/cmd/dmsg-discovery:e->github.com/skycoin/dmsg/cmd/dmsg-discovery/commands - - + + github.com/skycoin/dmsg/internal/discmetrics - -github.com/skycoin/dmsg/internal/discmetrics -44 / 1.5KB + +github.com/skycoin/dmsg/internal/discmetrics +44 / 1.5KB github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/internal/discmetrics - - + + github.com/skycoin/dmsg/internal/dmsg-discovery/api - - -github.com/skycoin/dmsg/internal/dmsg-discovery/api -470 / 15.9KB + + +github.com/skycoin/dmsg/internal/dmsg-discovery/api +536 / 17.8KB github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/internal/dmsg-discovery/api - - + + github.com/skycoin/dmsg/internal/dmsg-discovery/store - - -github.com/skycoin/dmsg/internal/dmsg-discovery/store -464 / 14.8KB + + +github.com/skycoin/dmsg/internal/dmsg-discovery/store +512 / 16.3KB github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/internal/dmsg-discovery/store - - + + github.com/skycoin/dmsg/pkg/direct - - -github.com/skycoin/dmsg/pkg/direct -157 / 4.8KB + + +github.com/skycoin/dmsg/pkg/direct +186 / 5.7KB github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/pkg/direct - - + + github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/pkg/dmsghttp - - -github.com/skycoin/dmsg/pkg/dmsghttp -181 / 5.2KB + + +github.com/skycoin/dmsg/pkg/dmsghttp +199 / 5.7KB github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/pkg/dmsghttp - - + + github.com/skycoin/dmsg/cmd/dmsg-server - -github.com/skycoin/dmsg/cmd/dmsg-server -12 / 282B + +github.com/skycoin/dmsg/cmd/dmsg-server +12 / 282B github.com/skycoin/dmsg/cmd/dmsg-server/commands - - -github.com/skycoin/dmsg/cmd/dmsg-server/commands -44 / 1.4KB + + +github.com/skycoin/dmsg/cmd/dmsg-server/commands +47 / 1.5KB github.com/skycoin/dmsg/cmd/dmsg-server:e->github.com/skycoin/dmsg/cmd/dmsg-server/commands - - + + github.com/skycoin/dmsg/cmd/dmsg-server/commands/config - -github.com/skycoin/dmsg/cmd/dmsg-server/commands/config -54 / 1.5KB + +github.com/skycoin/dmsg/cmd/dmsg-server/commands/config +54 / 1.5KB github.com/skycoin/dmsg/cmd/dmsg-server/commands:e->github.com/skycoin/dmsg/cmd/dmsg-server/commands/config - - + + github.com/skycoin/dmsg/cmd/dmsg-server/commands/start - -github.com/skycoin/dmsg/cmd/dmsg-server/commands/start -139 / 4.7KB + +github.com/skycoin/dmsg/cmd/dmsg-server/commands/start +139 / 4.7KB github.com/skycoin/dmsg/cmd/dmsg-server/commands:e->github.com/skycoin/dmsg/cmd/dmsg-server/commands/start - - + + github.com/skycoin/dmsg/pkg/dmsgserver - -github.com/skycoin/dmsg/pkg/dmsgserver -59 / 1.9KB + +github.com/skycoin/dmsg/pkg/dmsgserver +59 / 1.9KB github.com/skycoin/dmsg/cmd/dmsg-server/commands/config:e->github.com/skycoin/dmsg/pkg/dmsgserver - - + + github.com/skycoin/dmsg/internal/dmsg-server/api - - -github.com/skycoin/dmsg/internal/dmsg-server/api -231 / 7.1KB + + +github.com/skycoin/dmsg/internal/dmsg-server/api +228 / 7.1KB github.com/skycoin/dmsg/cmd/dmsg-server/commands/start:e->github.com/skycoin/dmsg/internal/dmsg-server/api - - + + github.com/skycoin/dmsg/internal/servermetrics - -github.com/skycoin/dmsg/internal/servermetrics -111 / 4.0KB + +github.com/skycoin/dmsg/internal/servermetrics +111 / 4.0KB github.com/skycoin/dmsg/cmd/dmsg-server/commands/start:e->github.com/skycoin/dmsg/internal/servermetrics - - + + github.com/skycoin/dmsg/cmd/dmsg-server/commands/start:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/cmd/dmsg-server/commands/start:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/cmd/dmsg-server/commands/start:e->github.com/skycoin/dmsg/pkg/dmsgserver - - + + github.com/skycoin/dmsg/cmd/dmsg-socks5 - -github.com/skycoin/dmsg/cmd/dmsg-socks5 -12 / 282B + +github.com/skycoin/dmsg/cmd/dmsg-socks5 +12 / 282B @@ -429,481 +429,481 @@ github.com/skycoin/dmsg/cmd/dmsg-socks5/commands - -github.com/skycoin/dmsg/cmd/dmsg-socks5/commands -249 / 8.2KB + +github.com/skycoin/dmsg/cmd/dmsg-socks5/commands +249 / 8.2KB github.com/skycoin/dmsg/cmd/dmsg-socks5:e->github.com/skycoin/dmsg/cmd/dmsg-socks5/commands - - + + github.com/skycoin/dmsg/cmd/dmsg-socks5/commands:e->github.com/skycoin/dmsg/internal/cli - - + + github.com/skycoin/dmsg/cmd/dmsg-socks5/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/conf/commands - - + + github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dial/commands - - + + github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsg-discovery/commands - - + + github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsg-server/commands - - + + github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsg-socks5/commands - - + + github.com/skycoin/dmsg/cmd/dmsgcurl/commands - - -github.com/skycoin/dmsg/cmd/dmsgcurl/commands -393 / 12.8KB + + +github.com/skycoin/dmsg/cmd/dmsgcurl/commands +398 / 12.9KB github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsgcurl/commands - - + + github.com/skycoin/dmsg/cmd/dmsghttp/commands - -github.com/skycoin/dmsg/cmd/dmsghttp/commands -286 / 8.0KB + +github.com/skycoin/dmsg/cmd/dmsghttp/commands +286 / 8.0KB github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsghttp/commands - - + + github.com/skycoin/dmsg/cmd/dmsgip/commands - -github.com/skycoin/dmsg/cmd/dmsgip/commands -124 / 4.1KB + +github.com/skycoin/dmsg/cmd/dmsgip/commands +124 / 4.1KB github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsgip/commands - - + + github.com/skycoin/dmsg/cmd/dmsgpty-cli/commands - -github.com/skycoin/dmsg/cmd/dmsgpty-cli/commands -195 / 5.6KB + +github.com/skycoin/dmsg/cmd/dmsgpty-cli/commands +195 / 5.6KB github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsgpty-cli/commands - - + + github.com/skycoin/dmsg/cmd/dmsgpty-host/commands - -github.com/skycoin/dmsg/cmd/dmsgpty-host/commands -328 / 10.1KB + +github.com/skycoin/dmsg/cmd/dmsgpty-host/commands +328 / 10.1KB github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsgpty-host/commands - - + + github.com/skycoin/dmsg/cmd/dmsgpty-ui/commands - -github.com/skycoin/dmsg/cmd/dmsgpty-ui/commands -67 / 2.2KB + +github.com/skycoin/dmsg/cmd/dmsgpty-ui/commands +67 / 2.2KB github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsgpty-ui/commands - - + + github.com/skycoin/dmsg/cmd/dmsgweb/commands - - -github.com/skycoin/dmsg/cmd/dmsgweb/commands -1084 / 33.9KB + + +github.com/skycoin/dmsg/cmd/dmsgweb/commands +1084 / 33.8KB github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsgweb/commands - - + + github.com/skycoin/dmsg/cmd/dmsgcurl - -github.com/skycoin/dmsg/cmd/dmsgcurl -12 / 273B + +github.com/skycoin/dmsg/cmd/dmsgcurl +12 / 273B github.com/skycoin/dmsg/cmd/dmsgcurl:e->github.com/skycoin/dmsg/cmd/dmsgcurl/commands - - + + github.com/skycoin/dmsg/cmd/dmsgcurl/commands:e->github.com/skycoin/dmsg/internal/cli - - + + github.com/skycoin/dmsg/cmd/dmsgcurl/commands:e->github.com/skycoin/dmsg/internal/flags - - + + github.com/skycoin/dmsg/cmd/dmsgcurl/commands:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/cmd/dmsgcurl/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/cmd/dmsgcurl/commands:e->github.com/skycoin/dmsg/pkg/dmsghttp - - + + github.com/skycoin/dmsg/cmd/dmsghttp - -github.com/skycoin/dmsg/cmd/dmsghttp -12 / 285B + +github.com/skycoin/dmsg/cmd/dmsghttp +12 / 285B github.com/skycoin/dmsg/cmd/dmsghttp:e->github.com/skycoin/dmsg/cmd/dmsghttp/commands - - + + github.com/skycoin/dmsg/cmd/dmsghttp/commands:e->github.com/skycoin/dmsg/internal/cli - - + + github.com/skycoin/dmsg/cmd/dmsghttp/commands:e->github.com/skycoin/dmsg/internal/flags - - + + github.com/skycoin/dmsg/cmd/dmsghttp/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/cmd/dmsgip - -github.com/skycoin/dmsg/cmd/dmsgip -12 / 271B + +github.com/skycoin/dmsg/cmd/dmsgip +12 / 271B github.com/skycoin/dmsg/cmd/dmsgip:e->github.com/skycoin/dmsg/cmd/dmsgip/commands - - + + github.com/skycoin/dmsg/cmd/dmsgip/commands:e->github.com/skycoin/dmsg/internal/cli - - + + github.com/skycoin/dmsg/cmd/dmsgip/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/cmd/dmsgpty-cli - -github.com/skycoin/dmsg/cmd/dmsgpty-cli -12 / 282B + +github.com/skycoin/dmsg/cmd/dmsgpty-cli +12 / 282B github.com/skycoin/dmsg/cmd/dmsgpty-cli:e->github.com/skycoin/dmsg/cmd/dmsgpty-cli/commands - - + + github.com/skycoin/dmsg/cmd/dmsgpty-cli/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/pkg/dmsgpty - - -github.com/skycoin/dmsg/pkg/dmsgpty -3747 / 248.6KB + + +github.com/skycoin/dmsg/pkg/dmsgpty +7557 / 597.5KB github.com/skycoin/dmsg/cmd/dmsgpty-cli/commands:e->github.com/skycoin/dmsg/pkg/dmsgpty - - + + github.com/skycoin/dmsg/cmd/dmsgpty-host - -github.com/skycoin/dmsg/cmd/dmsgpty-host -12 / 285B + +github.com/skycoin/dmsg/cmd/dmsgpty-host +12 / 285B github.com/skycoin/dmsg/cmd/dmsgpty-host:e->github.com/skycoin/dmsg/cmd/dmsgpty-host/commands - - + + github.com/skycoin/dmsg/internal/fsutil - -github.com/skycoin/dmsg/internal/fsutil -16 / 296B + +github.com/skycoin/dmsg/internal/fsutil +16 / 296B github.com/skycoin/dmsg/cmd/dmsgpty-host/commands:e->github.com/skycoin/dmsg/internal/fsutil - - + + github.com/skycoin/dmsg/cmd/dmsgpty-host/commands:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/cmd/dmsgpty-host/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/cmd/dmsgpty-host/commands:e->github.com/skycoin/dmsg/pkg/dmsgpty - - + + github.com/skycoin/dmsg/cmd/dmsgpty-ui - -github.com/skycoin/dmsg/cmd/dmsgpty-ui -12 / 279B + +github.com/skycoin/dmsg/cmd/dmsgpty-ui +12 / 279B github.com/skycoin/dmsg/cmd/dmsgpty-ui:e->github.com/skycoin/dmsg/cmd/dmsgpty-ui/commands - - + + github.com/skycoin/dmsg/cmd/dmsgpty-ui/commands:e->github.com/skycoin/dmsg/pkg/dmsgpty - - + + github.com/skycoin/dmsg/cmd/dmsgweb - -github.com/skycoin/dmsg/cmd/dmsgweb -12 / 270B + +github.com/skycoin/dmsg/cmd/dmsgweb +12 / 270B github.com/skycoin/dmsg/cmd/dmsgweb:e->github.com/skycoin/dmsg/cmd/dmsgweb/commands - - + + github.com/skycoin/dmsg/cmd/dmsgweb/commands:e->github.com/skycoin/dmsg/internal/cli - - + + github.com/skycoin/dmsg/cmd/dmsgweb/commands:e->github.com/skycoin/dmsg/internal/flags - - + + github.com/skycoin/dmsg/cmd/dmsgweb/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/cmd/dmsgweb/commands:e->github.com/skycoin/dmsg/pkg/dmsghttp - - + + github.com/skycoin/dmsg/examples/basics - -github.com/skycoin/dmsg/examples/basics -111 / 3.5KB + +github.com/skycoin/dmsg/examples/basics +111 / 3.5KB github.com/skycoin/dmsg/examples/basics:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/examples/basics:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/examples/dmsgcurl/dmsg-example-http-server - -github.com/skycoin/dmsg/examples/dmsgcurl/dmsg-example-http-server -80 / 2.1KB + +github.com/skycoin/dmsg/examples/dmsgcurl/dmsg-example-http-server +80 / 2.1KB github.com/skycoin/dmsg/examples/dmsgcurl/dmsg-example-http-server:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/examples/dmsgcurl/dmsg-example-http-server:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/examples/dmsgcurl/gen-keys - -github.com/skycoin/dmsg/examples/dmsgcurl/gen-keys -10 / 215B + +github.com/skycoin/dmsg/examples/dmsgcurl/gen-keys +10 / 215B @@ -911,81 +911,81 @@ github.com/skycoin/dmsg/examples/dmsghttp - -github.com/skycoin/dmsg/examples/dmsghttp -133 / 4.3KB + +github.com/skycoin/dmsg/examples/dmsghttp +133 / 4.3KB github.com/skycoin/dmsg/examples/dmsghttp:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/examples/dmsghttp:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/examples/dmsghttp-client - -github.com/skycoin/dmsg/examples/dmsghttp-client -46 / 1.3KB + +github.com/skycoin/dmsg/examples/dmsghttp-client +46 / 1.3KB github.com/skycoin/dmsg/examples/dmsghttp-client:e->github.com/skycoin/dmsg/internal/cli - - + + github.com/skycoin/dmsg/examples/dmsghttp-client:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/examples/dmsghttp-client:e->github.com/skycoin/dmsg/pkg/dmsghttp - - + + github.com/skycoin/dmsg/examples/dmsgtcp - -github.com/skycoin/dmsg/examples/dmsgtcp -144 / 4.5KB + +github.com/skycoin/dmsg/examples/dmsgtcp +144 / 4.5KB github.com/skycoin/dmsg/examples/dmsgtcp:e->github.com/skycoin/dmsg/internal/cli - - + + github.com/skycoin/dmsg/examples/dmsgtcp:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/examples/dmsgweb - -github.com/skycoin/dmsg/examples/dmsgweb -39 / 1.5KB + +github.com/skycoin/dmsg/examples/dmsgweb +39 / 1.5KB @@ -993,43 +993,43 @@ github.com/skycoin/dmsg/examples/dmsgweb/commands - -github.com/skycoin/dmsg/examples/dmsgweb/commands -342 / 10.4KB + +github.com/skycoin/dmsg/examples/dmsgweb/commands +342 / 10.4KB github.com/skycoin/dmsg/examples/dmsgweb:e->github.com/skycoin/dmsg/examples/dmsgweb/commands - - + + github.com/skycoin/dmsg/examples/dmsgweb/commands:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/examples/dmsgweb/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/examples/dmsgweb/commands:e->github.com/skycoin/dmsg/pkg/dmsghttp - - + + github.com/skycoin/dmsg/examples/gen-keys - -github.com/skycoin/dmsg/examples/gen-keys -10 / 211B + +github.com/skycoin/dmsg/examples/gen-keys +10 / 211B @@ -1037,9 +1037,9 @@ github.com/skycoin/dmsg/examples/http - -github.com/skycoin/dmsg/examples/http -30 / 0.7KB + +github.com/skycoin/dmsg/examples/http +30 / 0.7KB @@ -1047,31 +1047,31 @@ github.com/skycoin/dmsg/examples/proxified - -github.com/skycoin/dmsg/examples/proxified -97 / 3.3KB + +github.com/skycoin/dmsg/examples/proxified +97 / 3.3KB github.com/skycoin/dmsg/examples/proxified:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/examples/proxified:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/examples/tcp - -github.com/skycoin/dmsg/examples/tcp -37 / 0.8KB + +github.com/skycoin/dmsg/examples/tcp +37 / 0.8KB @@ -1079,31 +1079,31 @@ github.com/skycoin/dmsg/examples/tcp-multi-proxy-dmsg - -github.com/skycoin/dmsg/examples/tcp-multi-proxy-dmsg -146 / 4.7KB + +github.com/skycoin/dmsg/examples/tcp-multi-proxy-dmsg +146 / 4.7KB github.com/skycoin/dmsg/examples/tcp-multi-proxy-dmsg:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/examples/tcp-multi-proxy-dmsg:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/examples/tcp-proxy - -github.com/skycoin/dmsg/examples/tcp-proxy -73 / 1.8KB + +github.com/skycoin/dmsg/examples/tcp-proxy +73 / 1.8KB @@ -1111,131 +1111,131 @@ github.com/skycoin/dmsg/examples/tcp-proxy-dmsg - -github.com/skycoin/dmsg/examples/tcp-proxy-dmsg -219 / 7.1KB + +github.com/skycoin/dmsg/examples/tcp-proxy-dmsg +219 / 7.1KB github.com/skycoin/dmsg/examples/tcp-proxy-dmsg:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/examples/tcp-proxy-dmsg:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/examples/tcp-reverse-proxy-dmsg - -github.com/skycoin/dmsg/examples/tcp-reverse-proxy-dmsg -222 / 7.0KB + +github.com/skycoin/dmsg/examples/tcp-reverse-proxy-dmsg +222 / 7.0KB github.com/skycoin/dmsg/examples/tcp-reverse-proxy-dmsg:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/examples/tcp-reverse-proxy-dmsg:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/internal/cli:e->github.com/skycoin/dmsg/internal/flags - - + + github.com/skycoin/dmsg/internal/cli:e->github.com/skycoin/dmsg/pkg/direct - - + + github.com/skycoin/dmsg/internal/cli:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/internal/cli:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/internal/cli:e->github.com/skycoin/dmsg/pkg/dmsghttp - - + + github.com/skycoin/dmsg/internal/dmsg-discovery/api:e->github.com/skycoin/dmsg/internal/discmetrics - - + + github.com/skycoin/dmsg/internal/dmsg-discovery/api:e->github.com/skycoin/dmsg/internal/dmsg-discovery/store - - + + github.com/skycoin/dmsg/internal/dmsg-discovery/api:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/internal/dmsg-discovery/api:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/internal/dmsg-discovery/store:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/internal/dmsg-discovery/store:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/internal/dmsg-server/api:e->github.com/skycoin/dmsg/internal/servermetrics - - + + github.com/skycoin/dmsg/internal/dmsg-server/api:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/internal/e2e - -github.com/skycoin/dmsg/internal/e2e -0 / 0B + +github.com/skycoin/dmsg/internal/e2e +0 / 0B @@ -1243,157 +1243,163 @@ github.com/skycoin/dmsg/internal/e2e/testserver - -github.com/skycoin/dmsg/internal/e2e/testserver -43 / 1.2KB + +github.com/skycoin/dmsg/internal/e2e/testserver +43 / 1.2KB github.com/skycoin/dmsg/internal/flags:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/pkg/direct:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/pkg/direct:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/pkg/dmsg:e->github.com/skycoin/dmsg/internal/servermetrics - - + + github.com/skycoin/dmsg/pkg/dmsg:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/pkg/noise - - -github.com/skycoin/dmsg/pkg/noise -668 / 18.4KB + + +github.com/skycoin/dmsg/pkg/noise +702 / 19.5KB github.com/skycoin/dmsg/pkg/dmsg:e->github.com/skycoin/dmsg/pkg/noise - - + + github.com/skycoin/dmsg/pkg/dmsgctrl - - -github.com/skycoin/dmsg/pkg/dmsgctrl -145 / 3.2KB + + +github.com/skycoin/dmsg/pkg/dmsgctrl +179 / 4.0KB + + +github.com/skycoin/dmsg/pkg/dmsgctrl:e->github.com/skycoin/dmsg/pkg/dmsg + + + github.com/skycoin/dmsg/pkg/dmsgcurl - - -github.com/skycoin/dmsg/pkg/dmsgcurl -341 / 9.8KB + + +github.com/skycoin/dmsg/pkg/dmsgcurl +346 / 9.9KB - + github.com/skycoin/dmsg/pkg/dmsgcurl:e->github.com/skycoin/dmsg/pkg/disc - - + + - + github.com/skycoin/dmsg/pkg/dmsgcurl:e->github.com/skycoin/dmsg/pkg/dmsg - - + + - + github.com/skycoin/dmsg/pkg/dmsgcurl:e->github.com/skycoin/dmsg/pkg/dmsghttp - - + + - + github.com/skycoin/dmsg/pkg/dmsghttp:e->github.com/skycoin/dmsg/pkg/disc - - + + - + github.com/skycoin/dmsg/pkg/dmsghttp:e->github.com/skycoin/dmsg/pkg/dmsg - - + + - + github.com/skycoin/dmsg/pkg/dmsgpty:e->github.com/skycoin/dmsg/pkg/dmsg - - + + - + github.com/skycoin/dmsg/pkg/dmsgserver:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/pkg/dmsgtest - -github.com/skycoin/dmsg/pkg/dmsgtest -211 / 6.1KB + +github.com/skycoin/dmsg/pkg/dmsgtest +211 / 6.1KB - + github.com/skycoin/dmsg/pkg/dmsgtest:e->github.com/skycoin/dmsg/pkg/disc - - + + - + github.com/skycoin/dmsg/pkg/dmsgtest:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/pkg/ioutil - -github.com/skycoin/dmsg/pkg/ioutil -23 / 0.7KB + +github.com/skycoin/dmsg/pkg/ioutil +23 / 0.7KB - + github.com/skycoin/dmsg/pkg/noise:e->github.com/skycoin/dmsg/pkg/ioutil - - + + diff --git a/pkg/disc/client_test.go b/pkg/disc/client_test.go new file mode 100644 index 000000000..257c5d174 --- /dev/null +++ b/pkg/disc/client_test.go @@ -0,0 +1,980 @@ +package disc_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/skycoin/dmsg/pkg/disc" +) + +// --- Mock client Entry/PostEntry/PutEntry/DelEntry lifecycle --- + +func TestMockClientEntryLifecycle(t *testing.T) { + ctx := context.Background() + mock := disc.NewMock(0) + + pk, sk := cipher.GenerateKeyPair() + + // Entry should not exist yet. + _, err := mock.Entry(ctx, pk) + require.Error(t, err, "entry should not exist before being posted") + + // Create and post a server entry. + entry := disc.NewServerEntry(pk, 0, "localhost:9090", 10) + require.NoError(t, entry.Sign(sk)) + require.NoError(t, mock.PostEntry(ctx, entry)) + + // Retrieve and verify. + got, err := mock.Entry(ctx, pk) + require.NoError(t, err) + assert.Equal(t, pk, got.Static) + assert.Equal(t, "localhost:9090", got.Server.Address) + + // Update via PutEntry. + entry.Server.Address = "remotehost:9091" + require.NoError(t, mock.PutEntry(ctx, sk, entry)) + + got2, err := mock.Entry(ctx, pk) + require.NoError(t, err) + assert.Equal(t, "remotehost:9091", got2.Server.Address) + assert.Greater(t, got2.Sequence, got.Sequence) + + // Delete entry. + require.NoError(t, mock.DelEntry(ctx, entry)) + + _, err = mock.Entry(ctx, pk) + require.Error(t, err, "entry should be gone after deletion") +} + +func TestMockClientPostEntryValidatesIteration(t *testing.T) { + ctx := context.Background() + mock := disc.NewMock(0) + + pk, sk := cipher.GenerateKeyPair() + + // Post initial entry at sequence 0. + entry := disc.NewServerEntry(pk, 0, "addr:1234", 5) + require.NoError(t, entry.Sign(sk)) + require.NoError(t, mock.PostEntry(ctx, entry)) + + // Posting again with same sequence should fail (wrong sequence). + entry2 := disc.NewServerEntry(pk, 0, "addr:1234", 5) + require.NoError(t, entry2.Sign(sk)) + err := mock.PostEntry(ctx, entry2) + require.Error(t, err) +} + +func TestMockClientPutEntryWrongKey(t *testing.T) { + ctx := context.Background() + mock := disc.NewMock(0) + + pk, sk := cipher.GenerateKeyPair() + _, wrongSk := cipher.GenerateKeyPair() + + entry := disc.NewServerEntry(pk, 0, "addr:5555", 2) + require.NoError(t, entry.Sign(sk)) + require.NoError(t, mock.PostEntry(ctx, entry)) + + // PutEntry with wrong secret key should fail. + entry.Server.Address = "addr:6666" + err := mock.PutEntry(ctx, wrongSk, entry) + require.Error(t, err) +} + +func TestMockClientDelEntryNonExistent(t *testing.T) { + ctx := context.Background() + mock := disc.NewMock(0) + + pk, _ := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:1111", 1) + + // Deleting non-existent entry should not error (mock behavior). + require.NoError(t, mock.DelEntry(ctx, entry)) +} + +// --- AvailableServers / AllServers with mock --- + +func TestMockAvailableServersEmpty(t *testing.T) { + ctx := context.Background() + mock := disc.NewMock(0) + + servers, err := mock.AvailableServers(ctx) + require.NoError(t, err) + assert.Empty(t, servers) +} + +func TestMockAvailableServersOnlyReturnsServerEntries(t *testing.T) { + ctx := context.Background() + mock := disc.NewMock(0) + + // Add a client entry. + clientPK, clientSK := cipher.GenerateKeyPair() + serverPK, serverSK := cipher.GenerateKeyPair() + + clientEntry := disc.NewClientEntry(clientPK, 0, []cipher.PubKey{serverPK}) + require.NoError(t, clientEntry.Sign(clientSK)) + require.NoError(t, mock.PostEntry(ctx, clientEntry)) + + // Add a server entry. + serverEntry := disc.NewServerEntry(serverPK, 0, "server:8080", 10) + require.NoError(t, serverEntry.Sign(serverSK)) + require.NoError(t, mock.PostEntry(ctx, serverEntry)) + + servers, err := mock.AvailableServers(ctx) + require.NoError(t, err) + require.Len(t, servers, 1) + assert.Equal(t, serverPK, servers[0].Static) +} + +func TestMockAllServersReturnsServerEntries(t *testing.T) { + ctx := context.Background() + mock := disc.NewMock(0) + + pk1, sk1 := cipher.GenerateKeyPair() + pk2, sk2 := cipher.GenerateKeyPair() + + e1 := disc.NewServerEntry(pk1, 0, "s1:1111", 5) + require.NoError(t, e1.Sign(sk1)) + require.NoError(t, mock.PostEntry(ctx, e1)) + + e2 := disc.NewServerEntry(pk2, 0, "s2:2222", 3) + require.NoError(t, e2.Sign(sk2)) + require.NoError(t, mock.PostEntry(ctx, e2)) + + servers, err := mock.AllServers(ctx) + require.NoError(t, err) + assert.Len(t, servers, 2) +} + +// --- AllEntries --- + +func TestMockAllEntries(t *testing.T) { + ctx := context.Background() + mock := disc.NewMock(0) + + pk1, sk1 := cipher.GenerateKeyPair() + pk2, sk2 := cipher.GenerateKeyPair() + + e1 := disc.NewServerEntry(pk1, 0, "a:1", 1) + require.NoError(t, e1.Sign(sk1)) + require.NoError(t, mock.PostEntry(ctx, e1)) + + e2 := disc.NewClientEntry(pk2, 0, []cipher.PubKey{pk1}) + require.NoError(t, e2.Sign(sk2)) + require.NoError(t, mock.PostEntry(ctx, e2)) + + entries, err := mock.AllEntries(ctx) + require.NoError(t, err) + assert.Len(t, entries, 2) + + hexes := map[string]bool{pk1.Hex(): true, pk2.Hex(): true} + for _, h := range entries { + assert.True(t, hexes[h], "unexpected hex key in AllEntries") + } +} + +// --- AllClientsByServer --- + +func TestMockAllClientsByServer(t *testing.T) { + ctx := context.Background() + mock := disc.NewMock(0) + + serverPK, serverSK := cipher.GenerateKeyPair() + serverEntry := disc.NewServerEntry(serverPK, 0, "srv:1000", 10) + require.NoError(t, serverEntry.Sign(serverSK)) + require.NoError(t, mock.PostEntry(ctx, serverEntry)) + + clientPK1, clientSK1 := cipher.GenerateKeyPair() + clientPK2, clientSK2 := cipher.GenerateKeyPair() + + c1 := disc.NewClientEntry(clientPK1, 0, []cipher.PubKey{serverPK}) + require.NoError(t, c1.Sign(clientSK1)) + require.NoError(t, mock.PostEntry(ctx, c1)) + + c2 := disc.NewClientEntry(clientPK2, 0, []cipher.PubKey{serverPK}) + require.NoError(t, c2.Sign(clientSK2)) + require.NoError(t, mock.PostEntry(ctx, c2)) + + result, err := mock.AllClientsByServer(ctx) + require.NoError(t, err) + assert.Len(t, result[serverPK.Hex()], 2) +} + +// --- ClientsByServer --- + +func TestMockClientsByServer(t *testing.T) { + ctx := context.Background() + mock := disc.NewMock(0) + + serverPK1, serverSK1 := cipher.GenerateKeyPair() + serverPK2, serverSK2 := cipher.GenerateKeyPair() + + s1 := disc.NewServerEntry(serverPK1, 0, "s1:1", 5) + require.NoError(t, s1.Sign(serverSK1)) + require.NoError(t, mock.PostEntry(ctx, s1)) + + s2 := disc.NewServerEntry(serverPK2, 0, "s2:2", 5) + require.NoError(t, s2.Sign(serverSK2)) + require.NoError(t, mock.PostEntry(ctx, s2)) + + clientPK, clientSK := cipher.GenerateKeyPair() + c := disc.NewClientEntry(clientPK, 0, []cipher.PubKey{serverPK1}) + require.NoError(t, c.Sign(clientSK)) + require.NoError(t, mock.PostEntry(ctx, c)) + + // Should find client under serverPK1. + clients, err := mock.ClientsByServer(ctx, serverPK1) + require.NoError(t, err) + assert.Len(t, clients, 1) + assert.Equal(t, clientPK, clients[0].Static) + + // Should find no clients under serverPK2. + clients2, err := mock.ClientsByServer(ctx, serverPK2) + require.NoError(t, err) + assert.Empty(t, clients2) +} + +// --- HTTPMessage String() --- + +func TestHTTPMessageString(t *testing.T) { + msg := disc.HTTPMessage{Code: http.StatusOK, Message: "wrote a new entry"} + assert.Equal(t, "status code: 200. message: wrote a new entry", msg.String()) + + msg2 := disc.HTTPMessage{Code: http.StatusNotFound, Message: "entry of public key is not found"} + assert.Equal(t, "status code: 404. message: entry of public key is not found", msg2.String()) + + msg3 := disc.HTTPMessage{Code: 0, Message: ""} + assert.Equal(t, "status code: 0. message: ", msg3.String()) +} + +func TestExposedHTTPMessages(t *testing.T) { + assert.Equal(t, http.StatusOK, disc.MsgEntrySet.Code) + assert.Equal(t, "wrote a new entry", disc.MsgEntrySet.Message) + + assert.Equal(t, http.StatusOK, disc.MsgEntryUpdated.Code) + assert.Equal(t, "wrote new entry iteration", disc.MsgEntryUpdated.Message) + + assert.Equal(t, http.StatusOK, disc.MsgEntryDeleted.Code) + assert.Equal(t, "deleted entry", disc.MsgEntryDeleted.Message) +} + +// --- NewHTTP constructor --- + +func TestNewHTTPCreatesValidClient(t *testing.T) { + log := logging.MustGetLogger("disc_test") + client := disc.NewHTTP("http://localhost:9999", &http.Client{}, log) + require.NotNil(t, client) + + // Verify it implements APIClient by attempting a call that will fail with connection refused. + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + t.Cleanup(cancel) + + _, err := client.Entry(ctx, cipher.PubKey{}) + require.Error(t, err, "should fail because server is not running") +} + +func TestNewHTTPWithNilHTTPClient(t *testing.T) { + log := logging.MustGetLogger("disc_test") + // Passing nil http.Client -- NewHTTP stores it; calls will panic/error at request time. + client := disc.NewHTTP("http://localhost:9999", nil, log) + require.NotNil(t, client) +} + +// --- Entry Copy function --- + +func TestCopyServerEntry(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + src := disc.NewServerEntry(pk, 5, "addr:8080", 20) + require.NoError(t, src.Sign(sk)) + + dst := &disc.Entry{} + disc.Copy(dst, src) + + assert.Equal(t, src.Static, dst.Static) + assert.Equal(t, src.Version, dst.Version) + assert.Equal(t, src.Sequence, dst.Sequence) + assert.Equal(t, src.Timestamp, dst.Timestamp) + assert.Equal(t, src.Signature, dst.Signature) + require.NotNil(t, dst.Server) + assert.Equal(t, src.Server.Address, dst.Server.Address) + assert.Equal(t, src.Server.AvailableSessions, dst.Server.AvailableSessions) + assert.Nil(t, dst.Client) + + // Verify deep copy -- mutating dst should not affect src. + dst.Server.Address = "changed" + assert.NotEqual(t, src.Server.Address, dst.Server.Address) +} + +func TestCopyClientEntry(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + serverPK, _ := cipher.GenerateKeyPair() + src := disc.NewClientEntry(pk, 3, []cipher.PubKey{serverPK}) + require.NoError(t, src.Sign(sk)) + + dst := &disc.Entry{} + disc.Copy(dst, src) + + assert.Equal(t, src.Static, dst.Static) + require.NotNil(t, dst.Client) + require.Len(t, dst.Client.DelegatedServers, 1) + assert.Equal(t, serverPK, dst.Client.DelegatedServers[0]) + assert.Nil(t, dst.Server) + + // Verify deep copy of DelegatedServers slice. + dst.Client.DelegatedServers[0] = cipher.PubKey{} + assert.NotEqual(t, src.Client.DelegatedServers[0], dst.Client.DelegatedServers[0]) +} + +func TestCopyNilFields(t *testing.T) { + // Copy from empty to populated should clear dst fields. + src := &disc.Entry{} + dst := &disc.Entry{ + Client: &disc.Client{DelegatedServers: []cipher.PubKey{{}}}, + Server: &disc.Server{Address: "x"}, + } + disc.Copy(dst, src) + assert.Nil(t, dst.Client) + assert.Nil(t, dst.Server) +} + +// --- Error variables are distinct --- + +func TestErrorVariablesAreDistinct(t *testing.T) { + errs := []error{ + disc.ErrKeyNotFound, + disc.ErrNoAvailableServers, + disc.ErrUnexpected, + disc.ErrUnauthorized, + disc.ErrBadInput, + disc.ErrValidationNonZeroSequence, + disc.ErrValidationNilEphemerals, + disc.ErrValidationNilKeys, + disc.ErrValidationNonNilEphemerals, + disc.ErrValidationNoSignature, + disc.ErrValidationNoVersion, + disc.ErrValidationNoClientOrServer, + disc.ErrValidationWrongSequence, + disc.ErrValidationWrongTime, + disc.ErrValidationOutdatedTime, + disc.ErrValidationServerAddress, + disc.ErrValidationEmptyServerAddress, + disc.ErrUnauthorizedNetworkMonitor, + } + + // All errors must be non-nil and have distinct messages. + seen := make(map[string]bool, len(errs)) + for _, e := range errs { + require.NotNil(t, e) + msg := e.Error() + assert.False(t, seen[msg], "duplicate error message: %s", msg) + seen[msg] = true + } +} + +// --- EntryValidationError --- + +func TestEntryValidationErrorString(t *testing.T) { + err := disc.NewEntryValidationError("test cause") + assert.Equal(t, "entry validation error: test cause", err.Error()) +} + +// --- Entry and Server/Client String() methods --- + +func TestEntryString(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + serverPK, _ := cipher.GenerateKeyPair() + + t.Run("server entry string", func(t *testing.T) { + entry := disc.NewServerEntry(pk, 0, "addr:1234", 5) + require.NoError(t, entry.Sign(sk)) + s := entry.String() + assert.Contains(t, s, "registered as server") + assert.Contains(t, s, "addr:1234") + }) + + t.Run("client entry string", func(t *testing.T) { + entry := disc.NewClientEntry(pk, 0, []cipher.PubKey{serverPK}) + require.NoError(t, entry.Sign(sk)) + s := entry.String() + assert.Contains(t, s, "registered as client") + assert.Contains(t, s, "delegated servers") + }) +} + +// --- NewClientEntry / NewServerEntry construction --- + +func TestNewClientEntryFields(t *testing.T) { + pk, _ := cipher.GenerateKeyPair() + serverPK, _ := cipher.GenerateKeyPair() + + entry := disc.NewClientEntry(pk, 7, []cipher.PubKey{serverPK}) + assert.Equal(t, pk, entry.Static) + assert.Equal(t, uint64(7), entry.Sequence) + require.NotNil(t, entry.Client) + assert.Len(t, entry.Client.DelegatedServers, 1) + assert.Nil(t, entry.Server) + assert.NotEmpty(t, entry.Version) + assert.NotZero(t, entry.Timestamp) +} + +func TestNewServerEntryFields(t *testing.T) { + pk, _ := cipher.GenerateKeyPair() + + entry := disc.NewServerEntry(pk, 3, "myaddr:5555", 42) + assert.Equal(t, pk, entry.Static) + assert.Equal(t, uint64(3), entry.Sequence) + require.NotNil(t, entry.Server) + assert.Equal(t, "myaddr:5555", entry.Server.Address) + assert.Equal(t, 42, entry.Server.AvailableSessions) + assert.Nil(t, entry.Client) + assert.NotEmpty(t, entry.Version) + assert.NotZero(t, entry.Timestamp) +} + +// --- Validate edge cases --- + +func TestValidateNoVersion(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:1", 1) + entry.Version = "" + require.NoError(t, entry.Sign(sk)) + + err := entry.Validate(false) + assert.ErrorIs(t, err, disc.ErrValidationNoVersion) +} + +func TestValidateNoSignature(t *testing.T) { + pk, _ := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:1", 1) + // Don't sign. + err := entry.Validate(false) + assert.ErrorIs(t, err, disc.ErrValidationNoSignature) +} + +func TestValidateNoClientOrServer(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + entry := &disc.Entry{ + Version: "0.0.1", + Static: pk, + Timestamp: time.Now().UnixNano(), + } + require.NoError(t, entry.Sign(sk)) + err := entry.Validate(false) + assert.ErrorIs(t, err, disc.ErrValidationNoClientOrServer) +} + +func TestValidateEmptyServerAddress(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "", 1) + require.NoError(t, entry.Sign(sk)) + err := entry.Validate(false) + assert.ErrorIs(t, err, disc.ErrValidationEmptyServerAddress) +} + +func TestValidateTimestampOutdated(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:1", 1) + entry.Timestamp = time.Now().Add(-10 * time.Minute).UnixNano() + require.NoError(t, entry.Sign(sk)) + err := entry.Validate(true) + assert.ErrorIs(t, err, disc.ErrValidationOutdatedTime) +} + +func TestValidateTimestampFuture(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:1", 1) + entry.Timestamp = time.Now().Add(10 * time.Minute).UnixNano() + require.NoError(t, entry.Sign(sk)) + err := entry.Validate(true) + assert.ErrorIs(t, err, disc.ErrValidationOutdatedTime) +} + +func TestValidateTimestampSkipped(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:1", 1) + // Old timestamp but validateTimestamp=false should pass. + entry.Timestamp = time.Now().Add(-10 * time.Minute).UnixNano() + require.NoError(t, entry.Sign(sk)) + err := entry.Validate(false) + assert.NoError(t, err) +} + +// --- Sign / VerifySignature --- + +func TestSignAndVerify(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:1", 1) + + require.NoError(t, entry.Sign(sk)) + assert.NotEmpty(t, entry.Signature) + assert.NoError(t, entry.VerifySignature()) +} + +func TestVerifySignatureWrongKey(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + _, sk2 := cipher.GenerateKeyPair() + + entry := disc.NewServerEntry(pk, 0, "addr:1", 1) + require.NoError(t, entry.Sign(sk2)) // signed with wrong key + err := entry.VerifySignature() + assert.Error(t, err) + + _ = sk // use sk to avoid lint +} + +func TestVerifySignatureTampered(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:1", 1) + require.NoError(t, entry.Sign(sk)) + + // Tamper with entry after signing. + entry.Server.Address = "tampered" + err := entry.VerifySignature() + assert.Error(t, err) +} + +// --- ValidateIteration --- + +func TestValidateIterationSuccess(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + e1 := disc.NewServerEntry(pk, 0, "addr:1", 1) + require.NoError(t, e1.Sign(sk)) + + e2 := disc.NewServerEntry(pk, 1, "addr:1", 1) + e2.Timestamp = e1.Timestamp + 1 + require.NoError(t, e2.Sign(sk)) + + assert.NoError(t, e1.ValidateIteration(e2)) +} + +func TestValidateIterationWrongSeq(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + e1 := disc.NewServerEntry(pk, 5, "addr:1", 1) + require.NoError(t, e1.Sign(sk)) + + e2 := disc.NewServerEntry(pk, 3, "addr:1", 1) + require.NoError(t, e2.Sign(sk)) + + err := e1.ValidateIteration(e2) + assert.ErrorIs(t, err, disc.ErrValidationWrongSequence) +} + +func TestValidateIterationWrongTimestamp(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + e1 := disc.NewServerEntry(pk, 0, "addr:1", 1) + require.NoError(t, e1.Sign(sk)) + + e2 := disc.NewServerEntry(pk, 1, "addr:1", 1) + e2.Timestamp = e1.Timestamp - 1000 + require.NoError(t, e2.Sign(sk)) + + err := e1.ValidateIteration(e2) + assert.ErrorIs(t, err, disc.ErrValidationWrongTime) +} + +// --- Mock with timeout --- + +func TestMockClientWithTimeout(t *testing.T) { + ctx := context.Background() + mock := disc.NewMock(200 * time.Millisecond) + + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:1", 1) + require.NoError(t, entry.Sign(sk)) + require.NoError(t, mock.PostEntry(ctx, entry)) + + // Entry should exist immediately. + _, err := mock.Entry(ctx, pk) + require.NoError(t, err) + + // After timeout, entry should be removed. + time.Sleep(400 * time.Millisecond) + _, err = mock.Entry(ctx, pk) + require.Error(t, err, "entry should have been removed after timeout") +} + +// --- NilKeys validation --- + +func TestValidateNilKeys(t *testing.T) { + entry := &disc.Entry{ + Version: "0.0.1", + Signature: "something", + Client: &disc.Client{}, + } + err := entry.Validate(false) + assert.ErrorIs(t, err, disc.ErrValidationNilKeys) +} + +// =========================================================================== +// httpClient tests using httptest +// =========================================================================== + +// helper: create a test server + httpClient pair +func newTestHTTPClient(t *testing.T, handler http.Handler) disc.APIClient { + t.Helper() + srv := httptest.NewServer(handler) + t.Cleanup(srv.Close) + log := logging.MustGetLogger("disc_http_test") + return disc.NewHTTP(srv.URL, srv.Client(), log) +} + +func TestHTTPClientEntry_Success(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:9090", 5) + require.NoError(t, entry.Sign(sk)) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + assert.Contains(t, r.URL.Path, "/dmsg-discovery/entry/") + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(entry) + }) + + client := newTestHTTPClient(t, handler) + got, err := client.Entry(context.Background(), pk) + require.NoError(t, err) + assert.Equal(t, pk, got.Static) + assert.Equal(t, "addr:9090", got.Server.Address) +} + +func TestHTTPClientEntry_NotFound(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _ = json.NewEncoder(w).Encode(disc.HTTPMessage{ + Code: http.StatusNotFound, + Message: disc.ErrKeyNotFound.Error(), + }) + }) + + client := newTestHTTPClient(t, handler) + _, err := client.Entry(context.Background(), cipher.PubKey{}) + require.Error(t, err) + assert.Equal(t, disc.ErrKeyNotFound, err) +} + +func TestHTTPClientEntry_UnknownErrorBecomesUnexpected(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(w).Encode(disc.HTTPMessage{ + Code: http.StatusInternalServerError, + Message: "some unknown error", + }) + }) + + client := newTestHTTPClient(t, handler) + _, err := client.Entry(context.Background(), cipher.PubKey{}) + require.Error(t, err) + assert.Equal(t, disc.ErrUnexpected, err) +} + +func TestHTTPClientPostEntry_Success(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/dmsg-discovery/entry/", r.URL.Path) + w.WriteHeader(http.StatusOK) + }) + + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:1", 1) + require.NoError(t, entry.Sign(sk)) + + client := newTestHTTPClient(t, handler) + err := client.PostEntry(context.Background(), entry) + require.NoError(t, err) +} + +func TestHTTPClientPostEntry_Error(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnprocessableEntity) + body, _ := json.Marshal(disc.HTTPMessage{ + Code: http.StatusUnprocessableEntity, + Message: disc.ErrValidationWrongSequence.Error(), + }) + _, _ = w.Write(body) + }) + + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:1", 1) + require.NoError(t, entry.Sign(sk)) + + client := newTestHTTPClient(t, handler) + err := client.PostEntry(context.Background(), entry) + require.Error(t, err) + assert.Equal(t, disc.ErrValidationWrongSequence, err) +} + +func TestHTTPClientDelEntry_Success(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodDelete, r.Method) + assert.Equal(t, "/dmsg-discovery/entry", r.URL.Path) + w.WriteHeader(http.StatusOK) + }) + + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:1", 1) + require.NoError(t, entry.Sign(sk)) + + client := newTestHTTPClient(t, handler) + err := client.DelEntry(context.Background(), entry) + require.NoError(t, err) +} + +func TestHTTPClientDelEntry_Error(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + body, _ := json.Marshal(disc.HTTPMessage{ + Code: http.StatusUnauthorized, + Message: disc.ErrUnauthorized.Error(), + }) + _, _ = w.Write(body) + }) + + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:1", 1) + require.NoError(t, entry.Sign(sk)) + + client := newTestHTTPClient(t, handler) + err := client.DelEntry(context.Background(), entry) + require.Error(t, err) + assert.Equal(t, disc.ErrUnauthorized, err) +} + +func TestHTTPClientAvailableServers_Success(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:8080", 10) + require.NoError(t, entry.Sign(sk)) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/dmsg-discovery/available_servers", r.URL.Path) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode([]*disc.Entry{entry}) + }) + + client := newTestHTTPClient(t, handler) + servers, err := client.AvailableServers(context.Background()) + require.NoError(t, err) + require.Len(t, servers, 1) + assert.Equal(t, pk, servers[0].Static) +} + +func TestHTTPClientAvailableServers_Error(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(w).Encode(disc.HTTPMessage{ + Code: http.StatusInternalServerError, + Message: disc.ErrNoAvailableServers.Error(), + }) + }) + + client := newTestHTTPClient(t, handler) + _, err := client.AvailableServers(context.Background()) + require.Error(t, err) + assert.Equal(t, disc.ErrNoAvailableServers, err) +} + +func TestHTTPClientAllServers_Success(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:8080", 10) + require.NoError(t, entry.Sign(sk)) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/dmsg-discovery/all_servers", r.URL.Path) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode([]*disc.Entry{entry}) + }) + + client := newTestHTTPClient(t, handler) + servers, err := client.AllServers(context.Background()) + require.NoError(t, err) + require.Len(t, servers, 1) +} + +func TestHTTPClientAllServers_Error(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(w).Encode(disc.HTTPMessage{ + Code: http.StatusInternalServerError, + Message: disc.ErrUnexpected.Error(), + }) + }) + + client := newTestHTTPClient(t, handler) + _, err := client.AllServers(context.Background()) + require.Error(t, err) + assert.Equal(t, disc.ErrUnexpected, err) +} + +func TestHTTPClientAllEntries_Success(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/dmsg-discovery/entries", r.URL.Path) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode([]string{"abc123", "def456"}) + }) + + client := newTestHTTPClient(t, handler) + entries, err := client.AllEntries(context.Background()) + require.NoError(t, err) + assert.Len(t, entries, 2) +} + +func TestHTTPClientAllEntries_Error(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(w).Encode(disc.HTTPMessage{ + Code: http.StatusInternalServerError, + Message: disc.ErrBadInput.Error(), + }) + }) + + client := newTestHTTPClient(t, handler) + _, err := client.AllEntries(context.Background()) + require.Error(t, err) + assert.Equal(t, disc.ErrBadInput, err) +} + +func TestHTTPClientAllClientsByServer_Success(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + serverPK, _ := cipher.GenerateKeyPair() + entry := disc.NewClientEntry(pk, 0, []cipher.PubKey{serverPK}) + require.NoError(t, entry.Sign(sk)) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/dmsg-discovery/servers/clients", r.URL.Path) + w.Header().Set("Content-Type", "application/json") + result := map[string][]*disc.Entry{ + serverPK.Hex(): {entry}, + } + _ = json.NewEncoder(w).Encode(result) + }) + + client := newTestHTTPClient(t, handler) + result, err := client.AllClientsByServer(context.Background()) + require.NoError(t, err) + assert.Len(t, result[serverPK.Hex()], 1) +} + +func TestHTTPClientAllClientsByServer_Error(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(w).Encode(disc.HTTPMessage{ + Code: http.StatusInternalServerError, + Message: disc.ErrUnexpected.Error(), + }) + }) + + client := newTestHTTPClient(t, handler) + _, err := client.AllClientsByServer(context.Background()) + require.Error(t, err) + assert.Equal(t, disc.ErrUnexpected, err) +} + +func TestHTTPClientClientsByServer_Success(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + serverPK, _ := cipher.GenerateKeyPair() + entry := disc.NewClientEntry(pk, 0, []cipher.PubKey{serverPK}) + require.NoError(t, entry.Sign(sk)) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + expectedPath := fmt.Sprintf("/dmsg-discovery/server/%s/clients", serverPK.Hex()) + assert.Equal(t, expectedPath, r.URL.Path) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode([]*disc.Entry{entry}) + }) + + client := newTestHTTPClient(t, handler) + entries, err := client.ClientsByServer(context.Background(), serverPK) + require.NoError(t, err) + require.Len(t, entries, 1) + assert.Equal(t, pk, entries[0].Static) +} + +func TestHTTPClientClientsByServer_Error(t *testing.T) { + serverPK, _ := cipher.GenerateKeyPair() + + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _ = json.NewEncoder(w).Encode(disc.HTTPMessage{ + Code: http.StatusNotFound, + Message: disc.ErrKeyNotFound.Error(), + }) + }) + + client := newTestHTTPClient(t, handler) + _, err := client.ClientsByServer(context.Background(), serverPK) + require.Error(t, err) + assert.Equal(t, disc.ErrKeyNotFound, err) +} + +func TestHTTPClientPutEntry_Success(t *testing.T) { + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:1", 1) + require.NoError(t, entry.Sign(sk)) + + // PutEntry calls PostEntry internally. The server just returns OK. + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + w.WriteHeader(http.StatusOK) + return + } + // GET for Entry lookup (shouldn't be needed if PostEntry succeeds) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(entry) + }) + + client := newTestHTTPClient(t, handler) + err := client.PutEntry(context.Background(), sk, entry) + require.NoError(t, err) + assert.Equal(t, uint64(1), entry.Sequence) +} + +func TestHTTPClientEntry_ConnectionRefused(t *testing.T) { + log := logging.MustGetLogger("disc_http_test") + // Use a URL that will fail to connect. + client := disc.NewHTTP("http://127.0.0.1:1", &http.Client{Timeout: 100 * time.Millisecond}, log) + _, err := client.Entry(context.Background(), cipher.PubKey{}) + require.Error(t, err) +} + +func TestHTTPClientPostEntry_ConnectionRefused(t *testing.T) { + log := logging.MustGetLogger("disc_http_test") + client := disc.NewHTTP("http://127.0.0.1:1", &http.Client{Timeout: 100 * time.Millisecond}, log) + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:1", 1) + require.NoError(t, entry.Sign(sk)) + err := client.PostEntry(context.Background(), entry) + require.Error(t, err) +} + +func TestHTTPClientDelEntry_ConnectionRefused(t *testing.T) { + log := logging.MustGetLogger("disc_http_test") + client := disc.NewHTTP("http://127.0.0.1:1", &http.Client{Timeout: 100 * time.Millisecond}, log) + pk, sk := cipher.GenerateKeyPair() + entry := disc.NewServerEntry(pk, 0, "addr:1", 1) + require.NoError(t, entry.Sign(sk)) + err := client.DelEntry(context.Background(), entry) + require.Error(t, err) +} + +func TestHTTPClientAvailableServers_ConnectionRefused(t *testing.T) { + log := logging.MustGetLogger("disc_http_test") + client := disc.NewHTTP("http://127.0.0.1:1", &http.Client{Timeout: 100 * time.Millisecond}, log) + _, err := client.AvailableServers(context.Background()) + require.Error(t, err) +} + +func TestHTTPClientAllServers_ConnectionRefused(t *testing.T) { + log := logging.MustGetLogger("disc_http_test") + client := disc.NewHTTP("http://127.0.0.1:1", &http.Client{Timeout: 100 * time.Millisecond}, log) + _, err := client.AllServers(context.Background()) + require.Error(t, err) +} diff --git a/pkg/dmsgctrl/serve_listener_test.go b/pkg/dmsgctrl/serve_listener_test.go new file mode 100644 index 000000000..4e03ee48d --- /dev/null +++ b/pkg/dmsgctrl/serve_listener_test.go @@ -0,0 +1,354 @@ +package dmsgctrl_test + +import ( + "context" + "net" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/skycoin/dmsg/pkg/dmsgctrl" +) + +// TestServeListener_AcceptAndControl verifies that ServeListener accepts +// connections and delivers Controls through the returned channel. +func TestServeListener_AcceptAndControl(t *testing.T) { + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + ch := dmsgctrl.ServeListener(l, 4) + + // Dial two connections into the listener. + connA, err := net.Dial("tcp", l.Addr().String()) + require.NoError(t, err) + connB, err := net.Dial("tcp", l.Addr().String()) + require.NoError(t, err) + + // We should receive two Controls from the channel. + var ctrls []*dmsgctrl.Control + timeout := time.After(2 * time.Second) + for i := 0; i < 2; i++ { + select { + case ctrl, ok := <-ch: + require.True(t, ok, "channel closed unexpectedly") + require.NotNil(t, ctrl) + ctrls = append(ctrls, ctrl) + case <-timeout: + t.Fatal("timed out waiting for Control from channel") + } + } + + assert.Len(t, ctrls, 2) + + // Cleanup. + for _, ctrl := range ctrls { + _ = ctrl.Close() + } + _ = connA.Close() + _ = connB.Close() + _ = l.Close() +} + +// TestServeListener_ClosesChannelOnListenerClose verifies that the channel +// returned by ServeListener is closed when the underlying listener is closed. +func TestServeListener_ClosesChannelOnListenerClose(t *testing.T) { + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + ch := dmsgctrl.ServeListener(l, 4) + + // Close the listener; the goroutine should detect "use of closed" and + // close the channel. + require.NoError(t, l.Close()) + + select { + case _, ok := <-ch: + assert.False(t, ok, "expected channel to be closed") + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for channel to close") + } +} + +// TestServeListener_FullChannelDropsControl verifies that when the control +// channel is full, new Controls are dropped rather than blocking. +func TestServeListener_FullChannelDropsControl(t *testing.T) { + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + // Channel length of 1 — second accepted conn should be dropped. + ch := dmsgctrl.ServeListener(l, 1) + + conn1, err := net.Dial("tcp", l.Addr().String()) + require.NoError(t, err) + + // Wait for the first Control to land in the channel. + select { + case ctrl := <-ch: + require.NotNil(t, ctrl) + defer ctrl.Close() //nolint:errcheck + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for first control") + } + + // Dial a second connection — this should be dropped since the channel is + // still full (we already consumed the first, but let's fill it again). + // First, fill the channel again by dialing one more. + conn2, err := net.Dial("tcp", l.Addr().String()) + require.NoError(t, err) + + // Wait for the second to arrive. + select { + case ctrl := <-ch: + require.NotNil(t, ctrl) + defer ctrl.Close() //nolint:errcheck + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for second control") + } + + // Now the channel is empty but has cap 1. Fill it without consuming. + conn3, err := net.Dial("tcp", l.Addr().String()) + require.NoError(t, err) + time.Sleep(200 * time.Millisecond) // let it land + + // This fourth conn should be dropped. + conn4, err := net.Dial("tcp", l.Addr().String()) + require.NoError(t, err) + time.Sleep(200 * time.Millisecond) // let it be processed + + // Drain one — we should get exactly one more (the third). + select { + case ctrl := <-ch: + require.NotNil(t, ctrl) + defer ctrl.Close() //nolint:errcheck + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for third control") + } + + // Cleanup. + _ = conn1.Close() + _ = conn2.Close() + _ = conn3.Close() + _ = conn4.Close() + _ = l.Close() +} + +// TestControl_PingPongExchange tests that a ping on one side results in a +// measurable round-trip duration on the same side. +func TestControl_PingPongExchange(t *testing.T) { + connA, connB := net.Pipe() + ctrlA := dmsgctrl.ControlStream(connA) + ctrlB := dmsgctrl.ControlStream(connB) + + t.Cleanup(func() { + _ = ctrlA.Close() + _ = ctrlB.Close() + }) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + dur, err := ctrlA.Ping(ctx) + require.NoError(t, err) + assert.True(t, dur >= 0, "expected non-negative duration, got %v", dur) +} + +// TestControl_PingContextCancel verifies that Ping returns the context error +// when the context is cancelled while waiting for a pong. +func TestControl_PingContextCancel(t *testing.T) { + connA, connB := net.Pipe() + defer connB.Close() //nolint:errcheck + + ctrlA := dmsgctrl.ControlStream(connA) + + // Create a goroutine that reads (consumes the ping byte) but never + // sends a pong back, so ctrlA.Ping will block waiting for pong. + go func() { + buf := make([]byte, 1) + for { + _, err := connB.Read(buf) + if err != nil { + return + } + // Intentionally do not reply with pong. + } + }() + + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + + _, err := ctrlA.Ping(ctx) + assert.ErrorIs(t, err, context.DeadlineExceeded) + + _ = ctrlA.Close() +} + +// TestControl_Close verifies that Close sets the error and signals Done. +func TestControl_Close(t *testing.T) { + connA, connB := net.Pipe() + ctrlA := dmsgctrl.ControlStream(connA) + ctrlB := dmsgctrl.ControlStream(connB) + + require.NoError(t, ctrlA.Close()) + + // Wait for both sides to finish. + select { + case <-ctrlA.Done(): + case <-time.After(2 * time.Second): + t.Fatal("ctrlA.Done() did not close in time") + } + + select { + case <-ctrlB.Done(): + case <-time.After(2 * time.Second): + t.Fatal("ctrlB.Done() did not close in time") + } + + // Err should report ErrClosed on the side that initiated Close. + assert.ErrorIs(t, ctrlA.Err(), dmsgctrl.ErrClosed) + + // The remote side should have a non-nil error (EOF or similar). + assert.Error(t, ctrlB.Err()) +} + +// TestControl_DoubleClose ensures calling Close twice does not panic and +// returns the original error. +func TestControl_DoubleClose(t *testing.T) { + connA, connB := net.Pipe() + ctrlA := dmsgctrl.ControlStream(connA) + _ = dmsgctrl.ControlStream(connB) + + err1 := ctrlA.Close() + // Second close: the connection is already closed so conn.Close may + // return an error, but it must not panic. + _ = ctrlA.Close() + _ = err1 + + // Wait for done. + select { + case <-ctrlA.Done(): + case <-time.After(time.Second): + t.Fatal("ctrlA.Done() did not fire") + } +} + +// TestControl_ErrBeforeDone verifies that Err returns nil when the control +// is still running. +func TestControl_ErrBeforeDone(t *testing.T) { + connA, connB := net.Pipe() + ctrlA := dmsgctrl.ControlStream(connA) + ctrlB := dmsgctrl.ControlStream(connB) + + // Control is still alive — Err should be nil. + assert.Nil(t, ctrlA.Err()) + assert.Nil(t, ctrlB.Err()) + + _ = ctrlA.Close() + _ = ctrlB.Close() +} + +// TestControl_DoneChannel tests that the Done channel blocks while the +// control is active and unblocks after Close. +func TestControl_DoneChannel(t *testing.T) { + connA, connB := net.Pipe() + ctrlA := dmsgctrl.ControlStream(connA) + ctrlB := dmsgctrl.ControlStream(connB) + + // Done should block. + select { + case <-ctrlA.Done(): + t.Fatal("Done() should not be closed yet") + default: + // expected + } + + require.NoError(t, ctrlA.Close()) + + select { + case <-ctrlA.Done(): + case <-time.After(time.Second): + t.Fatal("Done() did not close after Close()") + } + + _ = ctrlB.Close() +} + +// TestControl_Conn verifies that Conn returns the underlying net.Conn. +func TestControl_Conn(t *testing.T) { + connA, connB := net.Pipe() + ctrlA := dmsgctrl.ControlStream(connA) + _ = dmsgctrl.ControlStream(connB) + + assert.Equal(t, connA, ctrlA.Conn()) + + _ = ctrlA.Close() +} + +// TestControl_ConcurrentPing tests that multiple goroutines can ping +// simultaneously without races or panics. +func TestControl_ConcurrentPing(t *testing.T) { + // Use TCP so writes don't block synchronously like net.Pipe. + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer l.Close() //nolint:errcheck + + var connB net.Conn + accepted := make(chan struct{}) + go func() { + var err error + connB, err = l.Accept() + if err != nil { + return + } + close(accepted) + }() + + connA, err := net.Dial("tcp", l.Addr().String()) + require.NoError(t, err) + <-accepted + + ctrlA := dmsgctrl.ControlStream(connA) + ctrlB := dmsgctrl.ControlStream(connB) + + t.Cleanup(func() { + _ = ctrlA.Close() + _ = ctrlB.Close() + }) + + const goroutines = 5 + var wg sync.WaitGroup + wg.Add(goroutines * 2) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + for i := 0; i < goroutines; i++ { + go func() { + defer wg.Done() + _, _ = ctrlA.Ping(ctx) + }() + go func() { + defer wg.Done() + _, _ = ctrlB.Ping(ctx) + }() + } + + wg.Wait() +} + +// TestControl_PingAfterClose verifies that Ping returns an error after the +// control has been closed. +func TestControl_PingAfterClose(t *testing.T) { + connA, connB := net.Pipe() + ctrlA := dmsgctrl.ControlStream(connA) + _ = dmsgctrl.ControlStream(connB) + + require.NoError(t, ctrlA.Close()) + + <-ctrlA.Done() + + _, err := ctrlA.Ping(context.Background()) + assert.Error(t, err) +} diff --git a/pkg/dmsgcurl/url_test.go b/pkg/dmsgcurl/url_test.go new file mode 100644 index 000000000..de0c4ee61 --- /dev/null +++ b/pkg/dmsgcurl/url_test.go @@ -0,0 +1,209 @@ +package dmsgcurl_test + +import ( + "bytes" + "context" + "errors" + "flag" + "io" + "strings" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/skycoin/dmsg/pkg/dmsgcurl" +) + +// A valid 66-char hex public key for testing. +const testPK = "024ec47420176680816e0406250e7156465e4531f5b26057c9f6297bb0303558c7" + +// --------------------------------------------------------------------------- +// URL.Fill tests +// --------------------------------------------------------------------------- + +func TestURL_Fill_Valid(t *testing.T) { + tests := []struct { + name string + url string + }{ + {"with port and path", "dmsg://" + testPK + ":80/some/path"}, + {"without port", "dmsg://" + testPK + "/file.txt"}, + {"http scheme", "http://" + testPK + ":8080/index.html"}, + {"root path", "dmsg://" + testPK + ":443/"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var u dmsgcurl.URL + err := u.Fill(tc.url) + assert.NoError(t, err) + assert.NotEmpty(t, u.URL.Host) + }) + } +} + +func TestURL_Fill_MissingScheme(t *testing.T) { + // URL without a scheme - url.Parse may return its own error or Fill detects missing scheme. + var u dmsgcurl.URL + err := u.Fill(testPK + ":80/path") + require.Error(t, err) + + // Also test a path-only URL that url.Parse accepts but has no scheme. + var u2 dmsgcurl.URL + err2 := u2.Fill("//example/path") + require.Error(t, err2) + assert.Contains(t, err2.Error(), "missing a scheme") +} + +func TestURL_Fill_MissingHost(t *testing.T) { + var u dmsgcurl.URL + err := u.Fill("dmsg:///path/only") + require.Error(t, err) + assert.Contains(t, err.Error(), "missing a host") +} + +func TestURL_Fill_InvalidHost(t *testing.T) { + // Host is not a valid public key. + var u dmsgcurl.URL + err := u.Fill("dmsg://not-a-pubkey:80/path") + require.Error(t, err) +} + +func TestURL_Fill_EmptyString(t *testing.T) { + var u dmsgcurl.URL + err := u.Fill("") + require.Error(t, err) +} + +// --------------------------------------------------------------------------- +// ProgressWriter tests +// --------------------------------------------------------------------------- + +func TestProgressWriter_Write_KnownTotal(t *testing.T) { + pw := &dmsgcurl.ProgressWriter{Total: 100} + + n, err := pw.Write(make([]byte, 30)) + assert.NoError(t, err) + assert.Equal(t, 30, n) + assert.Equal(t, int64(30), atomic.LoadInt64(&pw.Current)) + + n, err = pw.Write(make([]byte, 70)) + assert.NoError(t, err) + assert.Equal(t, 70, n) + assert.Equal(t, int64(100), atomic.LoadInt64(&pw.Current)) +} + +func TestProgressWriter_Write_UnknownTotal(t *testing.T) { + pw := &dmsgcurl.ProgressWriter{Total: 0} + + n, err := pw.Write(make([]byte, 42)) + assert.NoError(t, err) + assert.Equal(t, 42, n) + assert.Equal(t, int64(42), atomic.LoadInt64(&pw.Current)) +} + +func TestProgressWriter_Write_EmptySlice(t *testing.T) { + pw := &dmsgcurl.ProgressWriter{Total: 50} + + n, err := pw.Write([]byte{}) + assert.NoError(t, err) + assert.Equal(t, 0, n) + assert.Equal(t, int64(0), atomic.LoadInt64(&pw.Current)) +} + +// --------------------------------------------------------------------------- +// CancellableCopy tests +// --------------------------------------------------------------------------- + +func TestCancellableCopy_Normal(t *testing.T) { + src := io.NopCloser(strings.NewReader("hello world")) + var dst bytes.Buffer + + n, err := dmsgcurl.CancellableCopy(context.Background(), &dst, src, int64(len("hello world"))) + assert.NoError(t, err) + assert.Equal(t, int64(11), n) + assert.Equal(t, "hello world", dst.String()) +} + +func TestCancellableCopy_Cancelled(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() // cancel immediately + + src := io.NopCloser(strings.NewReader("data that should not be copied")) + var dst bytes.Buffer + + _, err := dmsgcurl.CancellableCopy(ctx, &dst, src, 30) + require.Error(t, err) + assert.Contains(t, err.Error(), "Canceled") +} + +func TestCancellableCopy_EmptyBody(t *testing.T) { + src := io.NopCloser(strings.NewReader("")) + var dst bytes.Buffer + + n, err := dmsgcurl.CancellableCopy(context.Background(), &dst, src, 0) + assert.NoError(t, err) + assert.Equal(t, int64(0), n) +} + +// --------------------------------------------------------------------------- +// New constructor tests +// --------------------------------------------------------------------------- + +func TestNew_ReturnsNonNil(t *testing.T) { + fs := flag.NewFlagSet("test", flag.ContinueOnError) + fs.SetOutput(io.Discard) + dg := dmsgcurl.New(fs) + require.NotNil(t, dg) +} + +func TestNew_RegistersFlags(t *testing.T) { + fs := flag.NewFlagSet("test", flag.ContinueOnError) + fs.SetOutput(io.Discard) + _ = dmsgcurl.New(fs) + + // Verify some expected flags were registered. + for _, name := range []string{"help", "h", "dmsg-disc", "dmsg-sessions", "O", "t", "w", "U"} { + assert.NotNilf(t, fs.Lookup(name), "expected flag %q to be registered", name) + } +} + +// --------------------------------------------------------------------------- +// DmsgCurl.String() tests +// --------------------------------------------------------------------------- + +func TestString_ContainsFlagGroupNames(t *testing.T) { + fs := flag.NewFlagSet("test", flag.ContinueOnError) + fs.SetOutput(io.Discard) + dg := dmsgcurl.New(fs) + + s := dg.String() + assert.Contains(t, s, "Startup") + assert.Contains(t, s, "Dmsg") + assert.Contains(t, s, "Download") + assert.Contains(t, s, "HTTP") +} + +func TestString_IsValidJSON(t *testing.T) { + fs := flag.NewFlagSet("test", flag.ContinueOnError) + fs.SetOutput(io.Discard) + dg := dmsgcurl.New(fs) + + s := dg.String() + // Quick check: valid JSON starts with '{' and ends with '}'. + assert.True(t, strings.HasPrefix(s, "{")) + assert.True(t, strings.HasSuffix(s, "}")) +} + +// --------------------------------------------------------------------------- +// Error variable tests +// --------------------------------------------------------------------------- + +func TestErr_Variables(t *testing.T) { + assert.True(t, errors.Is(dmsgcurl.ErrNoURLs, dmsgcurl.ErrNoURLs)) + assert.True(t, errors.Is(dmsgcurl.ErrMultipleURLsNotSupported, dmsgcurl.ErrMultipleURLsNotSupported)) + assert.NotEqual(t, dmsgcurl.ErrNoURLs, dmsgcurl.ErrMultipleURLsNotSupported) + assert.Equal(t, "no URLs provided", dmsgcurl.ErrNoURLs.Error()) + assert.Equal(t, "multiple URLs is not yet supported", dmsgcurl.ErrMultipleURLsNotSupported.Error()) +} diff --git a/pkg/dmsghttp/util_test.go b/pkg/dmsghttp/util_test.go new file mode 100644 index 000000000..ab1ef6ebb --- /dev/null +++ b/pkg/dmsghttp/util_test.go @@ -0,0 +1,507 @@ +// Package dmsghttp_test pkg/dmsghttp/util_test.go +package dmsghttp_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/go-chi/chi/v5" + "github.com/sirupsen/logrus" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/nettest" + + "github.com/skycoin/dmsg/pkg/disc" + "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsghttp" +) + +func init() { + logrus.SetLevel(logrus.WarnLevel) +} + +// --- MakeHTTPTransport tests --- + +func TestMakeHTTPTransport_ReturnsValidTransport(t *testing.T) { + // Verify MakeHTTPTransport returns a transport that implements http.RoundTripper. + dc := disc.NewMock(0) + + pk, sk := cipher.GenerateKeyPair() + dmsgC := dmsg.NewClient(pk, sk, dc, nil) + defer dmsgC.Close() //nolint:errcheck + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + transport := dmsghttp.MakeHTTPTransport(ctx, dmsgC) + + // Verify the returned value satisfies http.RoundTripper. + var _ http.RoundTripper = transport +} + +func TestMakeHTTPTransport_RoundTripInvalidHost(t *testing.T) { + // RoundTrip should fail with an invalid host address. + dc := disc.NewMock(0) + + pk, sk := cipher.GenerateKeyPair() + dmsgC := dmsg.NewClient(pk, sk, dc, nil) + defer dmsgC.Close() //nolint:errcheck + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + transport := dmsghttp.MakeHTTPTransport(ctx, dmsgC) + + req, err := http.NewRequest(http.MethodGet, "http://invalid-host:80/test", nil) + require.NoError(t, err) + + resp, err := transport.RoundTrip(req) + assert.Error(t, err) + assert.Nil(t, resp) + assert.Contains(t, err.Error(), "invalid host address") +} + +func TestMakeHTTPTransport_RoundTripDialFailure(t *testing.T) { + // RoundTrip should fail when dial fails (no servers available). + const maxSessions = 10 + dc := disc.NewMock(0) + + // Create a single server so the client can start. + srvPK, srvSK := cipher.GenerateKeyPair() + srvConf := dmsg.ServerConfig{MaxSessions: maxSessions, UpdateInterval: 0} + srv := dmsg.NewServer(srvPK, srvSK, dc, &srvConf, nil) + lis, err := nettest.NewLocalListener("tcp") + require.NoError(t, err) + go srv.Serve(lis, "") //nolint:errcheck + defer srv.Close() //nolint:errcheck + + pk, sk := cipher.GenerateKeyPair() + dmsgC := dmsg.NewClient(pk, sk, dc, &dmsg.Config{MinSessions: 1}) + go dmsgC.Serve(context.Background()) + defer dmsgC.Close() //nolint:errcheck + <-dmsgC.Ready() + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + transport := dmsghttp.MakeHTTPTransport(ctx, dmsgC) + + // Use a valid PK format but one that no client is listening on. + fakePK, _ := cipher.GenerateKeyPair() + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s:80/test", fakePK.String()), nil) + require.NoError(t, err) + req = req.WithContext(ctx) + + resp, err := transport.RoundTrip(req) + assert.Error(t, err) + assert.Nil(t, resp) +} + +func TestMakeHTTPTransport_FullRoundTrip(t *testing.T) { + // End-to-end: client dials to HTTP server over dmsg and gets a response. + const maxSessions = 20 + const dmsgHTTPPort = uint16(80) + + dc := disc.NewMock(0) + + // Start dmsg server. + srvPK, srvSK := cipher.GenerateKeyPair() + srvConf := dmsg.ServerConfig{MaxSessions: maxSessions, UpdateInterval: 0} + srv := dmsg.NewServer(srvPK, srvSK, dc, &srvConf, nil) + lis, err := nettest.NewLocalListener("tcp") + require.NoError(t, err) + go srv.Serve(lis, "") //nolint:errcheck + t.Cleanup(func() { srv.Close() }) //nolint:errcheck + <-srv.Ready() + + // Start dmsg client that hosts HTTP server. + hostPK, hostSK := cipher.GenerateKeyPair() + dmsgHost := dmsg.NewClient(hostPK, hostSK, dc, &dmsg.Config{MinSessions: 1}) + go dmsgHost.Serve(context.Background()) + t.Cleanup(func() { dmsgHost.Close() }) //nolint:errcheck + <-dmsgHost.Ready() + + dmsgLis, err := dmsgHost.Listen(dmsgHTTPPort) + require.NoError(t, err) + + r := chi.NewRouter() + r.Get("/hello", func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("world")) + }) + r.Post("/echo", func(w http.ResponseWriter, r *http.Request) { + data, _ := io.ReadAll(r.Body) + _, _ = w.Write(data) + }) + go http.Serve(dmsgLis, r) //nolint:errcheck + + // Start dmsg client that runs HTTP client. + clientPK, clientSK := cipher.GenerateKeyPair() + dmsgClient := dmsg.NewClient(clientPK, clientSK, dc, &dmsg.Config{MinSessions: 1}) + go dmsgClient.Serve(context.Background()) + t.Cleanup(func() { dmsgClient.Close() }) //nolint:errcheck + <-dmsgClient.Ready() + + // Allow time for dmsg sessions to stabilize. + time.Sleep(300 * time.Millisecond) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + httpC := http.Client{ + Transport: dmsghttp.MakeHTTPTransport(ctx, dmsgClient), + Timeout: 10 * time.Second, + } + + t.Run("GET_request", func(t *testing.T) { + resp, err := httpC.Get(fmt.Sprintf("http://%s:%d/hello", hostPK.String(), dmsgHTTPPort)) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, "world", string(body)) + }) + + t.Run("POST_echo", func(t *testing.T) { + resp, err := httpC.Post( + fmt.Sprintf("http://%s:%d/echo", hostPK.String(), dmsgHTTPPort), + "text/plain", + http.NoBody, + ) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + }) +} + +func TestMakeHTTPTransport_DefaultPort(t *testing.T) { + // When no port is specified in the host, default port 80 should be used. + const maxSessions = 20 + + dc := disc.NewMock(0) + + srvPK, srvSK := cipher.GenerateKeyPair() + srvConf := dmsg.ServerConfig{MaxSessions: maxSessions, UpdateInterval: 0} + srv := dmsg.NewServer(srvPK, srvSK, dc, &srvConf, nil) + lis, err := nettest.NewLocalListener("tcp") + require.NoError(t, err) + go srv.Serve(lis, "") //nolint:errcheck + t.Cleanup(func() { srv.Close() }) //nolint:errcheck + <-srv.Ready() + + // Host HTTP server on port 80. + hostPK, hostSK := cipher.GenerateKeyPair() + dmsgHost := dmsg.NewClient(hostPK, hostSK, dc, &dmsg.Config{MinSessions: 1}) + go dmsgHost.Serve(context.Background()) + t.Cleanup(func() { dmsgHost.Close() }) //nolint:errcheck + <-dmsgHost.Ready() + + dmsgLis, err := dmsgHost.Listen(80) + require.NoError(t, err) + + r := chi.NewRouter() + r.Get("/", func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("default-port")) + }) + go http.Serve(dmsgLis, r) //nolint:errcheck + + // Client. + clientPK, clientSK := cipher.GenerateKeyPair() + dmsgClient := dmsg.NewClient(clientPK, clientSK, dc, &dmsg.Config{MinSessions: 1}) + go dmsgClient.Serve(context.Background()) + t.Cleanup(func() { dmsgClient.Close() }) //nolint:errcheck + <-dmsgClient.Ready() + + // Allow time for dmsg sessions to stabilize. + time.Sleep(300 * time.Millisecond) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + httpC := http.Client{ + Transport: dmsghttp.MakeHTTPTransport(ctx, dmsgClient), + Timeout: 10 * time.Second, + } + + // URL without port — should default to 80. + resp, err := httpC.Get(fmt.Sprintf("http://%s/", hostPK.String())) + require.NoError(t, err) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, "default-port", string(body)) +} + +// --- GetServers tests --- + +// newDiscoveryMockServer creates an httptest.Server that mimics the dmsg-discovery +// AllServers endpoint at /dmsg-discovery/all_servers. +func newDiscoveryMockServer(t *testing.T, entries []*disc.Entry) *httptest.Server { + t.Helper() + mux := http.NewServeMux() + mux.HandleFunc("/dmsg-discovery/all_servers", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(entries); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + srv := httptest.NewServer(mux) + t.Cleanup(srv.Close) + return srv +} + +func TestGetServers_ReturnsServers(t *testing.T) { + pk1, _ := cipher.GenerateKeyPair() + pk2, _ := cipher.GenerateKeyPair() + + entries := []*disc.Entry{ + { + Static: pk1, + Server: &disc.Server{Address: "1.2.3.4:8080", AvailableSessions: 10}, + }, + { + Static: pk2, + Server: &disc.Server{Address: "5.6.7.8:8080", AvailableSessions: 5}, + }, + } + + srv := newDiscoveryMockServer(t, entries) + log := logging.MustGetLogger("test_get_servers") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + result := dmsghttp.GetServers(ctx, srv.URL, "", log) + require.Len(t, result, 2) +} + +func TestGetServers_FiltersServerType(t *testing.T) { + pk1, _ := cipher.GenerateKeyPair() + pk2, _ := cipher.GenerateKeyPair() + pk3, _ := cipher.GenerateKeyPair() + + entries := []*disc.Entry{ + { + Static: pk1, + Server: &disc.Server{Address: "1.2.3.4:8080", AvailableSessions: 10, ServerType: "official"}, + }, + { + Static: pk2, + Server: &disc.Server{Address: "5.6.7.8:8080", AvailableSessions: 5, ServerType: "community"}, + }, + { + Static: pk3, + Server: &disc.Server{Address: "9.10.11.12:8080", AvailableSessions: 3, ServerType: "official"}, + }, + } + + srv := newDiscoveryMockServer(t, entries) + log := logging.MustGetLogger("test_filter") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + t.Run("filter_official", func(t *testing.T) { + result := dmsghttp.GetServers(ctx, srv.URL, "official", log) + require.Len(t, result, 2) + for _, e := range result { + assert.Equal(t, "official", e.Server.ServerType) + } + }) + + t.Run("filter_community", func(t *testing.T) { + result := dmsghttp.GetServers(ctx, srv.URL, "community", log) + require.Len(t, result, 1) + assert.Equal(t, "community", result[0].Server.ServerType) + }) +} + +func TestGetServers_FilterRemovesAllRetries(t *testing.T) { + // When filtering removes all servers, GetServers retries. On context cancellation + // it should return empty. + pk1, _ := cipher.GenerateKeyPair() + + entries := []*disc.Entry{ + { + Static: pk1, + Server: &disc.Server{Address: "1.2.3.4:8080", AvailableSessions: 10, ServerType: "community"}, + }, + } + + srv := newDiscoveryMockServer(t, entries) + log := logging.MustGetLogger("test_filter_none") + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + // Filter for "official" but only "community" servers exist: should retry until context expires. + result := dmsghttp.GetServers(ctx, srv.URL, "official", log) + assert.Empty(t, result) +} + +func TestGetServers_ContextCancelledReturnsEmpty(t *testing.T) { + // If context is cancelled before any servers are found, return empty. + log := logging.MustGetLogger("test_ctx_cancel") + + // Use a URL that will fail (no server listening). + ctx, cancel := context.WithCancel(context.Background()) + cancel() // Cancel immediately. + + result := dmsghttp.GetServers(ctx, "http://127.0.0.1:1", "", log) + assert.Empty(t, result) +} + +func TestGetServers_EmptyResponseRetries(t *testing.T) { + // If the discovery returns an empty list, GetServers retries until context is done. + srv := newDiscoveryMockServer(t, []*disc.Entry{}) + log := logging.MustGetLogger("test_empty") + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + result := dmsghttp.GetServers(ctx, srv.URL, "", log) + assert.Empty(t, result) +} + +func TestGetServers_NoFilterReturnsAll(t *testing.T) { + // Empty dmsgServerType string should return all servers without filtering. + pk1, _ := cipher.GenerateKeyPair() + pk2, _ := cipher.GenerateKeyPair() + + entries := []*disc.Entry{ + { + Static: pk1, + Server: &disc.Server{Address: "1.2.3.4:8080", AvailableSessions: 10, ServerType: "official"}, + }, + { + Static: pk2, + Server: &disc.Server{Address: "5.6.7.8:8080", AvailableSessions: 5, ServerType: "community"}, + }, + } + + srv := newDiscoveryMockServer(t, entries) + log := logging.MustGetLogger("test_no_filter") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + result := dmsghttp.GetServers(ctx, srv.URL, "", log) + require.Len(t, result, 2) +} + +// --- ListenAndServe tests --- + +func TestListenAndServe_ServesHTTP(t *testing.T) { + const maxSessions = 20 + const dmsgHTTPPort = uint16(8080) + + dc := disc.NewMock(0) + + // Start dmsg server. + srvPK, srvSK := cipher.GenerateKeyPair() + srvConf := dmsg.ServerConfig{MaxSessions: maxSessions, UpdateInterval: 0} + srv := dmsg.NewServer(srvPK, srvSK, dc, &srvConf, nil) + lis, err := nettest.NewLocalListener("tcp") + require.NoError(t, err) + go srv.Serve(lis, "") //nolint:errcheck + t.Cleanup(func() { srv.Close() }) //nolint:errcheck + <-srv.Ready() + + // Host client. + hostPK, hostSK := cipher.GenerateKeyPair() + dmsgHost := dmsg.NewClient(hostPK, hostSK, dc, &dmsg.Config{MinSessions: 1}) + go dmsgHost.Serve(context.Background()) + t.Cleanup(func() { dmsgHost.Close() }) //nolint:errcheck + <-dmsgHost.Ready() + + log := logging.MustGetLogger("test_listen_serve") + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("listen-and-serve")) + }) + + errCh := make(chan error, 1) + go func() { + errCh <- dmsghttp.ListenAndServe(ctx, hostSK, handler, dc, dmsgHTTPPort, dmsgHost, log) + }() + + // Give the server a moment to start. + time.Sleep(100 * time.Millisecond) + + // Dial from another client. + clientPK, clientSK := cipher.GenerateKeyPair() + dmsgClient := dmsg.NewClient(clientPK, clientSK, dc, &dmsg.Config{MinSessions: 1}) + go dmsgClient.Serve(context.Background()) + t.Cleanup(func() { dmsgClient.Close() }) //nolint:errcheck + <-dmsgClient.Ready() + + // Allow time for dmsg sessions to stabilize. + time.Sleep(300 * time.Millisecond) + + httpC := http.Client{ + Transport: dmsghttp.MakeHTTPTransport(ctx, dmsgClient), + Timeout: 10 * time.Second, + } + + resp, err := httpC.Get(fmt.Sprintf("http://%s:%d/", hostPK.String(), dmsgHTTPPort)) + require.NoError(t, err) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, "listen-and-serve", string(body)) + + // Cancel context to shut down the server. + cancel() +} + +func TestListenAndServe_InvalidPort(t *testing.T) { + // Trying to listen on a port that's already in use should fail. + const maxSessions = 20 + const dmsgHTTPPort = uint16(8081) + + dc := disc.NewMock(0) + + srvPK, srvSK := cipher.GenerateKeyPair() + srvConf := dmsg.ServerConfig{MaxSessions: maxSessions, UpdateInterval: 0} + srv := dmsg.NewServer(srvPK, srvSK, dc, &srvConf, nil) + lis, err := nettest.NewLocalListener("tcp") + require.NoError(t, err) + go srv.Serve(lis, "") //nolint:errcheck + t.Cleanup(func() { srv.Close() }) //nolint:errcheck + <-srv.Ready() + + hostPK, hostSK := cipher.GenerateKeyPair() + dmsgHost := dmsg.NewClient(hostPK, hostSK, dc, &dmsg.Config{MinSessions: 1}) + go dmsgHost.Serve(context.Background()) + t.Cleanup(func() { dmsgHost.Close() }) //nolint:errcheck + <-dmsgHost.Ready() + + log := logging.MustGetLogger("test_invalid_port") + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {}) + + // First listen should succeed. + _, err = dmsgHost.Listen(dmsgHTTPPort) + require.NoError(t, err) + + // Second listen on the same port should fail. + err = dmsghttp.ListenAndServe(ctx, hostSK, handler, dc, dmsgHTTPPort, dmsgHost, log) + assert.Error(t, err) + assert.Contains(t, err.Error(), "dmsg listen") +} diff --git a/pkg/dmsgpty/whitelist_test.go b/pkg/dmsgpty/whitelist_test.go new file mode 100644 index 000000000..db15c96d7 --- /dev/null +++ b/pkg/dmsgpty/whitelist_test.go @@ -0,0 +1,475 @@ +// Package dmsgpty_test provides tests for whitelist and RPC components. +package dmsgpty_test + +import ( + "bytes" + "net" + "net/rpc" + "os" + "path/filepath" + "testing" + + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/skycoin/dmsg/pkg/dmsgpty" +) + +// --- MemoryWhitelist tests --- + +func TestMemoryWhitelist_AddGetRemoveAll(t *testing.T) { + wl := dmsgpty.NewMemoryWhitelist() + + pk1, _ := cipher.GenerateKeyPair() + pk2, _ := cipher.GenerateKeyPair() + pk3, _ := cipher.GenerateKeyPair() + + // Initially empty. + all, err := wl.All() + require.NoError(t, err) + require.Len(t, all, 0) + + // Get on missing key returns false. + ok, err := wl.Get(pk1) + require.NoError(t, err) + require.False(t, ok) + + // Add single key. + require.NoError(t, wl.Add(pk1)) + ok, err = wl.Get(pk1) + require.NoError(t, err) + require.True(t, ok) + + // Add multiple keys at once. + require.NoError(t, wl.Add(pk2, pk3)) + all, err = wl.All() + require.NoError(t, err) + require.Len(t, all, 3) + require.True(t, all[pk1]) + require.True(t, all[pk2]) + require.True(t, all[pk3]) + + // Remove one key. + require.NoError(t, wl.Remove(pk2)) + ok, err = wl.Get(pk2) + require.NoError(t, err) + require.False(t, ok) + + all, err = wl.All() + require.NoError(t, err) + require.Len(t, all, 2) + + // Remove remaining keys. + require.NoError(t, wl.Remove(pk1, pk3)) + all, err = wl.All() + require.NoError(t, err) + require.Len(t, all, 0) +} + +func TestMemoryWhitelist_AddDuplicate(t *testing.T) { + wl := dmsgpty.NewMemoryWhitelist() + pk, _ := cipher.GenerateKeyPair() + + require.NoError(t, wl.Add(pk)) + require.NoError(t, wl.Add(pk)) // duplicate add should not error + + all, err := wl.All() + require.NoError(t, err) + require.Len(t, all, 1) +} + +func TestMemoryWhitelist_RemoveNonExistent(t *testing.T) { + wl := dmsgpty.NewMemoryWhitelist() + pk, _ := cipher.GenerateKeyPair() + + // Removing a key that was never added should not error. + require.NoError(t, wl.Remove(pk)) +} + +// --- ConfigWhitelist tests --- + +func TestConfigWhitelist_AddGetRemoveAll(t *testing.T) { + tmpDir := t.TempDir() + confPath := filepath.Join(tmpDir, "test_wl_config.json") + + wl, err := dmsgpty.NewConfigWhitelist(confPath) + require.NoError(t, err) + + pk1, _ := cipher.GenerateKeyPair() + pk2, _ := cipher.GenerateKeyPair() + + // Record initial count (global state may carry over from other tests). + initAll, err := wl.All() + require.NoError(t, err) + initCount := len(initAll) + + // Get on missing key. + ok, err := wl.Get(pk1) + require.NoError(t, err) + require.False(t, ok) + + // Add key and verify. + require.NoError(t, wl.Add(pk1)) + ok, err = wl.Get(pk1) + require.NoError(t, err) + require.True(t, ok) + + // Add second key. + require.NoError(t, wl.Add(pk2)) + all, err := wl.All() + require.NoError(t, err) + require.Len(t, all, initCount+2) + + // Remove first key. + require.NoError(t, wl.Remove(pk1)) + ok, err = wl.Get(pk1) + require.NoError(t, err) + require.False(t, ok) + + all, err = wl.All() + require.NoError(t, err) + require.Len(t, all, initCount+1) + require.True(t, all[pk2]) +} + +func TestConfigWhitelist_Persistence(t *testing.T) { + tmpDir := t.TempDir() + confPath := filepath.Join(tmpDir, "persist_config.json") + + pk1, _ := cipher.GenerateKeyPair() + + // Create whitelist and add a key. + wl1, err := dmsgpty.NewConfigWhitelist(confPath) + require.NoError(t, err) + require.NoError(t, wl1.Add(pk1)) + + // Create a new whitelist instance pointing to the same file. + wl2, err := dmsgpty.NewConfigWhitelist(confPath) + require.NoError(t, err) + + ok, err := wl2.Get(pk1) + require.NoError(t, err) + require.True(t, ok, "key should persist across whitelist instances") +} + +func TestConfigWhitelist_AddDuplicate(t *testing.T) { + tmpDir := t.TempDir() + confPath := filepath.Join(tmpDir, "dup_config.json") + + wl, err := dmsgpty.NewConfigWhitelist(confPath) + require.NoError(t, err) + + pk, _ := cipher.GenerateKeyPair() + require.NoError(t, wl.Add(pk)) + + // Adding the same key again should return an error about duplicate. + err = wl.Add(pk) + require.Error(t, err) + assert.Contains(t, err.Error(), "Already exists") +} + +func TestConfigWhitelist_FileCreatedIfMissing(t *testing.T) { + tmpDir := t.TempDir() + confPath := filepath.Join(tmpDir, "subdir", "auto_config.json") + + wl, err := dmsgpty.NewConfigWhitelist(confPath) + require.NoError(t, err) + + // The file should be created after first operation. + _, err = wl.All() + require.NoError(t, err) + + _, err = os.Stat(confPath) + require.NoError(t, err, "config file should exist after first operation") +} + +// --- WhitelistGateway RPC method tests --- + +func TestWhitelistGateway_Whitelist(t *testing.T) { + wl := dmsgpty.NewMemoryWhitelist() + gw := dmsgpty.NewWhitelistGateway(wl) + + pk1, _ := cipher.GenerateKeyPair() + pk2, _ := cipher.GenerateKeyPair() + require.NoError(t, wl.Add(pk1, pk2)) + + var out []cipher.PubKey + err := gw.Whitelist(nil, &out) + require.NoError(t, err) + require.Len(t, out, 2) + + // Verify both keys are present. + found := make(map[cipher.PubKey]bool) + for _, pk := range out { + found[pk] = true + } + require.True(t, found[pk1]) + require.True(t, found[pk2]) +} + +func TestWhitelistGateway_WhitelistAdd(t *testing.T) { + wl := dmsgpty.NewMemoryWhitelist() + gw := dmsgpty.NewWhitelistGateway(wl) + + pk, _ := cipher.GenerateKeyPair() + pks := []cipher.PubKey{pk} + + var empty struct{} + err := gw.WhitelistAdd(&pks, &empty) + require.NoError(t, err) + + ok, err := wl.Get(pk) + require.NoError(t, err) + require.True(t, ok) +} + +func TestWhitelistGateway_WhitelistRemove(t *testing.T) { + wl := dmsgpty.NewMemoryWhitelist() + gw := dmsgpty.NewWhitelistGateway(wl) + + pk, _ := cipher.GenerateKeyPair() + require.NoError(t, wl.Add(pk)) + + pks := []cipher.PubKey{pk} + var empty struct{} + err := gw.WhitelistRemove(&pks, &empty) + require.NoError(t, err) + + ok, err := wl.Get(pk) + require.NoError(t, err) + require.False(t, ok) +} + +func TestWhitelistGateway_EmptyWhitelist(t *testing.T) { + wl := dmsgpty.NewMemoryWhitelist() + gw := dmsgpty.NewWhitelistGateway(wl) + + var out []cipher.PubKey + err := gw.Whitelist(nil, &out) + require.NoError(t, err) + require.Len(t, out, 0) +} + +// --- WhitelistClient + WhitelistGateway integration over net.Pipe --- + +func TestWhitelistClientGateway_Integration(t *testing.T) { + wl := dmsgpty.NewMemoryWhitelist() + gw := dmsgpty.NewWhitelistGateway(wl) + + connServer, connClient := net.Pipe() + + // Server side: read the request, write response, then serve RPC. + serverReady := make(chan error, 1) + go func() { + defer connServer.Close() //nolint:errcheck + + // Read the length-prefixed URI request. + prefix := make([]byte, 1) + if _, err := connServer.Read(prefix); err != nil { + serverReady <- err + return + } + uri := make([]byte, prefix[0]) + if _, err := connServer.Read(uri); err != nil { + serverReady <- err + return + } + + // Write accept response (0 byte). + if _, err := connServer.Write([]byte{0}); err != nil { + serverReady <- err + return + } + serverReady <- nil + + // Serve RPC on the connection. + rpcServer := rpc.NewServer() + if err := rpcServer.RegisterName("whitelist", gw); err != nil { + return + } + rpcServer.ServeConn(connServer) + }() + + // Client side. + wlClient, err := dmsgpty.NewWhitelistClient(connClient) + require.NoError(t, err) + require.NoError(t, <-serverReady) + + // View empty whitelist. + pks, err := wlClient.ViewWhitelist() + require.NoError(t, err) + require.Len(t, pks, 0) + + // Add keys. + pk1, _ := cipher.GenerateKeyPair() + pk2, _ := cipher.GenerateKeyPair() + require.NoError(t, wlClient.WhitelistAdd(pk1, pk2)) + + pks, err = wlClient.ViewWhitelist() + require.NoError(t, err) + require.Len(t, pks, 2) + + // Remove one key. + require.NoError(t, wlClient.WhitelistRemove(pk1)) + + pks, err = wlClient.ViewWhitelist() + require.NoError(t, err) + require.Len(t, pks, 1) + + require.NoError(t, connClient.Close()) +} + +// --- RPC util round-trip tests --- +// writeRequest/readRequest and writeResponse/readResponse are unexported, +// so we test them indirectly through the WhitelistClient handshake protocol. + +func TestRPCUtil_RequestResponseRoundTrip(t *testing.T) { + // Test the request/response protocol by simulating what NewWhitelistClient does. + connA, connB := net.Pipe() + + done := make(chan error, 1) + go func() { + defer connA.Close() //nolint:errcheck + + // Read length-prefixed request. + prefix := make([]byte, 1) + if _, err := connA.Read(prefix); err != nil { + done <- err + return + } + uri := make([]byte, prefix[0]) + if _, err := connA.Read(uri); err != nil { + done <- err + return + } + + // Verify the URI matches expected. + assert.Equal(t, "dmsgpty/whitelist", string(uri)) + + // Write accept (0). + _, err := connA.Write([]byte{0}) + done <- err + }() + + // Client side sends the request and reads the response. + // Write length prefix + URI (same as writeRequest). + uriStr := "dmsgpty/whitelist" + buf := make([]byte, 0, 1+len(uriStr)) + buf = append(buf, byte(len(uriStr))) + buf = append(buf, []byte(uriStr)...) + _, err := connB.Write(buf) + require.NoError(t, err) + + // Read response (same as readResponse). + resp := make([]byte, 1) + _, err = connB.Read(resp) + require.NoError(t, err) + require.Equal(t, byte(0), resp[0], "response should be accept (0)") + + require.NoError(t, <-done) + require.NoError(t, connB.Close()) +} + +func TestRPCUtil_RejectResponse(t *testing.T) { + // Test that a rejection byte (non-zero) is correctly detected. + connA, connB := net.Pipe() + + go func() { + defer connA.Close() //nolint:errcheck + // Read and discard the request. + prefix := make([]byte, 1) + connA.Read(prefix) //nolint:errcheck + uri := make([]byte, prefix[0]) + connA.Read(uri) //nolint:errcheck + + // Write reject (1). + connA.Write([]byte{1}) //nolint:errcheck + }() + + // NewWhitelistClient should fail when server rejects. + _, err := dmsgpty.NewWhitelistClient(connB) + require.Error(t, err) + assert.Contains(t, err.Error(), "rejected") + + require.NoError(t, connB.Close()) +} + +// --- Config tests --- + +func TestDefaultConfig(t *testing.T) { + c := dmsgpty.DefaultConfig() + require.NotEmpty(t, c.DmsgDisc) + require.NotZero(t, c.DmsgSessions) + require.Equal(t, uint16(22), c.DmsgPort) + require.NotEmpty(t, c.CLINet) + require.NotEmpty(t, c.CLIAddr) +} + +func TestWriteConfig(t *testing.T) { + tmpDir := t.TempDir() + confPath := filepath.Join(tmpDir, "write_test_config.json") + + c := dmsgpty.DefaultConfig() + c.PK = "testpk" + c.SK = "testsk" + + err := dmsgpty.WriteConfig(c, confPath) + require.NoError(t, err) + + // Verify file was written. + data, err := os.ReadFile(confPath) + require.NoError(t, err) + require.Contains(t, string(data), "testpk") + require.Contains(t, string(data), "testsk") +} + +func TestParseWindowsEnv_NonWindows(t *testing.T) { + // On non-Windows, ParseWindowsEnv should return the input unchanged. + input := "%HOMEDRIVE%%HOMEPATH%\\dmsgpty.sock" + result := dmsgpty.ParseWindowsEnv(input) + require.NotEmpty(t, result) +} + +// --- Additional edge case: large write/read round-trip --- + +func TestRPCUtil_LargeURI(t *testing.T) { + // Test with a URI close to the 255-byte limit. + connA, connB := net.Pipe() + + // Create a URI of exactly 255 bytes. + largeURI := bytes.Repeat([]byte("a"), 255) + + done := make(chan error, 1) + go func() { + defer connA.Close() //nolint:errcheck + prefix := make([]byte, 1) + if _, err := connA.Read(prefix); err != nil { + done <- err + return + } + uri := make([]byte, prefix[0]) + if _, err := connA.Read(uri); err != nil { + done <- err + return + } + assert.Equal(t, string(largeURI), string(uri)) + _, err := connA.Write([]byte{0}) + done <- err + }() + + // Write: length prefix (255) + URI. + buf := make([]byte, 0, 256) + buf = append(buf, byte(255)) + buf = append(buf, largeURI...) + _, err := connB.Write(buf) + require.NoError(t, err) + + resp := make([]byte, 1) + _, err = connB.Read(resp) + require.NoError(t, err) + require.Equal(t, byte(0), resp[0]) + + require.NoError(t, <-done) + require.NoError(t, connB.Close()) +} diff --git a/pkg/dmsgserver/config_test.go b/pkg/dmsgserver/config_test.go new file mode 100644 index 000000000..17ca4c751 --- /dev/null +++ b/pkg/dmsgserver/config_test.go @@ -0,0 +1,61 @@ +package dmsgserver_test + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/skycoin/skycoin/src/util/logging" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/skycoin/dmsg/pkg/dmsgserver" +) + +func TestGenerateDefaultConfig(t *testing.T) { + var c dmsgserver.Config + dmsgserver.GenerateDefaultConfig(&c) + + assert.Equal(t, dmsgserver.DefaultConfigPath, c.Path) + assert.False(t, c.PubKey.Null(), "PubKey should be populated") + assert.False(t, c.SecKey.Null(), "SecKey should be populated") + assert.NotEmpty(t, c.Discovery, "Discovery URL should be set") + assert.Equal(t, "127.0.0.1:8081", c.PublicAddress) + assert.Equal(t, ":8081", c.LocalAddress) + assert.Equal(t, ":8082", c.HTTPAddress) + assert.Equal(t, "info", c.LogLevel) + assert.Equal(t, 2048, c.MaxSessions) +} + +func TestConfigFlush(t *testing.T) { + var c dmsgserver.Config + dmsgserver.GenerateDefaultConfig(&c) + + tmpDir := t.TempDir() + c.Path = filepath.Join(tmpDir, "test_config.json") + + log := logging.MustGetLogger("test") + err := c.Flush(log) + require.NoError(t, err) + + data, err := os.ReadFile(c.Path) + require.NoError(t, err) + + var loaded dmsgserver.Config + err = json.Unmarshal(data, &loaded) + require.NoError(t, err) + + assert.Equal(t, c.PubKey, loaded.PubKey) + assert.Equal(t, c.SecKey, loaded.SecKey) + assert.Equal(t, c.Discovery, loaded.Discovery) + assert.Equal(t, c.PublicAddress, loaded.PublicAddress) + assert.Equal(t, c.LocalAddress, loaded.LocalAddress) + assert.Equal(t, c.HTTPAddress, loaded.HTTPAddress) + assert.Equal(t, c.LogLevel, loaded.LogLevel) + assert.Equal(t, c.MaxSessions, loaded.MaxSessions) +} + +func TestDefaultConfigPath(t *testing.T) { + assert.Equal(t, "config.json", dmsgserver.DefaultConfigPath) +} From 264e5a664846fca53bf0e5ebef7fe81bc6d29116 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Sun, 22 Mar 2026 18:21:00 -0500 Subject: [PATCH 02/15] Eliminate internal packages to enable external testing Move all internal packages to pkg/ so they can be imported and tested by external packages, addressing the testing infrastructure limitation. Package moves: - internal/servermetrics -> pkg/dmsg/metrics - internal/discmetrics -> pkg/disc/metrics - internal/cli + internal/flags -> pkg/dmsgclient (merged) - internal/dmsg-discovery/api -> pkg/discovery/api - internal/dmsg-discovery/store -> pkg/discovery/store - internal/dmsg-server/api -> pkg/dmsgserver (merged with existing config) - internal/fsutil -> deleted (inlined os.Stat at single call site) Only internal/e2e/ remains, containing integration test infrastructure that is legitimately test-only. API renames in pkg/dmsgserver: API -> ServerAPI, New -> NewServerAPI --- cmd/dial/commands/dial.go | 13 +++--- cmd/dmsg-discovery/commands/dmsg-discovery.go | 12 ++--- cmd/dmsg-server/commands/start/root.go | 19 ++++---- cmd/dmsg-socks5/commands/dmsg-socks5.go | 6 +-- cmd/dmsgcurl/commands/dmsgcurl.go | 19 ++++---- cmd/dmsghttp/commands/dmsghttp.go | 9 ++-- cmd/dmsgip/commands/dmsgip.go | 4 +- cmd/dmsgpty-host/commands/confgen.go | 10 ++-- cmd/dmsgweb/commands/dmsgweb.go | 13 +++--- cmd/dmsgweb/commands/dmsgwebsrv.go | 9 ++-- examples/dmsghttp-client/dmsghttp-client.go | 4 +- examples/dmsgtcp/dmsgtcp.go | 4 +- internal/fsutil/fsutil.go | 19 -------- .../discmetrics => pkg/disc/metrics}/empty.go | 4 +- .../disc/metrics}/metrics.go | 4 +- .../disc/metrics}/victoria_metrics.go | 4 +- .../discovery}/api/api.go | 10 ++-- .../discovery}/api/entries_endpoint_test.go | 8 ++-- .../discovery}/api/error_handler.go | 2 +- .../discovery}/api/error_handler_test.go | 8 ++-- .../api/get_available_servers_test.go | 8 ++-- .../discovery}/store/redis.go | 2 +- .../discovery}/store/redis_test.go | 2 +- .../discovery}/store/storer.go | 2 +- .../discovery}/store/testing.go | 2 +- .../dmsg/metrics}/delta.go | 4 +- .../dmsg/metrics}/empty.go | 4 +- .../dmsg/metrics}/metrics.go | 4 +- .../dmsg/metrics}/victoria_metrics.go | 4 +- pkg/dmsg/server.go | 8 ++-- pkg/dmsg/server_session.go | 28 +++++------ {internal/cli => pkg/dmsgclient}/cli.go | 31 ++++++------- {internal/flags => pkg/dmsgclient}/flags.go | 4 +- .../dmsg-server/api => pkg/dmsgserver}/api.go | 40 ++++++++-------- pkg/dmsgserver/api_test.go | 46 +++++++++++++++++++ 35 files changed, 194 insertions(+), 176 deletions(-) delete mode 100644 internal/fsutil/fsutil.go rename {internal/discmetrics => pkg/disc/metrics}/empty.go (81%) rename {internal/discmetrics => pkg/disc/metrics}/metrics.go (65%) rename {internal/discmetrics => pkg/disc/metrics}/victoria_metrics.go (91%) rename {internal/dmsg-discovery => pkg/discovery}/api/api.go (97%) rename {internal/dmsg-discovery => pkg/discovery}/api/entries_endpoint_test.go (96%) rename {internal/dmsg-discovery => pkg/discovery}/api/error_handler.go (95%) rename {internal/dmsg-discovery => pkg/discovery}/api/error_handler_test.go (81%) rename {internal/dmsg-discovery => pkg/discovery}/api/get_available_servers_test.go (94%) rename {internal/dmsg-discovery => pkg/discovery}/store/redis.go (99%) rename {internal/dmsg-discovery => pkg/discovery}/store/redis_test.go (98%) rename {internal/dmsg-discovery => pkg/discovery}/store/storer.go (97%) rename {internal/dmsg-discovery => pkg/discovery}/store/testing.go (99%) rename {internal/servermetrics => pkg/dmsg/metrics}/delta.go (70%) rename {internal/servermetrics => pkg/dmsg/metrics}/empty.go (88%) rename {internal/servermetrics => pkg/dmsg/metrics}/metrics.go (74%) rename {internal/servermetrics => pkg/dmsg/metrics}/victoria_metrics.go (96%) rename {internal/cli => pkg/dmsgclient}/cli.go (95%) rename {internal/flags => pkg/dmsgclient}/flags.go (96%) rename {internal/dmsg-server/api => pkg/dmsgserver}/api.go (85%) create mode 100644 pkg/dmsgserver/api_test.go diff --git a/cmd/dial/commands/dial.go b/cmd/dial/commands/dial.go index f7e6b13ae..0b058e273 100644 --- a/cmd/dial/commands/dial.go +++ b/cmd/dial/commands/dial.go @@ -20,9 +20,8 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" "github.com/spf13/cobra" - "github.com/skycoin/dmsg/internal/cli" - "github.com/skycoin/dmsg/internal/flags" "github.com/skycoin/dmsg/pkg/disc" + "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/dmsg" ) @@ -35,7 +34,7 @@ var ( ) func init() { - flags.InitFlags(RootCmd) + dmsgclient.InitFlags(RootCmd) RootCmd.Flags().StringVarP(&logLvl, "loglvl", "l", "info", "[ debug | warn | error | fatal | panic | trace | info ]\033[0m\n\r") RootCmd.Flags().IntVarP(&waitTime, "wait", "w", 0, "wait time in seconds before disconnecting\n\r\033[0m") RootCmd.Flags().VarP(&sk, "sk", "s", "a random key is generated if unspecified\n\r\033[0m") @@ -126,15 +125,15 @@ Default mode of operation is dmsghttp: defer cancel() var dmsgClients []*dmsg.Client - if flags.UseDC { + if dmsgclient.UseDC { dlog.Debug("Starting DMSG direct clients.") for _, server := range dmsg.Prod.DmsgServers { - if len(dmsgClients) >= flags.DmsgSessions { + if len(dmsgClients) >= dmsgclient.DmsgSessions { break } dest := dpk.String() - dmsgDC, closeFn, err := cli.StartDmsgDirectWithServers(ctx, dlog, pk, sk, "", []*disc.Entry{&server}, flags.DmsgSessions, dest) + dmsgDC, closeFn, err := dmsgclient.StartDmsgDirectWithServers(ctx, dlog, pk, sk, "", []*disc.Entry{&server}, dmsgclient.DmsgSessions, dest) if err != nil { dlog.WithError(err).Error("Failed to start DMSG direct client. Skipping server...") continue @@ -144,7 +143,7 @@ Default mode of operation is dmsghttp: dmsgClients = append(dmsgClients, dmsgDC) } } else { - dmsgC, closeDmsg, err := cli.InitDmsgWithFlags(ctx, dlog, pk, sk, httpClient, pk.String()) + dmsgC, closeDmsg, err := dmsgclient.InitDmsgWithFlags(ctx, dlog, pk, sk, httpClient, pk.String()) if err != nil { dlog.WithError(err).Error("Error connecting to dmsg network") return diff --git a/cmd/dmsg-discovery/commands/dmsg-discovery.go b/cmd/dmsg-discovery/commands/dmsg-discovery.go index 59feede0d..8fc1b0c56 100644 --- a/cmd/dmsg-discovery/commands/dmsg-discovery.go +++ b/cmd/dmsg-discovery/commands/dmsg-discovery.go @@ -25,9 +25,9 @@ import ( "github.com/spf13/cobra" "github.com/tidwall/pretty" - "github.com/skycoin/dmsg/internal/discmetrics" - "github.com/skycoin/dmsg/internal/dmsg-discovery/api" - "github.com/skycoin/dmsg/internal/dmsg-discovery/store" + "github.com/skycoin/dmsg/pkg/disc/metrics" + "github.com/skycoin/dmsg/pkg/discovery/api" + "github.com/skycoin/dmsg/pkg/discovery/store" "github.com/skycoin/dmsg/pkg/direct" "github.com/skycoin/dmsg/pkg/disc" dmsg "github.com/skycoin/dmsg/pkg/dmsg" @@ -342,11 +342,11 @@ Example: defer cancel() db := prepareDB(ctx, log) - var m discmetrics.Metrics + var m metrics.Metrics if sf.MetricsAddr == "" { - m = discmetrics.NewEmpty() + m = metrics.NewEmpty() } else { - m = discmetrics.NewVictoriaMetrics() + m = metrics.NewVictoriaMetrics() } var dmsgAddr string diff --git a/cmd/dmsg-server/commands/start/root.go b/cmd/dmsg-server/commands/start/root.go index e9261f4c2..d6b0cc9eb 100644 --- a/cmd/dmsg-server/commands/start/root.go +++ b/cmd/dmsg-server/commands/start/root.go @@ -21,8 +21,7 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/metricsutil" "github.com/spf13/cobra" - "github.com/skycoin/dmsg/internal/dmsg-server/api" - "github.com/skycoin/dmsg/internal/servermetrics" + "github.com/skycoin/dmsg/pkg/dmsg/metrics" "github.com/skycoin/dmsg/pkg/disc" dmsg "github.com/skycoin/dmsg/pkg/dmsg" "github.com/skycoin/dmsg/pkg/dmsgserver" @@ -109,11 +108,11 @@ var RootCmd = &cobra.Command{ conf.HTTPAddress = ":" + httpPort } - var m servermetrics.Metrics + var m metrics.Metrics if sf.MetricsAddr == "" { - m = servermetrics.NewEmpty() + m = metrics.NewEmpty() } else { - m = servermetrics.NewVictoriaMetrics() + m = metrics.NewVictoriaMetrics() } metricsutil.ServeHTTPMetrics(log, sf.MetricsAddr) @@ -124,7 +123,7 @@ var RootCmd = &cobra.Command{ r.Use(middleware.Logger) r.Use(middleware.Recoverer) - api := api.New(r, log, m) + srvAPI := dmsgserver.NewServerAPI(r, log, m) srvConf := dmsg.ServerConfig{ MaxSessions: conf.MaxSessions, @@ -134,16 +133,16 @@ var RootCmd = &cobra.Command{ srv := dmsg.NewServer(conf.PubKey, conf.SecKey, disc.NewHTTP(conf.Discovery, &http.Client{}, log), &srvConf, m) srv.SetLogger(log) - api.SetDmsgServer(srv) - defer func() { log.WithError(api.Close()).Info("Closed server.") }() + srvAPI.SetDmsgServer(srv) + defer func() { log.WithError(srvAPI.Close()).Info("Closed server.") }() ctx, cancel := cmdutil.SignalContext(context.Background(), log) defer cancel() - go api.RunBackgroundTasks(ctx) + go srvAPI.RunBackgroundTasks(ctx) log.WithField("addr", conf.HTTPAddress).Info("Serving server API...") go func() { - if err := api.ListenAndServe(conf.LocalAddress, conf.PublicAddress, conf.HTTPAddress); err != nil { + if err := srvAPI.ListenAndServe(conf.LocalAddress, conf.PublicAddress, conf.HTTPAddress); err != nil { log.Errorf("Serve: %v", err) cancel() } diff --git a/cmd/dmsg-socks5/commands/dmsg-socks5.go b/cmd/dmsg-socks5/commands/dmsg-socks5.go index 0fb2ea04a..29828d9e4 100644 --- a/cmd/dmsg-socks5/commands/dmsg-socks5.go +++ b/cmd/dmsg-socks5/commands/dmsg-socks5.go @@ -19,8 +19,8 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" "github.com/spf13/cobra" - "github.com/skycoin/dmsg/internal/cli" dmsg "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsgclient" ) var ( @@ -137,7 +137,7 @@ var serveCmd = &cobra.Command{ defer cancel() //TODO: implement whitelist logic - dmsgC, closeDmsg, err := cli.InitDmsgWithFlags(ctx, dlog, pk, sk, httpClient, pk.String()) + dmsgC, closeDmsg, err := dmsgclient.InitDmsgWithFlags(ctx, dlog, pk, sk, httpClient, pk.String()) if err != nil { dlog.WithError(err).Fatal("Error connecting to dmsg network") @@ -235,7 +235,7 @@ var proxyCmd = &cobra.Command{ ctx, cancel := cmdutil.SignalContext(context.Background(), dlog) defer cancel() - dmsgC, closeDmsg, err := cli.InitDmsgWithFlags(ctx, dlog, pk, sk, httpClient, pk.String()) + dmsgC, closeDmsg, err := dmsgclient.InitDmsgWithFlags(ctx, dlog, pk, sk, httpClient, pk.String()) if err != nil { dlog.WithError(err).Fatal("Error connecting to dmsg network") diff --git a/cmd/dmsgcurl/commands/dmsgcurl.go b/cmd/dmsgcurl/commands/dmsgcurl.go index a2f6d9dfe..b8a76cc96 100644 --- a/cmd/dmsgcurl/commands/dmsgcurl.go +++ b/cmd/dmsgcurl/commands/dmsgcurl.go @@ -25,9 +25,8 @@ import ( "github.com/spf13/cobra" "golang.org/x/net/proxy" - "github.com/skycoin/dmsg/internal/cli" - "github.com/skycoin/dmsg/internal/flags" "github.com/skycoin/dmsg/pkg/disc" + "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/dmsg" "github.com/skycoin/dmsg/pkg/dmsghttp" ) @@ -50,7 +49,7 @@ var ( func init() { RootCmd.Flags().SortFlags = false - flags.InitFlags(RootCmd) + dmsgclient.InitFlags(RootCmd) RootCmd.Flags().StringVarP(&proxyAddr, "proxy", "p", proxyAddr, "connect to DMSG via proxy (i.e. '127.0.0.1:1080')") RootCmd.Flags().StringVarP(&logLvl, "loglvl", "l", "fatal", "[ debug | warn | error | fatal | panic | trace | info ]\033[0m\n\r") RootCmd.Flags().StringVarP(&dmsgcurlData, "data", "d", "", "dmsghttp POST data") @@ -83,8 +82,8 @@ var RootCmd = &cobra.Command{ } } - if flags.DmsgHTTPPath != "" { - dmsg.DmsghttpJSON, err = os.ReadFile(flags.DmsgHTTPPath) //nolint + if dmsgclient.DmsgHTTPPath != "" { + dmsg.DmsghttpJSON, err = os.ReadFile(dmsgclient.DmsgHTTPPath) //nolint if err != nil { dlog.WithError(err).Fatal("Failed to read specified dmsghttp-config") } @@ -169,16 +168,16 @@ func handleRequest(ctx context.Context, pk cipher.PubKey, sk cipher.SecKey, http defer func() { closeAndCleanFile(file, err) }() var httpC http.Client - if flags.UseDC { + if dmsgclient.UseDC { var dmsgClients []*dmsg.Client dlog.Debug("Starting DMSG direct clients.") for _, server := range dmsg.Prod.DmsgServers { - if len(dmsgClients) >= flags.DmsgSessions { + if len(dmsgClients) >= dmsgclient.DmsgSessions { break } - dmsgDC, closeFn, err := cli.StartDmsgDirectWithServers(ctx, dlog, pk, sk, "", []*disc.Entry{&server}, flags.DmsgSessions, dmsg.ExtractPKFromDmsgAddr(parsedURL.String())) + dmsgDC, closeFn, err := dmsgclient.StartDmsgDirectWithServers(ctx, dlog, pk, sk, "", []*disc.Entry{&server}, dmsgclient.DmsgSessions, dmsg.ExtractPKFromDmsgAddr(parsedURL.String())) if err != nil { dlog.WithError(err).Error("Failed to start DMSG direct client. Skipping server...") continue @@ -194,10 +193,10 @@ func handleRequest(ctx context.Context, pk cipher.PubKey, sk cipher.SecKey, http // Build HTTP client with fallback round tripper httpC = http.Client{ - Transport: cli.NewFallbackRoundTripper(ctx, dmsgClients), + Transport: dmsgclient.NewFallbackRoundTripper(ctx, dmsgClients), } } else { - dmsgC, closeDmsg, err := cli.InitDmsgWithFlags(ctx, dlog, pk, sk, httpClient, parsedURL.String()) + dmsgC, closeDmsg, err := dmsgclient.InitDmsgWithFlags(ctx, dlog, pk, sk, httpClient, parsedURL.String()) if err != nil || dmsgC == nil { dlog.WithError(err).Debug("Error initializing DMSG client") return curlError{ diff --git a/cmd/dmsghttp/commands/dmsghttp.go b/cmd/dmsghttp/commands/dmsghttp.go index 8843291ad..a189a21cc 100644 --- a/cmd/dmsghttp/commands/dmsghttp.go +++ b/cmd/dmsghttp/commands/dmsghttp.go @@ -22,9 +22,8 @@ import ( "github.com/spf13/cobra" "golang.org/x/net/proxy" - "github.com/skycoin/dmsg/internal/cli" - "github.com/skycoin/dmsg/internal/flags" dmsg "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsgclient" ) var ( @@ -42,7 +41,7 @@ var ( func init() { RootCmd.Flags().SortFlags = false - flags.InitFlags(RootCmd) + dmsgclient.InitFlags(RootCmd) RootCmd.Flags().StringVarP(&proxyAddr, "proxy", "p", proxyAddr, "connect to DMSG via proxy (i.e. '127.0.0.1:1080')") RootCmd.Flags().StringVarP(&logLvl, "loglvl", "l", "debug", "[ debug | warn | error | fatal | panic | trace | info ]\033[0m\n\r") RootCmd.Flags().StringVarP(&serveDir, "dir", "r", ".", "local dir to serve via dmsghttp\033[0m\n\r") @@ -78,7 +77,7 @@ func server() { wg := new(sync.WaitGroup) wg.Add(1) - err = flags.InitConfig() + err = dmsgclient.InitConfig() if err != nil { dlog.WithError(err).Fatal("Failed to read specified dmsghttp-config") } @@ -127,7 +126,7 @@ func server() { var dmsgC *dmsg.Client var closeDmsg func() - dmsgC, closeDmsg, err = cli.InitDmsgWithFlags(ctx, dlog, pk, sk, httpClient, "") + dmsgC, closeDmsg, err = dmsgclient.InitDmsgWithFlags(ctx, dlog, pk, sk, httpClient, "") if err != nil { dlog.WithError(err).Error("Error connecting to dmsg network") return diff --git a/cmd/dmsgip/commands/dmsgip.go b/cmd/dmsgip/commands/dmsgip.go index 40cf01009..dec9726d1 100644 --- a/cmd/dmsgip/commands/dmsgip.go +++ b/cmd/dmsgip/commands/dmsgip.go @@ -18,8 +18,8 @@ import ( "github.com/spf13/cobra" "golang.org/x/net/proxy" - "github.com/skycoin/dmsg/internal/cli" "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsgclient" ) var ( @@ -116,7 +116,7 @@ var RootCmd = &cobra.Command{ ctx = context.WithValue(context.Background(), "socks5_proxy", proxyAddr) //nolint } - dmsgC, closeDmsg, err := cli.InitDmsgWithFlags(ctx, dlog, pk, sk, httpClient, pk.String()) + dmsgC, closeDmsg, err := dmsgclient.InitDmsgWithFlags(ctx, dlog, pk, sk, httpClient, pk.String()) if err != nil { dlog.WithError(err).Debug("Error connecting to dmsg network") diff --git a/cmd/dmsgpty-host/commands/confgen.go b/cmd/dmsgpty-host/commands/confgen.go index ba0c1e7d1..4c21b92af 100644 --- a/cmd/dmsgpty-host/commands/confgen.go +++ b/cmd/dmsgpty-host/commands/confgen.go @@ -3,10 +3,10 @@ package commands import ( "fmt" + "os" "github.com/spf13/cobra" - "github.com/skycoin/dmsg/internal/fsutil" "github.com/skycoin/dmsg/pkg/dmsgpty" ) @@ -39,12 +39,10 @@ var confgenCmd = &cobra.Command{ return dmsgpty.WriteConfig(conf, confPath) } - exists, err := fsutil.Exists(confPath) - if err != nil { - return fmt.Errorf("failed to check if config file exists: %w", err) - } - if exists { + if _, err := os.Stat(confPath); err == nil { return fmt.Errorf("config file %s already exists", confPath) + } else if !os.IsNotExist(err) { + return fmt.Errorf("failed to check if config file exists: %w", err) } return dmsgpty.WriteConfig(conf, confPath) diff --git a/cmd/dmsgweb/commands/dmsgweb.go b/cmd/dmsgweb/commands/dmsgweb.go index c4605ac7a..e517bc6eb 100644 --- a/cmd/dmsgweb/commands/dmsgweb.go +++ b/cmd/dmsgweb/commands/dmsgweb.go @@ -24,8 +24,7 @@ import ( "github.com/spf13/cobra" "golang.org/x/net/proxy" - "github.com/skycoin/dmsg/internal/cli" - "github.com/skycoin/dmsg/internal/flags" + "github.com/skycoin/dmsg/pkg/dmsgclient" dmsg "github.com/skycoin/dmsg/pkg/dmsg" "github.com/skycoin/dmsg/pkg/dmsghttp" ) @@ -64,7 +63,7 @@ func init() { } pk, _ = sk.PubKey() //nolint - flags.InitFlags(RootCmd) + dmsgclient.InitFlags(RootCmd) RootCmd.Flags().StringVarP(&filterDomainSuffix, "filter", "f", ".dmsg", "domain suffix to filter\033[0m\n\r") RootCmd.Flags().UintVarP(&proxyPort, "socks", "q", proxyPort, "port to serve the socks5 proxy\033[0m\n\r") RootCmd.Flags().StringVarP(&addProxy, "addproxy", "r", addProxy, "configure additional socks5 proxy for dmsgweb (i.e. 127.0.0.1:1080)\033[0m\n\r") @@ -113,15 +112,15 @@ dmsgweb conf file detected: ` + dwcfg } dlog = logging.MustGetLogger("dmsgweb") - err = flags.InitConfig() + err = dmsgclient.InitConfig() if err != nil { dlog.WithError(err).Fatal("Failed to read specified dmsghttp-config") } - if flags.DmsgDiscURL == "" { + if dmsgclient.DmsgDiscURL == "" { dlog.Fatal("Dmsg Discovery Server URL not specified") } - if flags.DmsgDiscURL == "" { + if dmsgclient.DmsgDiscURL == "" { dlog.Fatal("Dmsg Discovery Server dmsg address not specified") } @@ -222,7 +221,7 @@ dmsgweb conf file detected: ` + dwcfg ctx = context.WithValue(ctx, "socks5_proxy", proxyAddr) //nolint } - dmsgC, closeDmsg, err = cli.InitDmsgWithFlags(ctx, dlog, pk, sk, httpClient, "") + dmsgC, closeDmsg, err = dmsgclient.InitDmsgWithFlags(ctx, dlog, pk, sk, httpClient, "") if err != nil { dlog.WithError(err).Error("Error connecting to dmsg network") return diff --git a/cmd/dmsgweb/commands/dmsgwebsrv.go b/cmd/dmsgweb/commands/dmsgwebsrv.go index 4599f1ba1..dd19cff82 100644 --- a/cmd/dmsgweb/commands/dmsgwebsrv.go +++ b/cmd/dmsgweb/commands/dmsgwebsrv.go @@ -20,8 +20,7 @@ import ( "github.com/spf13/cobra" "golang.org/x/net/proxy" - "github.com/skycoin/dmsg/internal/cli" - "github.com/skycoin/dmsg/internal/flags" + "github.com/skycoin/dmsg/pkg/dmsgclient" ) const dwsenv = "DMSGWEBSRV" @@ -42,7 +41,7 @@ func init() { pk, _ = sk.PubKey() //nolint RootCmd.AddCommand(srvCmd) - flags.InitFlags(srvCmd) + dmsgclient.InitFlags(srvCmd) srvCmd.Flags().UintSliceVarP(&localPort, "lport", "p", localPort, "local application interface port(s)\033[0m\n\r") srvCmd.Flags().UintSliceVarP(&dmsgPort, "dport", "d", dmsgPort, "DMSG port(s) to serve\033[0m\n\r") srvCmd.Flags().StringSliceVarP(&wl, "wl", "w", wl, "whitelisted keys for DMSG authenticated routes"+func() string { @@ -84,7 +83,7 @@ var srvCmd = &cobra.Command{ } dlog = logging.MustGetLogger("dmsgwebsrv") - err = flags.InitConfig() + err = dmsgclient.InitConfig() if err != nil { dlog.WithError(err).Fatal("Failed to read specified dmsghttp-config") } @@ -133,7 +132,7 @@ func server() { ctx = context.WithValue(ctx, "socks5_proxy", proxyAddr) //nolint } - dmsgC, closeDmsg, err = cli.InitDmsgWithFlags(ctx, dlog, pk, sk, httpClient, "") + dmsgC, closeDmsg, err = dmsgclient.InitDmsgWithFlags(ctx, dlog, pk, sk, httpClient, "") if err != nil { dlog.WithError(err).Error("Error connecting to dmsg network") return diff --git a/examples/dmsghttp-client/dmsghttp-client.go b/examples/dmsghttp-client/dmsghttp-client.go index 05efc83b8..9c3119ab3 100644 --- a/examples/dmsghttp-client/dmsghttp-client.go +++ b/examples/dmsghttp-client/dmsghttp-client.go @@ -11,8 +11,8 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" - "github.com/skycoin/dmsg/internal/cli" "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/dmsghttp" ) @@ -25,7 +25,7 @@ func main() { } pk, sk := cipher.GenerateKeyPair() ctx := context.Background() - dmsgClient, closeDmsg, err := cli.StartDmsg(ctx, dLog, pk, sk, &http.Client{}, dmsgDisc, 1) + dmsgClient, closeDmsg, err := dmsgclient.StartDmsg(ctx, dLog, pk, sk, &http.Client{}, dmsgDisc, 1) if err != nil { dLog.Fatalf("Failed to start DMSG client: %v", err) } diff --git a/examples/dmsgtcp/dmsgtcp.go b/examples/dmsgtcp/dmsgtcp.go index 1a0592f22..0e0632272 100644 --- a/examples/dmsgtcp/dmsgtcp.go +++ b/examples/dmsgtcp/dmsgtcp.go @@ -17,8 +17,8 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" "github.com/spf13/cobra" - "github.com/skycoin/dmsg/internal/cli" dmsg "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsgclient" ) var ( @@ -60,7 +60,7 @@ var RootCmd = &cobra.Command{ } // Initialize the DMSG client - dmsgC, closeDmsg, err := cli.StartDmsg(ctx, log, pk, sk, &http.Client{}, dmsgDisc, 1) + dmsgC, closeDmsg, err := dmsgclient.StartDmsg(ctx, log, pk, sk, &http.Client{}, dmsgDisc, 1) if err != nil { log.WithError(err).Fatal("failed to start dmsg") } diff --git a/internal/fsutil/fsutil.go b/internal/fsutil/fsutil.go deleted file mode 100644 index 71e0f4ef1..000000000 --- a/internal/fsutil/fsutil.go +++ /dev/null @@ -1,19 +0,0 @@ -// Package fsutil internal/fsutil/fsutil.go - -package fsutil - -import ( - "os" -) - -// Exists checks if file exists at `path`. -func Exists(path string) (bool, error) { - _, err := os.Stat(path) - if err == nil { - return true, nil - } - if os.IsNotExist(err) { - return false, nil - } - return false, err -} diff --git a/internal/discmetrics/empty.go b/pkg/disc/metrics/empty.go similarity index 81% rename from internal/discmetrics/empty.go rename to pkg/disc/metrics/empty.go index e9fdd479f..da019b107 100644 --- a/internal/discmetrics/empty.go +++ b/pkg/disc/metrics/empty.go @@ -1,5 +1,5 @@ -// Package discmetrics internal/discmetrics/empty.go -package discmetrics +// Package metrics pkg/disc/metrics/empty.go +package metrics // NewEmpty constructs new empty metrics. func NewEmpty() Empty { diff --git a/internal/discmetrics/metrics.go b/pkg/disc/metrics/metrics.go similarity index 65% rename from internal/discmetrics/metrics.go rename to pkg/disc/metrics/metrics.go index d28f3d278..e7880ebcb 100644 --- a/internal/discmetrics/metrics.go +++ b/pkg/disc/metrics/metrics.go @@ -1,5 +1,5 @@ -// Package discmetrics internal/discmetrics/metrics.go -package discmetrics +// Package metrics pkg/disc/metrics/metrics.go +package metrics // Metrics collects metrics for metrics tracking system. type Metrics interface { diff --git a/internal/discmetrics/victoria_metrics.go b/pkg/disc/metrics/victoria_metrics.go similarity index 91% rename from internal/discmetrics/victoria_metrics.go rename to pkg/disc/metrics/victoria_metrics.go index 854afadb4..8b9046bfe 100644 --- a/internal/discmetrics/victoria_metrics.go +++ b/pkg/disc/metrics/victoria_metrics.go @@ -1,5 +1,5 @@ -// Package discmetrics internal/discmetrics/victoria_metrics.go -package discmetrics +// Package metrics pkg/disc/metrics/victoria_metrics.go +package metrics import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/metricsutil" diff --git a/internal/dmsg-discovery/api/api.go b/pkg/discovery/api/api.go similarity index 97% rename from internal/dmsg-discovery/api/api.go rename to pkg/discovery/api/api.go index c1d69a673..7edccada5 100644 --- a/internal/dmsg-discovery/api/api.go +++ b/pkg/discovery/api/api.go @@ -1,4 +1,4 @@ -// Package api internal/dmsg-discovery/api/api.go +// Package api pkg/discovery/api/api.go package api import ( @@ -19,9 +19,9 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/metricsutil" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/networkmonitor" - "github.com/skycoin/dmsg/internal/discmetrics" - "github.com/skycoin/dmsg/internal/dmsg-discovery/store" "github.com/skycoin/dmsg/pkg/disc" + "github.com/skycoin/dmsg/pkg/disc/metrics" + "github.com/skycoin/dmsg/pkg/discovery/store" "github.com/skycoin/dmsg/pkg/dmsg" ) @@ -37,7 +37,7 @@ const maxGetAvailableServersResult = 512 // API represents the api of the dmsg-discovery service` type API struct { http.Handler - metrics discmetrics.Metrics + metrics metrics.Metrics db store.Storer reqsInFlightCountMiddleware *metricsutil.RequestsInFlightCountMiddleware testMode bool @@ -50,7 +50,7 @@ type API struct { } // New returns a new API object, which can be started as a server -func New(log logrus.FieldLogger, db store.Storer, m discmetrics.Metrics, testMode, enableLoadTesting, enableMetrics bool, dmsgAddr, authPassphrase string) *API { +func New(log logrus.FieldLogger, db store.Storer, m metrics.Metrics, testMode, enableLoadTesting, enableMetrics bool, dmsgAddr, authPassphrase string) *API { if log == nil { log = logging.MustGetLogger("dmsg_disc") } diff --git a/internal/dmsg-discovery/api/entries_endpoint_test.go b/pkg/discovery/api/entries_endpoint_test.go similarity index 96% rename from internal/dmsg-discovery/api/entries_endpoint_test.go rename to pkg/discovery/api/entries_endpoint_test.go index bce1d0b2f..79f703a61 100644 --- a/internal/dmsg-discovery/api/entries_endpoint_test.go +++ b/pkg/discovery/api/entries_endpoint_test.go @@ -1,4 +1,4 @@ -// Package api internal/dmsg-discovery/api/entries_endpoint_test.go +// Package api pkg/discovery/api/entries_endpoint_test.go package api import ( @@ -14,9 +14,9 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" "github.com/stretchr/testify/require" - "github.com/skycoin/dmsg/internal/discmetrics" - store2 "github.com/skycoin/dmsg/internal/dmsg-discovery/store" "github.com/skycoin/dmsg/pkg/disc" + "github.com/skycoin/dmsg/pkg/disc/metrics" + store2 "github.com/skycoin/dmsg/pkg/discovery/store" ) func TestEntriesEndpoint(t *testing.T) { @@ -183,7 +183,7 @@ func TestEntriesEndpoint(t *testing.T) { tc.storerPreHook(t, dbMock, &tc.entry) } - api := New(nil, dbMock, discmetrics.NewEmpty(), true, false, true, "", "") + api := New(nil, dbMock, metrics.NewEmpty(), true, false, true, "", "") req, err := http.NewRequest(tc.method, tc.endpoint, bytes.NewBufferString(tc.httpBody)) require.NoError(t, err) diff --git a/internal/dmsg-discovery/api/error_handler.go b/pkg/discovery/api/error_handler.go similarity index 95% rename from internal/dmsg-discovery/api/error_handler.go rename to pkg/discovery/api/error_handler.go index 6ab202230..d41959bad 100644 --- a/internal/dmsg-discovery/api/error_handler.go +++ b/pkg/discovery/api/error_handler.go @@ -1,4 +1,4 @@ -// Package api internal/dmsg-discovery/api/error_handler.go +// Package api pkg/discovery/api/error_handler.go package api import ( diff --git a/internal/dmsg-discovery/api/error_handler_test.go b/pkg/discovery/api/error_handler_test.go similarity index 81% rename from internal/dmsg-discovery/api/error_handler_test.go rename to pkg/discovery/api/error_handler_test.go index 9bcb03517..50343f9af 100644 --- a/internal/dmsg-discovery/api/error_handler_test.go +++ b/pkg/discovery/api/error_handler_test.go @@ -1,4 +1,4 @@ -// Package api internal/dmsg-discovery/api/error_handler_test.go +// Package api pkg/discovery/api/error_handler_test.go package api import ( @@ -9,9 +9,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/skycoin/dmsg/internal/discmetrics" - "github.com/skycoin/dmsg/internal/dmsg-discovery/store" "github.com/skycoin/dmsg/pkg/disc" + "github.com/skycoin/dmsg/pkg/disc/metrics" + "github.com/skycoin/dmsg/pkg/discovery/store" ) var errHandlerTestCases = []struct { @@ -35,7 +35,7 @@ func TestErrorHandler(t *testing.T) { tc := tc t.Run(tc.err.Error(), func(t *testing.T) { w := httptest.NewRecorder() - api := New(nil, store.NewMock(), discmetrics.NewEmpty(), true, false, true, "", "") + api := New(nil, store.NewMock(), metrics.NewEmpty(), true, false, true, "", "") api.handleError(w, &http.Request{}, tc.err) msg := new(disc.HTTPMessage) diff --git a/internal/dmsg-discovery/api/get_available_servers_test.go b/pkg/discovery/api/get_available_servers_test.go similarity index 94% rename from internal/dmsg-discovery/api/get_available_servers_test.go rename to pkg/discovery/api/get_available_servers_test.go index 1f814058e..c185822c8 100644 --- a/internal/dmsg-discovery/api/get_available_servers_test.go +++ b/pkg/discovery/api/get_available_servers_test.go @@ -1,4 +1,4 @@ -// Package api internal/dmsg-discovery/api/get_available_servers_test.go +// Package api pkg/discovery/api/get_available_servers_test.go package api import ( @@ -13,9 +13,9 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" "github.com/stretchr/testify/require" - "github.com/skycoin/dmsg/internal/discmetrics" - store2 "github.com/skycoin/dmsg/internal/dmsg-discovery/store" "github.com/skycoin/dmsg/pkg/disc" + "github.com/skycoin/dmsg/pkg/disc/metrics" + store2 "github.com/skycoin/dmsg/pkg/discovery/store" ) func TestGetAvailableServers(t *testing.T) { @@ -117,7 +117,7 @@ func TestGetAvailableServers(t *testing.T) { t.Run(tc.name, func(t *testing.T) { db, entries := tc.databaseAndEntries(t) - api := New(nil, db, discmetrics.NewEmpty(), true, false, true, "", "") + api := New(nil, db, metrics.NewEmpty(), true, false, true, "", "") req, err := http.NewRequest(tc.method, tc.endpoint, nil) require.NoError(t, err) diff --git a/internal/dmsg-discovery/store/redis.go b/pkg/discovery/store/redis.go similarity index 99% rename from internal/dmsg-discovery/store/redis.go rename to pkg/discovery/store/redis.go index a115d4118..a18c50cc6 100644 --- a/internal/dmsg-discovery/store/redis.go +++ b/pkg/discovery/store/redis.go @@ -1,4 +1,4 @@ -// Package store internal/dmsg-discovery/store/redis.go +// Package store pkg/discovery/store/redis.go package store import ( diff --git a/internal/dmsg-discovery/store/redis_test.go b/pkg/discovery/store/redis_test.go similarity index 98% rename from internal/dmsg-discovery/store/redis_test.go rename to pkg/discovery/store/redis_test.go index cba0a6960..f5627586a 100644 --- a/internal/dmsg-discovery/store/redis_test.go +++ b/pkg/discovery/store/redis_test.go @@ -1,7 +1,7 @@ //go:build !no_ci // +build !no_ci -// Package store internal/dmsg-discovery/store/redis_test.go +// Package store pkg/discovery/store/redis_test.go package store import ( diff --git a/internal/dmsg-discovery/store/storer.go b/pkg/discovery/store/storer.go similarity index 97% rename from internal/dmsg-discovery/store/storer.go rename to pkg/discovery/store/storer.go index b0685b41d..5d0f0e91a 100644 --- a/internal/dmsg-discovery/store/storer.go +++ b/pkg/discovery/store/storer.go @@ -1,4 +1,4 @@ -// Package store internal/dmsg-discovery/store/storer.go +// Package store pkg/discovery/store/storer.go package store import ( diff --git a/internal/dmsg-discovery/store/testing.go b/pkg/discovery/store/testing.go similarity index 99% rename from internal/dmsg-discovery/store/testing.go rename to pkg/discovery/store/testing.go index 7e5095be3..d4655ee47 100644 --- a/internal/dmsg-discovery/store/testing.go +++ b/pkg/discovery/store/testing.go @@ -1,4 +1,4 @@ -// Package store internal/dmsg-discovery/store/testing.go +// Package store pkg/discovery/store/testing.go package store import ( diff --git a/internal/servermetrics/delta.go b/pkg/dmsg/metrics/delta.go similarity index 70% rename from internal/servermetrics/delta.go rename to pkg/dmsg/metrics/delta.go index 051012558..090cbca06 100644 --- a/internal/servermetrics/delta.go +++ b/pkg/dmsg/metrics/delta.go @@ -1,5 +1,5 @@ -// Package servermetrics internal/servermetrics/delta.go -package servermetrics +// Package metrics pkg/dmsg/metrics/delta.go +package metrics // DeltaType represents a change in metrics gauge. type DeltaType int diff --git a/internal/servermetrics/empty.go b/pkg/dmsg/metrics/empty.go similarity index 88% rename from internal/servermetrics/empty.go rename to pkg/dmsg/metrics/empty.go index b871d0717..d947e3538 100644 --- a/internal/servermetrics/empty.go +++ b/pkg/dmsg/metrics/empty.go @@ -1,5 +1,5 @@ -// Package servermetrics internal/servermetrics/empty.go -package servermetrics +// Package metrics pkg/dmsg/metrics/empty.go +package metrics // NewEmpty constructs new empty metrics. func NewEmpty() Empty { diff --git a/internal/servermetrics/metrics.go b/pkg/dmsg/metrics/metrics.go similarity index 74% rename from internal/servermetrics/metrics.go rename to pkg/dmsg/metrics/metrics.go index 23685270b..bd023e5dc 100644 --- a/internal/servermetrics/metrics.go +++ b/pkg/dmsg/metrics/metrics.go @@ -1,5 +1,5 @@ -// Package servermetrics internal/servermetrics/metrics.go -package servermetrics +// Package metrics pkg/dmsg/metrics/metrics.go +package metrics // Metrics collects metrics for metrics tracking system. type Metrics interface { diff --git a/internal/servermetrics/victoria_metrics.go b/pkg/dmsg/metrics/victoria_metrics.go similarity index 96% rename from internal/servermetrics/victoria_metrics.go rename to pkg/dmsg/metrics/victoria_metrics.go index b02f8673c..71a7847f4 100644 --- a/internal/servermetrics/victoria_metrics.go +++ b/pkg/dmsg/metrics/victoria_metrics.go @@ -1,5 +1,5 @@ -// Package servermetrics internal/servermetrics/victoria_metrics.go -package servermetrics +// Package metrics pkg/dmsg/metrics/victoria_metrics.go +package metrics import ( "fmt" diff --git a/pkg/dmsg/server.go b/pkg/dmsg/server.go index f69f56381..c3a64f0e0 100644 --- a/pkg/dmsg/server.go +++ b/pkg/dmsg/server.go @@ -13,7 +13,7 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/netutil" "github.com/xtaci/smux" - "github.com/skycoin/dmsg/internal/servermetrics" + "github.com/skycoin/dmsg/pkg/dmsg/metrics" "github.com/skycoin/dmsg/pkg/disc" ) @@ -36,7 +36,7 @@ func DefaultServerConfig() *ServerConfig { type Server struct { EntityCommon - m servermetrics.Metrics + m metrics.Metrics ready chan struct{} // Closed once dmsg.Server is serving. readyOnce sync.Once @@ -56,12 +56,12 @@ type Server struct { } // NewServer creates a new dmsg server entity. -func NewServer(pk cipher.PubKey, sk cipher.SecKey, dc disc.APIClient, conf *ServerConfig, m servermetrics.Metrics) *Server { +func NewServer(pk cipher.PubKey, sk cipher.SecKey, dc disc.APIClient, conf *ServerConfig, m metrics.Metrics) *Server { if conf == nil { conf = DefaultServerConfig() } if m == nil { - m = servermetrics.NewEmpty() + m = metrics.NewEmpty() } log := logging.MustGetLogger("dmsg_server") diff --git a/pkg/dmsg/server_session.go b/pkg/dmsg/server_session.go index ef2556baa..72632bfd0 100644 --- a/pkg/dmsg/server_session.go +++ b/pkg/dmsg/server_session.go @@ -11,22 +11,22 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/netutil" "github.com/xtaci/smux" - "github.com/skycoin/dmsg/internal/servermetrics" + "github.com/skycoin/dmsg/pkg/dmsg/metrics" "github.com/skycoin/dmsg/pkg/noise" ) // ServerSession represents a session from the perspective of a dmsg server. type ServerSession struct { *SessionCommon - m servermetrics.Metrics + m metrics.Metrics } -func makeServerSession(m servermetrics.Metrics, entity *EntityCommon, conn net.Conn) (ServerSession, error) { +func makeServerSession(m metrics.Metrics, entity *EntityCommon, conn net.Conn) (ServerSession, error) { var sSes ServerSession sSes.SessionCommon = new(SessionCommon) sSes.nMap = make(noise.NonceMap) if err := sSes.SessionCommon.initServer(entity, conn); err != nil { - m.RecordSession(servermetrics.DeltaFailed) // record failed connection + m.RecordSession(metrics.DeltaFailed) // record failed connection return sSes, err } sSes.m = m @@ -43,8 +43,8 @@ func (ss *ServerSession) Close() error { // Serve serves the session. func (ss *ServerSession) Serve() { - ss.m.RecordSession(servermetrics.DeltaConnect) // record successful connection - defer ss.m.RecordSession(servermetrics.DeltaDisconnect) // record disconnection + ss.m.RecordSession(metrics.DeltaConnect) // record successful connection + defer ss.m.RecordSession(metrics.DeltaDisconnect) // record disconnection if ss.sm.smux != nil { for { sStr, err := ss.sm.smux.AcceptStream() @@ -123,7 +123,7 @@ func (ss *ServerSession) serveStream(log logrus.FieldLogger, yStr io.ReadWriteCl // Read request. req, err := readRequest() if err != nil { - ss.m.RecordStream(servermetrics.DeltaFailed) // record failed stream + ss.m.RecordStream(metrics.DeltaFailed) // record failed stream return err } @@ -137,7 +137,7 @@ func (ss *ServerSession) serveStream(log logrus.FieldLogger, yStr io.ReadWriteCl ip, err := addrToIP(addr) if err != nil { - ss.m.RecordStream(servermetrics.DeltaFailed) // record failed stream + ss.m.RecordStream(metrics.DeltaFailed) // record failed stream return err } @@ -149,7 +149,7 @@ func (ss *ServerSession) serveStream(log logrus.FieldLogger, yStr io.ReadWriteCl obj := MakeSignedStreamResponse(&resp, ss.entity.LocalSK()) if err := ss.writeObject(yStr, obj); err != nil { - ss.m.RecordStream(servermetrics.DeltaFailed) // record failed stream + ss.m.RecordStream(metrics.DeltaFailed) // record failed stream return err } log.Debug("Wrote IP stream response.") @@ -159,7 +159,7 @@ func (ss *ServerSession) serveStream(log logrus.FieldLogger, yStr io.ReadWriteCl // Obtain next session. ss2, ok := ss.entity.serverSession(req.DstAddr.PK) if !ok { - ss.m.RecordStream(servermetrics.DeltaFailed) // record failed stream + ss.m.RecordStream(metrics.DeltaFailed) // record failed stream return ErrReqNoNextSession } log.Debug("Obtained next session.") @@ -167,22 +167,22 @@ func (ss *ServerSession) serveStream(log logrus.FieldLogger, yStr io.ReadWriteCl // Forward request and obtain/check response. yStr2, resp, err := ss2.forwardRequest(req) if err != nil { - ss.m.RecordStream(servermetrics.DeltaFailed) // record failed stream + ss.m.RecordStream(metrics.DeltaFailed) // record failed stream return err } log.Debug("Forwarded stream request.") // Forward response. if err := ss.writeObject(yStr, resp); err != nil { - ss.m.RecordStream(servermetrics.DeltaFailed) // record failed stream + ss.m.RecordStream(metrics.DeltaFailed) // record failed stream return err } log.Debug("Forwarded stream response.") // Serve stream. log.Info("Serving stream.") - ss.m.RecordStream(servermetrics.DeltaConnect) // record successful stream - defer ss.m.RecordStream(servermetrics.DeltaDisconnect) // record disconnection + ss.m.RecordStream(metrics.DeltaConnect) // record successful stream + defer ss.m.RecordStream(metrics.DeltaDisconnect) // record disconnection return netutil.CopyReadWriteCloser(yStr, yStr2) } diff --git a/internal/cli/cli.go b/pkg/dmsgclient/cli.go similarity index 95% rename from internal/cli/cli.go rename to pkg/dmsgclient/cli.go index d10310e5b..4db530dd0 100644 --- a/internal/cli/cli.go +++ b/pkg/dmsgclient/cli.go @@ -1,5 +1,5 @@ -// Package cli internal/cli/go -package cli +// Package dmsgclient pkg/dmsgclient/cli.go +package dmsgclient import ( "bytes" @@ -13,7 +13,6 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" - "github.com/skycoin/dmsg/internal/flags" "github.com/skycoin/dmsg/pkg/direct" "github.com/skycoin/dmsg/pkg/disc" "github.com/skycoin/dmsg/pkg/dmsg" @@ -46,11 +45,11 @@ Default mode of operation is dmsghttp: // InitDmsgWithFlags starts dmsg with flags from the flags package func InitDmsgWithFlags(ctx context.Context, dlog *logging.Logger, pk cipher.PubKey, sk cipher.SecKey, httpClient *http.Client, destination string) (dmsgC *dmsg.Client, stop func(), err error) { - if flags.UseDC { - return StartDmsgDirect(ctx, dlog, pk, sk, "", flags.DmsgSessions, dmsg.ExtractPKFromDmsgAddr(destination)) + if UseDC { + return StartDmsgDirect(ctx, dlog, pk, sk, "", DmsgSessions, dmsg.ExtractPKFromDmsgAddr(destination)) } - if flags.UseHTTP { - resp, err := httpClient.Get(flags.DmsgDiscURL + "/health") + if UseHTTP { + resp, err := httpClient.Get(DmsgDiscURL + "/health") if err != nil { dlog.WithError(err).Fatal("Error connecting to dmsg-discovery with http client") } @@ -60,12 +59,12 @@ func InitDmsgWithFlags(ctx context.Context, dlog *logging.Logger, pk cipher.PubK if err != nil { dlog.WithError(err).Error("Failed to read response body from discovery") } else { - dlog.Infof("Received response from dmsg-discovery server %s/health:\n%s", flags.DmsgDiscURL, string(body)) + dlog.Infof("Received response from dmsg-discovery server %s/health:\n%s", DmsgDiscURL, string(body)) } // Use direct client with synthetic entries for discovery server and all dmsg servers // This allows dialing the discovery server which doesn't register itself - return StartDmsgWithDirectClient(ctx, dlog, pk, sk, flags.DmsgSessions) + return StartDmsgWithDirectClient(ctx, dlog, pk, sk, DmsgSessions) } // Default dmsghttp mode @@ -75,11 +74,11 @@ func InitDmsgWithFlags(ctx context.Context, dlog *logging.Logger, pk cipher.PubK dlog.Debug("Starting DMSG direct clients.") for _, server := range dmsg.Prod.DmsgServers { - if len(dmsgClients) >= flags.DmsgSessions { + if len(dmsgClients) >= DmsgSessions { break } - dmsgDC, closeFn, err := StartDmsgDirectWithServers(ctx, dlog, pk, sk, flags.DmsgDiscAddr, []*disc.Entry{&server}, flags.DmsgSessions, dmsg.ExtractPKFromDmsgAddr(flags.DmsgDiscAddr)) + dmsgDC, closeFn, err := StartDmsgDirectWithServers(ctx, dlog, pk, sk, DmsgDiscAddr, []*disc.Entry{&server}, DmsgSessions, dmsg.ExtractPKFromDmsgAddr(DmsgDiscAddr)) if err != nil { dlog.WithError(err).Error("Failed to start DMSG direct client. Skipping server...") continue @@ -99,7 +98,7 @@ func InitDmsgWithFlags(ctx context.Context, dlog *logging.Logger, pk cipher.PubK } dlog.Debug("Checking discovery /health using DMSG HTTP client.") - resp, err := dmsgHTTP.Get(flags.DmsgDiscAddr + "/health") + resp, err := dmsgHTTP.Get(DmsgDiscAddr + "/health") if err != nil { for _, fn := range closeFns { fn() @@ -112,10 +111,10 @@ func InitDmsgWithFlags(ctx context.Context, dlog *logging.Logger, pk cipher.PubK if err != nil { dlog.WithError(err).Error("Failed to read discovery /health response body") } else { - dlog.Infof("Received response from dmsg-discovery server %s/health:\n%s", flags.DmsgDiscAddr, string(body)) + dlog.Infof("Received response from dmsg-discovery server %s/health:\n%s", DmsgDiscAddr, string(body)) } - return StartDmsgWithSyntheticDiscovery(ctx, dlog, pk, sk, dmsgHTTP, flags.DmsgDiscAddr, flags.DmsgSessions) + return StartDmsgWithSyntheticDiscovery(ctx, dlog, pk, sk, dmsgHTTP, DmsgDiscAddr, DmsgSessions) } // StartDmsgWithSyntheticDiscovery starts dmsg with a synthetic discovery entry for the discovery server itself @@ -303,7 +302,7 @@ func StartDmsgWithDirectClient(ctx context.Context, dlog *logging.Logger, pk cip } // Add synthetic entry for discovery server - discPK := dmsg.ExtractPKFromDmsgAddr(flags.DmsgDiscAddr) + discPK := dmsg.ExtractPKFromDmsgAddr(DmsgDiscAddr) if discPK != "" { var discoveryPK cipher.PubKey if err := discoveryPK.UnmarshalText([]byte(discPK)); err == nil { @@ -341,7 +340,7 @@ func StartDmsgWithDirectClient(ctx context.Context, dlog *logging.Logger, pk cip directClient := direct.NewClient(entries, dlog) // Create HTTP discovery client as fallback for unknown entries - httpDiscClient := disc.NewHTTP(flags.DmsgDiscURL, &http.Client{}, dlog) + httpDiscClient := disc.NewHTTP(DmsgDiscURL, &http.Client{}, dlog) // Wrap with fallback client that tries direct first, then HTTP discovery fallbackClient := newFallbackDiscClient(directClient, httpDiscClient, dlog) diff --git a/internal/flags/flags.go b/pkg/dmsgclient/flags.go similarity index 96% rename from internal/flags/flags.go rename to pkg/dmsgclient/flags.go index 2189a8ec2..004764fbe 100644 --- a/internal/flags/flags.go +++ b/pkg/dmsgclient/flags.go @@ -1,5 +1,5 @@ -// Package flags internal/flags/flags.go -package flags +// Package dmsgclient pkg/dmsgclient/flags.go +package dmsgclient import ( "os" diff --git a/internal/dmsg-server/api/api.go b/pkg/dmsgserver/api.go similarity index 85% rename from internal/dmsg-server/api/api.go rename to pkg/dmsgserver/api.go index 4249e497e..f5997e472 100644 --- a/internal/dmsg-server/api/api.go +++ b/pkg/dmsgserver/api.go @@ -1,5 +1,5 @@ -// Package api internal/dmsg-server/api/api.go -package api +// Package dmsgserver pkg/dmsgserver/api.go +package dmsgserver import ( "context" @@ -19,13 +19,13 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/httputil" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" - "github.com/skycoin/dmsg/internal/servermetrics" + "github.com/skycoin/dmsg/pkg/dmsg/metrics" dmsg "github.com/skycoin/dmsg/pkg/dmsg" ) -// API main object of the server -type API struct { - metrics servermetrics.Metrics +// ServerAPI main object of the server +type ServerAPI struct { + metrics metrics.Metrics startedAt time.Time dmsgServer *dmsg.Server sMu sync.Mutex @@ -36,9 +36,9 @@ type API struct { router *chi.Mux } -// New returns a new API object, which can be started as a server -func New(r *chi.Mux, log *logging.Logger, m servermetrics.Metrics) *API { - api := &API{ +// NewServerAPI returns a new ServerAPI object, which can be started as a server +func NewServerAPI(r *chi.Mux, log *logging.Logger, m metrics.Metrics) *ServerAPI { + api := &ServerAPI{ metrics: m, startedAt: time.Now(), minuteDecValues: make(map[*dmsg.SessionCommon]uint64), @@ -53,7 +53,7 @@ func New(r *chi.Mux, log *logging.Logger, m servermetrics.Metrics) *API { } // RunBackgroundTasks is function which runs periodic tasks of dmsg-server. -func (a *API) RunBackgroundTasks(ctx context.Context) { +func (a *ServerAPI) RunBackgroundTasks(ctx context.Context) { ticker := time.NewTicker(time.Second * 10) //tickerEverySecond := time.NewTicker(time.Second * 1) tickerEveryMinute := time.NewTicker(time.Second * 60) @@ -75,13 +75,13 @@ func (a *API) RunBackgroundTasks(ctx context.Context) { } } -// SetDmsgServer saves srv in the API -func (a *API) SetDmsgServer(srv *dmsg.Server) { +// SetDmsgServer saves srv in the ServerAPI +func (a *ServerAPI) SetDmsgServer(srv *dmsg.Server) { a.dmsgServer = srv } // ListenAndServe runs dmsg Serve function alongside health endpoint -func (a *API) ListenAndServe(lAddr, pAddr, httpAddr string) error { +func (a *ServerAPI) ListenAndServe(lAddr, pAddr, httpAddr string) error { errCh := make(chan error, 2) dmsgLn, err := net.Listen("tcp", lAddr) @@ -115,12 +115,12 @@ func (a *API) ListenAndServe(lAddr, pAddr, httpAddr string) error { } // Close closes connection to both http server and dmsg server -func (a *API) Close() error { +func (a *ServerAPI) Close() error { return a.dmsgServer.Close() } // Health serves health page -func (a *API) health(w http.ResponseWriter, r *http.Request) { +func (a *ServerAPI) health(w http.ResponseWriter, r *http.Request) { info := buildinfo.Get() a.writeJSON(w, r, http.StatusOK, httputil.HealthCheckResponse{ BuildInfo: info, @@ -129,7 +129,7 @@ func (a *API) health(w http.ResponseWriter, r *http.Request) { } // writeJSON writes a json object on a http.ResponseWriter with the given code. -func (a *API) writeJSON(w http.ResponseWriter, r *http.Request, code int, object interface{}) { +func (a *ServerAPI) writeJSON(w http.ResponseWriter, r *http.Request, code int, object interface{}) { jsonObject, err := json.Marshal(object) if err != nil { a.log(r).Warnf("Failed to encode json response: %s", err) @@ -144,19 +144,19 @@ func (a *API) writeJSON(w http.ResponseWriter, r *http.Request, code int, object } } -func (a *API) log(r *http.Request) logrus.FieldLogger { +func (a *ServerAPI) log(r *http.Request) logrus.FieldLogger { return httputil.GetLogger(r) } // UpdateInternalState is background function which updates numbers of clients. -func (a *API) updateInternalState() { +func (a *ServerAPI) updateInternalState() { if a.dmsgServer != nil { a.metrics.SetClientsCount(int64(len(a.dmsgServer.GetSessions()))) } } // UpdateAverageNumberOfPacketsPerMinute is function which needs to called every minute. -func (a *API) updateAverageNumberOfPacketsPerMinute() { +func (a *ServerAPI) updateAverageNumberOfPacketsPerMinute() { if a.dmsgServer != nil { a.sMu.Lock() defer a.sMu.Unlock() @@ -176,7 +176,7 @@ func (a *API) updateAverageNumberOfPacketsPerMinute() { // TODO (darkrengarius): reimplement efficiently /*// UpdateAverageNumberOfPacketsPerSecond is function which needs to called every second. -func (a *API) updateAverageNumberOfPacketsPerSecond() { +func (a *ServerAPI) updateAverageNumberOfPacketsPerSecond() { if a.dmsgServer != nil { newDecValues, newEncValues, average := calculateThroughput( a.dmsgServer.GetSessions(), diff --git a/pkg/dmsgserver/api_test.go b/pkg/dmsgserver/api_test.go new file mode 100644 index 000000000..1841ca2c0 --- /dev/null +++ b/pkg/dmsgserver/api_test.go @@ -0,0 +1,46 @@ +package dmsgserver_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/skycoin/dmsg/pkg/dmsg/metrics" + dmsg "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsgserver" +) + +func TestNew_HealthEndpoint(t *testing.T) { + r := chi.NewRouter() + log := logging.MustGetLogger("test") + m := metrics.NewEmpty() + + a := dmsgserver.NewServerAPI(r, log, m) + require.NotNil(t, a) + + req := httptest.NewRequest(http.MethodGet, "/health", nil) + rec := httptest.NewRecorder() + r.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "application/json", rec.Header().Get("Content-Type")) +} + +func TestSetDmsgServer(t *testing.T) { + r := chi.NewRouter() + log := logging.MustGetLogger("test") + m := metrics.NewEmpty() + + a := dmsgserver.NewServerAPI(r, log, m) + require.NotNil(t, a) + + // SetDmsgServer should not panic with a nil server + assert.NotPanics(t, func() { + a.SetDmsgServer((*dmsg.Server)(nil)) + }) +} From 715cf17272cd64f7a888531caab3290b7db298a0 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Sun, 22 Mar 2026 19:47:43 -0500 Subject: [PATCH 03/15] Refactor: extract cmd boilerplate, convert to go:embed, fix panics, add CloseQuietly Command boilerplate: - Add ExecName() and Execute() helpers to pkg/dmsgclient - Replace duplicated Use: expression and Execute() in all 13 cmd packages Embedded HTML: - Convert pkg/dmsgpty/ui_html.go from 5738-line hex literal to //go:embed with term.html.gz asset file (same runtime behavior) Panic fixes (library code only, tests left as-is): - pkg/dmsg/types.go: SignBytes, MakeSignedStreamRequest/Response now return errors - pkg/dmsg/util.go: encodeGob now returns error - pkg/dmsg/const.go: shuffleServers now returns error - pkg/dmsg/metrics/victoria_metrics.go: invalid delta logs instead of panicking - pkg/dmsgcurl/dmsgcurl.go: String() returns error string instead of panicking - pkg/dmsgpty/ui.go: writeHeader returns error instead of panicking - All callers updated to handle new error returns Error suppression: - Add pkg/ioutil.CloseQuietly for deferred Close() calls Also regenerate dependency graph SVG. --- cmd/conf/commands/root.go | 5 +- cmd/dial/commands/dial.go | 11 +- cmd/dmsg-discovery/commands/dmsg-discovery.go | 10 +- cmd/dmsg-server/commands/root.go | 14 +- cmd/dmsg-server/commands/start/root.go | 5 +- cmd/dmsg-socks5/commands/dmsg-socks5.go | 10 +- cmd/dmsg/commands/root.go | 13 +- cmd/dmsgcurl/commands/dmsgcurl.go | 7 +- cmd/dmsghttp/commands/dmsghttp.go | 13 +- cmd/dmsgip/commands/dmsgip.go | 11 +- cmd/dmsgpty-cli/commands/root.go | 12 +- cmd/dmsgpty-host/commands/root.go | 10 +- cmd/dmsgpty-ui/commands/dmsgpty-ui.go | 13 +- cmd/dmsgweb/commands/dmsgweb.go | 14 +- cmd/dmsgweb/commands/root.go | 5 +- docs/dmsg-goda-graph.svg | 1438 ++--- pkg/dmsg/const.go | 12 +- pkg/dmsg/metrics/victoria_metrics.go | 6 +- pkg/dmsg/server_session.go | 6 +- pkg/dmsg/stream.go | 15 +- pkg/dmsg/types.go | 34 +- pkg/dmsg/util.go | 6 +- pkg/dmsgclient/cli.go | 22 +- pkg/dmsgcurl/dmsgcurl.go | 2 +- pkg/dmsgpty/term.html.gz | Bin 0 -> 91470 bytes pkg/dmsgpty/ui.go | 2 +- pkg/dmsgpty/ui_html.go | 5726 +---------------- pkg/ioutil/close.go | 18 + 28 files changed, 845 insertions(+), 6595 deletions(-) create mode 100644 pkg/dmsgpty/term.html.gz create mode 100644 pkg/ioutil/close.go diff --git a/cmd/conf/commands/root.go b/cmd/conf/commands/root.go index ad1a6492e..0fbc39480 100644 --- a/cmd/conf/commands/root.go +++ b/cmd/conf/commands/root.go @@ -7,6 +7,7 @@ import ( "github.com/bitfield/script" "github.com/spf13/cobra" + "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/dmsg" ) @@ -28,7 +29,5 @@ var RootCmd = &cobra.Command{ // Execute executes root CLI command. func Execute() { - if err := RootCmd.Execute(); err != nil { - log.Fatal("Failed to execute command: ", err) - } + dmsgclient.Execute(RootCmd) } diff --git a/cmd/dial/commands/dial.go b/cmd/dial/commands/dial.go index 0b058e273..31145afd3 100644 --- a/cmd/dial/commands/dial.go +++ b/cmd/dial/commands/dial.go @@ -4,10 +4,7 @@ package commands import ( "context" "fmt" - "log" "net/http" - "os" - "path/filepath" "strconv" "strings" "time" @@ -42,9 +39,7 @@ func init() { // RootCmd contains the root dmsgcurl command var RootCmd = &cobra.Command{ - Use: func() string { - return strings.Split(filepath.Base(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", os.Args), "[", ""), "]", "")), " ")[0] - }(), + Use: dmsgclient.ExecName(), Short: "DMSG Dial network test utility", Long: calvin.AsciiFont("dmsgdial") + ` DMSG Dial network test utility @@ -185,7 +180,5 @@ Default mode of operation is dmsghttp: // Execute executes root CLI command. func Execute() { - if err := RootCmd.Execute(); err != nil { - log.Fatal("Failed to execute command: ", err) - } + dmsgclient.Execute(RootCmd) } diff --git a/cmd/dmsg-discovery/commands/dmsg-discovery.go b/cmd/dmsg-discovery/commands/dmsg-discovery.go index 8fc1b0c56..2f153050c 100644 --- a/cmd/dmsg-discovery/commands/dmsg-discovery.go +++ b/cmd/dmsg-discovery/commands/dmsg-discovery.go @@ -11,7 +11,6 @@ import ( "net/http" "net/http/pprof" "os" - "path/filepath" "strings" "time" @@ -28,6 +27,7 @@ import ( "github.com/skycoin/dmsg/pkg/disc/metrics" "github.com/skycoin/dmsg/pkg/discovery/api" "github.com/skycoin/dmsg/pkg/discovery/store" + "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/direct" "github.com/skycoin/dmsg/pkg/disc" dmsg "github.com/skycoin/dmsg/pkg/dmsg" @@ -256,9 +256,7 @@ func init() { // RootCmd contains commands for dmsg-discovery var RootCmd = &cobra.Command{ - Use: func() string { - return strings.Split(filepath.Base(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", os.Args), "[", ""), "]", "")), " ")[0] - }(), + Use: dmsgclient.ExecName(), Short: "DMSG Discovery Server", Long: ` ┌┬┐┌┬┐┌─┐┌─┐ ┌┬┐┬┌─┐┌─┐┌─┐┬ ┬┌─┐┬─┐┬ ┬ @@ -421,9 +419,7 @@ Example: // Execute executes root CLI command. func Execute() { - if err := RootCmd.Execute(); err != nil { - log.Fatal(err) - } + dmsgclient.Execute(RootCmd) } func prepareDB(ctx context.Context, log *logging.Logger) store.Storer { diff --git a/cmd/dmsg-server/commands/root.go b/cmd/dmsg-server/commands/root.go index 1baacb0b3..664d09c3f 100644 --- a/cmd/dmsg-server/commands/root.go +++ b/cmd/dmsg-server/commands/root.go @@ -2,15 +2,11 @@ package commands import ( - "fmt" - "log" - "os" - "path/filepath" - "strings" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/buildinfo" "github.com/spf13/cobra" + "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/cmd/dmsg-server/commands/config" "github.com/skycoin/dmsg/cmd/dmsg-server/commands/start" ) @@ -25,9 +21,7 @@ func init() { // RootCmd contains the root dmsg-server command var RootCmd = &cobra.Command{ - Use: func() string { - return strings.Split(filepath.Base(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", os.Args), "[", ""), "]", "")), " ")[0] - }(), + Use: dmsgclient.ExecName(), Short: "DMSG Server", Long: ` ┌┬┐┌┬┐┌─┐┌─┐ ┌─┐┌─┐┬─┐┬ ┬┌─┐┬─┐ @@ -50,7 +44,5 @@ Example: // Execute executes root CLI command. func Execute() { - if err := RootCmd.Execute(); err != nil { - log.Fatal("Failed to execute command: ", err) - } + dmsgclient.Execute(RootCmd) } diff --git a/cmd/dmsg-server/commands/start/root.go b/cmd/dmsg-server/commands/start/root.go index d6b0cc9eb..173e195d6 100644 --- a/cmd/dmsg-server/commands/start/root.go +++ b/cmd/dmsg-server/commands/start/root.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/cobra" "github.com/skycoin/dmsg/pkg/dmsg/metrics" + "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/disc" dmsg "github.com/skycoin/dmsg/pkg/dmsg" "github.com/skycoin/dmsg/pkg/dmsgserver" @@ -154,9 +155,7 @@ var RootCmd = &cobra.Command{ // Execute executes root CLI command. func Execute() { - if err := RootCmd.Execute(); err != nil { - log.Fatal("Failed to execute command: ", err) - } + dmsgclient.Execute(RootCmd) } func configNotFound() (io.ReadCloser, error) { diff --git a/cmd/dmsg-socks5/commands/dmsg-socks5.go b/cmd/dmsg-socks5/commands/dmsg-socks5.go index 29828d9e4..fc9646b10 100644 --- a/cmd/dmsg-socks5/commands/dmsg-socks5.go +++ b/cmd/dmsg-socks5/commands/dmsg-socks5.go @@ -4,10 +4,8 @@ package commands import ( "context" "fmt" - "log" "net/http" "os" - "path/filepath" "strings" "time" @@ -40,9 +38,7 @@ var ( // Execute executes root CLI command. func Execute() { - if err := RootCmd.Execute(); err != nil { - log.Fatal("Failed to execute command: ", err) - } + dmsgclient.Execute(RootCmd) } func init() { RootCmd.AddCommand( @@ -75,9 +71,7 @@ func init() { // RootCmd contains the root command var RootCmd = &cobra.Command{ - Use: func() string { - return strings.Split(filepath.Base(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", os.Args), "[", ""), "]", "")), " ")[0] - }(), + Use: dmsgclient.ExecName(), Short: "DMSG socks5 proxy server & client", Long: calvin.AsciiFont("dmsg-socks") + ` DMSG socks5 proxy server & client`, diff --git a/cmd/dmsg/commands/root.go b/cmd/dmsg/commands/root.go index 556f4a7cc..fe6ea8968 100644 --- a/cmd/dmsg/commands/root.go +++ b/cmd/dmsg/commands/root.go @@ -3,15 +3,12 @@ package commands import ( "fmt" - "log" - "os" - "path/filepath" - "strings" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/buildinfo" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/calvin" "github.com/spf13/cobra" + "github.com/skycoin/dmsg/pkg/dmsgclient" df "github.com/skycoin/dmsg/cmd/conf/commands" dl "github.com/skycoin/dmsg/cmd/dial/commands" dd "github.com/skycoin/dmsg/cmd/dmsg-discovery/commands" @@ -87,9 +84,7 @@ func modifySubcommands(cmd *cobra.Command) { // RootCmd contains all binaries which may be separately compiled as subcommands var RootCmd = &cobra.Command{ - Use: func() string { - return strings.Split(filepath.Base(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", os.Args), "[", ""), "]", "")), " ")[0] - }(), + Use: dmsgclient.ExecName(), Short: "DMSG services & utilities", Long: func() (ret string) { ret = calvin.AsciiFont("dmsg") @@ -125,7 +120,5 @@ DMSG pseudoterminal (pty)`, // Execute executes root CLI command. func Execute() { - if err := RootCmd.Execute(); err != nil { - log.Fatal("Failed to execute command: ", err) - } + dmsgclient.Execute(RootCmd) } diff --git a/cmd/dmsgcurl/commands/dmsgcurl.go b/cmd/dmsgcurl/commands/dmsgcurl.go index b8a76cc96..df953500d 100644 --- a/cmd/dmsgcurl/commands/dmsgcurl.go +++ b/cmd/dmsgcurl/commands/dmsgcurl.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "io/fs" - "log" "net" "net/http" "net/url" @@ -390,9 +389,5 @@ func (pw *progressWriter) Write(p []byte) (int, error) { // Execute executes the RootCmd func Execute() { - if err := RootCmd.Execute(); err != nil { - // WHY WON'T THIS PRINT?? - dlog.WithError(err).Debug("An error occurred\n") - log.Fatal("Failed to execute command: ", err) - } + dmsgclient.Execute(RootCmd) } diff --git a/cmd/dmsghttp/commands/dmsghttp.go b/cmd/dmsghttp/commands/dmsghttp.go index a189a21cc..ed3d07da4 100644 --- a/cmd/dmsghttp/commands/dmsghttp.go +++ b/cmd/dmsghttp/commands/dmsghttp.go @@ -4,12 +4,9 @@ package commands import ( "context" "fmt" - "log" "net" "net/http" "os" - "path/filepath" - "strings" "sync" "time" @@ -56,9 +53,7 @@ func init() { // RootCmd contains the root dmsghttp command var RootCmd = &cobra.Command{ - Use: func() string { - return strings.Split(filepath.Base(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", os.Args), "[", ""), "]", "")), " ")[0] - }(), + Use: dmsgclient.ExecName(), Short: "DMSG http file server", Long: calvin.AsciiFont("dmsghttp") + ` DMSG http file server`, @@ -313,9 +308,5 @@ const ( // Execute executes root CLI command. func Execute() { - if err := RootCmd.Execute(); err != nil { - // WHY WON'T THIS PRINT?? - dlog.WithError(err).Debug("An error occurred\n") - log.Fatal("Failed to execute command: ", err) - } + dmsgclient.Execute(RootCmd) } diff --git a/cmd/dmsgip/commands/dmsgip.go b/cmd/dmsgip/commands/dmsgip.go index dec9726d1..9e93839fc 100644 --- a/cmd/dmsgip/commands/dmsgip.go +++ b/cmd/dmsgip/commands/dmsgip.go @@ -4,11 +4,8 @@ package commands import ( "context" "fmt" - "log" "net/http" "os" - "path/filepath" - "strings" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/buildinfo" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/calvin" @@ -51,9 +48,7 @@ func init() { // RootCmd contains the root dmsgcurl command var RootCmd = &cobra.Command{ - Use: func() string { - return strings.Split(filepath.Base(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", os.Args), "[", ""), "]", "")), " ")[0] - }(), + Use: dmsgclient.ExecName(), Short: "DMSG IP utility", Long: calvin.AsciiFont("dmsgip") + ` DMSG IP utility`, @@ -135,7 +130,5 @@ var RootCmd = &cobra.Command{ // Execute executes root CLI command. func Execute() { - if err := RootCmd.Execute(); err != nil { - log.Fatal("Failed to execute command: ", err) - } + dmsgclient.Execute(RootCmd) } diff --git a/cmd/dmsgpty-cli/commands/root.go b/cmd/dmsgpty-cli/commands/root.go index cef9e9a12..4590becb8 100644 --- a/cmd/dmsgpty-cli/commands/root.go +++ b/cmd/dmsgpty-cli/commands/root.go @@ -4,11 +4,8 @@ package commands import ( "context" "encoding/json" - "fmt" "log" "os" - "path/filepath" - "strings" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/buildinfo" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/calvin" @@ -16,6 +13,7 @@ import ( "github.com/spf13/cobra" dmsg "github.com/skycoin/dmsg/pkg/dmsg" + dmsgcli "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/dmsgpty" ) @@ -43,9 +41,7 @@ func init() { // RootCmd contains commands for dmsgpty-cli; which interacts with the dmsgpty-host instance (i.e. skywire-visor) var RootCmd = &cobra.Command{ - Use: func() string { - return strings.Split(filepath.Base(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", os.Args), "[", ""), "]", "")), " ")[0] - }(), + Use: dmsgcli.ExecName(), Short: "DMSG pseudoterminal command line interface", Long: calvin.AsciiFont("dmsgpty-cli") + ` DMSG pseudoterminal command line interface`, @@ -121,7 +117,5 @@ var RootCmd = &cobra.Command{ // Execute executes the root command. func Execute() { - if err := RootCmd.Execute(); err != nil { - log.Fatal(err) - } + dmsgcli.Execute(RootCmd) } diff --git a/cmd/dmsgpty-host/commands/root.go b/cmd/dmsgpty-host/commands/root.go index 7b81fd26c..648cceb99 100644 --- a/cmd/dmsgpty-host/commands/root.go +++ b/cmd/dmsgpty-host/commands/root.go @@ -8,7 +8,6 @@ import ( "net" "net/http" "os" - "path/filepath" "strconv" "strings" "sync" @@ -22,6 +21,7 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" "github.com/spf13/cobra" + "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/disc" dmsg "github.com/skycoin/dmsg/pkg/dmsg" "github.com/skycoin/dmsg/pkg/dmsgpty" @@ -72,9 +72,7 @@ func init() { // RootCmd contains commands for dmsgpty-host var RootCmd = &cobra.Command{ - Use: func() string { - return strings.Split(filepath.Base(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", os.Args), "[", ""), "]", "")), " ")[0] - }(), + Use: dmsgclient.ExecName(), Short: "DMSG host for pseudoterminal command line interface", Long: calvin.AsciiFont("dmsgpty-host") + ` DMSG host for pseudoterminal (pty) command line interface`, @@ -160,9 +158,7 @@ var RootCmd = &cobra.Command{ // Execute executes the root command. func Execute() { - if err := RootCmd.Execute(); err != nil { - os.Exit(1) - } + dmsgclient.Execute(RootCmd) } func configFromJSON(conf dmsgpty.Config) (dmsgpty.Config, error) { diff --git a/cmd/dmsgpty-ui/commands/dmsgpty-ui.go b/cmd/dmsgpty-ui/commands/dmsgpty-ui.go index 665944339..3e04fe683 100644 --- a/cmd/dmsgpty-ui/commands/dmsgpty-ui.go +++ b/cmd/dmsgpty-ui/commands/dmsgpty-ui.go @@ -2,18 +2,15 @@ package commands import ( - "fmt" "log" "net/http" - "os" - "path/filepath" - "strings" "time" "github.com/sirupsen/logrus" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/buildinfo" "github.com/spf13/cobra" + "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/dmsgpty" ) @@ -34,9 +31,7 @@ func init() { // RootCmd contains commands to start a dmsgpty-ui server for a dmsgpty-host var RootCmd = &cobra.Command{ - Use: func() string { - return strings.Split(filepath.Base(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", os.Args), "[", ""), "]", "")), " ")[0] - }(), + Use: dmsgclient.ExecName(), Short: "DMSG pseudoterminal GUI", Long: ` ┌┬┐┌┬┐┌─┐┌─┐┌─┐┌┬┐┬ ┬ ┬ ┬┬ @@ -71,7 +66,5 @@ var RootCmd = &cobra.Command{ // Execute executes the root command. func Execute() { - if err := RootCmd.Execute(); err != nil { - os.Exit(1) - } + dmsgclient.Execute(RootCmd) } diff --git a/cmd/dmsgweb/commands/dmsgweb.go b/cmd/dmsgweb/commands/dmsgweb.go index e517bc6eb..119baec27 100644 --- a/cmd/dmsgweb/commands/dmsgweb.go +++ b/cmd/dmsgweb/commands/dmsgweb.go @@ -8,7 +8,6 @@ import ( "net" "net/http" "os" - "path/filepath" "regexp" "strconv" "strings" @@ -27,6 +26,7 @@ import ( "github.com/skycoin/dmsg/pkg/dmsgclient" dmsg "github.com/skycoin/dmsg/pkg/dmsg" "github.com/skycoin/dmsg/pkg/dmsghttp" + "github.com/skycoin/dmsg/pkg/ioutil" ) type customResolver struct{} @@ -79,9 +79,7 @@ func init() { // RootCmd contains the root command for dmsgweb var RootCmd = &cobra.Command{ - Use: func() string { - return strings.Split(filepath.Base(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", os.Args), "[", ""), "]", "")), " ")[0] - }(), + Use: dmsgclient.ExecName(), Short: "DMSG resolving proxy & browser client", Long: ` ┌┬┐┌┬┐┌─┐┌─┐┬ ┬┌─┐┌┐ @@ -315,7 +313,7 @@ func proxyTCPConn(n int) { if err != nil { dlog.WithError(err).Fatal(fmt.Sprintf("Failed to start TCP listener on port: %v", thiswebport)) } - defer listener.Close() //nolint + defer ioutil.CloseQuietly(listener, dlog) dlog.Debug("Serving TCP on 127.0.0.1:", thiswebport) if dmsgC == nil { dlog.Fatal("dmsgC is nil") @@ -329,7 +327,7 @@ func proxyTCPConn(n int) { } go func(conn net.Conn, n int, dmsgC *dmsg.Client) { - defer conn.Close() //nolint + defer ioutil.CloseQuietly(conn, dlog) dp, ok := safecast.To[uint16](dmsgPorts[n]) if !ok { dlog.Fatal("uint16 overflow when converting dmsg port") @@ -341,7 +339,7 @@ func proxyTCPConn(n int) { return } - defer dmsgConn.Close() //nolint + defer ioutil.CloseQuietly(dmsgConn, dlog) var wg sync.WaitGroup wg.Add(2) @@ -415,7 +413,7 @@ func proxyHTTPConn(n int) { dlog.WithError(err).Warn("Failed to connect to HTTP server") return } - defer resp.Body.Close() //nolint + defer ioutil.CloseQuietly(resp.Body, dlog) for header, values := range resp.Header { for _, value := range values { diff --git a/cmd/dmsgweb/commands/root.go b/cmd/dmsgweb/commands/root.go index e901cab74..bd22ce6b7 100644 --- a/cmd/dmsgweb/commands/root.go +++ b/cmd/dmsgweb/commands/root.go @@ -20,6 +20,7 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" "golang.org/x/net/proxy" + "github.com/skycoin/dmsg/pkg/dmsgclient" dmsg "github.com/skycoin/dmsg/pkg/dmsg" ) @@ -55,9 +56,7 @@ var ( // Execute executes root CLI command. func Execute() { - if err := RootCmd.Execute(); err != nil { - log.Fatal("Failed to execute command: ", err) - } + dmsgclient.Execute(RootCmd) } func printEnvs(envfile string) { diff --git a/docs/dmsg-goda-graph.svg b/docs/dmsg-goda-graph.svg index effe5d44e..4047bfb18 100644 --- a/docs/dmsg-goda-graph.svg +++ b/docs/dmsg-goda-graph.svg @@ -4,906 +4,904 @@ - - + + G - + github.com/skycoin/dmsg - -github.com/skycoin/dmsg -12 / 253B + +github.com/skycoin/dmsg +12 / 253B github.com/skycoin/dmsg/cmd/dmsg/commands - - -github.com/skycoin/dmsg/cmd/dmsg/commands -145 / 4.0KB + + +github.com/skycoin/dmsg/cmd/dmsg/commands +138 / 3.8KB github.com/skycoin/dmsg:e->github.com/skycoin/dmsg/cmd/dmsg/commands - - + + github.com/skycoin/dmsg/cmd/conf - -github.com/skycoin/dmsg/cmd/conf -12 / 261B + +github.com/skycoin/dmsg/cmd/conf +12 / 261B github.com/skycoin/dmsg/cmd/conf/commands - - -github.com/skycoin/dmsg/cmd/conf/commands -29 / 0.8KB + + +github.com/skycoin/dmsg/cmd/conf/commands +28 / 0.8KB github.com/skycoin/dmsg/cmd/conf:e->github.com/skycoin/dmsg/cmd/conf/commands - - + + - + github.com/skycoin/dmsg/pkg/dmsg - - -github.com/skycoin/dmsg/pkg/dmsg -2578 / 75.3KB + + +github.com/skycoin/dmsg/pkg/dmsg +2607 / 75.8KB github.com/skycoin/dmsg/cmd/conf/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + + + + +github.com/skycoin/dmsg/pkg/dmsgclient + + +github.com/skycoin/dmsg/pkg/dmsgclient +534 / 20.8KB + + + + + +github.com/skycoin/dmsg/cmd/conf/commands:e->github.com/skycoin/dmsg/pkg/dmsgclient + + github.com/skycoin/dmsg/cmd/dial - -github.com/skycoin/dmsg/cmd/dial -12 / 261B + +github.com/skycoin/dmsg/cmd/dial +12 / 261B github.com/skycoin/dmsg/cmd/dial/commands - - -github.com/skycoin/dmsg/cmd/dial/commands -170 / 6.1KB + + +github.com/skycoin/dmsg/cmd/dial/commands +162 / 5.8KB - -github.com/skycoin/dmsg/cmd/dial:e->github.com/skycoin/dmsg/cmd/dial/commands - - - - - -github.com/skycoin/dmsg/internal/cli - - -github.com/skycoin/dmsg/internal/cli -474 / 18.6KB - - - - -github.com/skycoin/dmsg/cmd/dial/commands:e->github.com/skycoin/dmsg/internal/cli - - - - - -github.com/skycoin/dmsg/internal/flags - - -github.com/skycoin/dmsg/internal/flags -45 / 1.7KB - - - - - -github.com/skycoin/dmsg/cmd/dial/commands:e->github.com/skycoin/dmsg/internal/flags - - +github.com/skycoin/dmsg/cmd/dial:e->github.com/skycoin/dmsg/cmd/dial/commands + + - + github.com/skycoin/dmsg/pkg/disc - - -github.com/skycoin/dmsg/pkg/disc -856 / 27.1KB + + +github.com/skycoin/dmsg/pkg/disc +856 / 27.1KB - + github.com/skycoin/dmsg/cmd/dial/commands:e->github.com/skycoin/dmsg/pkg/disc - - + + - + github.com/skycoin/dmsg/cmd/dial/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + + + + +github.com/skycoin/dmsg/cmd/dial/commands:e->github.com/skycoin/dmsg/pkg/dmsgclient + + github.com/skycoin/dmsg/cmd/dmsg - -github.com/skycoin/dmsg/cmd/dmsg -12 / 262B + +github.com/skycoin/dmsg/cmd/dmsg +12 / 262B github.com/skycoin/dmsg/cmd/dmsg:e->github.com/skycoin/dmsg/cmd/dmsg/commands - - + + github.com/skycoin/dmsg/cmd/dmsg-discovery - -github.com/skycoin/dmsg/cmd/dmsg-discovery -12 / 291B + +github.com/skycoin/dmsg/cmd/dmsg-discovery +12 / 291B github.com/skycoin/dmsg/cmd/dmsg-discovery/commands - - -github.com/skycoin/dmsg/cmd/dmsg-discovery/commands -468 / 16.3KB + + +github.com/skycoin/dmsg/cmd/dmsg-discovery/commands +464 / 16.1KB github.com/skycoin/dmsg/cmd/dmsg-discovery:e->github.com/skycoin/dmsg/cmd/dmsg-discovery/commands - - + + - - -github.com/skycoin/dmsg/internal/discmetrics - - -github.com/skycoin/dmsg/internal/discmetrics -44 / 1.5KB + + +github.com/skycoin/dmsg/pkg/direct + + +github.com/skycoin/dmsg/pkg/direct +186 / 5.7KB - + -github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/internal/discmetrics - - - - - -github.com/skycoin/dmsg/internal/dmsg-discovery/api - - -github.com/skycoin/dmsg/internal/dmsg-discovery/api -536 / 17.8KB - - +github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/pkg/direct + + - + -github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/internal/dmsg-discovery/api - - +github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/pkg/disc + + - - -github.com/skycoin/dmsg/internal/dmsg-discovery/store - - -github.com/skycoin/dmsg/internal/dmsg-discovery/store -512 / 16.3KB + + +github.com/skycoin/dmsg/pkg/disc/metrics + + +github.com/skycoin/dmsg/pkg/disc/metrics +44 / 1.5KB - + -github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/internal/dmsg-discovery/store - - +github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/pkg/disc/metrics + + - - -github.com/skycoin/dmsg/pkg/direct - - -github.com/skycoin/dmsg/pkg/direct -186 / 5.7KB + + +github.com/skycoin/dmsg/pkg/discovery/api + + +github.com/skycoin/dmsg/pkg/discovery/api +536 / 17.8KB - + -github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/pkg/direct - - +github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/pkg/discovery/api + + - + + +github.com/skycoin/dmsg/pkg/discovery/store + + +github.com/skycoin/dmsg/pkg/discovery/store +512 / 16.2KB + + + + -github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/pkg/disc - - +github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/pkg/discovery/store + + github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + + + + +github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/pkg/dmsgclient + + - + github.com/skycoin/dmsg/pkg/dmsghttp - - -github.com/skycoin/dmsg/pkg/dmsghttp -199 / 5.7KB + + +github.com/skycoin/dmsg/pkg/dmsghttp +199 / 5.7KB - + github.com/skycoin/dmsg/cmd/dmsg-discovery/commands:e->github.com/skycoin/dmsg/pkg/dmsghttp - - + + github.com/skycoin/dmsg/cmd/dmsg-server - -github.com/skycoin/dmsg/cmd/dmsg-server -12 / 282B + +github.com/skycoin/dmsg/cmd/dmsg-server +12 / 282B github.com/skycoin/dmsg/cmd/dmsg-server/commands - - -github.com/skycoin/dmsg/cmd/dmsg-server/commands -47 / 1.5KB + + +github.com/skycoin/dmsg/cmd/dmsg-server/commands +39 / 1.3KB - + github.com/skycoin/dmsg/cmd/dmsg-server:e->github.com/skycoin/dmsg/cmd/dmsg-server/commands - - + + github.com/skycoin/dmsg/cmd/dmsg-server/commands/config - -github.com/skycoin/dmsg/cmd/dmsg-server/commands/config -54 / 1.5KB + +github.com/skycoin/dmsg/cmd/dmsg-server/commands/config +54 / 1.5KB - + github.com/skycoin/dmsg/cmd/dmsg-server/commands:e->github.com/skycoin/dmsg/cmd/dmsg-server/commands/config - - + + github.com/skycoin/dmsg/cmd/dmsg-server/commands/start - - -github.com/skycoin/dmsg/cmd/dmsg-server/commands/start -139 / 4.7KB + + +github.com/skycoin/dmsg/cmd/dmsg-server/commands/start +137 / 4.7KB - + github.com/skycoin/dmsg/cmd/dmsg-server/commands:e->github.com/skycoin/dmsg/cmd/dmsg-server/commands/start - - + + + + + +github.com/skycoin/dmsg/cmd/dmsg-server/commands:e->github.com/skycoin/dmsg/pkg/dmsgclient + + - + github.com/skycoin/dmsg/pkg/dmsgserver - - -github.com/skycoin/dmsg/pkg/dmsgserver -59 / 1.9KB + + +github.com/skycoin/dmsg/pkg/dmsgserver +287 / 9.0KB - -github.com/skycoin/dmsg/cmd/dmsg-server/commands/config:e->github.com/skycoin/dmsg/pkg/dmsgserver - - - - - -github.com/skycoin/dmsg/internal/dmsg-server/api - - -github.com/skycoin/dmsg/internal/dmsg-server/api -228 / 7.1KB - - - - - -github.com/skycoin/dmsg/cmd/dmsg-server/commands/start:e->github.com/skycoin/dmsg/internal/dmsg-server/api - - - - - -github.com/skycoin/dmsg/internal/servermetrics - - -github.com/skycoin/dmsg/internal/servermetrics -111 / 4.0KB - - - - -github.com/skycoin/dmsg/cmd/dmsg-server/commands/start:e->github.com/skycoin/dmsg/internal/servermetrics - - +github.com/skycoin/dmsg/cmd/dmsg-server/commands/config:e->github.com/skycoin/dmsg/pkg/dmsgserver + + github.com/skycoin/dmsg/cmd/dmsg-server/commands/start:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/cmd/dmsg-server/commands/start:e->github.com/skycoin/dmsg/pkg/dmsg - - + + - + + +github.com/skycoin/dmsg/pkg/dmsg/metrics + + +github.com/skycoin/dmsg/pkg/dmsg/metrics +111 / 3.9KB + + + + +github.com/skycoin/dmsg/cmd/dmsg-server/commands/start:e->github.com/skycoin/dmsg/pkg/dmsg/metrics + + + + + +github.com/skycoin/dmsg/cmd/dmsg-server/commands/start:e->github.com/skycoin/dmsg/pkg/dmsgclient + + + + + github.com/skycoin/dmsg/cmd/dmsg-server/commands/start:e->github.com/skycoin/dmsg/pkg/dmsgserver - - + + github.com/skycoin/dmsg/cmd/dmsg-socks5 - -github.com/skycoin/dmsg/cmd/dmsg-socks5 -12 / 282B + +github.com/skycoin/dmsg/cmd/dmsg-socks5 +12 / 282B github.com/skycoin/dmsg/cmd/dmsg-socks5/commands - - -github.com/skycoin/dmsg/cmd/dmsg-socks5/commands -249 / 8.2KB + + +github.com/skycoin/dmsg/cmd/dmsg-socks5/commands +243 / 8.0KB - + github.com/skycoin/dmsg/cmd/dmsg-socks5:e->github.com/skycoin/dmsg/cmd/dmsg-socks5/commands - - - - - -github.com/skycoin/dmsg/cmd/dmsg-socks5/commands:e->github.com/skycoin/dmsg/internal/cli - - + + - + github.com/skycoin/dmsg/cmd/dmsg-socks5/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + + + + +github.com/skycoin/dmsg/cmd/dmsg-socks5/commands:e->github.com/skycoin/dmsg/pkg/dmsgclient + + - + github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/conf/commands - - + + - + github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dial/commands - - + + - + github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsg-discovery/commands - - + + - + github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsg-server/commands - - + + - + github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsg-socks5/commands - - + + github.com/skycoin/dmsg/cmd/dmsgcurl/commands - - -github.com/skycoin/dmsg/cmd/dmsgcurl/commands -398 / 12.9KB + + +github.com/skycoin/dmsg/cmd/dmsgcurl/commands +392 / 12.7KB - + github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsgcurl/commands - - + + github.com/skycoin/dmsg/cmd/dmsghttp/commands - - -github.com/skycoin/dmsg/cmd/dmsghttp/commands -286 / 8.0KB + + +github.com/skycoin/dmsg/cmd/dmsghttp/commands +276 / 7.6KB - + github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsghttp/commands - - + + github.com/skycoin/dmsg/cmd/dmsgip/commands - - -github.com/skycoin/dmsg/cmd/dmsgip/commands -124 / 4.1KB + + +github.com/skycoin/dmsg/cmd/dmsgip/commands +117 / 3.9KB - + github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsgip/commands - - + + github.com/skycoin/dmsg/cmd/dmsgpty-cli/commands - - -github.com/skycoin/dmsg/cmd/dmsgpty-cli/commands -195 / 5.6KB + + +github.com/skycoin/dmsg/cmd/dmsgpty-cli/commands +189 / 5.5KB - + github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsgpty-cli/commands - - + + github.com/skycoin/dmsg/cmd/dmsgpty-host/commands - - -github.com/skycoin/dmsg/cmd/dmsgpty-host/commands -328 / 10.1KB + + +github.com/skycoin/dmsg/cmd/dmsgpty-host/commands +322 / 9.9KB - + github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsgpty-host/commands - - + + github.com/skycoin/dmsg/cmd/dmsgpty-ui/commands - - -github.com/skycoin/dmsg/cmd/dmsgpty-ui/commands -67 / 2.2KB + + +github.com/skycoin/dmsg/cmd/dmsgpty-ui/commands +60 / 2.1KB - + github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsgpty-ui/commands - - + + github.com/skycoin/dmsg/cmd/dmsgweb/commands - - -github.com/skycoin/dmsg/cmd/dmsgweb/commands -1084 / 33.8KB + + +github.com/skycoin/dmsg/cmd/dmsgweb/commands +1079 / 33.7KB - + github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/cmd/dmsgweb/commands - - + + + + + +github.com/skycoin/dmsg/cmd/dmsg/commands:e->github.com/skycoin/dmsg/pkg/dmsgclient + + github.com/skycoin/dmsg/cmd/dmsgcurl - -github.com/skycoin/dmsg/cmd/dmsgcurl -12 / 273B + +github.com/skycoin/dmsg/cmd/dmsgcurl +12 / 273B - + github.com/skycoin/dmsg/cmd/dmsgcurl:e->github.com/skycoin/dmsg/cmd/dmsgcurl/commands - - - - - -github.com/skycoin/dmsg/cmd/dmsgcurl/commands:e->github.com/skycoin/dmsg/internal/cli - - - - - -github.com/skycoin/dmsg/cmd/dmsgcurl/commands:e->github.com/skycoin/dmsg/internal/flags - - + + - + github.com/skycoin/dmsg/cmd/dmsgcurl/commands:e->github.com/skycoin/dmsg/pkg/disc - - + + - + github.com/skycoin/dmsg/cmd/dmsgcurl/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + + + + +github.com/skycoin/dmsg/cmd/dmsgcurl/commands:e->github.com/skycoin/dmsg/pkg/dmsgclient + + - + github.com/skycoin/dmsg/cmd/dmsgcurl/commands:e->github.com/skycoin/dmsg/pkg/dmsghttp - - + + github.com/skycoin/dmsg/cmd/dmsghttp - -github.com/skycoin/dmsg/cmd/dmsghttp -12 / 285B + +github.com/skycoin/dmsg/cmd/dmsghttp +12 / 285B - -github.com/skycoin/dmsg/cmd/dmsghttp:e->github.com/skycoin/dmsg/cmd/dmsghttp/commands - - - - - -github.com/skycoin/dmsg/cmd/dmsghttp/commands:e->github.com/skycoin/dmsg/internal/cli - - - - -github.com/skycoin/dmsg/cmd/dmsghttp/commands:e->github.com/skycoin/dmsg/internal/flags - - +github.com/skycoin/dmsg/cmd/dmsghttp:e->github.com/skycoin/dmsg/cmd/dmsghttp/commands + + github.com/skycoin/dmsg/cmd/dmsghttp/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + + + + +github.com/skycoin/dmsg/cmd/dmsghttp/commands:e->github.com/skycoin/dmsg/pkg/dmsgclient + + github.com/skycoin/dmsg/cmd/dmsgip - -github.com/skycoin/dmsg/cmd/dmsgip -12 / 271B + +github.com/skycoin/dmsg/cmd/dmsgip +12 / 271B - -github.com/skycoin/dmsg/cmd/dmsgip:e->github.com/skycoin/dmsg/cmd/dmsgip/commands - - - - -github.com/skycoin/dmsg/cmd/dmsgip/commands:e->github.com/skycoin/dmsg/internal/cli - - +github.com/skycoin/dmsg/cmd/dmsgip:e->github.com/skycoin/dmsg/cmd/dmsgip/commands + + github.com/skycoin/dmsg/cmd/dmsgip/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + + + + +github.com/skycoin/dmsg/cmd/dmsgip/commands:e->github.com/skycoin/dmsg/pkg/dmsgclient + + github.com/skycoin/dmsg/cmd/dmsgpty-cli - -github.com/skycoin/dmsg/cmd/dmsgpty-cli -12 / 282B + +github.com/skycoin/dmsg/cmd/dmsgpty-cli +12 / 282B - + github.com/skycoin/dmsg/cmd/dmsgpty-cli:e->github.com/skycoin/dmsg/cmd/dmsgpty-cli/commands - - + + - + github.com/skycoin/dmsg/cmd/dmsgpty-cli/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + + + + +github.com/skycoin/dmsg/cmd/dmsgpty-cli/commands:e->github.com/skycoin/dmsg/pkg/dmsgclient + + - + github.com/skycoin/dmsg/pkg/dmsgpty - - -github.com/skycoin/dmsg/pkg/dmsgpty -7557 / 597.5KB + + +github.com/skycoin/dmsg/pkg/dmsgpty +1840 / 50.2KB - + github.com/skycoin/dmsg/cmd/dmsgpty-cli/commands:e->github.com/skycoin/dmsg/pkg/dmsgpty - - + + github.com/skycoin/dmsg/cmd/dmsgpty-host - -github.com/skycoin/dmsg/cmd/dmsgpty-host -12 / 285B + +github.com/skycoin/dmsg/cmd/dmsgpty-host +12 / 285B - + github.com/skycoin/dmsg/cmd/dmsgpty-host:e->github.com/skycoin/dmsg/cmd/dmsgpty-host/commands - - - - - -github.com/skycoin/dmsg/internal/fsutil - - -github.com/skycoin/dmsg/internal/fsutil -16 / 296B - - - - - -github.com/skycoin/dmsg/cmd/dmsgpty-host/commands:e->github.com/skycoin/dmsg/internal/fsutil - - + + - + github.com/skycoin/dmsg/cmd/dmsgpty-host/commands:e->github.com/skycoin/dmsg/pkg/disc - - + + - + github.com/skycoin/dmsg/cmd/dmsgpty-host/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + + + + +github.com/skycoin/dmsg/cmd/dmsgpty-host/commands:e->github.com/skycoin/dmsg/pkg/dmsgclient + + - + github.com/skycoin/dmsg/cmd/dmsgpty-host/commands:e->github.com/skycoin/dmsg/pkg/dmsgpty - - + + github.com/skycoin/dmsg/cmd/dmsgpty-ui - -github.com/skycoin/dmsg/cmd/dmsgpty-ui -12 / 279B + +github.com/skycoin/dmsg/cmd/dmsgpty-ui +12 / 279B - + github.com/skycoin/dmsg/cmd/dmsgpty-ui:e->github.com/skycoin/dmsg/cmd/dmsgpty-ui/commands - - + + + + + +github.com/skycoin/dmsg/cmd/dmsgpty-ui/commands:e->github.com/skycoin/dmsg/pkg/dmsgclient + + - + github.com/skycoin/dmsg/cmd/dmsgpty-ui/commands:e->github.com/skycoin/dmsg/pkg/dmsgpty - - + + github.com/skycoin/dmsg/cmd/dmsgweb - -github.com/skycoin/dmsg/cmd/dmsgweb -12 / 270B + +github.com/skycoin/dmsg/cmd/dmsgweb +12 / 270B - + github.com/skycoin/dmsg/cmd/dmsgweb:e->github.com/skycoin/dmsg/cmd/dmsgweb/commands - - - - - -github.com/skycoin/dmsg/cmd/dmsgweb/commands:e->github.com/skycoin/dmsg/internal/cli - - - - - -github.com/skycoin/dmsg/cmd/dmsgweb/commands:e->github.com/skycoin/dmsg/internal/flags - - + + - + github.com/skycoin/dmsg/cmd/dmsgweb/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + + + + +github.com/skycoin/dmsg/cmd/dmsgweb/commands:e->github.com/skycoin/dmsg/pkg/dmsgclient + + - + github.com/skycoin/dmsg/cmd/dmsgweb/commands:e->github.com/skycoin/dmsg/pkg/dmsghttp - - + + + + + +github.com/skycoin/dmsg/pkg/ioutil + + +github.com/skycoin/dmsg/pkg/ioutil +38 / 1.1KB + + + + + +github.com/skycoin/dmsg/cmd/dmsgweb/commands:e->github.com/skycoin/dmsg/pkg/ioutil + + github.com/skycoin/dmsg/examples/basics - -github.com/skycoin/dmsg/examples/basics -111 / 3.5KB + +github.com/skycoin/dmsg/examples/basics +111 / 3.5KB - + github.com/skycoin/dmsg/examples/basics:e->github.com/skycoin/dmsg/pkg/disc - - + + - + github.com/skycoin/dmsg/examples/basics:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/examples/dmsgcurl/dmsg-example-http-server - -github.com/skycoin/dmsg/examples/dmsgcurl/dmsg-example-http-server -80 / 2.1KB + +github.com/skycoin/dmsg/examples/dmsgcurl/dmsg-example-http-server +80 / 2.1KB - + github.com/skycoin/dmsg/examples/dmsgcurl/dmsg-example-http-server:e->github.com/skycoin/dmsg/pkg/disc - - + + - + github.com/skycoin/dmsg/examples/dmsgcurl/dmsg-example-http-server:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/examples/dmsgcurl/gen-keys - -github.com/skycoin/dmsg/examples/dmsgcurl/gen-keys -10 / 215B + +github.com/skycoin/dmsg/examples/dmsgcurl/gen-keys +10 / 215B @@ -911,81 +909,81 @@ github.com/skycoin/dmsg/examples/dmsghttp - -github.com/skycoin/dmsg/examples/dmsghttp -133 / 4.3KB + +github.com/skycoin/dmsg/examples/dmsghttp +133 / 4.3KB - + github.com/skycoin/dmsg/examples/dmsghttp:e->github.com/skycoin/dmsg/pkg/disc - - + + - + github.com/skycoin/dmsg/examples/dmsghttp:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/examples/dmsghttp-client - -github.com/skycoin/dmsg/examples/dmsghttp-client -46 / 1.3KB + +github.com/skycoin/dmsg/examples/dmsghttp-client +46 / 1.3KB - - -github.com/skycoin/dmsg/examples/dmsghttp-client:e->github.com/skycoin/dmsg/internal/cli - - - - + github.com/skycoin/dmsg/examples/dmsghttp-client:e->github.com/skycoin/dmsg/pkg/dmsg - - + + + + + +github.com/skycoin/dmsg/examples/dmsghttp-client:e->github.com/skycoin/dmsg/pkg/dmsgclient + + - + github.com/skycoin/dmsg/examples/dmsghttp-client:e->github.com/skycoin/dmsg/pkg/dmsghttp - - + + github.com/skycoin/dmsg/examples/dmsgtcp - -github.com/skycoin/dmsg/examples/dmsgtcp -144 / 4.5KB + +github.com/skycoin/dmsg/examples/dmsgtcp +144 / 4.5KB - - -github.com/skycoin/dmsg/examples/dmsgtcp:e->github.com/skycoin/dmsg/internal/cli - - - - + github.com/skycoin/dmsg/examples/dmsgtcp:e->github.com/skycoin/dmsg/pkg/dmsg - - + + + + + +github.com/skycoin/dmsg/examples/dmsgtcp:e->github.com/skycoin/dmsg/pkg/dmsgclient + + github.com/skycoin/dmsg/examples/dmsgweb - -github.com/skycoin/dmsg/examples/dmsgweb -39 / 1.5KB + +github.com/skycoin/dmsg/examples/dmsgweb +39 / 1.5KB @@ -993,43 +991,43 @@ github.com/skycoin/dmsg/examples/dmsgweb/commands - -github.com/skycoin/dmsg/examples/dmsgweb/commands -342 / 10.4KB + +github.com/skycoin/dmsg/examples/dmsgweb/commands +342 / 10.4KB - + github.com/skycoin/dmsg/examples/dmsgweb:e->github.com/skycoin/dmsg/examples/dmsgweb/commands - - + + - + github.com/skycoin/dmsg/examples/dmsgweb/commands:e->github.com/skycoin/dmsg/pkg/disc - - + + - + github.com/skycoin/dmsg/examples/dmsgweb/commands:e->github.com/skycoin/dmsg/pkg/dmsg - - + + - + github.com/skycoin/dmsg/examples/dmsgweb/commands:e->github.com/skycoin/dmsg/pkg/dmsghttp - - + + github.com/skycoin/dmsg/examples/gen-keys - -github.com/skycoin/dmsg/examples/gen-keys -10 / 211B + +github.com/skycoin/dmsg/examples/gen-keys +10 / 211B @@ -1037,9 +1035,9 @@ github.com/skycoin/dmsg/examples/http - -github.com/skycoin/dmsg/examples/http -30 / 0.7KB + +github.com/skycoin/dmsg/examples/http +30 / 0.7KB @@ -1047,31 +1045,31 @@ github.com/skycoin/dmsg/examples/proxified - -github.com/skycoin/dmsg/examples/proxified -97 / 3.3KB + +github.com/skycoin/dmsg/examples/proxified +97 / 3.3KB - + github.com/skycoin/dmsg/examples/proxified:e->github.com/skycoin/dmsg/pkg/disc - - + + - + github.com/skycoin/dmsg/examples/proxified:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/examples/tcp - -github.com/skycoin/dmsg/examples/tcp -37 / 0.8KB + +github.com/skycoin/dmsg/examples/tcp +37 / 0.8KB @@ -1079,31 +1077,31 @@ github.com/skycoin/dmsg/examples/tcp-multi-proxy-dmsg - -github.com/skycoin/dmsg/examples/tcp-multi-proxy-dmsg -146 / 4.7KB + +github.com/skycoin/dmsg/examples/tcp-multi-proxy-dmsg +146 / 4.7KB - + github.com/skycoin/dmsg/examples/tcp-multi-proxy-dmsg:e->github.com/skycoin/dmsg/pkg/disc - - + + - + github.com/skycoin/dmsg/examples/tcp-multi-proxy-dmsg:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/examples/tcp-proxy - -github.com/skycoin/dmsg/examples/tcp-proxy -73 / 1.8KB + +github.com/skycoin/dmsg/examples/tcp-proxy +73 / 1.8KB @@ -1111,295 +1109,273 @@ github.com/skycoin/dmsg/examples/tcp-proxy-dmsg - -github.com/skycoin/dmsg/examples/tcp-proxy-dmsg -219 / 7.1KB + +github.com/skycoin/dmsg/examples/tcp-proxy-dmsg +219 / 7.1KB - + github.com/skycoin/dmsg/examples/tcp-proxy-dmsg:e->github.com/skycoin/dmsg/pkg/disc - - + + - + github.com/skycoin/dmsg/examples/tcp-proxy-dmsg:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/examples/tcp-reverse-proxy-dmsg - -github.com/skycoin/dmsg/examples/tcp-reverse-proxy-dmsg -222 / 7.0KB + +github.com/skycoin/dmsg/examples/tcp-reverse-proxy-dmsg +222 / 7.0KB - + github.com/skycoin/dmsg/examples/tcp-reverse-proxy-dmsg:e->github.com/skycoin/dmsg/pkg/disc - - + + - + github.com/skycoin/dmsg/examples/tcp-reverse-proxy-dmsg:e->github.com/skycoin/dmsg/pkg/dmsg - - + + - - -github.com/skycoin/dmsg/internal/cli:e->github.com/skycoin/dmsg/internal/flags - - + + +github.com/skycoin/dmsg/internal/e2e + + +github.com/skycoin/dmsg/internal/e2e +0 / 0B + - - -github.com/skycoin/dmsg/internal/cli:e->github.com/skycoin/dmsg/pkg/direct - - - - -github.com/skycoin/dmsg/internal/cli:e->github.com/skycoin/dmsg/pkg/disc - - + + +github.com/skycoin/dmsg/internal/e2e/testserver + + +github.com/skycoin/dmsg/internal/e2e/testserver +43 / 1.2KB + + - + -github.com/skycoin/dmsg/internal/cli:e->github.com/skycoin/dmsg/pkg/dmsg - - +github.com/skycoin/dmsg/pkg/direct:e->github.com/skycoin/dmsg/pkg/disc + + - + -github.com/skycoin/dmsg/internal/cli:e->github.com/skycoin/dmsg/pkg/dmsghttp - - +github.com/skycoin/dmsg/pkg/direct:e->github.com/skycoin/dmsg/pkg/dmsg + + - + -github.com/skycoin/dmsg/internal/dmsg-discovery/api:e->github.com/skycoin/dmsg/internal/discmetrics - - +github.com/skycoin/dmsg/pkg/discovery/api:e->github.com/skycoin/dmsg/pkg/disc + + - + -github.com/skycoin/dmsg/internal/dmsg-discovery/api:e->github.com/skycoin/dmsg/internal/dmsg-discovery/store - - +github.com/skycoin/dmsg/pkg/discovery/api:e->github.com/skycoin/dmsg/pkg/disc/metrics + + - + -github.com/skycoin/dmsg/internal/dmsg-discovery/api:e->github.com/skycoin/dmsg/pkg/disc - - +github.com/skycoin/dmsg/pkg/discovery/api:e->github.com/skycoin/dmsg/pkg/discovery/store + + - + -github.com/skycoin/dmsg/internal/dmsg-discovery/api:e->github.com/skycoin/dmsg/pkg/dmsg - - +github.com/skycoin/dmsg/pkg/discovery/api:e->github.com/skycoin/dmsg/pkg/dmsg + + - + -github.com/skycoin/dmsg/internal/dmsg-discovery/store:e->github.com/skycoin/dmsg/pkg/disc - - +github.com/skycoin/dmsg/pkg/discovery/store:e->github.com/skycoin/dmsg/pkg/disc + + - + -github.com/skycoin/dmsg/internal/dmsg-discovery/store:e->github.com/skycoin/dmsg/pkg/dmsg - - +github.com/skycoin/dmsg/pkg/discovery/store:e->github.com/skycoin/dmsg/pkg/dmsg + + - + -github.com/skycoin/dmsg/internal/dmsg-server/api:e->github.com/skycoin/dmsg/internal/servermetrics - - +github.com/skycoin/dmsg/pkg/dmsg:e->github.com/skycoin/dmsg/pkg/disc + + - + -github.com/skycoin/dmsg/internal/dmsg-server/api:e->github.com/skycoin/dmsg/pkg/dmsg - - - - - -github.com/skycoin/dmsg/internal/e2e - - -github.com/skycoin/dmsg/internal/e2e -0 / 0B - - +github.com/skycoin/dmsg/pkg/dmsg:e->github.com/skycoin/dmsg/pkg/dmsg/metrics + + - - -github.com/skycoin/dmsg/internal/e2e/testserver - - -github.com/skycoin/dmsg/internal/e2e/testserver -43 / 1.2KB + + +github.com/skycoin/dmsg/pkg/noise + + +github.com/skycoin/dmsg/pkg/noise +702 / 19.5KB - + -github.com/skycoin/dmsg/internal/flags:e->github.com/skycoin/dmsg/pkg/dmsg - - +github.com/skycoin/dmsg/pkg/dmsg:e->github.com/skycoin/dmsg/pkg/noise + + - + -github.com/skycoin/dmsg/pkg/direct:e->github.com/skycoin/dmsg/pkg/disc - - +github.com/skycoin/dmsg/pkg/dmsgclient:e->github.com/skycoin/dmsg/pkg/direct + + - + -github.com/skycoin/dmsg/pkg/direct:e->github.com/skycoin/dmsg/pkg/dmsg - - +github.com/skycoin/dmsg/pkg/dmsgclient:e->github.com/skycoin/dmsg/pkg/disc + + - + -github.com/skycoin/dmsg/pkg/dmsg:e->github.com/skycoin/dmsg/internal/servermetrics - - +github.com/skycoin/dmsg/pkg/dmsgclient:e->github.com/skycoin/dmsg/pkg/dmsg + + - + -github.com/skycoin/dmsg/pkg/dmsg:e->github.com/skycoin/dmsg/pkg/disc - - - - - -github.com/skycoin/dmsg/pkg/noise - - -github.com/skycoin/dmsg/pkg/noise -702 / 19.5KB - +github.com/skycoin/dmsg/pkg/dmsgclient:e->github.com/skycoin/dmsg/pkg/dmsghttp + + - - + -github.com/skycoin/dmsg/pkg/dmsg:e->github.com/skycoin/dmsg/pkg/noise - - +github.com/skycoin/dmsg/pkg/dmsgclient:e->github.com/skycoin/dmsg/pkg/ioutil + + - + github.com/skycoin/dmsg/pkg/dmsgctrl - - -github.com/skycoin/dmsg/pkg/dmsgctrl -179 / 4.0KB + + +github.com/skycoin/dmsg/pkg/dmsgctrl +179 / 4.0KB github.com/skycoin/dmsg/pkg/dmsgctrl:e->github.com/skycoin/dmsg/pkg/dmsg - - + + - + github.com/skycoin/dmsg/pkg/dmsgcurl - - -github.com/skycoin/dmsg/pkg/dmsgcurl -346 / 9.9KB + + +github.com/skycoin/dmsg/pkg/dmsgcurl +346 / 10.0KB github.com/skycoin/dmsg/pkg/dmsgcurl:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/pkg/dmsgcurl:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/pkg/dmsgcurl:e->github.com/skycoin/dmsg/pkg/dmsghttp - - + + github.com/skycoin/dmsg/pkg/dmsghttp:e->github.com/skycoin/dmsg/pkg/disc - - + + github.com/skycoin/dmsg/pkg/dmsghttp:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/pkg/dmsgpty:e->github.com/skycoin/dmsg/pkg/dmsg - - + + github.com/skycoin/dmsg/pkg/dmsgserver:e->github.com/skycoin/dmsg/pkg/dmsg - - + + + + + +github.com/skycoin/dmsg/pkg/dmsgserver:e->github.com/skycoin/dmsg/pkg/dmsg/metrics + + - + github.com/skycoin/dmsg/pkg/dmsgtest - - -github.com/skycoin/dmsg/pkg/dmsgtest -211 / 6.1KB + + +github.com/skycoin/dmsg/pkg/dmsgtest +211 / 6.1KB - + github.com/skycoin/dmsg/pkg/dmsgtest:e->github.com/skycoin/dmsg/pkg/disc - - + + - + github.com/skycoin/dmsg/pkg/dmsgtest:e->github.com/skycoin/dmsg/pkg/dmsg - - - - - -github.com/skycoin/dmsg/pkg/ioutil - - -github.com/skycoin/dmsg/pkg/ioutil -23 / 0.7KB - - + + - + github.com/skycoin/dmsg/pkg/noise:e->github.com/skycoin/dmsg/pkg/ioutil - - + + diff --git a/pkg/dmsg/const.go b/pkg/dmsg/const.go index 065cfd7c3..331818eb6 100644 --- a/pkg/dmsg/const.go +++ b/pkg/dmsg/const.go @@ -4,6 +4,7 @@ package dmsg import ( "crypto/rand" "encoding/json" + "fmt" "log" "math/big" "regexp" @@ -93,7 +94,10 @@ func InitConfig() error { if err != nil { return err } - Prod.DmsgServers = shuffleServers(Prod.DmsgServers) + Prod.DmsgServers, err = shuffleServers(Prod.DmsgServers) + if err != nil { + return err + } err = json.Unmarshal(envServices.Test, &Test) if err != nil { return err @@ -101,15 +105,15 @@ func InitConfig() error { return nil } -func shuffleServers(in []disc.Entry) []disc.Entry { +func shuffleServers(in []disc.Entry) ([]disc.Entry, error) { n := len(in) for i := n - 1; i > 0; i-- { jBig, err := rand.Int(rand.Reader, big.NewInt(int64(i+1))) if err != nil { - panic(err) + return nil, fmt.Errorf("shuffleServers: %w", err) } j := int(jBig.Int64()) in[i], in[j] = in[j], in[i] } - return in + return in, nil } diff --git a/pkg/dmsg/metrics/victoria_metrics.go b/pkg/dmsg/metrics/victoria_metrics.go index 71a7847f4..4548961b1 100644 --- a/pkg/dmsg/metrics/victoria_metrics.go +++ b/pkg/dmsg/metrics/victoria_metrics.go @@ -2,7 +2,7 @@ package metrics import ( - "fmt" + "log" "github.com/VictoriaMetrics/metrics" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/metricsutil" @@ -62,7 +62,7 @@ func (m *VictoriaMetrics) RecordSession(delta DeltaType) { case -1: m.activeSessions.Dec() default: - panic(fmt.Errorf("invalid delta: %d", delta)) + log.Printf("RecordSession: invalid delta: %d", delta) } } @@ -77,6 +77,6 @@ func (m *VictoriaMetrics) RecordStream(delta DeltaType) { case -1: m.activeStreams.Dec() default: - panic(fmt.Errorf("invalid delta: %d", delta)) + log.Printf("RecordStream: invalid delta: %d", delta) } } diff --git a/pkg/dmsg/server_session.go b/pkg/dmsg/server_session.go index 72632bfd0..b2565578d 100644 --- a/pkg/dmsg/server_session.go +++ b/pkg/dmsg/server_session.go @@ -146,7 +146,11 @@ func (ss *ServerSession) serveStream(log logrus.FieldLogger, yStr io.ReadWriteCl Accepted: true, IP: ip, } - obj := MakeSignedStreamResponse(&resp, ss.entity.LocalSK()) + obj, err := MakeSignedStreamResponse(&resp, ss.entity.LocalSK()) + if err != nil { + ss.m.RecordStream(metrics.DeltaFailed) // record failed stream + return err + } if err := ss.writeObject(yStr, obj); err != nil { ss.m.RecordStream(metrics.DeltaFailed) // record failed stream diff --git a/pkg/dmsg/stream.go b/pkg/dmsg/stream.go index 4cc872ac9..d6a2927ae 100644 --- a/pkg/dmsg/stream.go +++ b/pkg/dmsg/stream.go @@ -102,7 +102,10 @@ func (s *Stream) writeRequest(rAddr Addr) (req StreamRequest, err error) { DstAddr: s.rAddr, NoiseMsg: nsMsg, } - obj := MakeSignedStreamRequest(&req, s.ses.localSK()) + obj, err := MakeSignedStreamRequest(&req, s.ses.localSK()) + if err != nil { + return req, err + } // Write request. if s.sStr != nil { @@ -131,7 +134,10 @@ func (s *Stream) writeIPRequest(rAddr Addr) (req StreamRequest, err error) { DstAddr: s.rAddr, IPinfo: true, } - obj := MakeSignedStreamRequest(&req, s.ses.localSK()) + obj, err := MakeSignedStreamRequest(&req, s.ses.localSK()) + if err != nil { + return req, err + } // Write request. if s.sStr != nil { @@ -196,7 +202,10 @@ func (s *Stream) writeResponse(reqHash cipher.SHA256) error { Accepted: true, NoiseMsg: nsMsg, } - obj := MakeSignedStreamResponse(&resp, s.ses.localSK()) + obj, err := MakeSignedStreamResponse(&resp, s.ses.localSK()) + if err != nil { + return err + } if s.sStr != nil { if err := s.ses.writeObject(s.sStr, obj); err != nil { diff --git a/pkg/dmsg/types.go b/pkg/dmsg/types.go index d98ed2911..d3839c8ac 100644 --- a/pkg/dmsg/types.go +++ b/pkg/dmsg/types.go @@ -102,21 +102,33 @@ const sigLen = len(cipher.Sig{}) type SignedObject []byte // MakeSignedStreamRequest encodes and signs a StreamRequest into a SignedObject format. -func MakeSignedStreamRequest(req *StreamRequest, sk cipher.SecKey) SignedObject { - obj := encodeGob(req) - sig := SignBytes(obj, sk) +func MakeSignedStreamRequest(req *StreamRequest, sk cipher.SecKey) (SignedObject, error) { + obj, err := encodeGob(req) + if err != nil { + return nil, fmt.Errorf("dmsg: encode stream request: %w", err) + } + sig, err := SignBytes(obj, sk) + if err != nil { + return nil, err + } signedObj := append(sig[:], obj...) req.raw = signedObj - return signedObj + return signedObj, nil } // MakeSignedStreamResponse encodes and signs a StreamResponse into a SignedObject format. -func MakeSignedStreamResponse(resp *StreamResponse, sk cipher.SecKey) SignedObject { - obj := encodeGob(resp) - sig := SignBytes(obj, sk) +func MakeSignedStreamResponse(resp *StreamResponse, sk cipher.SecKey) (SignedObject, error) { + obj, err := encodeGob(resp) + if err != nil { + return nil, fmt.Errorf("dmsg: encode stream response: %w", err) + } + sig, err := SignBytes(obj, sk) + if err != nil { + return nil, err + } signedObj := append(sig[:], obj...) resp.raw = signedObj - return signedObj + return signedObj, nil } // Valid returns true if the SignedObject has a valid length. @@ -237,10 +249,10 @@ func (resp StreamResponse) Verify(req StreamRequest) error { } // SignBytes signs the provided bytes with the given secret key. -func SignBytes(b []byte, sk cipher.SecKey) cipher.Sig { +func SignBytes(b []byte, sk cipher.SecKey) (cipher.Sig, error) { sig, err := cipher.SignPayload(b, sk) if err != nil { - panic(fmt.Errorf("dmsg: unexpected error occurred during StreamDialObject.Sign(): %v", err)) + return cipher.Sig{}, fmt.Errorf("dmsg: error during sign: %w", err) } - return sig + return sig, nil } diff --git a/pkg/dmsg/util.go b/pkg/dmsg/util.go index 125df9d20..a37466078 100644 --- a/pkg/dmsg/util.go +++ b/pkg/dmsg/util.go @@ -25,12 +25,12 @@ func isClosed(done chan struct{}) bool { /* Gob IO */ -func encodeGob(v interface{}) []byte { +func encodeGob(v interface{}) ([]byte, error) { var b bytes.Buffer if err := gob.NewEncoder(&b).Encode(v); err != nil { - panic(err) + return nil, err } - return b.Bytes() + return b.Bytes(), nil } func decodeGob(v interface{}, b []byte) error { diff --git a/pkg/dmsgclient/cli.go b/pkg/dmsgclient/cli.go index 4db530dd0..9d18471fe 100644 --- a/pkg/dmsgclient/cli.go +++ b/pkg/dmsgclient/cli.go @@ -8,17 +8,35 @@ import ( "io" "log" "net/http" + "os" + "path/filepath" + "strings" "time" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" + "github.com/spf13/cobra" "github.com/skycoin/dmsg/pkg/direct" "github.com/skycoin/dmsg/pkg/disc" "github.com/skycoin/dmsg/pkg/dmsg" "github.com/skycoin/dmsg/pkg/dmsghttp" + "github.com/skycoin/dmsg/pkg/ioutil" ) +// ExecName returns the name of the currently running executable, +// suitable for use as cobra.Command.Use. +func ExecName() string { + return strings.Split(filepath.Base(strings.ReplaceAll(strings.ReplaceAll(fmt.Sprintf("%v", os.Args), "[", ""), "]", "")), " ")[0] +} + +// Execute runs the given cobra command and exits on error. +func Execute(cmd *cobra.Command) { + if err := cmd.Execute(); err != nil { + log.Fatal("Failed to execute command: ", err) + } +} + /* Default mode of operation is dmsghttp: * Start dmsg-direct client ; connect directly to a dmsg server @@ -53,7 +71,7 @@ func InitDmsgWithFlags(ctx context.Context, dlog *logging.Logger, pk cipher.PubK if err != nil { dlog.WithError(err).Fatal("Error connecting to dmsg-discovery with http client") } - defer resp.Body.Close() //nolint + defer ioutil.CloseQuietly(resp.Body, dlog) body, err := io.ReadAll(resp.Body) if err != nil { @@ -105,7 +123,7 @@ func InitDmsgWithFlags(ctx context.Context, dlog *logging.Logger, pk cipher.PubK } dlog.WithError(err).Fatal("All DMSG transports failed to reach discovery /health") } - defer resp.Body.Close() //nolint + defer ioutil.CloseQuietly(resp.Body, dlog) body, err := io.ReadAll(resp.Body) if err != nil { diff --git a/pkg/dmsgcurl/dmsgcurl.go b/pkg/dmsgcurl/dmsgcurl.go index 2d4618100..8fff70b03 100644 --- a/pkg/dmsgcurl/dmsgcurl.go +++ b/pkg/dmsgcurl/dmsgcurl.go @@ -60,7 +60,7 @@ func (dg *DmsgCurl) String() string { } j, err := jsonite.Marshal(m) if err != nil { - panic(err) + return fmt.Sprintf("", err) } return string(j) } diff --git a/pkg/dmsgpty/term.html.gz b/pkg/dmsgpty/term.html.gz new file mode 100644 index 0000000000000000000000000000000000000000..dec350ce526e23fb9cf373149dd6d195aa880b82 GIT binary patch literal 91470 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�B8J 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�_S^rVifmPf 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 Date: Sun, 22 Mar 2026 19:56:28 -0500 Subject: [PATCH 04/15] Split large files and add composable sub-interfaces File splits (same package, no API changes): - pkg/dmsg/client.go (723 lines) -> client.go + client_sessions.go + client_dial.go - cmd/dmsg-discovery/commands/dmsg-discovery.go (533 lines) -> dmsg-discovery.go + examples.go - pkg/dmsgclient/cli.go (553 lines) -> cli.go + cli_fallback.go Interface segregation (backwards-compatible, existing interfaces unchanged): - pkg/disc: Add EntryReader and EntryWriter sub-interfaces; APIClient now embeds them - pkg/discovery/store: Add EntryStore, ServerLister, EntryEnumerator sub-interfaces; Storer now embeds them All existing implementations continue to satisfy the original interfaces. The new sub-interfaces allow callers to accept narrower types. --- cmd/dmsg-discovery/commands/dmsg-discovery.go | 184 ---------- cmd/dmsg-discovery/commands/examples.go | 195 ++++++++++ pkg/disc/client.go | 18 +- pkg/discovery/store/storer.go | 25 +- pkg/dmsg/client.go | 333 ----------------- pkg/dmsg/client_dial.go | 212 +++++++++++ pkg/dmsg/client_sessions.go | 141 ++++++++ pkg/dmsgclient/cli.go | 333 +---------------- pkg/dmsgclient/cli_fallback.go | 340 ++++++++++++++++++ 9 files changed, 927 insertions(+), 854 deletions(-) create mode 100644 cmd/dmsg-discovery/commands/examples.go create mode 100644 pkg/dmsg/client_dial.go create mode 100644 pkg/dmsg/client_sessions.go create mode 100644 pkg/dmsgclient/cli_fallback.go diff --git a/cmd/dmsg-discovery/commands/dmsg-discovery.go b/cmd/dmsg-discovery/commands/dmsg-discovery.go index 2f153050c..4e275efbf 100644 --- a/cmd/dmsg-discovery/commands/dmsg-discovery.go +++ b/cmd/dmsg-discovery/commands/dmsg-discovery.go @@ -3,7 +3,6 @@ package commands import ( "context" - "encoding/json" "errors" "fmt" "log" @@ -22,7 +21,6 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/metricsutil" "github.com/spf13/cobra" - "github.com/tidwall/pretty" "github.com/skycoin/dmsg/pkg/disc/metrics" "github.com/skycoin/dmsg/pkg/discovery/api" @@ -54,188 +52,6 @@ var ( pprofAddr string ) -// exampleJSON marshals v to indented JSON with color, returning empty string on error -func exampleJSON(v interface{}) string { - b, err := json.MarshalIndent(v, " ", " ") - if err != nil { - return "" - } - return string(pretty.Color(b, nil)) -} - -// generateExamples creates example responses from actual struct types -func generateExamples() string { - // Use actual build info with fallbacks - bi := buildinfo.Get() - version := bi.Version - if version == "" || version == "unknown" { - version = "v1.3.29" - } - commit := bi.Commit - if commit == "" || commit == "unknown" { - commit = "abc1234" - } - date := bi.Date - if date == "" || date == "unknown" { - date = "2024-01-15T10:30:00Z" - } - - // Use actual DMSG servers from embedded deployment config - var serverEntries []disc.Entry - var serverPKs []string - if len(dmsg.Prod.DmsgServers) > 0 { - // Use up to 2 real servers for examples - limit := 2 - if len(dmsg.Prod.DmsgServers) < limit { - limit = len(dmsg.Prod.DmsgServers) - } - for i := 0; i < limit; i++ { - serverEntries = append(serverEntries, dmsg.Prod.DmsgServers[i]) - serverPKs = append(serverPKs, dmsg.Prod.DmsgServers[i].Static.Hex()) - } - } - - // Fallback example PKs if no servers available - exClientPK := "02a49bc0aa1b5b78f638e9189be4c5d699e6d1358472d8a47f4c20daacd672d7e5" - exClientPK2 := "024ec47420176680816e0406250e7156465e4531f5b26057c9f6297bb0303558c7" - - // GET /health - use first real server PK if available - dmsgAddrPK := exClientPK - if len(serverPKs) > 0 { - dmsgAddrPK = serverPKs[0] - } - healthExample := map[string]interface{}{ - "build_info": map[string]interface{}{ - "version": version, - "commit": commit, - "date": date, - }, - "started_at": "2024-01-15T10:00:00Z", - "dmsg_address": dmsgAddrPK + ":80", - "dmsg_servers": serverPKs, - } - - // disc.Entry (client) - use real server PKs for delegated_servers - delegatedServers := serverPKs - if len(delegatedServers) == 0 { - delegatedServers = []string{"03b160fa44bac22cae9f7eb1311f1648aaab962e1e55d8d9a22a9586ded871eb5e"} - } - clientEntryExample := map[string]interface{}{ - "version": "1.0", - "sequence": 1, - "timestamp": 1705315200, - "static": exClientPK, - "client": map[string]interface{}{ - "delegated_servers": delegatedServers, - }, - } - - // POST response - disc.HTTPMessage - entrySetExample := map[string]interface{}{ - "code": 200, - "message": "wrote a new entry", - } - entryUpdatedExample := map[string]interface{}{ - "code": 200, - "message": "wrote new entry iteration", - } - entryDeletedExample := map[string]interface{}{ - "code": 200, - "message": "deleted entry", - } - - // GET /dmsg-discovery/servers/clients - map[server_pk][]client_pk - clientsByServerExample := make(map[string][]string) - if len(serverPKs) > 0 { - clientsByServerExample[serverPKs[0]] = []string{exClientPK, exClientPK2} - } else { - clientsByServerExample["03b160fa44bac22cae9f7eb1311f1648aaab962e1e55d8d9a22a9586ded871eb5e"] = []string{exClientPK, exClientPK2} - } - - // GET /dmsg-discovery/server/{pk}/clients - []client_pk - clientsForServerExample := []string{exClientPK, exClientPK2} - - // Use real server entries if available, otherwise use fallback - var serverEntryForExample interface{} - var serverEntriesForList []interface{} - if len(serverEntries) > 0 { - serverEntryForExample = serverEntries[0] - for _, entry := range serverEntries { - serverEntriesForList = append(serverEntriesForList, entry) - } - } else { - // Fallback server entry - serverEntryForExample = map[string]interface{}{ - "version": "1.0", - "sequence": 1, - "timestamp": 1705315200, - "static": "03b160fa44bac22cae9f7eb1311f1648aaab962e1e55d8d9a22a9586ded871eb5e", - "server": map[string]interface{}{ - "address": "192.168.1.100:8081", - "available_streams": 100, - "max_streams": 200, - "server_type": "official", - }, - } - serverEntriesForList = []interface{}{serverEntryForExample} - } - - // Arrays for list endpoints - entriesExample := append([]interface{}{clientEntryExample}, serverEntriesForList...) - visorEntriesExample := []interface{}{clientEntryExample} - - return fmt.Sprintf(` -Response Examples: - -GET /health -%s - -GET /dmsg-discovery/entry/{pk} (client entry) -%s - -GET /dmsg-discovery/entry/{pk} (server entry) -%s - -POST /dmsg-discovery/entry/ (new entry) -%s - -POST /dmsg-discovery/entry/ (update entry) -%s - -DEL /dmsg-discovery/entry -%s - -GET /dmsg-discovery/entries (all client and server entries) -%s - -GET /dmsg-discovery/visorEntries (client entries only) -%s - -GET /dmsg-discovery/available_servers (servers with available_streams > 0) -%s - -GET /dmsg-discovery/all_servers (all server entries) -%s - -GET /dmsg-discovery/servers/clients -%s - -GET /dmsg-discovery/server/{pk}/clients -%s`, - exampleJSON(healthExample), - exampleJSON(clientEntryExample), - exampleJSON(serverEntryForExample), - exampleJSON(entrySetExample), - exampleJSON(entryUpdatedExample), - exampleJSON(entryDeletedExample), - exampleJSON(entriesExample), - exampleJSON(visorEntriesExample), - exampleJSON(serverEntriesForList), - exampleJSON(serverEntriesForList), - exampleJSON(clientsByServerExample), - exampleJSON(clientsForServerExample)) -} - func init() { sf.Init(RootCmd, "dmsg_disc", "") diff --git a/cmd/dmsg-discovery/commands/examples.go b/cmd/dmsg-discovery/commands/examples.go new file mode 100644 index 000000000..9e21a208b --- /dev/null +++ b/cmd/dmsg-discovery/commands/examples.go @@ -0,0 +1,195 @@ +// Package commands cmd/dmsg-discovery/commands/examples.go +package commands + +import ( + "encoding/json" + "fmt" + + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/buildinfo" + "github.com/tidwall/pretty" + + "github.com/skycoin/dmsg/pkg/disc" + dmsg "github.com/skycoin/dmsg/pkg/dmsg" +) + +// exampleJSON marshals v to indented JSON with color, returning empty string on error +func exampleJSON(v interface{}) string { + b, err := json.MarshalIndent(v, " ", " ") + if err != nil { + return "" + } + return string(pretty.Color(b, nil)) +} + +// generateExamples creates example responses from actual struct types +func generateExamples() string { + // Use actual build info with fallbacks + bi := buildinfo.Get() + version := bi.Version + if version == "" || version == "unknown" { + version = "v1.3.29" + } + commit := bi.Commit + if commit == "" || commit == "unknown" { + commit = "abc1234" + } + date := bi.Date + if date == "" || date == "unknown" { + date = "2024-01-15T10:30:00Z" + } + + // Use actual DMSG servers from embedded deployment config + var serverEntries []disc.Entry + var serverPKs []string + if len(dmsg.Prod.DmsgServers) > 0 { + // Use up to 2 real servers for examples + limit := 2 + if len(dmsg.Prod.DmsgServers) < limit { + limit = len(dmsg.Prod.DmsgServers) + } + for i := 0; i < limit; i++ { + serverEntries = append(serverEntries, dmsg.Prod.DmsgServers[i]) + serverPKs = append(serverPKs, dmsg.Prod.DmsgServers[i].Static.Hex()) + } + } + + // Fallback example PKs if no servers available + exClientPK := "02a49bc0aa1b5b78f638e9189be4c5d699e6d1358472d8a47f4c20daacd672d7e5" + exClientPK2 := "024ec47420176680816e0406250e7156465e4531f5b26057c9f6297bb0303558c7" + + // GET /health - use first real server PK if available + dmsgAddrPK := exClientPK + if len(serverPKs) > 0 { + dmsgAddrPK = serverPKs[0] + } + healthExample := map[string]interface{}{ + "build_info": map[string]interface{}{ + "version": version, + "commit": commit, + "date": date, + }, + "started_at": "2024-01-15T10:00:00Z", + "dmsg_address": dmsgAddrPK + ":80", + "dmsg_servers": serverPKs, + } + + // disc.Entry (client) - use real server PKs for delegated_servers + delegatedServers := serverPKs + if len(delegatedServers) == 0 { + delegatedServers = []string{"03b160fa44bac22cae9f7eb1311f1648aaab962e1e55d8d9a22a9586ded871eb5e"} + } + clientEntryExample := map[string]interface{}{ + "version": "1.0", + "sequence": 1, + "timestamp": 1705315200, + "static": exClientPK, + "client": map[string]interface{}{ + "delegated_servers": delegatedServers, + }, + } + + // POST response - disc.HTTPMessage + entrySetExample := map[string]interface{}{ + "code": 200, + "message": "wrote a new entry", + } + entryUpdatedExample := map[string]interface{}{ + "code": 200, + "message": "wrote new entry iteration", + } + entryDeletedExample := map[string]interface{}{ + "code": 200, + "message": "deleted entry", + } + + // GET /dmsg-discovery/servers/clients - map[server_pk][]client_pk + clientsByServerExample := make(map[string][]string) + if len(serverPKs) > 0 { + clientsByServerExample[serverPKs[0]] = []string{exClientPK, exClientPK2} + } else { + clientsByServerExample["03b160fa44bac22cae9f7eb1311f1648aaab962e1e55d8d9a22a9586ded871eb5e"] = []string{exClientPK, exClientPK2} + } + + // GET /dmsg-discovery/server/{pk}/clients - []client_pk + clientsForServerExample := []string{exClientPK, exClientPK2} + + // Use real server entries if available, otherwise use fallback + var serverEntryForExample interface{} + var serverEntriesForList []interface{} + if len(serverEntries) > 0 { + serverEntryForExample = serverEntries[0] + for _, entry := range serverEntries { + serverEntriesForList = append(serverEntriesForList, entry) + } + } else { + // Fallback server entry + serverEntryForExample = map[string]interface{}{ + "version": "1.0", + "sequence": 1, + "timestamp": 1705315200, + "static": "03b160fa44bac22cae9f7eb1311f1648aaab962e1e55d8d9a22a9586ded871eb5e", + "server": map[string]interface{}{ + "address": "192.168.1.100:8081", + "available_streams": 100, + "max_streams": 200, + "server_type": "official", + }, + } + serverEntriesForList = []interface{}{serverEntryForExample} + } + + // Arrays for list endpoints + entriesExample := append([]interface{}{clientEntryExample}, serverEntriesForList...) + visorEntriesExample := []interface{}{clientEntryExample} + + return fmt.Sprintf(` +Response Examples: + +GET /health +%s + +GET /dmsg-discovery/entry/{pk} (client entry) +%s + +GET /dmsg-discovery/entry/{pk} (server entry) +%s + +POST /dmsg-discovery/entry/ (new entry) +%s + +POST /dmsg-discovery/entry/ (update entry) +%s + +DEL /dmsg-discovery/entry +%s + +GET /dmsg-discovery/entries (all client and server entries) +%s + +GET /dmsg-discovery/visorEntries (client entries only) +%s + +GET /dmsg-discovery/available_servers (servers with available_streams > 0) +%s + +GET /dmsg-discovery/all_servers (all server entries) +%s + +GET /dmsg-discovery/servers/clients +%s + +GET /dmsg-discovery/server/{pk}/clients +%s`, + exampleJSON(healthExample), + exampleJSON(clientEntryExample), + exampleJSON(serverEntryForExample), + exampleJSON(entrySetExample), + exampleJSON(entryUpdatedExample), + exampleJSON(entryDeletedExample), + exampleJSON(entriesExample), + exampleJSON(visorEntriesExample), + exampleJSON(serverEntriesForList), + exampleJSON(serverEntriesForList), + exampleJSON(clientsByServerExample), + exampleJSON(clientsForServerExample)) +} diff --git a/pkg/disc/client.go b/pkg/disc/client.go index a8160d221..b7d18461a 100644 --- a/pkg/disc/client.go +++ b/pkg/disc/client.go @@ -17,14 +17,24 @@ import ( var json = jsoniter.ConfigFastest -// APIClient implements dmsg discovery API client. -type APIClient interface { +// EntryReader provides read-only access to discovery entries. +type EntryReader interface { Entry(context.Context, cipher.PubKey) (*Entry, error) + AvailableServers(context.Context) ([]*Entry, error) + AllServers(context.Context) ([]*Entry, error) +} + +// EntryWriter provides write access to discovery entries. +type EntryWriter interface { PostEntry(context.Context, *Entry) error PutEntry(context.Context, cipher.SecKey, *Entry) error DelEntry(context.Context, *Entry) error - AvailableServers(context.Context) ([]*Entry, error) - AllServers(context.Context) ([]*Entry, error) +} + +// APIClient implements dmsg discovery API client. +type APIClient interface { + EntryReader + EntryWriter AllEntries(ctx context.Context) ([]string, error) AllClientsByServer(ctx context.Context) (map[string][]*Entry, error) ClientsByServer(ctx context.Context, serverPK cipher.PubKey) ([]*Entry, error) diff --git a/pkg/discovery/store/storer.go b/pkg/discovery/store/storer.go index 5d0f0e91a..775c9a8be 100644 --- a/pkg/discovery/store/storer.go +++ b/pkg/discovery/store/storer.go @@ -19,9 +19,8 @@ var ( ErrTooFewArgs = errors.New("too few args") ) -// Storer is an interface which allows to implement different kinds of stores -// and choose which one to use in the server -type Storer interface { +// EntryStore provides basic CRUD for discovery entries. +type EntryStore interface { // Entry obtains a single dmsg instance entry. Entry(ctx context.Context, staticPubKey cipher.PubKey) (*disc.Entry, error) @@ -31,19 +30,22 @@ type Storer interface { // DelEntry delete's an entry. DelEntry(ctx context.Context, staticPubKey cipher.PubKey) error +} +// ServerLister provides server enumeration. +type ServerLister interface { // AvailableServers discovers available dmsg servers. AvailableServers(ctx context.Context, maxCount int) ([]*disc.Entry, error) // AllServers discovers available dmsg servers. AllServers(ctx context.Context) ([]*disc.Entry, error) +} +// EntryEnumerator provides bulk entry access. +type EntryEnumerator interface { // CountEntries returns numbers of servers and clients. CountEntries(ctx context.Context) (int64, int64, error) - // RemoveOldServerEntries check and remove old server entries that left on redis because of unexpected server shutdown - RemoveOldServerEntries(ctx context.Context) error - // AllEntries returns all clients PKs. AllEntries(ctx context.Context) ([]string, error) @@ -54,6 +56,17 @@ type Storer interface { AllClientEntries(ctx context.Context) ([]*disc.Entry, error) } +// Storer is an interface which allows to implement different kinds of stores +// and choose which one to use in the server +type Storer interface { + EntryStore + ServerLister + EntryEnumerator + + // RemoveOldServerEntries check and remove old server entries that left on redis because of unexpected server shutdown + RemoveOldServerEntries(ctx context.Context) error +} + // Config configures the Store object. type Config struct { URL string // database URI diff --git a/pkg/dmsg/client.go b/pkg/dmsg/client.go index 18396ba0d..b42d1584a 100644 --- a/pkg/dmsg/client.go +++ b/pkg/dmsg/client.go @@ -5,19 +5,13 @@ import ( "context" crand "crypto/rand" "encoding/binary" - "errors" - "fmt" "math/rand" - "net" "sync" "time" - "github.com/hashicorp/yamux" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/netutil" - "github.com/xtaci/smux" - "golang.org/x/net/proxy" "github.com/skycoin/dmsg/pkg/disc" ) @@ -354,142 +348,6 @@ func (ce *Client) Close() error { return err } -// Listen listens on a given dmsg port. -func (ce *Client) Listen(port uint16) (*Listener, error) { - lis := newListener(ce.porter, Addr{PK: ce.pk, Port: port}) - ok, doneFn := ce.porter.Reserve(port, lis) - if !ok { - lis.close() - return nil, ErrPortOccupied - } - lis.addCloseCallback(doneFn) - return lis, nil -} - -// Dial wraps DialStream to output net.Conn instead of *Stream. -func (ce *Client) Dial(ctx context.Context, addr Addr) (net.Conn, error) { - return ce.DialStream(ctx, addr) -} - -// DialStream dials to a remote client entity with the given address. -func (ce *Client) DialStream(ctx context.Context, addr Addr) (*Stream, error) { - entry, err := getClientEntry(ctx, ce.dc, addr.PK) - if err != nil { - return nil, err - } - - // Range client's delegated servers. - // Try existing sessions first, falling back to next server on failure. - for _, srvPK := range entry.Client.DelegatedServers { - if dSes, ok := ce.clientSession(ce.porter, srvPK); ok { - stream, err := dSes.DialStream(addr) - if err != nil { - ce.log.WithError(err).WithField("server", srvPK). - Debug("DialStream failed via existing session, trying next server") - continue - } - return stream, nil - } - } - - // Range client's delegated servers. - // Attempt to connect to a delegated server. - for _, srvPK := range entry.Client.DelegatedServers { - dSes, err := ce.EnsureAndObtainSession(ctx, srvPK) - if err != nil { - continue - } - stream, err := dSes.DialStream(addr) - if err != nil { - ce.log.WithError(err).WithField("server", srvPK). - Debug("DialStream failed via new session, trying next server") - continue - } - return stream, nil - } - - return nil, ErrCannotConnectToDelegated -} - -// LookupIP dails to dmsg servers for public IP of the client. -func (ce *Client) LookupIP(ctx context.Context, servers []cipher.PubKey) (myIP net.IP, err error) { - - cancellabelCtx, cancel := context.WithCancel(ctx) - defer cancel() - - if servers == nil { - entries, err := ce.discoverServers(cancellabelCtx, true) - if err != nil { - return nil, err - } - for _, entry := range entries { - servers = append(servers, entry.Static) - } - } - - // Range client's delegated servers. - // See if we are already connected to a delegated server. - for _, srvPK := range servers { - if dSes, ok := ce.clientSession(ce.porter, srvPK); ok { - ip, err := dSes.LookupIP(Addr{PK: dSes.RemotePK(), Port: 1}) - if err != nil { - ce.log.WithError(err).WithField("server_pk", srvPK).Warn("Failed to dial server for IP.") - continue - } - - // If the client is test client then ignore Public IP check - if ce.conf.ClientType == "test" { - return ip, nil - } - - // Check if the IP is public, if not try other servers - if !netutil.IsPublicIP(ip) { - ce.log.WithField("server_pk", srvPK).WithField("ip", ip.String()).Warn("Received non-public IP address from dmsg server, trying other servers.") - continue - } - return ip, nil - } - } - - // Range client's delegated servers. - // Attempt to connect to a delegated server. - // And Close it after getting the IP. - for _, srvPK := range servers { - dSes, err := ce.EnsureAndObtainSession(ctx, srvPK) - if err != nil { - continue - } - ip, err := dSes.LookupIP(Addr{PK: dSes.RemotePK(), Port: 1}) - if err != nil { - ce.log.WithError(err).WithField("server_pk", srvPK).Warn("Failed to dial server for IP.") - continue - } - err = dSes.Close() - if err != nil { - ce.log.WithError(err).WithField("server_pk", srvPK).Warn("Failed to close session") - } - - // If the client is test client then ignore Public IP check - if ce.conf.ClientType == "test" { - return ip, nil - } - - // Check if the IP is public, if not try other servers - if !netutil.IsPublicIP(ip) { - ce.log.WithField("server_pk", srvPK).WithField("ip", ip.String()).Warn("Received non-public IP address from dmsg server, trying other servers.") - continue - } - return ip, nil - } - - return nil, ErrCannotConnectToDelegated -} - -// Session obtains an established session. -func (ce *Client) Session(pk cipher.PubKey) (ClientSession, bool) { - return ce.clientSession(ce.porter, pk) -} - // AllSessions obtains all established sessions. func (ce *Client) AllSessions() []ClientSession { return ce.allClientSessions(ce.porter) @@ -507,197 +365,6 @@ func (ce *Client) ConnectedServers() []string { return addrs } -// EnsureAndObtainSession attempts to obtain a session. -// If the session does not exist, we will attempt to establish one. -// It returns an error if the session does not exist AND cannot be established. -func (ce *Client) EnsureAndObtainSession(ctx context.Context, srvPK cipher.PubKey) (ClientSession, error) { - ce.sesMx.Lock() - defer ce.sesMx.Unlock() - - if dSes, ok := ce.clientSession(ce.porter, srvPK); ok { - return dSes, nil - } - - srvEntry, err := getServerEntry(ctx, ce.dc, srvPK) - if err != nil { - return ClientSession{}, err - } - return ce.dialSession(ctx, srvEntry) -} - -// EnsureSession ensures the existence of a session. -// It returns an error if the session does not exist AND cannot be established. -func (ce *Client) EnsureSession(ctx context.Context, entry *disc.Entry) error { - ce.sesMx.Lock() - defer ce.sesMx.Unlock() - - // If session with server of pk already exists, skip. - if _, ok := ce.clientSession(ce.porter, entry.Static); ok { - ce.log.WithField("remote_pk", entry.Static).Debug("Session already exists...") - return nil - } - entry.Protocol = ce.conf.Protocol - // Dial session. - _, err := ce.dialSession(ctx, entry) - return err -} - -// It is expected that the session is created and served before the context cancels, otherwise an error will be returned. -// NOTE: This should not be called directly as it may lead to session duplicates. -// Only `ensureSession` or `EnsureAndObtainSession` should call this function. -func (ce *Client) dialSession(ctx context.Context, entry *disc.Entry) (cs ClientSession, err error) { - ce.log.WithField("remote_pk", entry.Static).Debug("Dialing session...") - - const network = "tcp" - var conn net.Conn - - // Trigger dial callback. - if err := ce.conf.Callbacks.OnSessionDial(network, entry.Server.Address); err != nil { - return ClientSession{}, fmt.Errorf("session dial is rejected by callback: %w", err) - } - defer func() { - if err != nil { - // Trigger disconnect callback when dial fails. - ce.conf.Callbacks.OnSessionDisconnect(network, entry.Server.Address, err) - } - }() - - proxyAddr, ok := ctx.Value("socks5_proxy").(string) - if ok && proxyAddr != "" { - socksDialer, err := proxy.SOCKS5("tcp", proxyAddr, nil, proxy.Direct) - if err != nil { - return ClientSession{}, fmt.Errorf("failed to create SOCKS5 dialer: %w", err) - } - conn, err = socksDialer.Dial(network, entry.Server.Address) - if err != nil { - return ClientSession{}, fmt.Errorf("failed to dial through SOCKS5 proxy: %w", err) - } - } else { - conn, err = net.Dial(network, entry.Server.Address) - if err != nil { - return ClientSession{}, fmt.Errorf("failed to dial: %w", err) - } - } - - dSes, err := makeClientSession(&ce.EntityCommon, ce.porter, conn, entry.Static) - if err != nil { - return ClientSession{}, err - } - if entry.Protocol == "smux" { - dSes.sm.smux, err = smux.Client(conn, smux.DefaultConfig()) - if err != nil { - return ClientSession{}, err - } - ce.log.Infof("smux stream session initial for %s", dSes.RemotePK().String()) - } else { - dSes.sm.yamux, err = yamux.Client(conn, yamux.DefaultConfig()) - if err != nil { - return ClientSession{}, err - } - ce.log.Infof("yamux stream session initial for %s", dSes.RemotePK().String()) - } - - if !ce.setSession(ctx, dSes.SessionCommon) { - _ = dSes.Close() //nolint:errcheck - return ClientSession{}, errors.New("session already exists") - } - - go func() { - defer func() { - if r := recover(); r != nil { - ce.log.Warnf("recovered panic in session serve goroutine: %v", r) - } - }() - ce.log.WithField("remote_pk", dSes.RemotePK()).Debug("Serving session.") - err := dSes.serve() - if !isClosed(ce.done) { - ce.sesMx.Lock() - select { - case ce.errCh <- fmt.Errorf("failed to serve dialed session to %s: %v", dSes.RemotePK(), err): - default: - } - ce.sesMx.Unlock() - ce.delSession(ctx, dSes.RemotePK()) - } - - // Trigger disconnect callback. - ce.conf.Callbacks.OnSessionDisconnect(network, entry.Server.Address, err) - }() - - return dSes, nil -} - -// AllStreams returns all the streams of the current client. -func (ce *Client) AllStreams() (out []*Stream) { - fn := func(port uint16, pv netutil.PorterValue) (next bool) { //nolint - if str, ok := pv.Value.(*Stream); ok { - out = append(out, str) - return true - } - - for _, v := range pv.Children { - if str, ok := v.(*Stream); ok { - out = append(out, str) - } - } - return true - } - - ce.porter.RangePortValuesAndChildren(fn) - return out -} - -// AllEntries returns all the entries registered in discovery -func (ce *Client) AllEntries(ctx context.Context) (entries []string, err error) { - err = netutil.NewDefaultRetrier(ce.log).Do(ctx, func() error { - entries, err = ce.dc.AllEntries(ctx) - return err - }) - return entries, err -} - -// AllVisorEntries returns all the entries registered in discovery that are visor -func (ce *Client) AllVisorEntries(ctx context.Context) (entries []string, err error) { - err = netutil.NewDefaultRetrier(ce.log).Do(ctx, func() error { - entries, err = ce.dc.AllEntries(ctx) - return err - }) - return entries, err -} - -// ConnectedServersPK return keys of all connected dmsg servers -func (ce *Client) ConnectedServersPK() []string { - sessions := ce.allClientSessions(ce.porter) - addrs := make([]string, len(sessions)) - for i, s := range sessions { - addrs[i] = s.RemotePK().String() - } - return addrs -} - -// ConnectionsSummary associates connected clients, and the servers that connect such clients. -// Key: Client PK, Value: Slice of Server PKs -type ConnectionsSummary map[cipher.PubKey][]cipher.PubKey - -// ConnectionsSummary returns a summary of all connected clients, and the associated servers that connect them. -func (ce *Client) ConnectionsSummary() ConnectionsSummary { - streams := ce.AllStreams() - out := make(ConnectionsSummary, len(streams)) - - for _, s := range streams { - cPK := s.RawRemoteAddr().PK - sPK := s.ServerPK() - - sPKs, ok := out[cPK] - if ok && hasPK(sPKs, sPK) { - continue - } - out[cPK] = append(sPKs, sPK) - } - - return out -} - func (ce *Client) serveWait() { bo := ce.bo diff --git a/pkg/dmsg/client_dial.go b/pkg/dmsg/client_dial.go new file mode 100644 index 000000000..947b5c1e2 --- /dev/null +++ b/pkg/dmsg/client_dial.go @@ -0,0 +1,212 @@ +// Package dmsg pkg/dmsg/client_dial.go +package dmsg + +import ( + "context" + "net" + + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/netutil" +) + +// Listen listens on a given dmsg port. +func (ce *Client) Listen(port uint16) (*Listener, error) { + lis := newListener(ce.porter, Addr{PK: ce.pk, Port: port}) + ok, doneFn := ce.porter.Reserve(port, lis) + if !ok { + lis.close() + return nil, ErrPortOccupied + } + lis.addCloseCallback(doneFn) + return lis, nil +} + +// Dial wraps DialStream to output net.Conn instead of *Stream. +func (ce *Client) Dial(ctx context.Context, addr Addr) (net.Conn, error) { + return ce.DialStream(ctx, addr) +} + +// DialStream dials to a remote client entity with the given address. +func (ce *Client) DialStream(ctx context.Context, addr Addr) (*Stream, error) { + entry, err := getClientEntry(ctx, ce.dc, addr.PK) + if err != nil { + return nil, err + } + + // Range client's delegated servers. + // Try existing sessions first, falling back to next server on failure. + for _, srvPK := range entry.Client.DelegatedServers { + if dSes, ok := ce.clientSession(ce.porter, srvPK); ok { + stream, err := dSes.DialStream(addr) + if err != nil { + ce.log.WithError(err).WithField("server", srvPK). + Debug("DialStream failed via existing session, trying next server") + continue + } + return stream, nil + } + } + + // Range client's delegated servers. + // Attempt to connect to a delegated server. + for _, srvPK := range entry.Client.DelegatedServers { + dSes, err := ce.EnsureAndObtainSession(ctx, srvPK) + if err != nil { + continue + } + stream, err := dSes.DialStream(addr) + if err != nil { + ce.log.WithError(err).WithField("server", srvPK). + Debug("DialStream failed via new session, trying next server") + continue + } + return stream, nil + } + + return nil, ErrCannotConnectToDelegated +} + +// LookupIP dails to dmsg servers for public IP of the client. +func (ce *Client) LookupIP(ctx context.Context, servers []cipher.PubKey) (myIP net.IP, err error) { + + cancellabelCtx, cancel := context.WithCancel(ctx) + defer cancel() + + if servers == nil { + entries, err := ce.discoverServers(cancellabelCtx, true) + if err != nil { + return nil, err + } + for _, entry := range entries { + servers = append(servers, entry.Static) + } + } + + // Range client's delegated servers. + // See if we are already connected to a delegated server. + for _, srvPK := range servers { + if dSes, ok := ce.clientSession(ce.porter, srvPK); ok { + ip, err := dSes.LookupIP(Addr{PK: dSes.RemotePK(), Port: 1}) + if err != nil { + ce.log.WithError(err).WithField("server_pk", srvPK).Warn("Failed to dial server for IP.") + continue + } + + // If the client is test client then ignore Public IP check + if ce.conf.ClientType == "test" { + return ip, nil + } + + // Check if the IP is public, if not try other servers + if !netutil.IsPublicIP(ip) { + ce.log.WithField("server_pk", srvPK).WithField("ip", ip.String()).Warn("Received non-public IP address from dmsg server, trying other servers.") + continue + } + return ip, nil + } + } + + // Range client's delegated servers. + // Attempt to connect to a delegated server. + // And Close it after getting the IP. + for _, srvPK := range servers { + dSes, err := ce.EnsureAndObtainSession(ctx, srvPK) + if err != nil { + continue + } + ip, err := dSes.LookupIP(Addr{PK: dSes.RemotePK(), Port: 1}) + if err != nil { + ce.log.WithError(err).WithField("server_pk", srvPK).Warn("Failed to dial server for IP.") + continue + } + err = dSes.Close() + if err != nil { + ce.log.WithError(err).WithField("server_pk", srvPK).Warn("Failed to close session") + } + + // If the client is test client then ignore Public IP check + if ce.conf.ClientType == "test" { + return ip, nil + } + + // Check if the IP is public, if not try other servers + if !netutil.IsPublicIP(ip) { + ce.log.WithField("server_pk", srvPK).WithField("ip", ip.String()).Warn("Received non-public IP address from dmsg server, trying other servers.") + continue + } + return ip, nil + } + + return nil, ErrCannotConnectToDelegated +} + +// AllStreams returns all the streams of the current client. +func (ce *Client) AllStreams() (out []*Stream) { + fn := func(port uint16, pv netutil.PorterValue) (next bool) { //nolint + if str, ok := pv.Value.(*Stream); ok { + out = append(out, str) + return true + } + + for _, v := range pv.Children { + if str, ok := v.(*Stream); ok { + out = append(out, str) + } + } + return true + } + + ce.porter.RangePortValuesAndChildren(fn) + return out +} + +// AllEntries returns all the entries registered in discovery +func (ce *Client) AllEntries(ctx context.Context) (entries []string, err error) { + err = netutil.NewDefaultRetrier(ce.log).Do(ctx, func() error { + entries, err = ce.dc.AllEntries(ctx) + return err + }) + return entries, err +} + +// AllVisorEntries returns all the entries registered in discovery that are visor +func (ce *Client) AllVisorEntries(ctx context.Context) (entries []string, err error) { + err = netutil.NewDefaultRetrier(ce.log).Do(ctx, func() error { + entries, err = ce.dc.AllEntries(ctx) + return err + }) + return entries, err +} + +// ConnectedServersPK return keys of all connected dmsg servers +func (ce *Client) ConnectedServersPK() []string { + sessions := ce.allClientSessions(ce.porter) + addrs := make([]string, len(sessions)) + for i, s := range sessions { + addrs[i] = s.RemotePK().String() + } + return addrs +} + +// ConnectionsSummary associates connected clients, and the servers that connect such clients. +// Key: Client PK, Value: Slice of Server PKs +type ConnectionsSummary map[cipher.PubKey][]cipher.PubKey + +// ConnectionsSummary returns a summary of all connected clients, and the associated servers that connect them. +func (ce *Client) ConnectionsSummary() ConnectionsSummary { + streams := ce.AllStreams() + out := make(ConnectionsSummary, len(streams)) + + for _, s := range streams { + cPK := s.RawRemoteAddr().PK + sPK := s.ServerPK() + + sPKs, ok := out[cPK] + if ok && hasPK(sPKs, sPK) { + continue + } + out[cPK] = append(sPKs, sPK) + } + + return out +} diff --git a/pkg/dmsg/client_sessions.go b/pkg/dmsg/client_sessions.go new file mode 100644 index 000000000..7d21846bf --- /dev/null +++ b/pkg/dmsg/client_sessions.go @@ -0,0 +1,141 @@ +// Package dmsg pkg/dmsg/client_sessions.go +package dmsg + +import ( + "context" + "errors" + "fmt" + "net" + + "github.com/hashicorp/yamux" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" + "github.com/xtaci/smux" + "golang.org/x/net/proxy" + + "github.com/skycoin/dmsg/pkg/disc" +) + +// EnsureAndObtainSession attempts to obtain a session. +// If the session does not exist, we will attempt to establish one. +// It returns an error if the session does not exist AND cannot be established. +func (ce *Client) EnsureAndObtainSession(ctx context.Context, srvPK cipher.PubKey) (ClientSession, error) { + ce.sesMx.Lock() + defer ce.sesMx.Unlock() + + if dSes, ok := ce.clientSession(ce.porter, srvPK); ok { + return dSes, nil + } + + srvEntry, err := getServerEntry(ctx, ce.dc, srvPK) + if err != nil { + return ClientSession{}, err + } + return ce.dialSession(ctx, srvEntry) +} + +// EnsureSession ensures the existence of a session. +// It returns an error if the session does not exist AND cannot be established. +func (ce *Client) EnsureSession(ctx context.Context, entry *disc.Entry) error { + ce.sesMx.Lock() + defer ce.sesMx.Unlock() + + // If session with server of pk already exists, skip. + if _, ok := ce.clientSession(ce.porter, entry.Static); ok { + ce.log.WithField("remote_pk", entry.Static).Debug("Session already exists...") + return nil + } + entry.Protocol = ce.conf.Protocol + // Dial session. + _, err := ce.dialSession(ctx, entry) + return err +} + +// It is expected that the session is created and served before the context cancels, otherwise an error will be returned. +// NOTE: This should not be called directly as it may lead to session duplicates. +// Only `ensureSession` or `EnsureAndObtainSession` should call this function. +func (ce *Client) dialSession(ctx context.Context, entry *disc.Entry) (cs ClientSession, err error) { + ce.log.WithField("remote_pk", entry.Static).Debug("Dialing session...") + + const network = "tcp" + var conn net.Conn + + // Trigger dial callback. + if err := ce.conf.Callbacks.OnSessionDial(network, entry.Server.Address); err != nil { + return ClientSession{}, fmt.Errorf("session dial is rejected by callback: %w", err) + } + defer func() { + if err != nil { + // Trigger disconnect callback when dial fails. + ce.conf.Callbacks.OnSessionDisconnect(network, entry.Server.Address, err) + } + }() + + proxyAddr, ok := ctx.Value("socks5_proxy").(string) + if ok && proxyAddr != "" { + socksDialer, err := proxy.SOCKS5("tcp", proxyAddr, nil, proxy.Direct) + if err != nil { + return ClientSession{}, fmt.Errorf("failed to create SOCKS5 dialer: %w", err) + } + conn, err = socksDialer.Dial(network, entry.Server.Address) + if err != nil { + return ClientSession{}, fmt.Errorf("failed to dial through SOCKS5 proxy: %w", err) + } + } else { + conn, err = net.Dial(network, entry.Server.Address) + if err != nil { + return ClientSession{}, fmt.Errorf("failed to dial: %w", err) + } + } + + dSes, err := makeClientSession(&ce.EntityCommon, ce.porter, conn, entry.Static) + if err != nil { + return ClientSession{}, err + } + if entry.Protocol == "smux" { + dSes.sm.smux, err = smux.Client(conn, smux.DefaultConfig()) + if err != nil { + return ClientSession{}, err + } + ce.log.Infof("smux stream session initial for %s", dSes.RemotePK().String()) + } else { + dSes.sm.yamux, err = yamux.Client(conn, yamux.DefaultConfig()) + if err != nil { + return ClientSession{}, err + } + ce.log.Infof("yamux stream session initial for %s", dSes.RemotePK().String()) + } + + if !ce.setSession(ctx, dSes.SessionCommon) { + _ = dSes.Close() //nolint:errcheck + return ClientSession{}, errors.New("session already exists") + } + + go func() { + defer func() { + if r := recover(); r != nil { + ce.log.Warnf("recovered panic in session serve goroutine: %v", r) + } + }() + ce.log.WithField("remote_pk", dSes.RemotePK()).Debug("Serving session.") + err := dSes.serve() + if !isClosed(ce.done) { + ce.sesMx.Lock() + select { + case ce.errCh <- fmt.Errorf("failed to serve dialed session to %s: %v", dSes.RemotePK(), err): + default: + } + ce.sesMx.Unlock() + ce.delSession(ctx, dSes.RemotePK()) + } + + // Trigger disconnect callback. + ce.conf.Callbacks.OnSessionDisconnect(network, entry.Server.Address, err) + }() + + return dSes, nil +} + +// Session obtains an established session. +func (ce *Client) Session(pk cipher.PubKey) (ClientSession, bool) { + return ce.clientSession(ce.porter, pk) +} diff --git a/pkg/dmsgclient/cli.go b/pkg/dmsgclient/cli.go index 9d18471fe..f9ebfb9bb 100644 --- a/pkg/dmsgclient/cli.go +++ b/pkg/dmsgclient/cli.go @@ -2,7 +2,6 @@ package dmsgclient import ( - "bytes" "context" "fmt" "io" @@ -43,11 +42,12 @@ Default mode of operation is dmsghttp: * HTTP client is configured with a dmsg HTTP transport provided by the dmsg-direct client * HTTP client is used to make HTTP GET request to '/health' of dmsg discovery dmsg address * If the dmsg-discovery is unreachable via the configured http client: - - Shuffle dmsg servers - - Re-make dmsg direct clent - - Reconfigure HTTP client with dmsg HTTP transport provided by the dmsg-direct client - - Fetch '/health' from dmsg discovery dmsg address [:] - - Repeat the previous 4 steps on error / until no error + - Shuffle dmsg servers + - Re-make dmsg direct clent + - Reconfigure HTTP client with dmsg HTTP transport provided by the dmsg-direct client + - Fetch '/health' from dmsg discovery dmsg address [:] + - Repeat the previous 4 steps on error / until no error + * Start dmsghttp client * Connect to dmsg client address (if specified) @@ -135,61 +135,6 @@ func InitDmsgWithFlags(ctx context.Context, dlog *logging.Logger, pk cipher.PubK return StartDmsgWithSyntheticDiscovery(ctx, dlog, pk, sk, dmsgHTTP, DmsgDiscAddr, DmsgSessions) } -// StartDmsgWithSyntheticDiscovery starts dmsg with a synthetic discovery entry for the discovery server itself -func StartDmsgWithSyntheticDiscovery(ctx context.Context, dlog *logging.Logger, pk cipher.PubKey, sk cipher.SecKey, httpClient *http.Client, dmsgDisc string, dmsgSessions int) (dmsgC *dmsg.Client, stop func(), err error) { - if dlog == nil { - return nil, nil, fmt.Errorf("nil logger") - } - - // Create base discovery client - baseDiscClient := disc.NewHTTP(dmsgDisc, httpClient, dlog) - - // Wrap with caching client that includes synthetic entry for discovery server - discPK := dmsg.ExtractPKFromDmsgAddr(dmsgDisc) - if discPK != "" { - var discoveryPK cipher.PubKey - if err := discoveryPK.UnmarshalText([]byte(discPK)); err == nil { - // Get all available dmsg servers as delegated servers - var delegatedServers []cipher.PubKey - for _, server := range dmsg.Prod.DmsgServers { - delegatedServers = append(delegatedServers, server.Static) - } - syntheticEntry := &disc.Entry{ - Version: "0.0.1", - Static: discoveryPK, - Client: &disc.Client{ - DelegatedServers: delegatedServers, - }, - } - baseDiscClient = newCachingDiscClient(baseDiscClient, syntheticEntry, dlog) - dlog.Debug("Created synthetic discovery entry for dialing") - } - } - - dmsgC = dmsg.NewClient(pk, sk, baseDiscClient, &dmsg.Config{MinSessions: dmsgSessions}) - dlog.Debug("Created dmsg client.") - - go dmsgC.Serve(ctx) - dlog.Debug("dmsgclient.Serve(ctx)") - - stop = func() { - err := dmsgC.Close() - dlog.WithError(err).Debug("Disconnected from dmsg network.\n") - log.Println() - } - dlog.WithField("dmsg_disc", dmsgDisc).Debug("Connecting to dmsg network...\n") - dlog.WithField("client public_key", pk.String()).Debug("\n") - select { - case <-ctx.Done(): - stop() - return nil, nil, ctx.Err() - - case <-dmsgC.Ready(): - dlog.Debug("Dmsg network ready.") - return dmsgC, stop, nil - } -} - // StartDmsg starts dmsg returns a dmsg client for the given dmsg discovery func StartDmsg(ctx context.Context, dlog *logging.Logger, pk cipher.PubKey, sk cipher.SecKey, httpClient *http.Client, dmsgDisc string, dmsgSessions int) (dmsgC *dmsg.Client, stop func(), err error) { if dlog == nil { @@ -303,269 +248,3 @@ func StartDmsgDirectWithServers(ctx context.Context, dlog *logging.Logger, pk ci return dmsgC, stop, nil } - -// StartDmsgWithDirectClient starts dmsg with a fallback discovery client -// This allows dialing any client including the discovery server which doesn't register itself -// It uses direct client for known entries (servers, discovery, local client) and falls back -// to HTTP discovery for unknown entries (arbitrary target clients) -func StartDmsgWithDirectClient(ctx context.Context, dlog *logging.Logger, pk cipher.PubKey, sk cipher.SecKey, dmsgSessions int) (dmsgC *dmsg.Client, stop func(), err error) { - if dlog == nil { - return nil, nil, fmt.Errorf("nil logger") - } - - // Build entries for all dmsg servers - var entries []*disc.Entry - for _, server := range dmsg.Prod.DmsgServers { - entries = append(entries, &server) - } - - // Add synthetic entry for discovery server - discPK := dmsg.ExtractPKFromDmsgAddr(DmsgDiscAddr) - if discPK != "" { - var discoveryPK cipher.PubKey - if err := discoveryPK.UnmarshalText([]byte(discPK)); err == nil { - var delegatedServers []cipher.PubKey - for _, server := range dmsg.Prod.DmsgServers { - delegatedServers = append(delegatedServers, server.Static) - } - discoveryEntry := &disc.Entry{ - Version: "0.0.1", - Static: discoveryPK, - Client: &disc.Client{ - DelegatedServers: delegatedServers, - }, - } - entries = append(entries, discoveryEntry) - dlog.Debug("Added synthetic discovery entry to direct client") - } - } - - // Add synthetic entry for our own client - var delegatedServers []cipher.PubKey - for _, server := range dmsg.Prod.DmsgServers { - delegatedServers = append(delegatedServers, server.Static) - } - clientEntry := &disc.Entry{ - Version: "0.0.1", - Static: pk, - Client: &disc.Client{ - DelegatedServers: delegatedServers, - }, - } - entries = append(entries, clientEntry) - - // Create direct client with known entries - directClient := direct.NewClient(entries, dlog) - - // Create HTTP discovery client as fallback for unknown entries - httpDiscClient := disc.NewHTTP(DmsgDiscURL, &http.Client{}, dlog) - - // Wrap with fallback client that tries direct first, then HTTP discovery - fallbackClient := newFallbackDiscClient(directClient, httpDiscClient, dlog) - - dmsgC = dmsg.NewClient(pk, sk, fallbackClient, &dmsg.Config{MinSessions: dmsgSessions}) - dlog.Debug("Created dmsg client with fallback discovery client (direct + HTTP).") - - go dmsgC.Serve(ctx) - dlog.Debug("dmsgclient.Serve(ctx)") - - stop = func() { - err := dmsgC.Close() - dlog.WithError(err).Debug("Disconnected from dmsg network.\n") - log.Println() - } - dlog.Debug("Connecting to dmsg network...\n") - dlog.WithField("client public_key", pk.String()).Debug("\n") - select { - case <-ctx.Done(): - stop() - return nil, nil, ctx.Err() - - case <-dmsgC.Ready(): - dlog.Debug("Dmsg network ready.") - return dmsgC, stop, nil - } -} - -// cachingDiscClient wraps a discovery client and caches a synthetic entry -type cachingDiscClient struct { - base disc.APIClient - syntheticEntry *disc.Entry - log *logging.Logger -} - -// newCachingDiscClient creates a discovery client that caches a synthetic entry -func newCachingDiscClient(base disc.APIClient, syntheticEntry *disc.Entry, log *logging.Logger) disc.APIClient { - return &cachingDiscClient{ - base: base, - syntheticEntry: syntheticEntry, - log: log, - } -} - -// Entry returns the synthetic entry if PK matches, otherwise queries base client -func (c *cachingDiscClient) Entry(ctx context.Context, pk cipher.PubKey) (*disc.Entry, error) { - if c.syntheticEntry != nil && c.syntheticEntry.Static == pk { - c.log.WithField("pk", pk.String()).Debug("Returning synthetic discovery entry") - return c.syntheticEntry, nil - } - return c.base.Entry(ctx, pk) -} - -// PostEntry delegates to base client -func (c *cachingDiscClient) PostEntry(ctx context.Context, entry *disc.Entry) error { - return c.base.PostEntry(ctx, entry) -} - -// PutEntry delegates to base client -func (c *cachingDiscClient) PutEntry(ctx context.Context, sk cipher.SecKey, entry *disc.Entry) error { - return c.base.PutEntry(ctx, sk, entry) -} - -// DelEntry delegates to base client -func (c *cachingDiscClient) DelEntry(ctx context.Context, entry *disc.Entry) error { - return c.base.DelEntry(ctx, entry) -} - -// AvailableServers delegates to base client -func (c *cachingDiscClient) AvailableServers(ctx context.Context) ([]*disc.Entry, error) { - return c.base.AvailableServers(ctx) -} - -// AllServers delegates to base client -func (c *cachingDiscClient) AllServers(ctx context.Context) ([]*disc.Entry, error) { - return c.base.AllServers(ctx) -} - -// AllEntries delegates to base client -func (c *cachingDiscClient) AllEntries(ctx context.Context) ([]string, error) { - return c.base.AllEntries(ctx) -} - -// AllClientsByServer delegates to base client -func (c *cachingDiscClient) AllClientsByServer(ctx context.Context) (map[string][]*disc.Entry, error) { - return c.base.AllClientsByServer(ctx) -} - -// ClientsByServer delegates to base client -func (c *cachingDiscClient) ClientsByServer(ctx context.Context, serverPK cipher.PubKey) ([]*disc.Entry, error) { - return c.base.ClientsByServer(ctx, serverPK) -} - -// fallbackDiscClient tries direct client first, falls back to HTTP discovery for unknown entries -type fallbackDiscClient struct { - direct disc.APIClient - http disc.APIClient - log *logging.Logger -} - -// newFallbackDiscClient creates a discovery client that tries direct first, then HTTP -func newFallbackDiscClient(direct, http disc.APIClient, log *logging.Logger) disc.APIClient { - return &fallbackDiscClient{ - direct: direct, - http: http, - log: log, - } -} - -// Entry tries direct client first, falls back to HTTP for unknown entries -func (f *fallbackDiscClient) Entry(ctx context.Context, pk cipher.PubKey) (*disc.Entry, error) { - // Try direct client first - entry, err := f.direct.Entry(ctx, pk) - if err == nil && entry.Static == pk { - return entry, nil - } - - // Fall back to HTTP discovery for unknown entries - f.log.WithField("pk", pk.String()).Debug("Entry not in direct client, querying HTTP discovery") - return f.http.Entry(ctx, pk) -} - -// PostEntry delegates to direct client -func (f *fallbackDiscClient) PostEntry(ctx context.Context, entry *disc.Entry) error { - return f.direct.PostEntry(ctx, entry) -} - -// PutEntry delegates to HTTP client (direct client doesn't support updates) -func (f *fallbackDiscClient) PutEntry(ctx context.Context, sk cipher.SecKey, entry *disc.Entry) error { - return f.http.PutEntry(ctx, sk, entry) -} - -// DelEntry delegates to direct client -func (f *fallbackDiscClient) DelEntry(ctx context.Context, entry *disc.Entry) error { - return f.direct.DelEntry(ctx, entry) -} - -// AvailableServers delegates to direct client -func (f *fallbackDiscClient) AvailableServers(ctx context.Context) ([]*disc.Entry, error) { - return f.direct.AvailableServers(ctx) -} - -// AllServers delegates to direct client -func (f *fallbackDiscClient) AllServers(ctx context.Context) ([]*disc.Entry, error) { - return f.direct.AllServers(ctx) -} - -// AllEntries delegates to direct client -func (f *fallbackDiscClient) AllEntries(ctx context.Context) ([]string, error) { - return f.direct.AllEntries(ctx) -} - -// AllClientsByServer delegates to HTTP client -func (f *fallbackDiscClient) AllClientsByServer(ctx context.Context) (map[string][]*disc.Entry, error) { - return f.http.AllClientsByServer(ctx) -} - -// ClientsByServer delegates to HTTP client -func (f *fallbackDiscClient) ClientsByServer(ctx context.Context, serverPK cipher.PubKey) ([]*disc.Entry, error) { - return f.http.ClientsByServer(ctx, serverPK) -} - -// FallbackRoundTripper tries multiple DMSG transports until one succeeds. -type FallbackRoundTripper struct { - ctx context.Context - clients []*dmsg.Client -} - -// NewFallbackRoundTripper initializes the fallback round tripper. -func NewFallbackRoundTripper(ctx context.Context, clients []*dmsg.Client) http.RoundTripper { - return &FallbackRoundTripper{ - ctx: ctx, - clients: clients, - } -} - -// RoundTrip tries each DMSG client in order until a successful response is received. -func (f *FallbackRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - // Buffer the request body so it can be replayed on retry. - // Without this, the first failed transport consumes the body - // and subsequent transports receive an empty body. - var bodyBytes []byte - if req.Body != nil { - var err error - bodyBytes, err = io.ReadAll(req.Body) - if err != nil { - return nil, fmt.Errorf("failed to read request body for retry: %w", err) - } - req.Body.Close() //nolint:errcheck,gosec - } - - var lastErr error - for _, client := range f.clients { - // Reset the body for each attempt - if bodyBytes != nil { - req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) - } else { - req.Body = nil - } - - rt := dmsghttp.MakeHTTPTransport(f.ctx, client) - resp, err := rt.RoundTrip(req) - if err != nil { - lastErr = err - continue - } - return resp, nil - } - return nil, fmt.Errorf("all DMSG transports failed: last error: %w", lastErr) -} diff --git a/pkg/dmsgclient/cli_fallback.go b/pkg/dmsgclient/cli_fallback.go new file mode 100644 index 000000000..9c5edcbd8 --- /dev/null +++ b/pkg/dmsgclient/cli_fallback.go @@ -0,0 +1,340 @@ +// Package dmsgclient pkg/dmsgclient/cli_fallback.go +package dmsgclient + +import ( + "bytes" + "context" + "fmt" + "io" + "log" + "net/http" + + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/cipher" + "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" + + "github.com/skycoin/dmsg/pkg/direct" + "github.com/skycoin/dmsg/pkg/disc" + "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsghttp" +) + +// StartDmsgWithSyntheticDiscovery starts dmsg with a synthetic discovery entry for the discovery server itself +func StartDmsgWithSyntheticDiscovery(ctx context.Context, dlog *logging.Logger, pk cipher.PubKey, sk cipher.SecKey, httpClient *http.Client, dmsgDisc string, dmsgSessions int) (dmsgC *dmsg.Client, stop func(), err error) { + if dlog == nil { + return nil, nil, fmt.Errorf("nil logger") + } + + // Create base discovery client + baseDiscClient := disc.NewHTTP(dmsgDisc, httpClient, dlog) + + // Wrap with caching client that includes synthetic entry for discovery server + discPK := dmsg.ExtractPKFromDmsgAddr(dmsgDisc) + if discPK != "" { + var discoveryPK cipher.PubKey + if err := discoveryPK.UnmarshalText([]byte(discPK)); err == nil { + // Get all available dmsg servers as delegated servers + var delegatedServers []cipher.PubKey + for _, server := range dmsg.Prod.DmsgServers { + delegatedServers = append(delegatedServers, server.Static) + } + syntheticEntry := &disc.Entry{ + Version: "0.0.1", + Static: discoveryPK, + Client: &disc.Client{ + DelegatedServers: delegatedServers, + }, + } + baseDiscClient = newCachingDiscClient(baseDiscClient, syntheticEntry, dlog) + dlog.Debug("Created synthetic discovery entry for dialing") + } + } + + dmsgC = dmsg.NewClient(pk, sk, baseDiscClient, &dmsg.Config{MinSessions: dmsgSessions}) + dlog.Debug("Created dmsg client.") + + go dmsgC.Serve(ctx) + dlog.Debug("dmsgclient.Serve(ctx)") + + stop = func() { + err := dmsgC.Close() + dlog.WithError(err).Debug("Disconnected from dmsg network.\n") + log.Println() + } + dlog.WithField("dmsg_disc", dmsgDisc).Debug("Connecting to dmsg network...\n") + dlog.WithField("client public_key", pk.String()).Debug("\n") + select { + case <-ctx.Done(): + stop() + return nil, nil, ctx.Err() + + case <-dmsgC.Ready(): + dlog.Debug("Dmsg network ready.") + return dmsgC, stop, nil + } +} + +// StartDmsgWithDirectClient starts dmsg with a fallback discovery client +// This allows dialing any client including the discovery server which doesn't register itself +// It uses direct client for known entries (servers, discovery, local client) and falls back +// to HTTP discovery for unknown entries (arbitrary target clients) +func StartDmsgWithDirectClient(ctx context.Context, dlog *logging.Logger, pk cipher.PubKey, sk cipher.SecKey, dmsgSessions int) (dmsgC *dmsg.Client, stop func(), err error) { + if dlog == nil { + return nil, nil, fmt.Errorf("nil logger") + } + + // Build entries for all dmsg servers + var entries []*disc.Entry + for _, server := range dmsg.Prod.DmsgServers { + entries = append(entries, &server) + } + + // Add synthetic entry for discovery server + discPK := dmsg.ExtractPKFromDmsgAddr(DmsgDiscAddr) + if discPK != "" { + var discoveryPK cipher.PubKey + if err := discoveryPK.UnmarshalText([]byte(discPK)); err == nil { + var delegatedServers []cipher.PubKey + for _, server := range dmsg.Prod.DmsgServers { + delegatedServers = append(delegatedServers, server.Static) + } + discoveryEntry := &disc.Entry{ + Version: "0.0.1", + Static: discoveryPK, + Client: &disc.Client{ + DelegatedServers: delegatedServers, + }, + } + entries = append(entries, discoveryEntry) + dlog.Debug("Added synthetic discovery entry to direct client") + } + } + + // Add synthetic entry for our own client + var delegatedServers []cipher.PubKey + for _, server := range dmsg.Prod.DmsgServers { + delegatedServers = append(delegatedServers, server.Static) + } + clientEntry := &disc.Entry{ + Version: "0.0.1", + Static: pk, + Client: &disc.Client{ + DelegatedServers: delegatedServers, + }, + } + entries = append(entries, clientEntry) + + // Create direct client with known entries + directClient := direct.NewClient(entries, dlog) + + // Create HTTP discovery client as fallback for unknown entries + httpDiscClient := disc.NewHTTP(DmsgDiscURL, &http.Client{}, dlog) + + // Wrap with fallback client that tries direct first, then HTTP discovery + fallbackClient := newFallbackDiscClient(directClient, httpDiscClient, dlog) + + dmsgC = dmsg.NewClient(pk, sk, fallbackClient, &dmsg.Config{MinSessions: dmsgSessions}) + dlog.Debug("Created dmsg client with fallback discovery client (direct + HTTP).") + + go dmsgC.Serve(ctx) + dlog.Debug("dmsgclient.Serve(ctx)") + + stop = func() { + err := dmsgC.Close() + dlog.WithError(err).Debug("Disconnected from dmsg network.\n") + log.Println() + } + dlog.Debug("Connecting to dmsg network...\n") + dlog.WithField("client public_key", pk.String()).Debug("\n") + select { + case <-ctx.Done(): + stop() + return nil, nil, ctx.Err() + + case <-dmsgC.Ready(): + dlog.Debug("Dmsg network ready.") + return dmsgC, stop, nil + } +} + +// cachingDiscClient wraps a discovery client and caches a synthetic entry +type cachingDiscClient struct { + base disc.APIClient + syntheticEntry *disc.Entry + log *logging.Logger +} + +// newCachingDiscClient creates a discovery client that caches a synthetic entry +func newCachingDiscClient(base disc.APIClient, syntheticEntry *disc.Entry, log *logging.Logger) disc.APIClient { + return &cachingDiscClient{ + base: base, + syntheticEntry: syntheticEntry, + log: log, + } +} + +// Entry returns the synthetic entry if PK matches, otherwise queries base client +func (c *cachingDiscClient) Entry(ctx context.Context, pk cipher.PubKey) (*disc.Entry, error) { + if c.syntheticEntry != nil && c.syntheticEntry.Static == pk { + c.log.WithField("pk", pk.String()).Debug("Returning synthetic discovery entry") + return c.syntheticEntry, nil + } + return c.base.Entry(ctx, pk) +} + +// PostEntry delegates to base client +func (c *cachingDiscClient) PostEntry(ctx context.Context, entry *disc.Entry) error { + return c.base.PostEntry(ctx, entry) +} + +// PutEntry delegates to base client +func (c *cachingDiscClient) PutEntry(ctx context.Context, sk cipher.SecKey, entry *disc.Entry) error { + return c.base.PutEntry(ctx, sk, entry) +} + +// DelEntry delegates to base client +func (c *cachingDiscClient) DelEntry(ctx context.Context, entry *disc.Entry) error { + return c.base.DelEntry(ctx, entry) +} + +// AvailableServers delegates to base client +func (c *cachingDiscClient) AvailableServers(ctx context.Context) ([]*disc.Entry, error) { + return c.base.AvailableServers(ctx) +} + +// AllServers delegates to base client +func (c *cachingDiscClient) AllServers(ctx context.Context) ([]*disc.Entry, error) { + return c.base.AllServers(ctx) +} + +// AllEntries delegates to base client +func (c *cachingDiscClient) AllEntries(ctx context.Context) ([]string, error) { + return c.base.AllEntries(ctx) +} + +// AllClientsByServer delegates to base client +func (c *cachingDiscClient) AllClientsByServer(ctx context.Context) (map[string][]*disc.Entry, error) { + return c.base.AllClientsByServer(ctx) +} + +// ClientsByServer delegates to base client +func (c *cachingDiscClient) ClientsByServer(ctx context.Context, serverPK cipher.PubKey) ([]*disc.Entry, error) { + return c.base.ClientsByServer(ctx, serverPK) +} + +// fallbackDiscClient tries direct client first, falls back to HTTP discovery for unknown entries +type fallbackDiscClient struct { + direct disc.APIClient + http disc.APIClient + log *logging.Logger +} + +// newFallbackDiscClient creates a discovery client that tries direct first, then HTTP +func newFallbackDiscClient(direct, http disc.APIClient, log *logging.Logger) disc.APIClient { + return &fallbackDiscClient{ + direct: direct, + http: http, + log: log, + } +} + +// Entry tries direct client first, falls back to HTTP for unknown entries +func (f *fallbackDiscClient) Entry(ctx context.Context, pk cipher.PubKey) (*disc.Entry, error) { + // Try direct client first + entry, err := f.direct.Entry(ctx, pk) + if err == nil && entry.Static == pk { + return entry, nil + } + + // Fall back to HTTP discovery for unknown entries + f.log.WithField("pk", pk.String()).Debug("Entry not in direct client, querying HTTP discovery") + return f.http.Entry(ctx, pk) +} + +// PostEntry delegates to direct client +func (f *fallbackDiscClient) PostEntry(ctx context.Context, entry *disc.Entry) error { + return f.direct.PostEntry(ctx, entry) +} + +// PutEntry delegates to HTTP client (direct client doesn't support updates) +func (f *fallbackDiscClient) PutEntry(ctx context.Context, sk cipher.SecKey, entry *disc.Entry) error { + return f.http.PutEntry(ctx, sk, entry) +} + +// DelEntry delegates to direct client +func (f *fallbackDiscClient) DelEntry(ctx context.Context, entry *disc.Entry) error { + return f.direct.DelEntry(ctx, entry) +} + +// AvailableServers delegates to direct client +func (f *fallbackDiscClient) AvailableServers(ctx context.Context) ([]*disc.Entry, error) { + return f.direct.AvailableServers(ctx) +} + +// AllServers delegates to direct client +func (f *fallbackDiscClient) AllServers(ctx context.Context) ([]*disc.Entry, error) { + return f.direct.AllServers(ctx) +} + +// AllEntries delegates to direct client +func (f *fallbackDiscClient) AllEntries(ctx context.Context) ([]string, error) { + return f.direct.AllEntries(ctx) +} + +// AllClientsByServer delegates to HTTP client +func (f *fallbackDiscClient) AllClientsByServer(ctx context.Context) (map[string][]*disc.Entry, error) { + return f.http.AllClientsByServer(ctx) +} + +// ClientsByServer delegates to HTTP client +func (f *fallbackDiscClient) ClientsByServer(ctx context.Context, serverPK cipher.PubKey) ([]*disc.Entry, error) { + return f.http.ClientsByServer(ctx, serverPK) +} + +// FallbackRoundTripper tries multiple DMSG transports until one succeeds. +type FallbackRoundTripper struct { + ctx context.Context + clients []*dmsg.Client +} + +// NewFallbackRoundTripper initializes the fallback round tripper. +func NewFallbackRoundTripper(ctx context.Context, clients []*dmsg.Client) http.RoundTripper { + return &FallbackRoundTripper{ + ctx: ctx, + clients: clients, + } +} + +// RoundTrip tries each DMSG client in order until a successful response is received. +func (f *FallbackRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + // Buffer the request body so it can be replayed on retry. + // Without this, the first failed transport consumes the body + // and subsequent transports receive an empty body. + var bodyBytes []byte + if req.Body != nil { + var err error + bodyBytes, err = io.ReadAll(req.Body) + if err != nil { + return nil, fmt.Errorf("failed to read request body for retry: %w", err) + } + req.Body.Close() //nolint:errcheck,gosec + } + + var lastErr error + for _, client := range f.clients { + // Reset the body for each attempt + if bodyBytes != nil { + req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) + } else { + req.Body = nil + } + + rt := dmsghttp.MakeHTTPTransport(f.ctx, client) + resp, err := rt.RoundTrip(req) + if err != nil { + lastErr = err + continue + } + return resp, nil + } + return nil, fmt.Errorf("all DMSG transports failed: last error: %w", lastErr) +} From f0782683a72d468ca6af2662d20ae99f4ddaeb18 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Sun, 22 Mar 2026 20:08:24 -0500 Subject: [PATCH 05/15] Fix CI lint errors: gofmt formatting and errcheck in test files - Run gofmt on cmd files with formatting issues - Add //nolint:errcheck to test cleanup Close() calls - Fix indentation in test files --- cmd/conf/commands/root.go | 2 +- cmd/dial/commands/dial.go | 4 +- cmd/dmsg-discovery/commands/dmsg-discovery.go | 8 ++-- pkg/disc/client_test.go | 32 ++++++------- pkg/dmsgctrl/serve_listener_test.go | 48 +++++++++---------- pkg/dmsgcurl/url_test.go | 2 +- pkg/dmsghttp/util_test.go | 16 +++---- 7 files changed, 56 insertions(+), 56 deletions(-) diff --git a/cmd/conf/commands/root.go b/cmd/conf/commands/root.go index 0fbc39480..bf6e5d1bd 100644 --- a/cmd/conf/commands/root.go +++ b/cmd/conf/commands/root.go @@ -7,8 +7,8 @@ import ( "github.com/bitfield/script" "github.com/spf13/cobra" - "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsgclient" ) // RootCmd is the root command diff --git a/cmd/dial/commands/dial.go b/cmd/dial/commands/dial.go index 31145afd3..1e538c6cc 100644 --- a/cmd/dial/commands/dial.go +++ b/cmd/dial/commands/dial.go @@ -18,8 +18,8 @@ import ( "github.com/spf13/cobra" "github.com/skycoin/dmsg/pkg/disc" - "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsgclient" ) var ( @@ -39,7 +39,7 @@ func init() { // RootCmd contains the root dmsgcurl command var RootCmd = &cobra.Command{ - Use: dmsgclient.ExecName(), + Use: dmsgclient.ExecName(), Short: "DMSG Dial network test utility", Long: calvin.AsciiFont("dmsgdial") + ` DMSG Dial network test utility diff --git a/cmd/dmsg-discovery/commands/dmsg-discovery.go b/cmd/dmsg-discovery/commands/dmsg-discovery.go index 4e275efbf..06409dab2 100644 --- a/cmd/dmsg-discovery/commands/dmsg-discovery.go +++ b/cmd/dmsg-discovery/commands/dmsg-discovery.go @@ -22,13 +22,13 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/metricsutil" "github.com/spf13/cobra" + "github.com/skycoin/dmsg/pkg/direct" + "github.com/skycoin/dmsg/pkg/disc" "github.com/skycoin/dmsg/pkg/disc/metrics" "github.com/skycoin/dmsg/pkg/discovery/api" "github.com/skycoin/dmsg/pkg/discovery/store" - "github.com/skycoin/dmsg/pkg/dmsgclient" - "github.com/skycoin/dmsg/pkg/direct" - "github.com/skycoin/dmsg/pkg/disc" dmsg "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/dmsghttp" ) @@ -72,7 +72,7 @@ func init() { // RootCmd contains commands for dmsg-discovery var RootCmd = &cobra.Command{ - Use: dmsgclient.ExecName(), + Use: dmsgclient.ExecName(), Short: "DMSG Discovery Server", Long: ` ┌┬┐┌┬┐┌─┐┌─┐ ┌┬┐┬┌─┐┌─┐┌─┐┬ ┬┌─┐┬─┐┬ ┬ diff --git a/pkg/disc/client_test.go b/pkg/disc/client_test.go index 257c5d174..dc0bd425d 100644 --- a/pkg/disc/client_test.go +++ b/pkg/disc/client_test.go @@ -637,7 +637,7 @@ func TestHTTPClientEntry_Success(t *testing.T) { assert.Equal(t, http.MethodGet, r.Method) assert.Contains(t, r.URL.Path, "/dmsg-discovery/entry/") w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(entry) + json.NewEncoder(w).Encode(entry) //nolint:errcheck }) client := newTestHTTPClient(t, handler) @@ -650,7 +650,7 @@ func TestHTTPClientEntry_Success(t *testing.T) { func TestHTTPClientEntry_NotFound(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(disc.HTTPMessage{ + json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck Code: http.StatusNotFound, Message: disc.ErrKeyNotFound.Error(), }) @@ -665,7 +665,7 @@ func TestHTTPClientEntry_NotFound(t *testing.T) { func TestHTTPClientEntry_UnknownErrorBecomesUnexpected(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(disc.HTTPMessage{ + json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck Code: http.StatusInternalServerError, Message: "some unknown error", }) @@ -700,7 +700,7 @@ func TestHTTPClientPostEntry_Error(t *testing.T) { Code: http.StatusUnprocessableEntity, Message: disc.ErrValidationWrongSequence.Error(), }) - _, _ = w.Write(body) + w.Write(body) //nolint:errcheck }) pk, sk := cipher.GenerateKeyPair() @@ -736,7 +736,7 @@ func TestHTTPClientDelEntry_Error(t *testing.T) { Code: http.StatusUnauthorized, Message: disc.ErrUnauthorized.Error(), }) - _, _ = w.Write(body) + w.Write(body) //nolint:errcheck }) pk, sk := cipher.GenerateKeyPair() @@ -757,7 +757,7 @@ func TestHTTPClientAvailableServers_Success(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/dmsg-discovery/available_servers", r.URL.Path) w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode([]*disc.Entry{entry}) + json.NewEncoder(w).Encode([]*disc.Entry{entry}) //nolint:errcheck }) client := newTestHTTPClient(t, handler) @@ -770,7 +770,7 @@ func TestHTTPClientAvailableServers_Success(t *testing.T) { func TestHTTPClientAvailableServers_Error(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(disc.HTTPMessage{ + json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck Code: http.StatusInternalServerError, Message: disc.ErrNoAvailableServers.Error(), }) @@ -790,7 +790,7 @@ func TestHTTPClientAllServers_Success(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/dmsg-discovery/all_servers", r.URL.Path) w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode([]*disc.Entry{entry}) + json.NewEncoder(w).Encode([]*disc.Entry{entry}) //nolint:errcheck }) client := newTestHTTPClient(t, handler) @@ -802,7 +802,7 @@ func TestHTTPClientAllServers_Success(t *testing.T) { func TestHTTPClientAllServers_Error(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(disc.HTTPMessage{ + json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck Code: http.StatusInternalServerError, Message: disc.ErrUnexpected.Error(), }) @@ -818,7 +818,7 @@ func TestHTTPClientAllEntries_Success(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/dmsg-discovery/entries", r.URL.Path) w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode([]string{"abc123", "def456"}) + json.NewEncoder(w).Encode([]string{"abc123", "def456"}) //nolint:errcheck }) client := newTestHTTPClient(t, handler) @@ -830,7 +830,7 @@ func TestHTTPClientAllEntries_Success(t *testing.T) { func TestHTTPClientAllEntries_Error(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(disc.HTTPMessage{ + json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck Code: http.StatusInternalServerError, Message: disc.ErrBadInput.Error(), }) @@ -854,7 +854,7 @@ func TestHTTPClientAllClientsByServer_Success(t *testing.T) { result := map[string][]*disc.Entry{ serverPK.Hex(): {entry}, } - _ = json.NewEncoder(w).Encode(result) + json.NewEncoder(w).Encode(result) //nolint:errcheck }) client := newTestHTTPClient(t, handler) @@ -866,7 +866,7 @@ func TestHTTPClientAllClientsByServer_Success(t *testing.T) { func TestHTTPClientAllClientsByServer_Error(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(disc.HTTPMessage{ + json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck Code: http.StatusInternalServerError, Message: disc.ErrUnexpected.Error(), }) @@ -888,7 +888,7 @@ func TestHTTPClientClientsByServer_Success(t *testing.T) { expectedPath := fmt.Sprintf("/dmsg-discovery/server/%s/clients", serverPK.Hex()) assert.Equal(t, expectedPath, r.URL.Path) w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode([]*disc.Entry{entry}) + json.NewEncoder(w).Encode([]*disc.Entry{entry}) //nolint:errcheck }) client := newTestHTTPClient(t, handler) @@ -903,7 +903,7 @@ func TestHTTPClientClientsByServer_Error(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(disc.HTTPMessage{ + json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck Code: http.StatusNotFound, Message: disc.ErrKeyNotFound.Error(), }) @@ -928,7 +928,7 @@ func TestHTTPClientPutEntry_Success(t *testing.T) { } // GET for Entry lookup (shouldn't be needed if PostEntry succeeds) w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(entry) + json.NewEncoder(w).Encode(entry) //nolint:errcheck }) client := newTestHTTPClient(t, handler) diff --git a/pkg/dmsgctrl/serve_listener_test.go b/pkg/dmsgctrl/serve_listener_test.go index 4e03ee48d..0992f5c2f 100644 --- a/pkg/dmsgctrl/serve_listener_test.go +++ b/pkg/dmsgctrl/serve_listener_test.go @@ -45,11 +45,11 @@ func TestServeListener_AcceptAndControl(t *testing.T) { // Cleanup. for _, ctrl := range ctrls { - _ = ctrl.Close() + ctrl.Close() //nolint:errcheck } - _ = connA.Close() - _ = connB.Close() - _ = l.Close() + connA.Close() //nolint:errcheck + connB.Close() //nolint:errcheck + l.Close() //nolint:errcheck } // TestServeListener_ClosesChannelOnListenerClose verifies that the channel @@ -128,11 +128,11 @@ func TestServeListener_FullChannelDropsControl(t *testing.T) { } // Cleanup. - _ = conn1.Close() - _ = conn2.Close() - _ = conn3.Close() - _ = conn4.Close() - _ = l.Close() + conn1.Close() //nolint:errcheck + conn2.Close() //nolint:errcheck + conn3.Close() //nolint:errcheck + conn4.Close() //nolint:errcheck + l.Close() //nolint:errcheck } // TestControl_PingPongExchange tests that a ping on one side results in a @@ -143,8 +143,8 @@ func TestControl_PingPongExchange(t *testing.T) { ctrlB := dmsgctrl.ControlStream(connB) t.Cleanup(func() { - _ = ctrlA.Close() - _ = ctrlB.Close() + ctrlA.Close() //nolint:errcheck + ctrlB.Close() //nolint:errcheck }) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) @@ -182,7 +182,7 @@ func TestControl_PingContextCancel(t *testing.T) { _, err := ctrlA.Ping(ctx) assert.ErrorIs(t, err, context.DeadlineExceeded) - _ = ctrlA.Close() + ctrlA.Close() //nolint:errcheck } // TestControl_Close verifies that Close sets the error and signals Done. @@ -218,12 +218,12 @@ func TestControl_Close(t *testing.T) { func TestControl_DoubleClose(t *testing.T) { connA, connB := net.Pipe() ctrlA := dmsgctrl.ControlStream(connA) - _ = dmsgctrl.ControlStream(connB) + dmsgctrl.ControlStream(connB) //nolint:errcheck err1 := ctrlA.Close() // Second close: the connection is already closed so conn.Close may // return an error, but it must not panic. - _ = ctrlA.Close() + ctrlA.Close() //nolint:errcheck _ = err1 // Wait for done. @@ -245,8 +245,8 @@ func TestControl_ErrBeforeDone(t *testing.T) { assert.Nil(t, ctrlA.Err()) assert.Nil(t, ctrlB.Err()) - _ = ctrlA.Close() - _ = ctrlB.Close() + ctrlA.Close() //nolint:errcheck + ctrlB.Close() //nolint:errcheck } // TestControl_DoneChannel tests that the Done channel blocks while the @@ -272,18 +272,18 @@ func TestControl_DoneChannel(t *testing.T) { t.Fatal("Done() did not close after Close()") } - _ = ctrlB.Close() + ctrlB.Close() //nolint:errcheck } // TestControl_Conn verifies that Conn returns the underlying net.Conn. func TestControl_Conn(t *testing.T) { connA, connB := net.Pipe() ctrlA := dmsgctrl.ControlStream(connA) - _ = dmsgctrl.ControlStream(connB) + dmsgctrl.ControlStream(connB) //nolint:errcheck assert.Equal(t, connA, ctrlA.Conn()) - _ = ctrlA.Close() + ctrlA.Close() //nolint:errcheck } // TestControl_ConcurrentPing tests that multiple goroutines can ping @@ -313,8 +313,8 @@ func TestControl_ConcurrentPing(t *testing.T) { ctrlB := dmsgctrl.ControlStream(connB) t.Cleanup(func() { - _ = ctrlA.Close() - _ = ctrlB.Close() + ctrlA.Close() //nolint:errcheck + ctrlB.Close() //nolint:errcheck }) const goroutines = 5 @@ -327,11 +327,11 @@ func TestControl_ConcurrentPing(t *testing.T) { for i := 0; i < goroutines; i++ { go func() { defer wg.Done() - _, _ = ctrlA.Ping(ctx) + _, _ = ctrlA.Ping(ctx) //nolint:errcheck }() go func() { defer wg.Done() - _, _ = ctrlB.Ping(ctx) + _, _ = ctrlB.Ping(ctx) //nolint:errcheck }() } @@ -343,7 +343,7 @@ func TestControl_ConcurrentPing(t *testing.T) { func TestControl_PingAfterClose(t *testing.T) { connA, connB := net.Pipe() ctrlA := dmsgctrl.ControlStream(connA) - _ = dmsgctrl.ControlStream(connB) + dmsgctrl.ControlStream(connB) //nolint:errcheck require.NoError(t, ctrlA.Close()) diff --git a/pkg/dmsgcurl/url_test.go b/pkg/dmsgcurl/url_test.go index de0c4ee61..ae45120b9 100644 --- a/pkg/dmsgcurl/url_test.go +++ b/pkg/dmsgcurl/url_test.go @@ -161,7 +161,7 @@ func TestNew_ReturnsNonNil(t *testing.T) { func TestNew_RegistersFlags(t *testing.T) { fs := flag.NewFlagSet("test", flag.ContinueOnError) fs.SetOutput(io.Discard) - _ = dmsgcurl.New(fs) + dmsgcurl.New(fs) //nolint:errcheck // Verify some expected flags were registered. for _, name := range []string{"help", "h", "dmsg-disc", "dmsg-sessions", "O", "t", "w", "U"} { diff --git a/pkg/dmsghttp/util_test.go b/pkg/dmsghttp/util_test.go index ab1ef6ebb..8a18f0354 100644 --- a/pkg/dmsghttp/util_test.go +++ b/pkg/dmsghttp/util_test.go @@ -118,7 +118,7 @@ func TestMakeHTTPTransport_FullRoundTrip(t *testing.T) { srv := dmsg.NewServer(srvPK, srvSK, dc, &srvConf, nil) lis, err := nettest.NewLocalListener("tcp") require.NoError(t, err) - go srv.Serve(lis, "") //nolint:errcheck + go srv.Serve(lis, "") //nolint:errcheck t.Cleanup(func() { srv.Close() }) //nolint:errcheck <-srv.Ready() @@ -135,11 +135,11 @@ func TestMakeHTTPTransport_FullRoundTrip(t *testing.T) { r := chi.NewRouter() r.Get("/hello", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("world")) + w.Write([]byte("world")) //nolint:errcheck }) r.Post("/echo", func(w http.ResponseWriter, r *http.Request) { data, _ := io.ReadAll(r.Body) - _, _ = w.Write(data) + w.Write(data) //nolint:errcheck }) go http.Serve(dmsgLis, r) //nolint:errcheck @@ -195,7 +195,7 @@ func TestMakeHTTPTransport_DefaultPort(t *testing.T) { srv := dmsg.NewServer(srvPK, srvSK, dc, &srvConf, nil) lis, err := nettest.NewLocalListener("tcp") require.NoError(t, err) - go srv.Serve(lis, "") //nolint:errcheck + go srv.Serve(lis, "") //nolint:errcheck t.Cleanup(func() { srv.Close() }) //nolint:errcheck <-srv.Ready() @@ -211,7 +211,7 @@ func TestMakeHTTPTransport_DefaultPort(t *testing.T) { r := chi.NewRouter() r.Get("/", func(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte("default-port")) + w.Write([]byte("default-port")) //nolint:errcheck }) go http.Serve(dmsgLis, r) //nolint:errcheck @@ -414,7 +414,7 @@ func TestListenAndServe_ServesHTTP(t *testing.T) { srv := dmsg.NewServer(srvPK, srvSK, dc, &srvConf, nil) lis, err := nettest.NewLocalListener("tcp") require.NoError(t, err) - go srv.Serve(lis, "") //nolint:errcheck + go srv.Serve(lis, "") //nolint:errcheck t.Cleanup(func() { srv.Close() }) //nolint:errcheck <-srv.Ready() @@ -430,7 +430,7 @@ func TestListenAndServe_ServesHTTP(t *testing.T) { defer cancel() handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - _, _ = w.Write([]byte("listen-and-serve")) + w.Write([]byte("listen-and-serve")) //nolint:errcheck }) errCh := make(chan error, 1) @@ -480,7 +480,7 @@ func TestListenAndServe_InvalidPort(t *testing.T) { srv := dmsg.NewServer(srvPK, srvSK, dc, &srvConf, nil) lis, err := nettest.NewLocalListener("tcp") require.NoError(t, err) - go srv.Serve(lis, "") //nolint:errcheck + go srv.Serve(lis, "") //nolint:errcheck t.Cleanup(func() { srv.Close() }) //nolint:errcheck <-srv.Ready() From c4f4a84fbe5058bd3d7f6a2df8105a1b38d877d5 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Sun, 22 Mar 2026 20:23:22 -0500 Subject: [PATCH 06/15] Fix bugs: resource leaks, race conditions, missing error handling HIGH: - cmd/dmsgweb: Fix nil deref crash when url.Parse fails in reverse proxy - cmd/dmsgweb: Add missing wg.Done() in SOCKS5 goroutine (deadlock on shutdown) - cmd/dmsgweb: Use defer for wg.Done() in proxyHTTPConn (deadlock if panic) - pkg/dmsg/client: Make errCh send non-blocking to prevent goroutine hang MEDIUM: - pkg/dmsg/server: Server.Close() now returns actual error instead of nil - pkg/dmsg/server: Close conn when smux/yamux Server init fails (TCP leak) - pkg/dmsg/client_sessions: Close conn on makeClientSession/mux failure (TCP leak) - pkg/dmsg/client: Retry initial post on failure instead of giving up - pkg/dmsg/entity_common: Copy session keys while holding mutex (was empty) - pkg/dmsg/entity_common: Wrap error context in getServerEntry/getClientEntry - pkg/dmsg/listener: Close drained streams on listener shutdown (resource leak) - pkg/dmsgserver: Acquire mutex in SetDmsgServer (data race) - pkg/dmsgcurl: Use caller's context for dmsgC.Serve (cancellation propagation) - cmd/dmsgweb: Fix duplicate DmsgDiscURL check (second should be DmsgDiscAddr) - cmd/dmsgweb: Close both conns after io.Copy to unblock goroutine LOW: - pkg/dmsg/client: Fix typo "successed" -> "succeeded", stop ticker leak - pkg/dmsgpty: Use %w instead of %v for error wrapping --- cmd/dmsgweb/commands/dmsgweb.go | 11 ++++++----- cmd/dmsgweb/commands/dmsgwebsrv.go | 10 +++++++++- pkg/dmsg/client.go | 11 ++++++++--- pkg/dmsg/client_sessions.go | 3 +++ pkg/dmsg/entity_common.go | 8 ++++++-- pkg/dmsg/listener.go | 3 ++- pkg/dmsg/server.go | 10 ++++++---- pkg/dmsgcurl/dmsgcurl.go | 2 +- pkg/dmsgpty/cli.go | 4 ++-- pkg/dmsgpty/host.go | 4 ++-- pkg/dmsgserver/api.go | 2 ++ 11 files changed, 47 insertions(+), 21 deletions(-) diff --git a/cmd/dmsgweb/commands/dmsgweb.go b/cmd/dmsgweb/commands/dmsgweb.go index 119baec27..8d80abac7 100644 --- a/cmd/dmsgweb/commands/dmsgweb.go +++ b/cmd/dmsgweb/commands/dmsgweb.go @@ -23,8 +23,8 @@ import ( "github.com/spf13/cobra" "golang.org/x/net/proxy" - "github.com/skycoin/dmsg/pkg/dmsgclient" dmsg "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/dmsghttp" "github.com/skycoin/dmsg/pkg/ioutil" ) @@ -79,7 +79,7 @@ func init() { // RootCmd contains the root command for dmsgweb var RootCmd = &cobra.Command{ - Use: dmsgclient.ExecName(), + Use: dmsgclient.ExecName(), Short: "DMSG resolving proxy & browser client", Long: ` ┌┬┐┌┬┐┌─┐┌─┐┬ ┬┌─┐┌┐ @@ -118,7 +118,7 @@ dmsgweb conf file detected: ` + dwcfg if dmsgclient.DmsgDiscURL == "" { dlog.Fatal("Dmsg Discovery Server URL not specified") } - if dmsgclient.DmsgDiscURL == "" { + if dmsgclient.DmsgDiscAddr == "" { dlog.Fatal("Dmsg Discovery Server dmsg address not specified") } @@ -268,12 +268,13 @@ dmsgweb conf file detected: ` + dwcfg wg.Add(1) go func() { + defer wg.Done() + defer server.Close() //nolint:errcheck dlog.Debug("Serving SOCKS5 proxy on " + socksAddr) err := server.ListenAndServe("tcp", socksAddr) if err != nil { dlog.WithError(err).Fatal("Failed to start SOCKS5 server") } - defer server.Close() //nolint dlog.Debug("Stopped serving SOCKS5 proxy on " + socksAddr) }() } @@ -429,6 +430,7 @@ func proxyHTTPConn(n int) { }) wg.Add(1) go func() { + defer wg.Done() var thiswebport uint if n == -1 { thiswebport = webPort[0] @@ -438,7 +440,6 @@ func proxyHTTPConn(n int) { dlog.Debug(fmt.Sprintf("Serving http on: http://127.0.0.1:%v", thiswebport)) r.Run(":" + fmt.Sprintf("%v", thiswebport)) //nolint dlog.Debug(fmt.Sprintf("Stopped serving http on: http://127.0.0.1:%v", thiswebport)) - wg.Done() }() } diff --git a/cmd/dmsgweb/commands/dmsgwebsrv.go b/cmd/dmsgweb/commands/dmsgwebsrv.go index dd19cff82..6bf775846 100644 --- a/cmd/dmsgweb/commands/dmsgwebsrv.go +++ b/cmd/dmsgweb/commands/dmsgwebsrv.go @@ -172,7 +172,12 @@ func proxyHTTPConnections(ctx context.Context, localPort uint, listener net.List authRoute.Any("/*path", func(c *gin.Context) { targetURL := fmt.Sprintf("http://127.0.0.1:%d%s?%s", localPort, c.Request.URL.Path, c.Request.URL.RawQuery) proxy := httputil.ReverseProxy{Director: func(req *http.Request) { - req.URL, _ = url.Parse(targetURL) //nolint + parsed, err := url.Parse(targetURL) + if err != nil { + dlog.Errorf("failed to parse target URL %q: %v", targetURL, err) + return + } + req.URL = parsed req.Host = req.URL.Host }} proxy.ServeHTTP(c.Writer, c.Request) @@ -277,6 +282,9 @@ func proxyTCPConnections(ctx context.Context, localPort uint, listener net.Liste if err2 != nil { dlog.WithError(err2).Warn("Error on io.Copy(localConn, dmsgConn)") } + // Close both to unblock the goroutine + dmsgConn.Close() //nolint + localConn.Close() //nolint connMutex.Lock() delete(activeConns, dmsgConn) diff --git a/pkg/dmsg/client.go b/pkg/dmsg/client.go index b42d1584a..dbadadffa 100644 --- a/pkg/dmsg/client.go +++ b/pkg/dmsg/client.go @@ -146,6 +146,7 @@ func (ce *Client) Serve(ctx context.Context) { defer cancel() setupNodeTicker := time.NewTicker(1 * time.Minute) + defer setupNodeTicker.Stop() go func(ctx context.Context) { select { @@ -180,6 +181,7 @@ func (ce *Client) Serve(ctx context.Context) { for ind, entry := range entries { if dmsgServer, ok := ctx.Value("dmsgServer").(string); ok && entry.Static.Hex() == dmsgServer { entries = entries[ind : ind+1] + break } } } else if ctx.Value("setupNode") != nil { @@ -226,9 +228,9 @@ func (ce *Client) Serve(ctx context.Context) { if err != nil { ce.log.WithError(err).Warn("Initial post entry failed") } else { - ce.log.WithError(err).Info("Initial post entry successed") + ce.log.Info("Initial post entry succeeded") + needInitialPost = false } - needInitialPost = false } for n, entry := range entries { @@ -270,7 +272,10 @@ func (ce *Client) Serve(ctx context.Context) { if n == (len(entries) - 1) { if !isClosed(ce.done) { ce.sesMx.Lock() - ce.errCh <- err + select { + case ce.errCh <- err: + default: + } ce.sesMx.Unlock() } } diff --git a/pkg/dmsg/client_sessions.go b/pkg/dmsg/client_sessions.go index 7d21846bf..35e06da0c 100644 --- a/pkg/dmsg/client_sessions.go +++ b/pkg/dmsg/client_sessions.go @@ -89,17 +89,20 @@ func (ce *Client) dialSession(ctx context.Context, entry *disc.Entry) (cs Client dSes, err := makeClientSession(&ce.EntityCommon, ce.porter, conn, entry.Static) if err != nil { + conn.Close() return ClientSession{}, err } if entry.Protocol == "smux" { dSes.sm.smux, err = smux.Client(conn, smux.DefaultConfig()) if err != nil { + conn.Close() return ClientSession{}, err } ce.log.Infof("smux stream session initial for %s", dSes.RemotePK().String()) } else { dSes.sm.yamux, err = yamux.Client(conn, yamux.DefaultConfig()) if err != nil { + conn.Close() return ClientSession{}, err } ce.log.Infof("yamux stream session initial for %s", dSes.RemotePK().String()) diff --git a/pkg/dmsg/entity_common.go b/pkg/dmsg/entity_common.go index 4c40c43a1..348a251ea 100644 --- a/pkg/dmsg/entity_common.go +++ b/pkg/dmsg/entity_common.go @@ -4,6 +4,7 @@ package dmsg import ( "context" "errors" + "fmt" "sync" "sync/atomic" "time" @@ -236,6 +237,9 @@ func (c *EntityCommon) initilizeClientEntry(ctx context.Context, clientType stri c.sessionsMx.Lock() srvPKs := make([]cipher.PubKey, 0, len(c.sessions)) + for pk := range c.sessions { + srvPKs = append(srvPKs, pk) + } c.sessionsMx.Unlock() _, err = c.dc.Entry(ctx, c.pk) @@ -352,7 +356,7 @@ func (c *EntityCommon) delEntry(ctx context.Context) (err error) { func getServerEntry(ctx context.Context, dc disc.APIClient, srvPK cipher.PubKey) (*disc.Entry, error) { entry, err := dc.Entry(ctx, srvPK) if err != nil { - return nil, ErrDiscEntryNotFound + return nil, fmt.Errorf("%w: %v", ErrDiscEntryNotFound, err) } if entry.Server == nil { return nil, ErrDiscEntryIsNotServer @@ -363,7 +367,7 @@ func getServerEntry(ctx context.Context, dc disc.APIClient, srvPK cipher.PubKey) func getClientEntry(ctx context.Context, dc disc.APIClient, clientPK cipher.PubKey) (*disc.Entry, error) { entry, err := dc.Entry(ctx, clientPK) if err != nil { - return nil, ErrDiscEntryNotFound + return nil, fmt.Errorf("%w: %v", ErrDiscEntryNotFound, err) } if entry.Client == nil { return nil, ErrDiscEntryIsNotClient diff --git a/pkg/dmsg/listener.go b/pkg/dmsg/listener.go index 24d01ce72..50b5c5cac 100644 --- a/pkg/dmsg/listener.go +++ b/pkg/dmsg/listener.go @@ -111,7 +111,8 @@ func (l *Listener) close() (closed bool) { close(l.done) for { select { - case <-l.accept: + case stream := <-l.accept: + stream.Close() //nolint:errcheck default: close(l.accept) return diff --git a/pkg/dmsg/server.go b/pkg/dmsg/server.go index c3a64f0e0..c85e8c976 100644 --- a/pkg/dmsg/server.go +++ b/pkg/dmsg/server.go @@ -100,16 +100,16 @@ func (s *Server) Close() error { if s == nil { return nil } - var err error + var closeErr error s.once.Do(func() { close(s.done) s.wg.Wait() - err = s.delEntry(context.Background()) - if err != nil { + closeErr = s.delEntry(context.Background()) + if closeErr != nil { s.log.Warn("Cannot delete entry from db.") } }) - return nil + return closeErr } // Serve serves the server. @@ -247,6 +247,7 @@ func (s *Server) handleSession(conn net.Conn) { dSes.sm.smux, err = smux.Server(conn, smux.DefaultConfig()) if err != nil { dSes.sm.mutx.Unlock() + conn.Close() cancel() return } @@ -256,6 +257,7 @@ func (s *Server) handleSession(conn net.Conn) { dSes.sm.yamux, err = yamux.Server(conn, yamux.DefaultConfig()) if err != nil { dSes.sm.mutx.Unlock() + conn.Close() cancel() return } diff --git a/pkg/dmsgcurl/dmsgcurl.go b/pkg/dmsgcurl/dmsgcurl.go index 8fff70b03..7faab3682 100644 --- a/pkg/dmsgcurl/dmsgcurl.go +++ b/pkg/dmsgcurl/dmsgcurl.go @@ -195,7 +195,7 @@ func parseOutputFile(name string, urlPath string) (*os.File, error) { // StartDmsg create dsmg client instance func (dg *DmsgCurl) StartDmsg(ctx context.Context, log *logging.Logger, pk cipher.PubKey, sk cipher.SecKey) (dmsgC *dmsg.Client, stop func(), err error) { dmsgC = dmsg.NewClient(pk, sk, disc.NewHTTP(dg.dmsgF.Disc, &http.Client{}, log), &dmsg.Config{MinSessions: dg.dmsgF.Sessions}) - go dmsgC.Serve(context.Background()) + go dmsgC.Serve(ctx) stop = func() { err := dmsgC.Close() diff --git a/pkg/dmsgpty/cli.go b/pkg/dmsgpty/cli.go index f1ad51b40..cfbb543dd 100644 --- a/pkg/dmsgpty/cli.go +++ b/pkg/dmsgpty/cli.go @@ -103,7 +103,7 @@ func (cli *CLI) prepareConn() (net.Conn, error) { conn, err := net.Dial(cli.Net, cli.Addr) if err != nil { - return nil, fmt.Errorf("failed to connect to dmsgpty-host: %v", err) + return nil, fmt.Errorf("failed to connect to dmsgpty-host: %w", err) } return conn, nil } @@ -121,7 +121,7 @@ func (cli *CLI) servePty(ctx context.Context, ptyC *PtyClient, cmd string, args env := cli.captureEnv() if err := ptyC.Start(cmd, env, args...); err != nil { - return fmt.Errorf("failed to start command on pty: %v", err) + return fmt.Errorf("failed to start command on pty: %w", err) } // Window resize loop. diff --git a/pkg/dmsgpty/host.go b/pkg/dmsgpty/host.go index b98813cc8..68040b659 100644 --- a/pkg/dmsgpty/host.go +++ b/pkg/dmsgpty/host.go @@ -237,11 +237,11 @@ func handleProxy(h *Host) handleFunc { // Get query values. var pk cipher.PubKey if err := pk.Set(q.Get("pk")); err != nil { - return fmt.Errorf("invalid query value 'pk': %v", err) + return fmt.Errorf("invalid query value 'pk': %w", err) } var port uint16 if _, err := fmt.Sscan(q.Get("port"), &port); err != nil { - return fmt.Errorf("invalid query value 'port': %v", err) + return fmt.Errorf("invalid query value 'port': %w", err) } // Proxy request. diff --git a/pkg/dmsgserver/api.go b/pkg/dmsgserver/api.go index f5997e472..3e2065c1d 100644 --- a/pkg/dmsgserver/api.go +++ b/pkg/dmsgserver/api.go @@ -77,7 +77,9 @@ func (a *ServerAPI) RunBackgroundTasks(ctx context.Context) { // SetDmsgServer saves srv in the ServerAPI func (a *ServerAPI) SetDmsgServer(srv *dmsg.Server) { + a.sMu.Lock() a.dmsgServer = srv + a.sMu.Unlock() } // ListenAndServe runs dmsg Serve function alongside health endpoint From 8ed491f8038b7b213439ac56fde177dc28f5a69b Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Sun, 22 Mar 2026 20:25:57 -0500 Subject: [PATCH 07/15] Fix CI lint: gofmt, errcheck, gosec, misspellings in test files - Run gofmt on dmsg-server commands and dmsgserver api - Add //nolint:errcheck,gosec to test helper functions - Fix "cancelled" -> "canceled" misspelling (3 test files) - Add //nolint:gosec for G304 (file variable in test) and G114 (test http.Serve) --- cmd/dmsg-server/commands/root.go | 5 ++-- cmd/dmsg-server/commands/start/root.go | 4 +-- pkg/disc/client_test.go | 36 +++++++++++++------------- pkg/dmsgctrl/serve_listener_test.go | 2 +- pkg/dmsgcurl/url_test.go | 2 +- pkg/dmsghttp/util_test.go | 20 +++++++------- pkg/dmsgpty/whitelist_test.go | 2 +- pkg/dmsgserver/api.go | 2 +- 8 files changed, 36 insertions(+), 37 deletions(-) diff --git a/cmd/dmsg-server/commands/root.go b/cmd/dmsg-server/commands/root.go index 664d09c3f..2af212dc6 100644 --- a/cmd/dmsg-server/commands/root.go +++ b/cmd/dmsg-server/commands/root.go @@ -2,13 +2,12 @@ package commands import ( - "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/buildinfo" "github.com/spf13/cobra" - "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/cmd/dmsg-server/commands/config" "github.com/skycoin/dmsg/cmd/dmsg-server/commands/start" + "github.com/skycoin/dmsg/pkg/dmsgclient" ) func init() { @@ -21,7 +20,7 @@ func init() { // RootCmd contains the root dmsg-server command var RootCmd = &cobra.Command{ - Use: dmsgclient.ExecName(), + Use: dmsgclient.ExecName(), Short: "DMSG Server", Long: ` ┌┬┐┌┬┐┌─┐┌─┐ ┌─┐┌─┐┬─┐┬ ┬┌─┐┬─┐ diff --git a/cmd/dmsg-server/commands/start/root.go b/cmd/dmsg-server/commands/start/root.go index 173e195d6..f48f49bcf 100644 --- a/cmd/dmsg-server/commands/start/root.go +++ b/cmd/dmsg-server/commands/start/root.go @@ -21,10 +21,10 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/metricsutil" "github.com/spf13/cobra" - "github.com/skycoin/dmsg/pkg/dmsg/metrics" - "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/disc" dmsg "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsg/metrics" + "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/dmsgserver" ) diff --git a/pkg/disc/client_test.go b/pkg/disc/client_test.go index dc0bd425d..294b559f0 100644 --- a/pkg/disc/client_test.go +++ b/pkg/disc/client_test.go @@ -637,7 +637,7 @@ func TestHTTPClientEntry_Success(t *testing.T) { assert.Equal(t, http.MethodGet, r.Method) assert.Contains(t, r.URL.Path, "/dmsg-discovery/entry/") w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(entry) //nolint:errcheck + json.NewEncoder(w).Encode(entry) //nolint:errcheck,gosec }) client := newTestHTTPClient(t, handler) @@ -650,7 +650,7 @@ func TestHTTPClientEntry_Success(t *testing.T) { func TestHTTPClientEntry_NotFound(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) - json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck + json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck,gosec Code: http.StatusNotFound, Message: disc.ErrKeyNotFound.Error(), }) @@ -665,7 +665,7 @@ func TestHTTPClientEntry_NotFound(t *testing.T) { func TestHTTPClientEntry_UnknownErrorBecomesUnexpected(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck + json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck,gosec Code: http.StatusInternalServerError, Message: "some unknown error", }) @@ -696,11 +696,11 @@ func TestHTTPClientPostEntry_Success(t *testing.T) { func TestHTTPClientPostEntry_Error(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) - body, _ := json.Marshal(disc.HTTPMessage{ + body, _ := json.Marshal(disc.HTTPMessage{ //nolint:errcheck,gosec Code: http.StatusUnprocessableEntity, Message: disc.ErrValidationWrongSequence.Error(), }) - w.Write(body) //nolint:errcheck + w.Write(body) //nolint:errcheck,gosec }) pk, sk := cipher.GenerateKeyPair() @@ -732,11 +732,11 @@ func TestHTTPClientDelEntry_Success(t *testing.T) { func TestHTTPClientDelEntry_Error(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnauthorized) - body, _ := json.Marshal(disc.HTTPMessage{ + body, _ := json.Marshal(disc.HTTPMessage{ //nolint:errcheck,gosec Code: http.StatusUnauthorized, Message: disc.ErrUnauthorized.Error(), }) - w.Write(body) //nolint:errcheck + w.Write(body) //nolint:errcheck,gosec }) pk, sk := cipher.GenerateKeyPair() @@ -757,7 +757,7 @@ func TestHTTPClientAvailableServers_Success(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/dmsg-discovery/available_servers", r.URL.Path) w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode([]*disc.Entry{entry}) //nolint:errcheck + json.NewEncoder(w).Encode([]*disc.Entry{entry}) //nolint:errcheck,gosec }) client := newTestHTTPClient(t, handler) @@ -770,7 +770,7 @@ func TestHTTPClientAvailableServers_Success(t *testing.T) { func TestHTTPClientAvailableServers_Error(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck + json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck,gosec Code: http.StatusInternalServerError, Message: disc.ErrNoAvailableServers.Error(), }) @@ -790,7 +790,7 @@ func TestHTTPClientAllServers_Success(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/dmsg-discovery/all_servers", r.URL.Path) w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode([]*disc.Entry{entry}) //nolint:errcheck + json.NewEncoder(w).Encode([]*disc.Entry{entry}) //nolint:errcheck,gosec }) client := newTestHTTPClient(t, handler) @@ -802,7 +802,7 @@ func TestHTTPClientAllServers_Success(t *testing.T) { func TestHTTPClientAllServers_Error(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck + json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck,gosec Code: http.StatusInternalServerError, Message: disc.ErrUnexpected.Error(), }) @@ -818,7 +818,7 @@ func TestHTTPClientAllEntries_Success(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/dmsg-discovery/entries", r.URL.Path) w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode([]string{"abc123", "def456"}) //nolint:errcheck + json.NewEncoder(w).Encode([]string{"abc123", "def456"}) //nolint:errcheck,gosec }) client := newTestHTTPClient(t, handler) @@ -830,7 +830,7 @@ func TestHTTPClientAllEntries_Success(t *testing.T) { func TestHTTPClientAllEntries_Error(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck + json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck,gosec Code: http.StatusInternalServerError, Message: disc.ErrBadInput.Error(), }) @@ -854,7 +854,7 @@ func TestHTTPClientAllClientsByServer_Success(t *testing.T) { result := map[string][]*disc.Entry{ serverPK.Hex(): {entry}, } - json.NewEncoder(w).Encode(result) //nolint:errcheck + json.NewEncoder(w).Encode(result) //nolint:errcheck,gosec }) client := newTestHTTPClient(t, handler) @@ -866,7 +866,7 @@ func TestHTTPClientAllClientsByServer_Success(t *testing.T) { func TestHTTPClientAllClientsByServer_Error(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck + json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck,gosec Code: http.StatusInternalServerError, Message: disc.ErrUnexpected.Error(), }) @@ -888,7 +888,7 @@ func TestHTTPClientClientsByServer_Success(t *testing.T) { expectedPath := fmt.Sprintf("/dmsg-discovery/server/%s/clients", serverPK.Hex()) assert.Equal(t, expectedPath, r.URL.Path) w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode([]*disc.Entry{entry}) //nolint:errcheck + json.NewEncoder(w).Encode([]*disc.Entry{entry}) //nolint:errcheck,gosec }) client := newTestHTTPClient(t, handler) @@ -903,7 +903,7 @@ func TestHTTPClientClientsByServer_Error(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) - json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck + json.NewEncoder(w).Encode(disc.HTTPMessage{ //nolint:errcheck,gosec Code: http.StatusNotFound, Message: disc.ErrKeyNotFound.Error(), }) @@ -928,7 +928,7 @@ func TestHTTPClientPutEntry_Success(t *testing.T) { } // GET for Entry lookup (shouldn't be needed if PostEntry succeeds) w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(entry) //nolint:errcheck + json.NewEncoder(w).Encode(entry) //nolint:errcheck,gosec }) client := newTestHTTPClient(t, handler) diff --git a/pkg/dmsgctrl/serve_listener_test.go b/pkg/dmsgctrl/serve_listener_test.go index 0992f5c2f..8903274e7 100644 --- a/pkg/dmsgctrl/serve_listener_test.go +++ b/pkg/dmsgctrl/serve_listener_test.go @@ -156,7 +156,7 @@ func TestControl_PingPongExchange(t *testing.T) { } // TestControl_PingContextCancel verifies that Ping returns the context error -// when the context is cancelled while waiting for a pong. +// when the context is canceled while waiting for a pong. func TestControl_PingContextCancel(t *testing.T) { connA, connB := net.Pipe() defer connB.Close() //nolint:errcheck diff --git a/pkg/dmsgcurl/url_test.go b/pkg/dmsgcurl/url_test.go index ae45120b9..0ce44e507 100644 --- a/pkg/dmsgcurl/url_test.go +++ b/pkg/dmsgcurl/url_test.go @@ -126,7 +126,7 @@ func TestCancellableCopy_Normal(t *testing.T) { assert.Equal(t, "hello world", dst.String()) } -func TestCancellableCopy_Cancelled(t *testing.T) { +func TestCancellableCopy_Canceled(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() // cancel immediately diff --git a/pkg/dmsghttp/util_test.go b/pkg/dmsghttp/util_test.go index 8a18f0354..89eebc6f9 100644 --- a/pkg/dmsghttp/util_test.go +++ b/pkg/dmsghttp/util_test.go @@ -138,10 +138,10 @@ func TestMakeHTTPTransport_FullRoundTrip(t *testing.T) { w.Write([]byte("world")) //nolint:errcheck }) r.Post("/echo", func(w http.ResponseWriter, r *http.Request) { - data, _ := io.ReadAll(r.Body) - w.Write(data) //nolint:errcheck + data, _ := io.ReadAll(r.Body) //nolint:errcheck + w.Write(data) //nolint:errcheck }) - go http.Serve(dmsgLis, r) //nolint:errcheck + go http.Serve(dmsgLis, r) //nolint:errcheck,gosec // Start dmsg client that runs HTTP client. clientPK, clientSK := cipher.GenerateKeyPair() @@ -164,7 +164,7 @@ func TestMakeHTTPTransport_FullRoundTrip(t *testing.T) { t.Run("GET_request", func(t *testing.T) { resp, err := httpC.Get(fmt.Sprintf("http://%s:%d/hello", hostPK.String(), dmsgHTTPPort)) require.NoError(t, err) - defer resp.Body.Close() + defer resp.Body.Close() //nolint:errcheck assert.Equal(t, http.StatusOK, resp.StatusCode) body, err := io.ReadAll(resp.Body) @@ -179,7 +179,7 @@ func TestMakeHTTPTransport_FullRoundTrip(t *testing.T) { http.NoBody, ) require.NoError(t, err) - defer resp.Body.Close() + defer resp.Body.Close() //nolint:errcheck assert.Equal(t, http.StatusOK, resp.StatusCode) }) } @@ -213,7 +213,7 @@ func TestMakeHTTPTransport_DefaultPort(t *testing.T) { r.Get("/", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("default-port")) //nolint:errcheck }) - go http.Serve(dmsgLis, r) //nolint:errcheck + go http.Serve(dmsgLis, r) //nolint:errcheck,gosec // Client. clientPK, clientSK := cipher.GenerateKeyPair() @@ -236,7 +236,7 @@ func TestMakeHTTPTransport_DefaultPort(t *testing.T) { // URL without port — should default to 80. resp, err := httpC.Get(fmt.Sprintf("http://%s/", hostPK.String())) require.NoError(t, err) - defer resp.Body.Close() + defer resp.Body.Close() //nolint:errcheck body, err := io.ReadAll(resp.Body) require.NoError(t, err) @@ -350,8 +350,8 @@ func TestGetServers_FilterRemovesAllRetries(t *testing.T) { assert.Empty(t, result) } -func TestGetServers_ContextCancelledReturnsEmpty(t *testing.T) { - // If context is cancelled before any servers are found, return empty. +func TestGetServers_ContextCanceledReturnsEmpty(t *testing.T) { + // If context is canceled before any servers are found, return empty. log := logging.MustGetLogger("test_ctx_cancel") // Use a URL that will fail (no server listening). @@ -458,7 +458,7 @@ func TestListenAndServe_ServesHTTP(t *testing.T) { resp, err := httpC.Get(fmt.Sprintf("http://%s:%d/", hostPK.String(), dmsgHTTPPort)) require.NoError(t, err) - defer resp.Body.Close() + defer resp.Body.Close() //nolint:errcheck body, err := io.ReadAll(resp.Body) require.NoError(t, err) diff --git a/pkg/dmsgpty/whitelist_test.go b/pkg/dmsgpty/whitelist_test.go index db15c96d7..8b4225fee 100644 --- a/pkg/dmsgpty/whitelist_test.go +++ b/pkg/dmsgpty/whitelist_test.go @@ -418,7 +418,7 @@ func TestWriteConfig(t *testing.T) { require.NoError(t, err) // Verify file was written. - data, err := os.ReadFile(confPath) + data, err := os.ReadFile(confPath) //nolint:gosec require.NoError(t, err) require.Contains(t, string(data), "testpk") require.Contains(t, string(data), "testsk") diff --git a/pkg/dmsgserver/api.go b/pkg/dmsgserver/api.go index 3e2065c1d..536ffc684 100644 --- a/pkg/dmsgserver/api.go +++ b/pkg/dmsgserver/api.go @@ -19,8 +19,8 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/httputil" "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" - "github.com/skycoin/dmsg/pkg/dmsg/metrics" dmsg "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsg/metrics" ) // ServerAPI main object of the server From 7e0b572d2dc82aa1b9b20356b5b378918d9bf402 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Sun, 22 Mar 2026 20:34:03 -0500 Subject: [PATCH 08/15] Fix remaining CI lint: gofmt, errcheck, gosec annotations --- cmd/dmsg-socks5/commands/dmsg-socks5.go | 2 +- cmd/dmsgcurl/commands/dmsgcurl.go | 2 +- cmd/dmsghttp/commands/dmsghttp.go | 2 +- pkg/dmsg/client_sessions.go | 6 +-- pkg/dmsg/listener.go | 2 +- pkg/dmsgctrl/serve_listener_test.go | 58 ++++++++++++------------- pkg/dmsghttp/util_test.go | 58 ++++++++++++------------- 7 files changed, 65 insertions(+), 65 deletions(-) diff --git a/cmd/dmsg-socks5/commands/dmsg-socks5.go b/cmd/dmsg-socks5/commands/dmsg-socks5.go index fc9646b10..9b86e10a9 100644 --- a/cmd/dmsg-socks5/commands/dmsg-socks5.go +++ b/cmd/dmsg-socks5/commands/dmsg-socks5.go @@ -71,7 +71,7 @@ func init() { // RootCmd contains the root command var RootCmd = &cobra.Command{ - Use: dmsgclient.ExecName(), + Use: dmsgclient.ExecName(), Short: "DMSG socks5 proxy server & client", Long: calvin.AsciiFont("dmsg-socks") + ` DMSG socks5 proxy server & client`, diff --git a/cmd/dmsgcurl/commands/dmsgcurl.go b/cmd/dmsgcurl/commands/dmsgcurl.go index df953500d..cf3366ad5 100644 --- a/cmd/dmsgcurl/commands/dmsgcurl.go +++ b/cmd/dmsgcurl/commands/dmsgcurl.go @@ -25,8 +25,8 @@ import ( "golang.org/x/net/proxy" "github.com/skycoin/dmsg/pkg/disc" - "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/dmsghttp" ) diff --git a/cmd/dmsghttp/commands/dmsghttp.go b/cmd/dmsghttp/commands/dmsghttp.go index ed3d07da4..a2ff3c05f 100644 --- a/cmd/dmsghttp/commands/dmsghttp.go +++ b/cmd/dmsghttp/commands/dmsghttp.go @@ -53,7 +53,7 @@ func init() { // RootCmd contains the root dmsghttp command var RootCmd = &cobra.Command{ - Use: dmsgclient.ExecName(), + Use: dmsgclient.ExecName(), Short: "DMSG http file server", Long: calvin.AsciiFont("dmsghttp") + ` DMSG http file server`, diff --git a/pkg/dmsg/client_sessions.go b/pkg/dmsg/client_sessions.go index 35e06da0c..5c3666a3a 100644 --- a/pkg/dmsg/client_sessions.go +++ b/pkg/dmsg/client_sessions.go @@ -89,20 +89,20 @@ func (ce *Client) dialSession(ctx context.Context, entry *disc.Entry) (cs Client dSes, err := makeClientSession(&ce.EntityCommon, ce.porter, conn, entry.Static) if err != nil { - conn.Close() + conn.Close() //nolint:errcheck return ClientSession{}, err } if entry.Protocol == "smux" { dSes.sm.smux, err = smux.Client(conn, smux.DefaultConfig()) if err != nil { - conn.Close() + conn.Close() //nolint:errcheck return ClientSession{}, err } ce.log.Infof("smux stream session initial for %s", dSes.RemotePK().String()) } else { dSes.sm.yamux, err = yamux.Client(conn, yamux.DefaultConfig()) if err != nil { - conn.Close() + conn.Close() //nolint:errcheck return ClientSession{}, err } ce.log.Infof("yamux stream session initial for %s", dSes.RemotePK().String()) diff --git a/pkg/dmsg/listener.go b/pkg/dmsg/listener.go index 50b5c5cac..520183a00 100644 --- a/pkg/dmsg/listener.go +++ b/pkg/dmsg/listener.go @@ -112,7 +112,7 @@ func (l *Listener) close() (closed bool) { for { select { case stream := <-l.accept: - stream.Close() //nolint:errcheck + stream.Close() //nolint:errcheck,gosec default: close(l.accept) return diff --git a/pkg/dmsgctrl/serve_listener_test.go b/pkg/dmsgctrl/serve_listener_test.go index 8903274e7..5443dbdaf 100644 --- a/pkg/dmsgctrl/serve_listener_test.go +++ b/pkg/dmsgctrl/serve_listener_test.go @@ -45,11 +45,11 @@ func TestServeListener_AcceptAndControl(t *testing.T) { // Cleanup. for _, ctrl := range ctrls { - ctrl.Close() //nolint:errcheck + ctrl.Close() //nolint:errcheck,gosec } - connA.Close() //nolint:errcheck - connB.Close() //nolint:errcheck - l.Close() //nolint:errcheck + connA.Close() //nolint:errcheck,gosec + connB.Close() //nolint:errcheck,gosec + l.Close() //nolint:errcheck,gosec } // TestServeListener_ClosesChannelOnListenerClose verifies that the channel @@ -88,7 +88,7 @@ func TestServeListener_FullChannelDropsControl(t *testing.T) { select { case ctrl := <-ch: require.NotNil(t, ctrl) - defer ctrl.Close() //nolint:errcheck + defer ctrl.Close() //nolint:errcheck,gosec case <-time.After(2 * time.Second): t.Fatal("timed out waiting for first control") } @@ -103,7 +103,7 @@ func TestServeListener_FullChannelDropsControl(t *testing.T) { select { case ctrl := <-ch: require.NotNil(t, ctrl) - defer ctrl.Close() //nolint:errcheck + defer ctrl.Close() //nolint:errcheck,gosec case <-time.After(2 * time.Second): t.Fatal("timed out waiting for second control") } @@ -122,17 +122,17 @@ func TestServeListener_FullChannelDropsControl(t *testing.T) { select { case ctrl := <-ch: require.NotNil(t, ctrl) - defer ctrl.Close() //nolint:errcheck + defer ctrl.Close() //nolint:errcheck,gosec case <-time.After(2 * time.Second): t.Fatal("timed out waiting for third control") } // Cleanup. - conn1.Close() //nolint:errcheck - conn2.Close() //nolint:errcheck - conn3.Close() //nolint:errcheck - conn4.Close() //nolint:errcheck - l.Close() //nolint:errcheck + conn1.Close() //nolint:errcheck,gosec + conn2.Close() //nolint:errcheck,gosec + conn3.Close() //nolint:errcheck,gosec + conn4.Close() //nolint:errcheck,gosec + l.Close() //nolint:errcheck,gosec } // TestControl_PingPongExchange tests that a ping on one side results in a @@ -143,8 +143,8 @@ func TestControl_PingPongExchange(t *testing.T) { ctrlB := dmsgctrl.ControlStream(connB) t.Cleanup(func() { - ctrlA.Close() //nolint:errcheck - ctrlB.Close() //nolint:errcheck + ctrlA.Close() //nolint:errcheck,gosec + ctrlB.Close() //nolint:errcheck,gosec }) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) @@ -159,7 +159,7 @@ func TestControl_PingPongExchange(t *testing.T) { // when the context is canceled while waiting for a pong. func TestControl_PingContextCancel(t *testing.T) { connA, connB := net.Pipe() - defer connB.Close() //nolint:errcheck + defer connB.Close() //nolint:errcheck,gosec ctrlA := dmsgctrl.ControlStream(connA) @@ -182,7 +182,7 @@ func TestControl_PingContextCancel(t *testing.T) { _, err := ctrlA.Ping(ctx) assert.ErrorIs(t, err, context.DeadlineExceeded) - ctrlA.Close() //nolint:errcheck + ctrlA.Close() //nolint:errcheck,gosec } // TestControl_Close verifies that Close sets the error and signals Done. @@ -218,12 +218,12 @@ func TestControl_Close(t *testing.T) { func TestControl_DoubleClose(t *testing.T) { connA, connB := net.Pipe() ctrlA := dmsgctrl.ControlStream(connA) - dmsgctrl.ControlStream(connB) //nolint:errcheck + dmsgctrl.ControlStream(connB) //nolint:errcheck,gosec err1 := ctrlA.Close() // Second close: the connection is already closed so conn.Close may // return an error, but it must not panic. - ctrlA.Close() //nolint:errcheck + ctrlA.Close() //nolint:errcheck,gosec _ = err1 // Wait for done. @@ -245,8 +245,8 @@ func TestControl_ErrBeforeDone(t *testing.T) { assert.Nil(t, ctrlA.Err()) assert.Nil(t, ctrlB.Err()) - ctrlA.Close() //nolint:errcheck - ctrlB.Close() //nolint:errcheck + ctrlA.Close() //nolint:errcheck,gosec + ctrlB.Close() //nolint:errcheck,gosec } // TestControl_DoneChannel tests that the Done channel blocks while the @@ -272,18 +272,18 @@ func TestControl_DoneChannel(t *testing.T) { t.Fatal("Done() did not close after Close()") } - ctrlB.Close() //nolint:errcheck + ctrlB.Close() //nolint:errcheck,gosec } // TestControl_Conn verifies that Conn returns the underlying net.Conn. func TestControl_Conn(t *testing.T) { connA, connB := net.Pipe() ctrlA := dmsgctrl.ControlStream(connA) - dmsgctrl.ControlStream(connB) //nolint:errcheck + dmsgctrl.ControlStream(connB) //nolint:errcheck,gosec assert.Equal(t, connA, ctrlA.Conn()) - ctrlA.Close() //nolint:errcheck + ctrlA.Close() //nolint:errcheck,gosec } // TestControl_ConcurrentPing tests that multiple goroutines can ping @@ -292,7 +292,7 @@ func TestControl_ConcurrentPing(t *testing.T) { // Use TCP so writes don't block synchronously like net.Pipe. l, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) - defer l.Close() //nolint:errcheck + defer l.Close() //nolint:errcheck,gosec var connB net.Conn accepted := make(chan struct{}) @@ -313,8 +313,8 @@ func TestControl_ConcurrentPing(t *testing.T) { ctrlB := dmsgctrl.ControlStream(connB) t.Cleanup(func() { - ctrlA.Close() //nolint:errcheck - ctrlB.Close() //nolint:errcheck + ctrlA.Close() //nolint:errcheck,gosec + ctrlB.Close() //nolint:errcheck,gosec }) const goroutines = 5 @@ -327,11 +327,11 @@ func TestControl_ConcurrentPing(t *testing.T) { for i := 0; i < goroutines; i++ { go func() { defer wg.Done() - _, _ = ctrlA.Ping(ctx) //nolint:errcheck + _, _ = ctrlA.Ping(ctx) //nolint:errcheck,gosec }() go func() { defer wg.Done() - _, _ = ctrlB.Ping(ctx) //nolint:errcheck + _, _ = ctrlB.Ping(ctx) //nolint:errcheck,gosec }() } @@ -343,7 +343,7 @@ func TestControl_ConcurrentPing(t *testing.T) { func TestControl_PingAfterClose(t *testing.T) { connA, connB := net.Pipe() ctrlA := dmsgctrl.ControlStream(connA) - dmsgctrl.ControlStream(connB) //nolint:errcheck + dmsgctrl.ControlStream(connB) //nolint:errcheck,gosec require.NoError(t, ctrlA.Close()) diff --git a/pkg/dmsghttp/util_test.go b/pkg/dmsghttp/util_test.go index 89eebc6f9..fd1166075 100644 --- a/pkg/dmsghttp/util_test.go +++ b/pkg/dmsghttp/util_test.go @@ -36,7 +36,7 @@ func TestMakeHTTPTransport_ReturnsValidTransport(t *testing.T) { pk, sk := cipher.GenerateKeyPair() dmsgC := dmsg.NewClient(pk, sk, dc, nil) - defer dmsgC.Close() //nolint:errcheck + defer dmsgC.Close() //nolint:errcheck,gosec ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -53,7 +53,7 @@ func TestMakeHTTPTransport_RoundTripInvalidHost(t *testing.T) { pk, sk := cipher.GenerateKeyPair() dmsgC := dmsg.NewClient(pk, sk, dc, nil) - defer dmsgC.Close() //nolint:errcheck + defer dmsgC.Close() //nolint:errcheck,gosec ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -80,13 +80,13 @@ func TestMakeHTTPTransport_RoundTripDialFailure(t *testing.T) { srv := dmsg.NewServer(srvPK, srvSK, dc, &srvConf, nil) lis, err := nettest.NewLocalListener("tcp") require.NoError(t, err) - go srv.Serve(lis, "") //nolint:errcheck - defer srv.Close() //nolint:errcheck + go srv.Serve(lis, "") //nolint:errcheck,gosec + defer srv.Close() //nolint:errcheck,gosec pk, sk := cipher.GenerateKeyPair() dmsgC := dmsg.NewClient(pk, sk, dc, &dmsg.Config{MinSessions: 1}) go dmsgC.Serve(context.Background()) - defer dmsgC.Close() //nolint:errcheck + defer dmsgC.Close() //nolint:errcheck,gosec <-dmsgC.Ready() ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) @@ -118,15 +118,15 @@ func TestMakeHTTPTransport_FullRoundTrip(t *testing.T) { srv := dmsg.NewServer(srvPK, srvSK, dc, &srvConf, nil) lis, err := nettest.NewLocalListener("tcp") require.NoError(t, err) - go srv.Serve(lis, "") //nolint:errcheck - t.Cleanup(func() { srv.Close() }) //nolint:errcheck + go srv.Serve(lis, "") //nolint:errcheck,gosec + t.Cleanup(func() { srv.Close() }) //nolint:errcheck,gosec <-srv.Ready() // Start dmsg client that hosts HTTP server. hostPK, hostSK := cipher.GenerateKeyPair() dmsgHost := dmsg.NewClient(hostPK, hostSK, dc, &dmsg.Config{MinSessions: 1}) go dmsgHost.Serve(context.Background()) - t.Cleanup(func() { dmsgHost.Close() }) //nolint:errcheck + t.Cleanup(func() { dmsgHost.Close() }) //nolint:errcheck,gosec <-dmsgHost.Ready() dmsgLis, err := dmsgHost.Listen(dmsgHTTPPort) @@ -135,11 +135,11 @@ func TestMakeHTTPTransport_FullRoundTrip(t *testing.T) { r := chi.NewRouter() r.Get("/hello", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) - w.Write([]byte("world")) //nolint:errcheck + w.Write([]byte("world")) //nolint:errcheck,gosec }) r.Post("/echo", func(w http.ResponseWriter, r *http.Request) { - data, _ := io.ReadAll(r.Body) //nolint:errcheck - w.Write(data) //nolint:errcheck + data, _ := io.ReadAll(r.Body) //nolint:errcheck,gosec + w.Write(data) //nolint:errcheck,gosec }) go http.Serve(dmsgLis, r) //nolint:errcheck,gosec @@ -147,7 +147,7 @@ func TestMakeHTTPTransport_FullRoundTrip(t *testing.T) { clientPK, clientSK := cipher.GenerateKeyPair() dmsgClient := dmsg.NewClient(clientPK, clientSK, dc, &dmsg.Config{MinSessions: 1}) go dmsgClient.Serve(context.Background()) - t.Cleanup(func() { dmsgClient.Close() }) //nolint:errcheck + t.Cleanup(func() { dmsgClient.Close() }) //nolint:errcheck,gosec <-dmsgClient.Ready() // Allow time for dmsg sessions to stabilize. @@ -164,7 +164,7 @@ func TestMakeHTTPTransport_FullRoundTrip(t *testing.T) { t.Run("GET_request", func(t *testing.T) { resp, err := httpC.Get(fmt.Sprintf("http://%s:%d/hello", hostPK.String(), dmsgHTTPPort)) require.NoError(t, err) - defer resp.Body.Close() //nolint:errcheck + defer resp.Body.Close() //nolint:errcheck,gosec assert.Equal(t, http.StatusOK, resp.StatusCode) body, err := io.ReadAll(resp.Body) @@ -179,7 +179,7 @@ func TestMakeHTTPTransport_FullRoundTrip(t *testing.T) { http.NoBody, ) require.NoError(t, err) - defer resp.Body.Close() //nolint:errcheck + defer resp.Body.Close() //nolint:errcheck,gosec assert.Equal(t, http.StatusOK, resp.StatusCode) }) } @@ -195,15 +195,15 @@ func TestMakeHTTPTransport_DefaultPort(t *testing.T) { srv := dmsg.NewServer(srvPK, srvSK, dc, &srvConf, nil) lis, err := nettest.NewLocalListener("tcp") require.NoError(t, err) - go srv.Serve(lis, "") //nolint:errcheck - t.Cleanup(func() { srv.Close() }) //nolint:errcheck + go srv.Serve(lis, "") //nolint:errcheck,gosec + t.Cleanup(func() { srv.Close() }) //nolint:errcheck,gosec <-srv.Ready() // Host HTTP server on port 80. hostPK, hostSK := cipher.GenerateKeyPair() dmsgHost := dmsg.NewClient(hostPK, hostSK, dc, &dmsg.Config{MinSessions: 1}) go dmsgHost.Serve(context.Background()) - t.Cleanup(func() { dmsgHost.Close() }) //nolint:errcheck + t.Cleanup(func() { dmsgHost.Close() }) //nolint:errcheck,gosec <-dmsgHost.Ready() dmsgLis, err := dmsgHost.Listen(80) @@ -211,7 +211,7 @@ func TestMakeHTTPTransport_DefaultPort(t *testing.T) { r := chi.NewRouter() r.Get("/", func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte("default-port")) //nolint:errcheck + w.Write([]byte("default-port")) //nolint:errcheck,gosec }) go http.Serve(dmsgLis, r) //nolint:errcheck,gosec @@ -219,7 +219,7 @@ func TestMakeHTTPTransport_DefaultPort(t *testing.T) { clientPK, clientSK := cipher.GenerateKeyPair() dmsgClient := dmsg.NewClient(clientPK, clientSK, dc, &dmsg.Config{MinSessions: 1}) go dmsgClient.Serve(context.Background()) - t.Cleanup(func() { dmsgClient.Close() }) //nolint:errcheck + t.Cleanup(func() { dmsgClient.Close() }) //nolint:errcheck,gosec <-dmsgClient.Ready() // Allow time for dmsg sessions to stabilize. @@ -236,7 +236,7 @@ func TestMakeHTTPTransport_DefaultPort(t *testing.T) { // URL without port — should default to 80. resp, err := httpC.Get(fmt.Sprintf("http://%s/", hostPK.String())) require.NoError(t, err) - defer resp.Body.Close() //nolint:errcheck + defer resp.Body.Close() //nolint:errcheck,gosec body, err := io.ReadAll(resp.Body) require.NoError(t, err) @@ -414,15 +414,15 @@ func TestListenAndServe_ServesHTTP(t *testing.T) { srv := dmsg.NewServer(srvPK, srvSK, dc, &srvConf, nil) lis, err := nettest.NewLocalListener("tcp") require.NoError(t, err) - go srv.Serve(lis, "") //nolint:errcheck - t.Cleanup(func() { srv.Close() }) //nolint:errcheck + go srv.Serve(lis, "") //nolint:errcheck,gosec + t.Cleanup(func() { srv.Close() }) //nolint:errcheck,gosec <-srv.Ready() // Host client. hostPK, hostSK := cipher.GenerateKeyPair() dmsgHost := dmsg.NewClient(hostPK, hostSK, dc, &dmsg.Config{MinSessions: 1}) go dmsgHost.Serve(context.Background()) - t.Cleanup(func() { dmsgHost.Close() }) //nolint:errcheck + t.Cleanup(func() { dmsgHost.Close() }) //nolint:errcheck,gosec <-dmsgHost.Ready() log := logging.MustGetLogger("test_listen_serve") @@ -430,7 +430,7 @@ func TestListenAndServe_ServesHTTP(t *testing.T) { defer cancel() handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Write([]byte("listen-and-serve")) //nolint:errcheck + w.Write([]byte("listen-and-serve")) //nolint:errcheck,gosec }) errCh := make(chan error, 1) @@ -445,7 +445,7 @@ func TestListenAndServe_ServesHTTP(t *testing.T) { clientPK, clientSK := cipher.GenerateKeyPair() dmsgClient := dmsg.NewClient(clientPK, clientSK, dc, &dmsg.Config{MinSessions: 1}) go dmsgClient.Serve(context.Background()) - t.Cleanup(func() { dmsgClient.Close() }) //nolint:errcheck + t.Cleanup(func() { dmsgClient.Close() }) //nolint:errcheck,gosec <-dmsgClient.Ready() // Allow time for dmsg sessions to stabilize. @@ -458,7 +458,7 @@ func TestListenAndServe_ServesHTTP(t *testing.T) { resp, err := httpC.Get(fmt.Sprintf("http://%s:%d/", hostPK.String(), dmsgHTTPPort)) require.NoError(t, err) - defer resp.Body.Close() //nolint:errcheck + defer resp.Body.Close() //nolint:errcheck,gosec body, err := io.ReadAll(resp.Body) require.NoError(t, err) @@ -480,14 +480,14 @@ func TestListenAndServe_InvalidPort(t *testing.T) { srv := dmsg.NewServer(srvPK, srvSK, dc, &srvConf, nil) lis, err := nettest.NewLocalListener("tcp") require.NoError(t, err) - go srv.Serve(lis, "") //nolint:errcheck - t.Cleanup(func() { srv.Close() }) //nolint:errcheck + go srv.Serve(lis, "") //nolint:errcheck,gosec + t.Cleanup(func() { srv.Close() }) //nolint:errcheck,gosec <-srv.Ready() hostPK, hostSK := cipher.GenerateKeyPair() dmsgHost := dmsg.NewClient(hostPK, hostSK, dc, &dmsg.Config{MinSessions: 1}) go dmsgHost.Serve(context.Background()) - t.Cleanup(func() { dmsgHost.Close() }) //nolint:errcheck + t.Cleanup(func() { dmsgHost.Close() }) //nolint:errcheck,gosec <-dmsgHost.Ready() log := logging.MustGetLogger("test_invalid_port") From 356f215db9f11b7e4c5a991b2232ba9fb8feaf52 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Sun, 22 Mar 2026 20:39:05 -0500 Subject: [PATCH 09/15] Fix CI lint: gofmt all files, add errcheck/gosec nolint on conn.Close --- cmd/dmsg/commands/root.go | 4 ++-- cmd/dmsgip/commands/dmsgip.go | 2 +- cmd/dmsgpty-cli/commands/root.go | 2 +- cmd/dmsgpty-host/commands/root.go | 4 ++-- cmd/dmsgpty-ui/commands/dmsgpty-ui.go | 2 +- cmd/dmsgweb/commands/root.go | 2 +- pkg/dmsg/client_sessions.go | 6 +++--- pkg/dmsg/server.go | 6 +++--- pkg/dmsgserver/api_test.go | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cmd/dmsg/commands/root.go b/cmd/dmsg/commands/root.go index fe6ea8968..560bf2f06 100644 --- a/cmd/dmsg/commands/root.go +++ b/cmd/dmsg/commands/root.go @@ -8,7 +8,6 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/calvin" "github.com/spf13/cobra" - "github.com/skycoin/dmsg/pkg/dmsgclient" df "github.com/skycoin/dmsg/cmd/conf/commands" dl "github.com/skycoin/dmsg/cmd/dial/commands" dd "github.com/skycoin/dmsg/cmd/dmsg-discovery/commands" @@ -21,6 +20,7 @@ import ( dph "github.com/skycoin/dmsg/cmd/dmsgpty-host/commands" dpu "github.com/skycoin/dmsg/cmd/dmsgpty-ui/commands" dw "github.com/skycoin/dmsg/cmd/dmsgweb/commands" + "github.com/skycoin/dmsg/pkg/dmsgclient" ) var ( @@ -84,7 +84,7 @@ func modifySubcommands(cmd *cobra.Command) { // RootCmd contains all binaries which may be separately compiled as subcommands var RootCmd = &cobra.Command{ - Use: dmsgclient.ExecName(), + Use: dmsgclient.ExecName(), Short: "DMSG services & utilities", Long: func() (ret string) { ret = calvin.AsciiFont("dmsg") diff --git a/cmd/dmsgip/commands/dmsgip.go b/cmd/dmsgip/commands/dmsgip.go index 9e93839fc..b52b9a354 100644 --- a/cmd/dmsgip/commands/dmsgip.go +++ b/cmd/dmsgip/commands/dmsgip.go @@ -48,7 +48,7 @@ func init() { // RootCmd contains the root dmsgcurl command var RootCmd = &cobra.Command{ - Use: dmsgclient.ExecName(), + Use: dmsgclient.ExecName(), Short: "DMSG IP utility", Long: calvin.AsciiFont("dmsgip") + ` DMSG IP utility`, diff --git a/cmd/dmsgpty-cli/commands/root.go b/cmd/dmsgpty-cli/commands/root.go index 4590becb8..7d47b6866 100644 --- a/cmd/dmsgpty-cli/commands/root.go +++ b/cmd/dmsgpty-cli/commands/root.go @@ -41,7 +41,7 @@ func init() { // RootCmd contains commands for dmsgpty-cli; which interacts with the dmsgpty-host instance (i.e. skywire-visor) var RootCmd = &cobra.Command{ - Use: dmsgcli.ExecName(), + Use: dmsgcli.ExecName(), Short: "DMSG pseudoterminal command line interface", Long: calvin.AsciiFont("dmsgpty-cli") + ` DMSG pseudoterminal command line interface`, diff --git a/cmd/dmsgpty-host/commands/root.go b/cmd/dmsgpty-host/commands/root.go index 648cceb99..cc2c8a3f8 100644 --- a/cmd/dmsgpty-host/commands/root.go +++ b/cmd/dmsgpty-host/commands/root.go @@ -21,9 +21,9 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" "github.com/spf13/cobra" - "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/disc" dmsg "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsgclient" "github.com/skycoin/dmsg/pkg/dmsgpty" ) @@ -72,7 +72,7 @@ func init() { // RootCmd contains commands for dmsgpty-host var RootCmd = &cobra.Command{ - Use: dmsgclient.ExecName(), + Use: dmsgclient.ExecName(), Short: "DMSG host for pseudoterminal command line interface", Long: calvin.AsciiFont("dmsgpty-host") + ` DMSG host for pseudoterminal (pty) command line interface`, diff --git a/cmd/dmsgpty-ui/commands/dmsgpty-ui.go b/cmd/dmsgpty-ui/commands/dmsgpty-ui.go index 3e04fe683..531f7f742 100644 --- a/cmd/dmsgpty-ui/commands/dmsgpty-ui.go +++ b/cmd/dmsgpty-ui/commands/dmsgpty-ui.go @@ -31,7 +31,7 @@ func init() { // RootCmd contains commands to start a dmsgpty-ui server for a dmsgpty-host var RootCmd = &cobra.Command{ - Use: dmsgclient.ExecName(), + Use: dmsgclient.ExecName(), Short: "DMSG pseudoterminal GUI", Long: ` ┌┬┐┌┬┐┌─┐┌─┐┌─┐┌┬┐┬ ┬ ┬ ┬┬ diff --git a/cmd/dmsgweb/commands/root.go b/cmd/dmsgweb/commands/root.go index bd22ce6b7..3d1d24da4 100644 --- a/cmd/dmsgweb/commands/root.go +++ b/cmd/dmsgweb/commands/root.go @@ -20,8 +20,8 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/logging" "golang.org/x/net/proxy" - "github.com/skycoin/dmsg/pkg/dmsgclient" dmsg "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsgclient" ) var ( diff --git a/pkg/dmsg/client_sessions.go b/pkg/dmsg/client_sessions.go index 5c3666a3a..de5534385 100644 --- a/pkg/dmsg/client_sessions.go +++ b/pkg/dmsg/client_sessions.go @@ -89,20 +89,20 @@ func (ce *Client) dialSession(ctx context.Context, entry *disc.Entry) (cs Client dSes, err := makeClientSession(&ce.EntityCommon, ce.porter, conn, entry.Static) if err != nil { - conn.Close() //nolint:errcheck + conn.Close() //nolint:errcheck,gosec return ClientSession{}, err } if entry.Protocol == "smux" { dSes.sm.smux, err = smux.Client(conn, smux.DefaultConfig()) if err != nil { - conn.Close() //nolint:errcheck + conn.Close() //nolint:errcheck,gosec return ClientSession{}, err } ce.log.Infof("smux stream session initial for %s", dSes.RemotePK().String()) } else { dSes.sm.yamux, err = yamux.Client(conn, yamux.DefaultConfig()) if err != nil { - conn.Close() //nolint:errcheck + conn.Close() //nolint:errcheck,gosec return ClientSession{}, err } ce.log.Infof("yamux stream session initial for %s", dSes.RemotePK().String()) diff --git a/pkg/dmsg/server.go b/pkg/dmsg/server.go index c85e8c976..4302b0ff3 100644 --- a/pkg/dmsg/server.go +++ b/pkg/dmsg/server.go @@ -13,8 +13,8 @@ import ( "github.com/skycoin/skywire/pkg/skywire-utilities/pkg/netutil" "github.com/xtaci/smux" - "github.com/skycoin/dmsg/pkg/dmsg/metrics" "github.com/skycoin/dmsg/pkg/disc" + "github.com/skycoin/dmsg/pkg/dmsg/metrics" ) // ServerConfig configues the Server @@ -247,7 +247,7 @@ func (s *Server) handleSession(conn net.Conn) { dSes.sm.smux, err = smux.Server(conn, smux.DefaultConfig()) if err != nil { dSes.sm.mutx.Unlock() - conn.Close() + conn.Close() //nolint:errcheck cancel() return } @@ -257,7 +257,7 @@ func (s *Server) handleSession(conn net.Conn) { dSes.sm.yamux, err = yamux.Server(conn, yamux.DefaultConfig()) if err != nil { dSes.sm.mutx.Unlock() - conn.Close() + conn.Close() //nolint:errcheck cancel() return } diff --git a/pkg/dmsgserver/api_test.go b/pkg/dmsgserver/api_test.go index 1841ca2c0..ccfbc76b6 100644 --- a/pkg/dmsgserver/api_test.go +++ b/pkg/dmsgserver/api_test.go @@ -10,8 +10,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/skycoin/dmsg/pkg/dmsg/metrics" dmsg "github.com/skycoin/dmsg/pkg/dmsg" + "github.com/skycoin/dmsg/pkg/dmsg/metrics" "github.com/skycoin/dmsg/pkg/dmsgserver" ) From 4406f77eeb5fc2d72eb63039aa82eca3c4f623f1 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Sun, 22 Mar 2026 20:55:13 -0500 Subject: [PATCH 10/15] Fix CI: add gosec to remaining nolint annotations --- pkg/dmsg/server.go | 4 ++-- pkg/dmsgpty/whitelist_test.go | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/dmsg/server.go b/pkg/dmsg/server.go index 4302b0ff3..eef196baa 100644 --- a/pkg/dmsg/server.go +++ b/pkg/dmsg/server.go @@ -247,7 +247,7 @@ func (s *Server) handleSession(conn net.Conn) { dSes.sm.smux, err = smux.Server(conn, smux.DefaultConfig()) if err != nil { dSes.sm.mutx.Unlock() - conn.Close() //nolint:errcheck + conn.Close() //nolint:errcheck,gosec cancel() return } @@ -257,7 +257,7 @@ func (s *Server) handleSession(conn net.Conn) { dSes.sm.yamux, err = yamux.Server(conn, yamux.DefaultConfig()) if err != nil { dSes.sm.mutx.Unlock() - conn.Close() //nolint:errcheck + conn.Close() //nolint:errcheck,gosec cancel() return } diff --git a/pkg/dmsgpty/whitelist_test.go b/pkg/dmsgpty/whitelist_test.go index 8b4225fee..1e92b9e28 100644 --- a/pkg/dmsgpty/whitelist_test.go +++ b/pkg/dmsgpty/whitelist_test.go @@ -262,7 +262,7 @@ func TestWhitelistClientGateway_Integration(t *testing.T) { // Server side: read the request, write response, then serve RPC. serverReady := make(chan error, 1) go func() { - defer connServer.Close() //nolint:errcheck + defer connServer.Close() //nolint:errcheck,gosec // Read the length-prefixed URI request. prefix := make([]byte, 1) @@ -330,7 +330,7 @@ func TestRPCUtil_RequestResponseRoundTrip(t *testing.T) { done := make(chan error, 1) go func() { - defer connA.Close() //nolint:errcheck + defer connA.Close() //nolint:errcheck,gosec // Read length-prefixed request. prefix := make([]byte, 1) @@ -376,15 +376,15 @@ func TestRPCUtil_RejectResponse(t *testing.T) { connA, connB := net.Pipe() go func() { - defer connA.Close() //nolint:errcheck + defer connA.Close() //nolint:errcheck,gosec // Read and discard the request. prefix := make([]byte, 1) - connA.Read(prefix) //nolint:errcheck + connA.Read(prefix) //nolint:errcheck,gosec uri := make([]byte, prefix[0]) - connA.Read(uri) //nolint:errcheck + connA.Read(uri) //nolint:errcheck,gosec // Write reject (1). - connA.Write([]byte{1}) //nolint:errcheck + connA.Write([]byte{1}) //nolint:errcheck,gosec }() // NewWhitelistClient should fail when server rejects. @@ -442,7 +442,7 @@ func TestRPCUtil_LargeURI(t *testing.T) { done := make(chan error, 1) go func() { - defer connA.Close() //nolint:errcheck + defer connA.Close() //nolint:errcheck,gosec prefix := make([]byte, 1) if _, err := connA.Read(prefix); err != nil { done <- err From fed68c8a33436c80483016f371305da503537c0f Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Sun, 22 Mar 2026 20:57:33 -0500 Subject: [PATCH 11/15] Update vendor dependencies - github.com/skycoin/skycoin v0.28.3 -> v0.28.5-alpha1 (90b668188f85) - github.com/skycoin/skywire v1.3.35 -> v1.3.37 - golang.org/x/crypto v0.48.0 -> v0.49.0 - golang.org/x/net v0.51.0 -> v0.52.0 - golang.org/x/sys v0.41.0 -> v0.42.0 - Various other minor updates (smux, VictoriaMetrics, etc.) --- go.mod | 31 +- go.sum | 72 +- .../VictoriaMetrics/metrics/histogram.go | 10 +- .../VictoriaMetrics/metrics/metrics.go | 23 + .../github.com/VictoriaMetrics/metrics/set.go | 9 +- .../bytedance/gopkg/lang/dirtmake/bytes.go | 7 + .../gopkg/lang/dirtmake/bytes_tinygo.go | 24 + vendor/github.com/fatih/color/color.go | 112 +- .../github.com/fatih/color/color_windows.go | 3 + .../goccy/go-json/internal/encoder/code.go | 1 + .../skycoin/src/cipher/base58/base58_old.go | 190 --- .../skycoin/src/cipher/bech32/bech32.go | 212 +++ .../skycoin/src/cipher/bitcoin_segwit.go | 115 ++ .../skywire/deployment/dmsghttp-config.json | 32 +- .../skywire/deployment/services-config.json | 8 +- .../pkg/buildinfo/buildinfo.go | 17 +- .../skywire-utilities/pkg/httputil/health.go | 1 + .../pkg/httputil/httputil.go | 34 +- vendor/github.com/xtaci/smux/alloc.go | 6 +- vendor/github.com/xtaci/smux/session.go | 2 +- vendor/github.com/xtaci/smux/stream.go | 2 +- vendor/golang.org/x/net/http2/http2.go | 16 +- vendor/golang.org/x/net/http2/server.go | 2 + vendor/golang.org/x/net/http2/transport.go | 8 - vendor/golang.org/x/net/http2/writesched.go | 6 + .../net/http2/writesched_priority_rfc7540.go | 5 + .../x/net/http2/writesched_random.go | 2 + .../x/sys/cpu/asm_darwin_arm64_gc.s | 12 + vendor/golang.org/x/sys/cpu/cpu_arm64.go | 9 +- .../golang.org/x/sys/cpu/cpu_darwin_arm64.go | 67 + .../x/sys/cpu/cpu_darwin_arm64_other.go | 29 + .../golang.org/x/sys/cpu/cpu_gccgo_arm64.go | 1 + .../golang.org/x/sys/cpu/cpu_other_arm64.go | 6 +- .../x/sys/cpu/syscall_darwin_arm64_gc.go | 54 + .../golang.org/x/sys/plan9/syscall_plan9.go | 8 +- vendor/golang.org/x/sys/unix/ztypes_linux.go | 229 +-- vendor/golang.org/x/sys/windows/aliases.go | 1 + .../x/sys/windows/syscall_windows.go | 14 - vendor/modules.txt | 50 +- vendor/mvdan.cc/sh/v3/expand/arith.go | 8 +- vendor/mvdan.cc/sh/v3/expand/environ.go | 76 +- vendor/mvdan.cc/sh/v3/expand/expand.go | 163 ++- vendor/mvdan.cc/sh/v3/expand/param.go | 45 +- .../mvdan.cc/sh/v3/expand/valuekind_string.go | 5 +- vendor/mvdan.cc/sh/v3/internal/pattern.go | 77 + vendor/mvdan.cc/sh/v3/internal/testing.go | 52 + vendor/mvdan.cc/sh/v3/pattern/pattern.go | 128 +- vendor/mvdan.cc/sh/v3/shell/expand.go | 14 +- vendor/mvdan.cc/sh/v3/syntax/braces.go | 18 +- vendor/mvdan.cc/sh/v3/syntax/lexer.go | 526 ++++--- vendor/mvdan.cc/sh/v3/syntax/nodes.go | 119 +- vendor/mvdan.cc/sh/v3/syntax/parser.go | 1257 +++++++++++------ vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go | 62 +- vendor/mvdan.cc/sh/v3/syntax/printer.go | 378 ++--- vendor/mvdan.cc/sh/v3/syntax/quote.go | 9 +- .../sh/v3/syntax/quotestate_string.go | 61 - vendor/mvdan.cc/sh/v3/syntax/simplify.go | 5 +- vendor/mvdan.cc/sh/v3/syntax/token_string.go | 268 ++-- vendor/mvdan.cc/sh/v3/syntax/tokens.go | 164 ++- vendor/mvdan.cc/sh/v3/syntax/walk.go | 18 +- 60 files changed, 3173 insertions(+), 1710 deletions(-) create mode 100644 vendor/github.com/bytedance/gopkg/lang/dirtmake/bytes_tinygo.go delete mode 100644 vendor/github.com/skycoin/skycoin/src/cipher/base58/base58_old.go create mode 100644 vendor/github.com/skycoin/skycoin/src/cipher/bech32/bech32.go create mode 100644 vendor/github.com/skycoin/skycoin/src/cipher/bitcoin_segwit.go create mode 100644 vendor/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s create mode 100644 vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go create mode 100644 vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go create mode 100644 vendor/golang.org/x/sys/cpu/syscall_darwin_arm64_gc.go create mode 100644 vendor/mvdan.cc/sh/v3/internal/pattern.go create mode 100644 vendor/mvdan.cc/sh/v3/internal/testing.go delete mode 100644 vendor/mvdan.cc/sh/v3/syntax/quotestate_string.go diff --git a/go.mod b/go.mod index d611dad80..9ff1dbe79 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/skycoin/dmsg -go 1.25.6 +go 1.26.1 require ( github.com/ActiveState/termtest/conpty v0.5.0 - github.com/VictoriaMetrics/metrics v1.41.2 + github.com/VictoriaMetrics/metrics v1.42.0 github.com/bitfield/script v0.24.1 github.com/chen3feng/safecast v0.0.0-20220908170618-81b2ecd47937 github.com/coder/websocket v1.8.14 @@ -19,25 +19,25 @@ require ( github.com/pires/go-proxyproto v0.11.0 github.com/sirupsen/logrus v1.9.4 github.com/skycoin/noise v0.0.0-20180327030543-2492fe189ae6 - github.com/skycoin/skycoin v0.28.3 - github.com/skycoin/skywire v1.3.35-0.20260222235451-f11c46808634 + github.com/skycoin/skycoin v0.28.5-alpha1.0.20260323015226-90b668188f85 + github.com/skycoin/skywire v1.3.37 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 - golang.org/x/net v0.51.0 - golang.org/x/sys v0.41.0 - golang.org/x/term v0.40.0 + golang.org/x/net v0.52.0 + golang.org/x/sys v0.42.0 + golang.org/x/term v0.41.0 ) require ( github.com/docker/docker v28.5.2+incompatible github.com/tidwall/pretty v1.2.1 - github.com/xtaci/smux v1.5.56 + github.com/xtaci/smux v1.5.57 ) require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/gopkg v0.1.4 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -50,7 +50,7 @@ require ( github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect - github.com/fatih/color v1.18.0 // indirect + github.com/fatih/color v1.19.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gin-contrib/sse v1.1.0 // indirect @@ -59,7 +59,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect - github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/itchyny/gojq v0.12.18 // indirect @@ -96,14 +96,13 @@ require ( go.opentelemetry.io/otel/sdk v1.39.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect go.opentelemetry.io/otel/trace v1.39.0 // indirect - golang.org/x/arch v0.24.0 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect + golang.org/x/arch v0.25.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/text v0.35.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.2 // indirect - mvdan.cc/sh/v3 v3.12.0 // indirect + mvdan.cc/sh/v3 v3.13.0 // indirect ) // IT IS FORBIDDEN TO USE REPLACE DIRECTIVES diff --git a/go.sum b/go.sum index a6bac0e88..507835333 100644 --- a/go.sum +++ b/go.sum @@ -5,12 +5,12 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/VictoriaMetrics/metrics v1.41.2 h1:pLQ4Mw9TqXFq3ZsZVJkz88JHpjL9LY5NHTY3v2gBNAw= -github.com/VictoriaMetrics/metrics v1.41.2/go.mod h1:xDM82ULLYCYdFRgQ2JBxi8Uf1+8En1So9YUwlGTOqTc= +github.com/VictoriaMetrics/metrics v1.42.0 h1:t/OGs3BjMUYhxw/h83Z28qAss8DuA4QEVwO4NwJ9hZc= +github.com/VictoriaMetrics/metrics v1.42.0/go.mod h1:xDM82ULLYCYdFRgQ2JBxi8Uf1+8En1So9YUwlGTOqTc= github.com/bitfield/script v0.24.1 h1:D4ZWu72qWL/at0rXFF+9xgs17VwyrpT6PkkBTdEz9xU= github.com/bitfield/script v0.24.1/go.mod h1:fv+6x4OzVsRs6qAlc7wiGq8fq1b5orhtQdtW0dwjUHI= -github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= -github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= +github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= @@ -53,8 +53,8 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -84,8 +84,8 @@ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -170,10 +170,10 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/skycoin/noise v0.0.0-20180327030543-2492fe189ae6 h1:1Nc5EBY6pjfw1kwW0duwyG+7WliWz5u9kgk1h5MnLuA= github.com/skycoin/noise v0.0.0-20180327030543-2492fe189ae6/go.mod h1:UXghlricA7J3aRD/k7p/zBObQfmBawwCxIVPVjz2Q3o= -github.com/skycoin/skycoin v0.28.3 h1:lakAtKaT1T0jKcIynov0oK/jfeUPrDR9M+kqwl+GjPs= -github.com/skycoin/skycoin v0.28.3/go.mod h1:FxRzVp/LzhrJXdPSDxVr1AUTp0v36PwwPNTvEVxrVpI= -github.com/skycoin/skywire v1.3.35-0.20260222235451-f11c46808634 h1:RkIZqoeTr2z00NUc0ruJgac1jaAZm7+Po3BiVc29VEY= -github.com/skycoin/skywire v1.3.35-0.20260222235451-f11c46808634/go.mod h1:5+Wk6fNmovhPwoPkptN4Kxh2lvsVcpoH3SOrytminYg= +github.com/skycoin/skycoin v0.28.5-alpha1.0.20260323015226-90b668188f85 h1:IiU8PjIzg/BlTpSXXgq1pVCEbKnDfGMnUAqgwwjNt4s= +github.com/skycoin/skycoin v0.28.5-alpha1.0.20260323015226-90b668188f85/go.mod h1:tgVxjBBV4/OxVBDrcpsVK0q/awGxqBjwTUPDBMh9ZcA= +github.com/skycoin/skywire v1.3.37 h1:LIgOrj6PqdH6RAOWsD8TSI/vTyp7kUYE2Ale6pkvjJw= +github.com/skycoin/skywire v1.3.37/go.mod h1:k3TA1edIXR96Jtec5XYVy7EGHQlZL524pCPYRTzBBok= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= @@ -202,8 +202,8 @@ github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= -github.com/xtaci/smux v1.5.56 h1:Eyv/dUULmkGZZNucLUisnkzJ/4UQ5YZTschhugFBM0U= -github.com/xtaci/smux v1.5.56/go.mod h1:IGQ9QYrBphmb/4aTnLEcJby0TNr3NV+OslIOMrX825Q= +github.com/xtaci/smux v1.5.57 h1:N72VbGoSYxgcm6mPOYX0QzEZNVD3UI/JlVvAtXF+WrY= +github.com/xtaci/smux v1.5.57/go.mod h1:IGQ9QYrBphmb/4aTnLEcJby0TNr3NV+OslIOMrX825Q= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -229,34 +229,34 @@ go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzK go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y= -golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE= +golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200428200454-593003d681fa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s= google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -270,5 +270,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI= -mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg= +mvdan.cc/sh/v3 v3.13.0 h1:dSfq/MVsY4w0Vsi6Lbs0IcQquMVqLdKLESAOZjuHdLg= +mvdan.cc/sh/v3 v3.13.0/go.mod h1:KV1GByGPc/Ho0X1E6Uz9euhsIQEj4hwyKnodLlFLoDM= diff --git a/vendor/github.com/VictoriaMetrics/metrics/histogram.go b/vendor/github.com/VictoriaMetrics/metrics/histogram.go index f2257bc0f..8a5ee9e13 100644 --- a/vendor/github.com/VictoriaMetrics/metrics/histogram.go +++ b/vendor/github.com/VictoriaMetrics/metrics/histogram.go @@ -266,5 +266,13 @@ func (h *Histogram) getSum() float64 { } func (h *Histogram) metricType() string { - return "histogram" + // The Prometheus data model requires histogram metrics to expose "le" labels. + // Some collectors, such as the OpenTelemetry (OTEL) Collector, strictly enforce + // this data model and apply transformations based on the metric type. + // + // Because Prometheus metric types are strongly typed and we don't have control over it, + // introducing a custom "vm_histogram" type is not possible. + // + // So it's better to use untyped metric type. + return "untyped" } diff --git a/vendor/github.com/VictoriaMetrics/metrics/metrics.go b/vendor/github.com/VictoriaMetrics/metrics/metrics.go index fc121f81b..0525ddcfe 100644 --- a/vendor/github.com/VictoriaMetrics/metrics/metrics.go +++ b/vendor/github.com/VictoriaMetrics/metrics/metrics.go @@ -13,6 +13,7 @@ package metrics import ( + "bytes" "fmt" "io" "sort" @@ -42,6 +43,11 @@ func init() { var ( registeredSets = make(map[*Set]struct{}) registeredSetsLock sync.Mutex + bufPool = sync.Pool{ + New: func() any { + return bytes.NewBuffer(make([]byte, 0, 64*1024)) + }, + } ) // RegisterSet registers the given set s for metrics export via global WritePrometheus() call. @@ -248,6 +254,23 @@ func WriteProcessMetrics(w io.Writer) { writePushMetrics(w) } +// WriteGoMetrics writes Go runtime metrics to w. +// This includes runtime/metrics such as memory stats, GC stats, goroutine counts, etc. +func WriteGoMetrics(w io.Writer) { + writeGoMetrics(w) +} + +// WriteProcMetrics writes OS-level process metrics to w by reading +// the /proc filesystem (CPU, memory, file descriptors, PSI, etc.). +func WriteProcMetrics(w io.Writer) { + writeProcessMetrics(w) +} + +// WritePushMetrics writes push-mode related metrics to w. +func WritePushMetrics(w io.Writer) { + writePushMetrics(w) +} + // WriteFDMetrics writes `process_max_fds` and `process_open_fds` metrics to w. func WriteFDMetrics(w io.Writer) { writeFDMetrics(w) diff --git a/vendor/github.com/VictoriaMetrics/metrics/set.go b/vendor/github.com/VictoriaMetrics/metrics/set.go index b8b81b92c..b75d208d5 100644 --- a/vendor/github.com/VictoriaMetrics/metrics/set.go +++ b/vendor/github.com/VictoriaMetrics/metrics/set.go @@ -35,7 +35,10 @@ func NewSet() *Set { // WritePrometheus writes all the metrics from s to w in Prometheus format. func (s *Set) WritePrometheus(w io.Writer) { // Collect all the metrics in in-memory buffer in order to prevent from long locking due to slow w. - var bb bytes.Buffer + bb := bufPool.Get().(*bytes.Buffer) + bb.Reset() + defer bufPool.Put(bb) + lessFunc := func(i, j int) bool { // the sorting must be stable. // see edge cases why we can't simply do `s.a[i].name < s.a[j].name` here: @@ -77,7 +80,7 @@ func (s *Set) WritePrometheus(w io.Writer) { if !isMetadataEnabled() { // Call marshalTo without the global lock, since certain metric types such as Gauge // can call a callback, which, in turn, can try calling s.mu.Lock again. - nm.metric.marshalTo(nm.name, &bb) + nm.metric.marshalTo(nm.name, bb) continue } @@ -93,7 +96,7 @@ func (s *Set) WritePrometheus(w io.Writer) { if metricFamily != prevMetricFamily { // write metadata only once per metric family metricType := nm.metric.metricType() - writeMetadata(&bb, metricFamily, metricType) + writeMetadata(bb, metricFamily, metricType) prevMetricFamily = metricFamily } bb.Write(metricsWithMetadataBuf.Bytes()) diff --git a/vendor/github.com/bytedance/gopkg/lang/dirtmake/bytes.go b/vendor/github.com/bytedance/gopkg/lang/dirtmake/bytes.go index 1daa27904..d539b3b2f 100644 --- a/vendor/github.com/bytedance/gopkg/lang/dirtmake/bytes.go +++ b/vendor/github.com/bytedance/gopkg/lang/dirtmake/bytes.go @@ -12,6 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build !tinygo + +// Bytes provides optimized memory allocation using runtime.mallocgc. +// This implementation is excluded from TinyGo builds (including WASM environments) +// because TinyGo doesn't support the same unsafe operations and runtime symbols +// as standard Go. For TinyGo compatibility, use the implementation in bytes_tinygo.go. + package dirtmake import ( diff --git a/vendor/github.com/bytedance/gopkg/lang/dirtmake/bytes_tinygo.go b/vendor/github.com/bytedance/gopkg/lang/dirtmake/bytes_tinygo.go new file mode 100644 index 000000000..3d90240c7 --- /dev/null +++ b/vendor/github.com/bytedance/gopkg/lang/dirtmake/bytes_tinygo.go @@ -0,0 +1,24 @@ +// Copyright 2024 ByteDance Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build tinygo + +package dirtmake + +// Bytes allocates a byte slice for TinyGo environment. +// Uses make() to avoid go:linkname with runtime.mallocgc, which causes +// "undefined symbol: runtime.mallocgc" errors in TinyGo. +func Bytes(len, cap int) (b []byte) { + return make([]byte, len, cap) +} diff --git a/vendor/github.com/fatih/color/color.go b/vendor/github.com/fatih/color/color.go index ee39b408e..d3906bfbd 100644 --- a/vendor/github.com/fatih/color/color.go +++ b/vendor/github.com/fatih/color/color.go @@ -19,15 +19,15 @@ var ( // set (regardless of its value). This is a global option and affects all // colors. For more control over each color block use the methods // DisableColor() individually. - NoColor = noColorIsSet() || os.Getenv("TERM") == "dumb" || - (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) + NoColor = noColorIsSet() || os.Getenv("TERM") == "dumb" || !stdoutIsTerminal() // Output defines the standard output of the print functions. By default, - // os.Stdout is used. - Output = colorable.NewColorableStdout() + // stdOut() is used. + Output = stdOut() - // Error defines a color supporting writer for os.Stderr. - Error = colorable.NewColorableStderr() + // Error defines the standard error of the print functions. By default, + // stdErr() is used. + Error = stdErr() // colorsCache is used to reduce the count of created Color objects and // allows to reuse already created objects with required Attribute. @@ -40,6 +40,33 @@ func noColorIsSet() bool { return os.Getenv("NO_COLOR") != "" } +// stdoutIsTerminal returns true if os.Stdout is a terminal. +// Returns false if os.Stdout is nil (e.g., when running as a Windows service). +func stdoutIsTerminal() bool { + if os.Stdout == nil { + return false + } + return isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) +} + +// stdOut returns a writer for color output. +// Returns io.Discard if os.Stdout is nil (e.g., when running as a Windows service). +func stdOut() io.Writer { + if os.Stdout == nil { + return io.Discard + } + return colorable.NewColorableStdout() +} + +// stdErr returns a writer for color error output. +// Returns io.Discard if os.Stderr is nil (e.g., when running as a Windows service). +func stdErr() io.Writer { + if os.Stderr == nil { + return io.Discard + } + return colorable.NewColorableStderr() +} + // Color defines a custom color object which is defined by SGR parameters. type Color struct { params []Attribute @@ -220,26 +247,30 @@ func (c *Color) unset() { // a low-level function, and users should use the higher-level functions, such // as color.Fprint, color.Print, etc. func (c *Color) SetWriter(w io.Writer) *Color { + _, _ = c.setWriter(w) + return c +} + +func (c *Color) setWriter(w io.Writer) (int, error) { if c.isNoColorSet() { - return c + return 0, nil } - fmt.Fprint(w, c.format()) - return c + return fmt.Fprint(w, c.format()) } // UnsetWriter resets all escape attributes and clears the output with the give // io.Writer. Usually should be called after SetWriter(). func (c *Color) UnsetWriter(w io.Writer) { - if c.isNoColorSet() { - return - } + _, _ = c.unsetWriter(w) +} - if NoColor { - return +func (c *Color) unsetWriter(w io.Writer) (int, error) { + if c.isNoColorSet() { + return 0, nil } - fmt.Fprintf(w, "%s[%dm", escape, Reset) + return fmt.Fprintf(w, "%s[%dm", escape, Reset) } // Add is used to chain SGR parameters. Use as many as parameters to combine @@ -255,10 +286,20 @@ func (c *Color) Add(value ...Attribute) *Color { // On Windows, users should wrap w with colorable.NewColorable() if w is of // type *os.File. func (c *Color) Fprint(w io.Writer, a ...interface{}) (n int, err error) { - c.SetWriter(w) - defer c.UnsetWriter(w) + n, err = c.setWriter(w) + if err != nil { + return n, err + } + + nn, err := fmt.Fprint(w, a...) + n += nn + if err != nil { + return + } - return fmt.Fprint(w, a...) + nn, err = c.unsetWriter(w) + n += nn + return n, err } // Print formats using the default formats for its operands and writes to @@ -278,10 +319,20 @@ func (c *Color) Print(a ...interface{}) (n int, err error) { // On Windows, users should wrap w with colorable.NewColorable() if w is of // type *os.File. func (c *Color) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { - c.SetWriter(w) - defer c.UnsetWriter(w) + n, err = c.setWriter(w) + if err != nil { + return n, err + } + + nn, err := fmt.Fprintf(w, format, a...) + n += nn + if err != nil { + return + } - return fmt.Fprintf(w, format, a...) + nn, err = c.unsetWriter(w) + n += nn + return n, err } // Printf formats according to a format specifier and writes to standard output. @@ -475,27 +526,24 @@ func (c *Color) Equals(c2 *Color) bool { if c == nil || c2 == nil { return false } + if len(c.params) != len(c2.params) { return false } + counts := make(map[Attribute]int, len(c.params)) for _, attr := range c.params { - if !c2.attrExists(attr) { - return false - } + counts[attr]++ } - return true -} - -func (c *Color) attrExists(a Attribute) bool { - for _, attr := range c.params { - if attr == a { - return true + for _, attr := range c2.params { + if counts[attr] == 0 { + return false } + counts[attr]-- } - return false + return true } func boolPtr(v bool) *bool { diff --git a/vendor/github.com/fatih/color/color_windows.go b/vendor/github.com/fatih/color/color_windows.go index be01c558e..97e5a765a 100644 --- a/vendor/github.com/fatih/color/color_windows.go +++ b/vendor/github.com/fatih/color/color_windows.go @@ -9,6 +9,9 @@ import ( func init() { // Opt-in for ansi color support for current process. // https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#output-sequences + if os.Stdout == nil { + return + } var outMode uint32 out := windows.Handle(os.Stdout.Fd()) if err := windows.GetConsoleMode(out, &outMode); err != nil { diff --git a/vendor/github.com/goccy/go-json/internal/encoder/code.go b/vendor/github.com/goccy/go-json/internal/encoder/code.go index 5b08faefc..fec45a4b8 100644 --- a/vendor/github.com/goccy/go-json/internal/encoder/code.go +++ b/vendor/github.com/goccy/go-json/internal/encoder/code.go @@ -518,6 +518,7 @@ func (c *StructCode) ToAnonymousOpcode(ctx *compileContext) Opcodes { prevField = firstField codes = codes.Add(fieldCodes...) } + ctx.structTypeToCodes[uintptr(unsafe.Pointer(c.typ))] = codes return codes } diff --git a/vendor/github.com/skycoin/skycoin/src/cipher/base58/base58_old.go b/vendor/github.com/skycoin/skycoin/src/cipher/base58/base58_old.go deleted file mode 100644 index 0c321c410..000000000 --- a/vendor/github.com/skycoin/skycoin/src/cipher/base58/base58_old.go +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright 2011 ThePiachu. All rights reserved. -// Copyright 2019 gz-c, Skycoin developers. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package base58 - -import ( - "errors" - "fmt" - "math/big" -) - -// alphabet used by Bitcoins -var alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" - -var ( - // errInvalidBase58Char Invalid base58 character - errInvalidBase58Char = errors.New("Invalid base58 character") - // errInvalidBase58String Invalid base58 string - errInvalidBase58String = errors.New("Invalid base58 string") - // errInvalidBase58Length Invalid base58 length - errInvalidBase58Length = errors.New("base58 invalid length") -) - -// oldBase58 type to hold the oldBase58 string -type oldBase58 string - -// reverse alphabet used for quckly converting base58 strings into numbers -var revalp = map[string]int{ - "1": 0, "2": 1, "3": 2, "4": 3, "5": 4, "6": 5, "7": 6, "8": 7, "9": 8, "A": 9, - "B": 10, "C": 11, "D": 12, "E": 13, "F": 14, "G": 15, "H": 16, "J": 17, "K": 18, "L": 19, - "M": 20, "N": 21, "P": 22, "Q": 23, "R": 24, "S": 25, "T": 26, "U": 27, "V": 28, "W": 29, - "X": 30, "Y": 31, "Z": 32, "a": 33, "b": 34, "c": 35, "d": 36, "e": 37, "f": 38, "g": 39, - "h": 40, "i": 41, "j": 42, "k": 43, "m": 44, "n": 45, "o": 46, "p": 47, "q": 48, "r": 49, - "s": 50, "t": 51, "u": 52, "v": 53, "w": 54, "x": 55, "y": 56, "z": 57, -} - -// oldHex2Big converts hex to big -func oldHex2Big(b []byte) *big.Int { - answer := big.NewInt(0) - - for i := 0; i < len(b); i++ { - answer.Lsh(answer, 8) - answer.Add(answer, big.NewInt(int64(b[i]))) - } - - return answer -} - -// ToBig convert base58 to big.Int -func (b oldBase58) ToBig() (*big.Int, error) { - answer := new(big.Int) - for i := 0; i < len(b); i++ { - answer.Mul(answer, big.NewInt(58)) //multiply current value by 58 - c, ok := revalp[string(b[i:i+1])] - if !ok { - return nil, errInvalidBase58Char - } - answer.Add(answer, big.NewInt(int64(c))) //add value of the current letter - } - return answer, nil -} - -// ToInt converts base58 to int -func (b oldBase58) ToInt() (int, error) { - answer := 0 - for i := 0; i < len(b); i++ { - answer *= 58 //multiply current value by 58 - c, ok := revalp[string(b[i:i+1])] - if !ok { - return 0, errInvalidBase58Char - } - answer += c //add value of the current letter - } - return answer, nil -} - -// ToHex converts base58 to hex bytes -func (b oldBase58) ToHex() ([]byte, error) { - value, err := b.ToBig() //convert to big.Int - if err != nil { - return nil, err - } - oneCount := 0 - bs := string(b) - if len(bs) == 0 { - return nil, fmt.Errorf("%v - len(bs) == 0", errInvalidBase58String) - } - for bs[oneCount] == '1' { - oneCount++ - if oneCount >= len(bs) { - return nil, fmt.Errorf("%v - oneCount >= len(bs)", errInvalidBase58String) - } - } - //convert big.Int to bytes - return append(make([]byte, oneCount), value.Bytes()...), nil -} - -// Base582Big converts base58 to big -func (b oldBase58) Base582Big() (*big.Int, error) { - answer := new(big.Int) - for i := 0; i < len(b); i++ { - answer.Mul(answer, big.NewInt(58)) //multiply current value by 58 - c, ok := revalp[string(b[i:i+1])] - if !ok { - return nil, errInvalidBase58Char - } - answer.Add(answer, big.NewInt(int64(c))) //add value of the current letter - } - return answer, nil -} - -// Base582Int converts base58 to int -func (b oldBase58) Base582Int() (int, error) { - answer := 0 - for i := 0; i < len(b); i++ { - answer *= 58 //multiply current value by 58 - c, ok := revalp[string(b[i:i+1])] - if !ok { - return 0, errInvalidBase58Char - } - answer += c //add value of the current letter - } - return answer, nil -} - -// oldBase582Hex converts base58 to hex bytes -func oldBase582Hex(b string) ([]byte, error) { - return oldBase58(b).ToHex() -} - -// BitHex converts base58 to hexes used by Bitcoins (keeping the zeroes on the front, 25 bytes long) -func (b oldBase58) BitHex() ([]byte, error) { - value, err := b.ToBig() //convert to big.Int - if err != nil { - return nil, err - } - - tmp := value.Bytes() //convert to hex bytes - if len(tmp) == 25 { //if it is exactly 25 bytes, return - return tmp, nil - } else if len(tmp) > 25 { //if it is longer than 25, return nothing - return nil, errInvalidBase58Length - } - answer := make([]byte, 25) //make 25 byte container - for i := 0; i < len(tmp); i++ { //copy converted bytes - answer[24-i] = tmp[len(tmp)-1-i] - } - return answer, nil -} - -// oldBig2Base58 encodes big.Int to base58 string -func oldBig2Base58(val *big.Int) oldBase58 { - answer := "" - valCopy := new(big.Int).Abs(val) //copies big.Int - - if val.Cmp(big.NewInt(0)) <= 0 { //if it is less than 0, returns empty string - return oldBase58("") - } - - tmpStr := "" - tmp := new(big.Int) - for valCopy.Cmp(big.NewInt(0)) > 0 { //converts the number into base58 - tmp.Mod(valCopy, big.NewInt(58)) //takes modulo 58 value - valCopy.Div(valCopy, big.NewInt(58)) //divides the rest by 58 - tmpStr += alphabet[tmp.Int64() : tmp.Int64()+1] //encodes - } - for i := (len(tmpStr) - 1); i > -1; i-- { - answer += tmpStr[i : i+1] //reverses the order - } - return oldBase58(answer) //returns -} - -// oldHex2Base58 encodes hex bytes into base58 -func oldHex2Base58(val []byte) oldBase58 { - tmp := oldBig2Base58(oldHex2Big(val)) //encoding of the number without zeroes in front - - //looking for zeros at the beginning - i := 0 - for i = 0; val[i] == 0 && i < len(val); i++ { //nolint:revive - } - answer := "" - for j := 0; j < i; j++ { //adds zeroes from the front - answer += alphabet[0:1] - } - answer += string(tmp) //concatenates - - return oldBase58(answer) //returns -} diff --git a/vendor/github.com/skycoin/skycoin/src/cipher/bech32/bech32.go b/vendor/github.com/skycoin/skycoin/src/cipher/bech32/bech32.go new file mode 100644 index 000000000..4f54929de --- /dev/null +++ b/vendor/github.com/skycoin/skycoin/src/cipher/bech32/bech32.go @@ -0,0 +1,212 @@ +// Package bech32 implements BIP173 bech32 encoding for segwit addresses. +package bech32 + +import ( + "fmt" + "strings" +) + +const charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + +var charsetRev = [128]int8{ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, +} + +func polymod(values []int) uint32 { + gen := [5]uint32{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3} + chk := uint32(1) + for _, v := range values { + b := chk >> 25 + chk = ((chk & 0x1ffffff) << 5) ^ uint32(v) //nolint:gosec // v is a 5-bit value (0-31) + for i := 0; i < 5; i++ { + if (b>>uint(i))&1 == 1 { //nolint:gosec // i is 0-4 + chk ^= gen[i] + } + } + } + return chk +} + +func hrpExpand(hrp string) []int { + ret := make([]int, 0, len(hrp)*2+1) + for _, c := range hrp { + ret = append(ret, int(c>>5)) + } + ret = append(ret, 0) + for _, c := range hrp { + ret = append(ret, int(c&31)) + } + return ret +} + +func verifyChecksum(hrp string, data []int) bool { + values := append(hrpExpand(hrp), data...) + return polymod(values) == 1 +} + +func createChecksum(hrp string, data []int) []int { + values := append(hrpExpand(hrp), data...) + values = append(values, 0, 0, 0, 0, 0, 0) + mod := polymod(values) ^ 1 + ret := make([]int, 6) + for i := 0; i < 6; i++ { + ret[i] = int((mod >> uint(5*(5-i))) & 31) //nolint:gosec // result fits in int + } + return ret +} + +// Encode encodes a bech32 string from HRP and 5-bit data values. +func Encode(hrp string, data []int) (string, error) { + chk := createChecksum(hrp, data) + combined := append(data, chk...) + + var ret strings.Builder + ret.WriteString(hrp) + ret.WriteByte('1') + for _, d := range combined { + if d < 0 || d >= 32 { + return "", fmt.Errorf("invalid data value: %d", d) + } + ret.WriteByte(charset[d]) + } + return ret.String(), nil +} + +// Decode decodes a bech32 string into its HRP and 5-bit data values. +func Decode(s string) (string, []int, error) { + if len(s) > 90 { + return "", nil, fmt.Errorf("bech32 string too long: %d", len(s)) + } + + // Find separator + pos := strings.LastIndex(s, "1") + if pos < 1 || pos+7 > len(s) { + return "", nil, fmt.Errorf("invalid bech32 separator position") + } + + hrp := strings.ToLower(s[:pos]) + dataStr := strings.ToLower(s[pos+1:]) + + data := make([]int, len(dataStr)) + for i, c := range dataStr { + if c >= 128 || charsetRev[c] == -1 { + return "", nil, fmt.Errorf("invalid bech32 character: %c", c) + } + data[i] = int(charsetRev[c]) + } + + if !verifyChecksum(hrp, data) { + return "", nil, fmt.Errorf("invalid bech32 checksum") + } + + return hrp, data[:len(data)-6], nil +} + +// ConvertBits converts a byte slice from one bit grouping to another. +func ConvertBits(data []byte, fromBits, toBits uint8, pad bool) ([]byte, error) { + acc := 0 + bits := uint8(0) + maxv := (1 << toBits) - 1 + var ret []byte + + for _, value := range data { + if value>>fromBits != 0 { + return nil, fmt.Errorf("invalid data: value %d exceeds %d bits", value, fromBits) + } + acc = (acc << fromBits) | int(value) + bits += fromBits + for bits >= toBits { + bits -= toBits + ret = append(ret, byte((acc>>bits)&maxv)) + } + } + + if pad { + if bits > 0 { + ret = append(ret, byte((acc<<(toBits-bits))&maxv)) + } + } else if bits >= fromBits { + return nil, fmt.Errorf("invalid padding") + } else if (acc<<(toBits-bits))&maxv != 0 { + return nil, fmt.Errorf("non-zero padding") + } + + return ret, nil +} + +// SegwitEncode encodes a segwit address from HRP, witness version, and witness program. +func SegwitEncode(hrp string, version byte, program []byte) (string, error) { + if version > 16 { + return "", fmt.Errorf("invalid witness version: %d", version) + } + if len(program) < 2 || len(program) > 40 { + return "", fmt.Errorf("invalid witness program length: %d", len(program)) + } + if version == 0 && len(program) != 20 && len(program) != 32 { + return "", fmt.Errorf("invalid witness v0 program length: %d", len(program)) + } + + conv, err := ConvertBits(program, 8, 5, true) + if err != nil { + return "", err + } + + data := append([]int{int(version)}, intsFromBytes(conv)...) + return Encode(hrp, data) +} + +// SegwitDecode decodes a segwit address into witness version and program. +func SegwitDecode(hrp, addr string) (byte, []byte, error) { + decHRP, data, err := Decode(addr) + if err != nil { + return 0, nil, err + } + if decHRP != hrp { + return 0, nil, fmt.Errorf("HRP mismatch: expected %q, got %q", hrp, decHRP) + } + if len(data) < 1 { + return 0, nil, fmt.Errorf("empty data") + } + + version := byte(data[0]) + if version > 16 { + return 0, nil, fmt.Errorf("invalid witness version: %d", version) + } + + program, err := ConvertBits(bytesFromInts(data[1:]), 5, 8, false) + if err != nil { + return 0, nil, err + } + + if len(program) < 2 || len(program) > 40 { + return 0, nil, fmt.Errorf("invalid witness program length: %d", len(program)) + } + if version == 0 && len(program) != 20 && len(program) != 32 { + return 0, nil, fmt.Errorf("invalid witness v0 program length: %d", len(program)) + } + + return version, program, nil +} + +func intsFromBytes(b []byte) []int { + r := make([]int, len(b)) + for i, v := range b { + r[i] = int(v) + } + return r +} + +func bytesFromInts(ints []int) []byte { + b := make([]byte, len(ints)) + for i, v := range ints { + b[i] = byte(v) + } + return b +} diff --git a/vendor/github.com/skycoin/skycoin/src/cipher/bitcoin_segwit.go b/vendor/github.com/skycoin/skycoin/src/cipher/bitcoin_segwit.go new file mode 100644 index 000000000..5f8aaea8e --- /dev/null +++ b/vendor/github.com/skycoin/skycoin/src/cipher/bitcoin_segwit.go @@ -0,0 +1,115 @@ +package cipher + +import ( + "fmt" + "log" + "strings" + + "github.com/skycoin/skycoin/src/cipher/bech32" +) + +// BitcoinSegwitAddress is a native segwit (P2WPKH) bitcoin address (bc1q...). +type BitcoinSegwitAddress struct { + Version byte // Witness version (0 for P2WPKH) + Key Ripemd160 // 20-byte witness program (hash160 of compressed pubkey) +} + +// BitcoinSegwitAddressFromPubKey creates a P2WPKH address from a compressed public key. +// The witness program is RIPEMD160(SHA256(pubkey)), same as P2PKH but encoded as bech32. +func BitcoinSegwitAddressFromPubKey(pubKey PubKey) BitcoinSegwitAddress { + return BitcoinSegwitAddress{ + Version: 0, + Key: BitcoinPubKeyRipemd160(pubKey), + } +} + +// BitcoinSegwitAddressFromSecKey generates a BitcoinSegwitAddress from SecKey. +func BitcoinSegwitAddressFromSecKey(secKey SecKey) (BitcoinSegwitAddress, error) { + p, err := PubKeyFromSecKey(secKey) + if err != nil { + return BitcoinSegwitAddress{}, err + } + return BitcoinSegwitAddressFromPubKey(p), nil +} + +// MustBitcoinSegwitAddressFromSecKey generates a BitcoinSegwitAddress from SecKey, panics on error. +func MustBitcoinSegwitAddressFromSecKey(secKey SecKey) BitcoinSegwitAddress { + return BitcoinSegwitAddressFromPubKey(MustPubKeyFromSecKey(secKey)) +} + +// DecodeBech32BitcoinAddress decodes a bech32 segwit address string into a BitcoinSegwitAddress. +func DecodeBech32BitcoinAddress(addr string) (BitcoinSegwitAddress, error) { + hrp := "bc" + if strings.HasPrefix(strings.ToLower(addr), "tb1") { + hrp = "tb" + } + + version, program, err := bech32.SegwitDecode(hrp, addr) + if err != nil { + return BitcoinSegwitAddress{}, fmt.Errorf("invalid bech32 address: %w", err) + } + + if version != 0 { + return BitcoinSegwitAddress{}, fmt.Errorf("unsupported witness version: %d", version) + } + if len(program) != 20 { + return BitcoinSegwitAddress{}, fmt.Errorf("invalid P2WPKH program length: %d (expected 20)", len(program)) + } + + a := BitcoinSegwitAddress{Version: version} + copy(a.Key[:], program) + return a, nil +} + +// MustDecodeBech32BitcoinAddress decodes a bech32 segwit address, panics on error. +func MustDecodeBech32BitcoinAddress(addr string) BitcoinSegwitAddress { + a, err := DecodeBech32BitcoinAddress(addr) + if err != nil { + log.Panicf("Invalid bech32 bitcoin address %s: %v", addr, err) + } + return a +} + +// Null returns true if the address is null. +func (addr BitcoinSegwitAddress) Null() bool { + return addr == BitcoinSegwitAddress{} +} + +// Bytes returns the raw witness program with version prefix. +func (addr BitcoinSegwitAddress) Bytes() []byte { + b := make([]byte, 1+20) + b[0] = addr.Version + copy(b[1:], addr.Key[:]) + return b +} + +// Verify checks that the address is valid for the given public key. +func (addr BitcoinSegwitAddress) Verify(key PubKey) error { + if addr.Version != 0 { + return ErrAddressInvalidVersion + } + if addr.Key != BitcoinPubKeyRipemd160(key) { + return ErrAddressInvalidPubKey + } + return nil +} + +// String returns the bech32-encoded address (bc1q...). +func (addr BitcoinSegwitAddress) String() string { + s, err := bech32.SegwitEncode("bc", addr.Version, addr.Key[:]) + if err != nil { + log.Panicf("BitcoinSegwitAddress.String failed: %v", err) + } + return s +} + +// Checksum returns the first 4 bytes of SHA256(version+key). +// This satisfies the Addresser interface. For bech32 addresses, the real +// checksum is embedded in the bech32 encoding, but this provides compatibility. +func (addr BitcoinSegwitAddress) Checksum() Checksum { + r := append([]byte{addr.Version}, addr.Key[:]...) + h := SumSHA256(r) + c := Checksum{} + copy(c[:], h[:4]) + return c +} diff --git a/vendor/github.com/skycoin/skywire/deployment/dmsghttp-config.json b/vendor/github.com/skycoin/skywire/deployment/dmsghttp-config.json index 1bc7d8b0d..ccc6d44e3 100644 --- a/vendor/github.com/skycoin/skywire/deployment/dmsghttp-config.json +++ b/vendor/github.com/skycoin/skywire/deployment/dmsghttp-config.json @@ -2,15 +2,15 @@ "test": { "dmsg_servers": [ { - "static": "0326978f5a53aff537dbb47fed58b1f123af3b00132d365f1309a14db4168dcff7", + "static": "02a49bc0aa1b5b78f638e9189be4ed095bac5d6839c828465a8350f80ac07629c0", "server": { - "address": "70.121.13.123:9082" + "address": "45.79.213.251:30080" } }, { - "static": "024716428e6315d954356e9ad72bea32bb2b41aab5a54a9b5cb4313964016e64d8", + "static": "0326978f5a53aff537dbb47fed58b1f123af3b00132d365f1309a14db4168dcff7", "server": { - "address": "45.79.213.251:30080" + "address": "70.121.13.123:9082" } } ], @@ -24,39 +24,39 @@ "prod": { "dmsg_servers": [ { - "static": "0371ab4bcff7b121f4b91f6856d6740c6f9dc1fe716977850aeb5d84378b300a13", + "static": "0281a102c82820e811368c8d028cf11b1a985043b726b1bcdb8fce89b27384b2cb", "server": { - "address": "172.105.179.5:30087" + "address": "139.162.160.227:30086" } }, { - "static": "0326978f5a53aff537dbb47fed58b1f123af3b00132d365f1309a14db4168dcff7", + "static": "0371ab4bcff7b121f4b91f6856d6740c6f9dc1fe716977850aeb5d84378b300a13", "server": { - "address": "70.121.13.123:9083" + "address": "139.162.160.227:30087" } }, { - "static": "0281a102c82820e811368c8d028cf11b1a985043b726b1bcdb8fce89b27384b2cb", + "static": "02a49bc0aa1b5b78f638e9189be4ed095bac5d6839c828465a8350f80ac07629c0", "server": { - "address": "172.104.166.8:30086" + "address": "139.162.160.227:30081" } }, { - "static": "03717576ada5b1744e395c66c2bb11cea73b0e23d0dcd54422139b1a7f12e962c4", + "static": "0326978f5a53aff537dbb47fed58b1f123af3b00132d365f1309a14db4168dcff7", "server": { - "address": "139.162.173.101:30083" + "address": "70.121.13.123:9083" } }, { - "static": "02a49bc0aa1b5b78f638e9189be4ed095bac5d6839c828465a8350f80ac07629c0", + "static": "02a2d4c346dabd165fd555dfdba4a7f4d18786fe7e055e562397cd5102bdd7f8dd", "server": { - "address": "74.207.231.164:30081" + "address": "139.162.173.101:30082" } }, { - "static": "02a2d4c346dabd165fd555dfdba4a7f4d18786fe7e055e562397cd5102bdd7f8dd", + "static": "03717576ada5b1744e395c66c2bb11cea73b0e23d0dcd54422139b1a7f12e962c4", "server": { - "address": "139.162.173.101:30082" + "address": "139.162.173.101:30083" } } ], diff --git a/vendor/github.com/skycoin/skywire/deployment/services-config.json b/vendor/github.com/skycoin/skywire/deployment/services-config.json index 1e44e3f19..ad54bf252 100644 --- a/vendor/github.com/skycoin/skywire/deployment/services-config.json +++ b/vendor/github.com/skycoin/skywire/deployment/services-config.json @@ -24,8 +24,8 @@ "uptime_tracker": "http://ut.skywire.dev", "service_discovery": "http://sd.skywire.dev", "stun_servers": [ - "139.162.160.227:3479", - "172.104.145.184:3479" + "139.162.160.227:3478", + "172.104.247.120:3478" ], "dns_server": "1.1.1.1", "survey_whitelist": [ @@ -63,8 +63,8 @@ "uptime_tracker": "http://ut.skywire.skycoin.com", "service_discovery": "http://sd.skycoin.com", "stun_servers": [ - "139.162.160.227:3479", - "172.104.145.184:3479" + "139.162.160.227:3478", + "172.104.247.120:3478" ], "dns_server": "1.1.1.1", "survey_whitelist": [ diff --git a/vendor/github.com/skycoin/skywire/pkg/skywire-utilities/pkg/buildinfo/buildinfo.go b/vendor/github.com/skycoin/skywire/pkg/skywire-utilities/pkg/buildinfo/buildinfo.go index b89d870be..46d4711ec 100644 --- a/vendor/github.com/skycoin/skywire/pkg/skywire-utilities/pkg/buildinfo/buildinfo.go +++ b/vendor/github.com/skycoin/skywire/pkg/skywire-utilities/pkg/buildinfo/buildinfo.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "regexp" + "runtime" "runtime/debug" "strings" ) @@ -138,11 +139,23 @@ func DebugBuildInfo() *debug.BuildInfo { // Get returns a summary of build information. func Get() *Info { + // Build complete version string with commit + ver := Version() + if c := Commit(); c != "" && c != unknown && !strings.Contains(ver, c) { + // Append commit to version if not already present + if len(c) > 12 { + c = c[:12] + } + ver = ver + "-" + c + } + // Note: Go's module system already adds +dirty to version when built from dirty repo return &Info{ - Version: Version(), + Version: ver, Commit: Commit(), Date: Date(), Go: Go(), + OS: runtime.GOOS, + Arch: runtime.GOARCH, } } @@ -166,6 +179,8 @@ type Info struct { Version string `json:"version"` Commit string `json:"commit"` Date string `json:"date"` + OS string `json:"os"` + Arch string `json:"arch"` } // WriteTo writes build info summary to an io.Writer. diff --git a/vendor/github.com/skycoin/skywire/pkg/skywire-utilities/pkg/httputil/health.go b/vendor/github.com/skycoin/skywire/pkg/skywire-utilities/pkg/httputil/health.go index 0b66108e7..09d9f6e2a 100644 --- a/vendor/github.com/skycoin/skywire/pkg/skywire-utilities/pkg/httputil/health.go +++ b/vendor/github.com/skycoin/skywire/pkg/skywire-utilities/pkg/httputil/health.go @@ -13,6 +13,7 @@ var path = "/health" // HealthCheckResponse is struct of /health endpoint type HealthCheckResponse struct { + ServiceName string `json:"service_name,omitempty"` BuildInfo *buildinfo.Info `json:"build_info,omitempty"` StartedAt time.Time `json:"started_at"` DmsgAddr string `json:"dmsg_address,omitempty"` diff --git a/vendor/github.com/skycoin/skywire/pkg/skywire-utilities/pkg/httputil/httputil.go b/vendor/github.com/skycoin/skywire/pkg/skywire-utilities/pkg/httputil/httputil.go index 758362244..170a06656 100644 --- a/vendor/github.com/skycoin/skywire/pkg/skywire-utilities/pkg/httputil/httputil.go +++ b/vendor/github.com/skycoin/skywire/pkg/skywire-utilities/pkg/httputil/httputil.go @@ -3,7 +3,9 @@ package httputil import ( "context" + "errors" "fmt" + "net" "net/http" "strconv" "strings" @@ -19,8 +21,9 @@ var json = jsoniter.ConfigFastest var log = logging.MustGetLogger("httputil") -// WriteJSON writes a json object on a http.ResponseWriter with the given code, -// panics on marshaling error +// WriteJSON writes a json object on a http.ResponseWriter with the given code. +// I/O errors (client disconnect, timeout) are logged but don't cause panics. +// Marshaling errors panic as they indicate a programming error. func WriteJSON(w http.ResponseWriter, r *http.Request, code int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) @@ -36,10 +39,37 @@ func WriteJSON(w http.ResponseWriter, r *http.Request, code int, v interface{}) v = map[string]interface{}{"error": err.Error()} } if err := json.NewEncoder(w).Encode(v); err != nil { + // Check if it's an I/O error (client disconnect, timeout, etc.) + if isIOError(err) { + log.WithError(err).Debug("Failed to write JSON response (client likely disconnected)") + return + } + // Marshaling errors indicate programming bugs, so panic panic(err) } } +// isIOError checks if an error is related to I/O (network issues, timeouts, etc.) +func isIOError(err error) bool { + if err == nil { + return false + } + // Check for common I/O error patterns + errStr := err.Error() + if strings.Contains(errStr, "i/o timeout") || + strings.Contains(errStr, "connection reset") || + strings.Contains(errStr, "broken pipe") || + strings.Contains(errStr, "use of closed network connection") { + return true + } + // Check for net.Error (timeout or temporary) + var netErr net.Error + if ok := errors.As(err, &netErr); ok { + return netErr.Timeout() + } + return false +} + // ReadJSON reads the request body to a json object. func ReadJSON(r *http.Request, v interface{}) error { dec := json.NewDecoder(r.Body) diff --git a/vendor/github.com/xtaci/smux/alloc.go b/vendor/github.com/xtaci/smux/alloc.go index 32629349d..9bf935e67 100644 --- a/vendor/github.com/xtaci/smux/alloc.go +++ b/vendor/github.com/xtaci/smux/alloc.go @@ -29,7 +29,7 @@ import ( var ( defaultAllocator *Allocator - debruijinPos = [...]byte{0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30, 8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31} + debruijnPos = [...]byte{0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30, 8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31} ) func init() { @@ -88,7 +88,7 @@ func (alloc *Allocator) Put(p *[]byte) error { return nil } -// msb return the pos of most significiant bit +// msb returns the pos of most significant bit // http://supertech.csail.mit.edu/papers/debruijn.pdf func msb(size int) byte { v := uint32(size) @@ -97,5 +97,5 @@ func msb(size int) byte { v |= v >> 4 v |= v >> 8 v |= v >> 16 - return debruijinPos[(v*0x07C4ACDD)>>27] + return debruijnPos[(v*0x07C4ACDD)>>27] } diff --git a/vendor/github.com/xtaci/smux/session.go b/vendor/github.com/xtaci/smux/session.go index 65ed5ff43..085547d01 100644 --- a/vendor/github.com/xtaci/smux/session.go +++ b/vendor/github.com/xtaci/smux/session.go @@ -544,7 +544,7 @@ func (s *Session) keepalive() { // shaperLoop implements a priority queue and bandwidth shaping for write requests. // Eg: Control messages are prioritized over data messages, and shaper tries -// it's best to keep fair bandwidth among streams. +// its best to keep fair bandwidth among streams. func (s *Session) shaperLoop() { chShaper := s.shaper diff --git a/vendor/github.com/xtaci/smux/stream.go b/vendor/github.com/xtaci/smux/stream.go index 0a80b8019..ec1177cff 100644 --- a/vendor/github.com/xtaci/smux/stream.go +++ b/vendor/github.com/xtaci/smux/stream.go @@ -577,7 +577,7 @@ func (s *stream) writeV2(b []byte) (n int, err error) { // eg1: uint32(0) - uint32(math.MaxUint32) = 1 // eg2: int32(uint32(0) - uint32(1)) = -1 // - // basicially, you can take it as a MODULAR ARITHMETIC + // basically, you can take it as a MODULAR ARITHMETIC inflight := int32(atomic.LoadUint32(&s.numWritten) - atomic.LoadUint32(&s.peerConsumed)) if inflight < 0 { // security check for malformed data return 0, ErrConsumed diff --git a/vendor/golang.org/x/net/http2/http2.go b/vendor/golang.org/x/net/http2/http2.go index 6320f4eb4..0b99d832f 100644 --- a/vendor/golang.org/x/net/http2/http2.go +++ b/vendor/golang.org/x/net/http2/http2.go @@ -4,13 +4,17 @@ // Package http2 implements the HTTP/2 protocol. // -// This package is low-level and intended to be used directly by very -// few people. Most users will use it indirectly through the automatic -// use by the net/http package (from Go 1.6 and later). -// For use in earlier Go versions see ConfigureServer. (Transport support -// requires Go 1.6 or later) +// Almost no users should need to import this package directly. +// The net/http package supports HTTP/2 natively. // -// See https://http2.github.io/ for more information on HTTP/2. +// To enable or disable HTTP/2 support in net/http clients and servers, see +// [http.Transport.Protocols] and [http.Server.Protocols]. +// +// To configure HTTP/2 parameters, see +// [http.Transport.HTTP2] and [http.Server.HTTP2]. +// +// To create HTTP/1 or HTTP/2 connections, see +// [http.Transport.NewClientConn]. package http2 // import "golang.org/x/net/http2" import ( diff --git a/vendor/golang.org/x/net/http2/server.go b/vendor/golang.org/x/net/http2/server.go index 7ef807f79..65da5175c 100644 --- a/vendor/golang.org/x/net/http2/server.go +++ b/vendor/golang.org/x/net/http2/server.go @@ -164,6 +164,8 @@ type Server struct { // NewWriteScheduler constructs a write scheduler for a connection. // If nil, a default scheduler is chosen. + // + // Deprecated: User-provided write schedulers are deprecated. NewWriteScheduler func() WriteScheduler // CountError, if non-nil, is called on HTTP/2 server errors. diff --git a/vendor/golang.org/x/net/http2/transport.go b/vendor/golang.org/x/net/http2/transport.go index 8cf64b78e..2e9c2f6a5 100644 --- a/vendor/golang.org/x/net/http2/transport.go +++ b/vendor/golang.org/x/net/http2/transport.go @@ -712,10 +712,6 @@ func canRetryError(err error) bool { return true } if se, ok := err.(StreamError); ok { - if se.Code == ErrCodeProtocol && se.Cause == errFromPeer { - // See golang/go#47635, golang/go#42777 - return true - } return se.Code == ErrCodeRefusedStream } return false @@ -3233,10 +3229,6 @@ func (gz *gzipReader) Close() error { return gz.body.Close() } -type errorReader struct{ err error } - -func (r errorReader) Read(p []byte) (int, error) { return 0, r.err } - // isConnectionCloseRequest reports whether req should use its own // connection for a single request and then close the connection. func isConnectionCloseRequest(req *http.Request) bool { diff --git a/vendor/golang.org/x/net/http2/writesched.go b/vendor/golang.org/x/net/http2/writesched.go index 7de27be52..551545f31 100644 --- a/vendor/golang.org/x/net/http2/writesched.go +++ b/vendor/golang.org/x/net/http2/writesched.go @@ -8,6 +8,8 @@ import "fmt" // WriteScheduler is the interface implemented by HTTP/2 write schedulers. // Methods are never called concurrently. +// +// Deprecated: User-provided write schedulers are deprecated. type WriteScheduler interface { // OpenStream opens a new stream in the write scheduler. // It is illegal to call this with streamID=0 or with a streamID that is @@ -38,6 +40,8 @@ type WriteScheduler interface { } // OpenStreamOptions specifies extra options for WriteScheduler.OpenStream. +// +// Deprecated: User-provided write schedulers are deprecated. type OpenStreamOptions struct { // PusherID is zero if the stream was initiated by the client. Otherwise, // PusherID names the stream that pushed the newly opened stream. @@ -47,6 +51,8 @@ type OpenStreamOptions struct { } // FrameWriteRequest is a request to write a frame. +// +// Deprecated: User-provided write schedulers are deprecated. type FrameWriteRequest struct { // write is the interface value that does the writing, once the // WriteScheduler has selected this frame to write. The write diff --git a/vendor/golang.org/x/net/http2/writesched_priority_rfc7540.go b/vendor/golang.org/x/net/http2/writesched_priority_rfc7540.go index 7803a9261..c3d3e9bed 100644 --- a/vendor/golang.org/x/net/http2/writesched_priority_rfc7540.go +++ b/vendor/golang.org/x/net/http2/writesched_priority_rfc7540.go @@ -14,6 +14,8 @@ import ( const priorityDefaultWeightRFC7540 = 15 // 16 = 15 + 1 // PriorityWriteSchedulerConfig configures a priorityWriteScheduler. +// +// Deprecated: User-provided write schedulers are deprecated. type PriorityWriteSchedulerConfig struct { // MaxClosedNodesInTree controls the maximum number of closed streams to // retain in the priority tree. Setting this to zero saves a small amount @@ -55,6 +57,9 @@ type PriorityWriteSchedulerConfig struct { // NewPriorityWriteScheduler constructs a WriteScheduler that schedules // frames by following HTTP/2 priorities as described in RFC 7540 Section 5.3. // If cfg is nil, default options are used. +// +// Deprecated: The RFC 7540 write scheduler has known bugs and performance issues, +// and RFC 7540 prioritization was deprecated in RFC 9113. func NewPriorityWriteScheduler(cfg *PriorityWriteSchedulerConfig) WriteScheduler { return newPriorityWriteSchedulerRFC7540(cfg) } diff --git a/vendor/golang.org/x/net/http2/writesched_random.go b/vendor/golang.org/x/net/http2/writesched_random.go index f2e55e05c..d5d4e2214 100644 --- a/vendor/golang.org/x/net/http2/writesched_random.go +++ b/vendor/golang.org/x/net/http2/writesched_random.go @@ -10,6 +10,8 @@ import "math" // priorities. Control frames like SETTINGS and PING are written before DATA // frames, but if no control frames are queued and multiple streams have queued // HEADERS or DATA frames, Pop selects a ready stream arbitrarily. +// +// Deprecated: User-provided write schedulers are deprecated. func NewRandomWriteScheduler() WriteScheduler { return &randomWriteScheduler{sq: make(map[uint32]*writeQueue)} } diff --git a/vendor/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s b/vendor/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s new file mode 100644 index 000000000..e07fa75eb --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s @@ -0,0 +1,12 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && arm64 && gc + +#include "textflag.h" + +TEXT libc_sysctlbyname_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_sysctlbyname(SB) +GLOBL ·libc_sysctlbyname_trampoline_addr(SB), RODATA, $8 +DATA ·libc_sysctlbyname_trampoline_addr(SB)/8, $libc_sysctlbyname_trampoline<>(SB) diff --git a/vendor/golang.org/x/sys/cpu/cpu_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_arm64.go index 6d8eb784b..5fc09e293 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_arm64.go @@ -44,14 +44,11 @@ func initOptions() { } func archInit() { - switch runtime.GOOS { - case "freebsd": + if runtime.GOOS == "freebsd" { readARM64Registers() - case "linux", "netbsd", "openbsd", "windows": + } else { + // Most platforms don't seem to allow directly reading these registers. doinit() - default: - // Many platforms don't seem to allow reading these registers. - setMinimalFeatures() } } diff --git a/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go new file mode 100644 index 000000000..0b470744a --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go @@ -0,0 +1,67 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && arm64 && gc + +package cpu + +func doinit() { + setMinimalFeatures() + + // The feature flags are explained in [Instruction Set Detection]. + // There are some differences between MacOS versions: + // + // MacOS 11 and 12 do not have "hw.optional" sysctl values for some of the features. + // + // MacOS 13 changed some of the naming conventions to align with ARM Architecture Reference Manual. + // For example "hw.optional.armv8_2_sha512" became "hw.optional.arm.FEAT_SHA512". + // It currently checks both to stay compatible with MacOS 11 and 12. + // The old names also work with MacOS 13, however it's not clear whether + // they will continue working with future OS releases. + // + // Once MacOS 12 is no longer supported the old names can be removed. + // + // [Instruction Set Detection]: https://developer.apple.com/documentation/kernel/1387446-sysctlbyname/determining_instruction_set_characteristics + + // Encryption, hashing and checksum capabilities + + // For the following flags there are no MacOS 11 sysctl flags. + ARM64.HasAES = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_AES\x00")) + ARM64.HasPMULL = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_PMULL\x00")) + ARM64.HasSHA1 = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA1\x00")) + ARM64.HasSHA2 = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA256\x00")) + + ARM64.HasSHA3 = darwinSysctlEnabled([]byte("hw.optional.armv8_2_sha3\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA3\x00")) + ARM64.HasSHA512 = darwinSysctlEnabled([]byte("hw.optional.armv8_2_sha512\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA512\x00")) + + ARM64.HasCRC32 = darwinSysctlEnabled([]byte("hw.optional.armv8_crc32\x00")) + + // Atomic and memory ordering + ARM64.HasATOMICS = darwinSysctlEnabled([]byte("hw.optional.armv8_1_atomics\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_LSE\x00")) + ARM64.HasLRCPC = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_LRCPC\x00")) + + // SIMD and floating point capabilities + ARM64.HasFPHP = darwinSysctlEnabled([]byte("hw.optional.neon_fp16\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FP16\x00")) + ARM64.HasASIMDHP = darwinSysctlEnabled([]byte("hw.optional.neon_hpfp\x00")) || darwinSysctlEnabled([]byte("hw.optional.AdvSIMD_HPFPCvt\x00")) + ARM64.HasASIMDRDM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_RDM\x00")) + ARM64.HasASIMDDP = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DotProd\x00")) + ARM64.HasASIMDFHM = darwinSysctlEnabled([]byte("hw.optional.armv8_2_fhm\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FHM\x00")) + ARM64.HasI8MM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_I8MM\x00")) + + ARM64.HasJSCVT = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_JSCVT\x00")) + ARM64.HasFCMA = darwinSysctlEnabled([]byte("hw.optional.armv8_3_compnum\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FCMA\x00")) + + // Miscellaneous + ARM64.HasDCPOP = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DPB\x00")) + ARM64.HasEVTSTRM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_ECV\x00")) + ARM64.HasDIT = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DIT\x00")) + + // Not supported, but added for completeness + ARM64.HasCPUID = false + + ARM64.HasSM3 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SM3\x00")) + ARM64.HasSM4 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SM4\x00")) + ARM64.HasSVE = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SVE\x00")) + ARM64.HasSVE2 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SVE2\x00")) +} diff --git a/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go new file mode 100644 index 000000000..4ee68e38d --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go @@ -0,0 +1,29 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && arm64 && !gc + +package cpu + +func doinit() { + setMinimalFeatures() + + ARM64.HasASIMD = true + ARM64.HasFP = true + + // Go already assumes these to be available because they were on the M1 + // and these are supported on all Apple arm64 chips. + ARM64.HasAES = true + ARM64.HasPMULL = true + ARM64.HasSHA1 = true + ARM64.HasSHA2 = true + + if runtime.GOOS != "ios" { + // Apple A7 processors do not support these, however + // M-series SoCs are at least armv8.4-a + ARM64.HasCRC32 = true // armv8.1 + ARM64.HasATOMICS = true // armv8.2 + ARM64.HasJSCVT = true // armv8.3, if HasFP + } +} diff --git a/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go index 7f1946780..05913081e 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go @@ -9,3 +9,4 @@ package cpu func getisar0() uint64 { return 0 } func getisar1() uint64 { return 0 } func getpfr0() uint64 { return 0 } +func getzfr0() uint64 { return 0 } diff --git a/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go index ff74d7afa..6c7c5bfd5 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go @@ -2,8 +2,10 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !linux && !netbsd && !openbsd && !windows && arm64 +//go:build !darwin && !linux && !netbsd && !openbsd && !windows && arm64 package cpu -func doinit() {} +func doinit() { + setMinimalFeatures() +} diff --git a/vendor/golang.org/x/sys/cpu/syscall_darwin_arm64_gc.go b/vendor/golang.org/x/sys/cpu/syscall_darwin_arm64_gc.go new file mode 100644 index 000000000..7b4e67ff9 --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/syscall_darwin_arm64_gc.go @@ -0,0 +1,54 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Minimal copy from internal/cpu and runtime to make sysctl calls. + +//go:build darwin && arm64 && gc + +package cpu + +import ( + "syscall" + "unsafe" +) + +type Errno = syscall.Errno + +// adapted from internal/cpu/cpu_arm64_darwin.go +func darwinSysctlEnabled(name []byte) bool { + out := int32(0) + nout := unsafe.Sizeof(out) + if ret := sysctlbyname(&name[0], (*byte)(unsafe.Pointer(&out)), &nout, nil, 0); ret != nil { + return false + } + return out > 0 +} + +//go:cgo_import_dynamic libc_sysctl sysctl "/usr/lib/libSystem.B.dylib" + +var libc_sysctlbyname_trampoline_addr uintptr + +// adapted from runtime/sys_darwin.go in the pattern of sysctl() above, as defined in x/sys/unix +func sysctlbyname(name *byte, old *byte, oldlen *uintptr, new *byte, newlen uintptr) error { + if _, _, err := syscall_syscall6( + libc_sysctlbyname_trampoline_addr, + uintptr(unsafe.Pointer(name)), + uintptr(unsafe.Pointer(old)), + uintptr(unsafe.Pointer(oldlen)), + uintptr(unsafe.Pointer(new)), + uintptr(newlen), + 0, + ); err != 0 { + return err + } + + return nil +} + +//go:cgo_import_dynamic libc_sysctlbyname sysctlbyname "/usr/lib/libSystem.B.dylib" + +// Implemented in the runtime package (runtime/sys_darwin.go) +func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno) + +//go:linkname syscall_syscall6 syscall.syscall6 diff --git a/vendor/golang.org/x/sys/plan9/syscall_plan9.go b/vendor/golang.org/x/sys/plan9/syscall_plan9.go index d079d8116..761912237 100644 --- a/vendor/golang.org/x/sys/plan9/syscall_plan9.go +++ b/vendor/golang.org/x/sys/plan9/syscall_plan9.go @@ -19,13 +19,7 @@ import ( // A Note is a string describing a process note. // It implements the os.Signal interface. -type Note string - -func (n Note) Signal() {} - -func (n Note) String() string { - return string(n) -} +type Note = syscall.Note var ( Stdin = 0 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux.go b/vendor/golang.org/x/sys/unix/ztypes_linux.go index c1a467017..45476a73c 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux.go @@ -593,110 +593,115 @@ const ( ) const ( - NDA_UNSPEC = 0x0 - NDA_DST = 0x1 - NDA_LLADDR = 0x2 - NDA_CACHEINFO = 0x3 - NDA_PROBES = 0x4 - NDA_VLAN = 0x5 - NDA_PORT = 0x6 - NDA_VNI = 0x7 - NDA_IFINDEX = 0x8 - NDA_MASTER = 0x9 - NDA_LINK_NETNSID = 0xa - NDA_SRC_VNI = 0xb - NTF_USE = 0x1 - NTF_SELF = 0x2 - NTF_MASTER = 0x4 - NTF_PROXY = 0x8 - NTF_EXT_LEARNED = 0x10 - NTF_OFFLOADED = 0x20 - NTF_ROUTER = 0x80 - NUD_INCOMPLETE = 0x1 - NUD_REACHABLE = 0x2 - NUD_STALE = 0x4 - NUD_DELAY = 0x8 - NUD_PROBE = 0x10 - NUD_FAILED = 0x20 - NUD_NOARP = 0x40 - NUD_PERMANENT = 0x80 - NUD_NONE = 0x0 - IFA_UNSPEC = 0x0 - IFA_ADDRESS = 0x1 - IFA_LOCAL = 0x2 - IFA_LABEL = 0x3 - IFA_BROADCAST = 0x4 - IFA_ANYCAST = 0x5 - IFA_CACHEINFO = 0x6 - IFA_MULTICAST = 0x7 - IFA_FLAGS = 0x8 - IFA_RT_PRIORITY = 0x9 - IFA_TARGET_NETNSID = 0xa - IFAL_LABEL = 0x2 - IFAL_ADDRESS = 0x1 - RT_SCOPE_UNIVERSE = 0x0 - RT_SCOPE_SITE = 0xc8 - RT_SCOPE_LINK = 0xfd - RT_SCOPE_HOST = 0xfe - RT_SCOPE_NOWHERE = 0xff - RT_TABLE_UNSPEC = 0x0 - RT_TABLE_COMPAT = 0xfc - RT_TABLE_DEFAULT = 0xfd - RT_TABLE_MAIN = 0xfe - RT_TABLE_LOCAL = 0xff - RT_TABLE_MAX = 0xffffffff - RTA_UNSPEC = 0x0 - RTA_DST = 0x1 - RTA_SRC = 0x2 - RTA_IIF = 0x3 - RTA_OIF = 0x4 - RTA_GATEWAY = 0x5 - RTA_PRIORITY = 0x6 - RTA_PREFSRC = 0x7 - RTA_METRICS = 0x8 - RTA_MULTIPATH = 0x9 - RTA_FLOW = 0xb - RTA_CACHEINFO = 0xc - RTA_TABLE = 0xf - RTA_MARK = 0x10 - RTA_MFC_STATS = 0x11 - RTA_VIA = 0x12 - RTA_NEWDST = 0x13 - RTA_PREF = 0x14 - RTA_ENCAP_TYPE = 0x15 - RTA_ENCAP = 0x16 - RTA_EXPIRES = 0x17 - RTA_PAD = 0x18 - RTA_UID = 0x19 - RTA_TTL_PROPAGATE = 0x1a - RTA_IP_PROTO = 0x1b - RTA_SPORT = 0x1c - RTA_DPORT = 0x1d - RTN_UNSPEC = 0x0 - RTN_UNICAST = 0x1 - RTN_LOCAL = 0x2 - RTN_BROADCAST = 0x3 - RTN_ANYCAST = 0x4 - RTN_MULTICAST = 0x5 - RTN_BLACKHOLE = 0x6 - RTN_UNREACHABLE = 0x7 - RTN_PROHIBIT = 0x8 - RTN_THROW = 0x9 - RTN_NAT = 0xa - RTN_XRESOLVE = 0xb - SizeofNlMsghdr = 0x10 - SizeofNlMsgerr = 0x14 - SizeofRtGenmsg = 0x1 - SizeofNlAttr = 0x4 - SizeofRtAttr = 0x4 - SizeofIfInfomsg = 0x10 - SizeofIfAddrmsg = 0x8 - SizeofIfAddrlblmsg = 0xc - SizeofIfaCacheinfo = 0x10 - SizeofRtMsg = 0xc - SizeofRtNexthop = 0x8 - SizeofNdUseroptmsg = 0x10 - SizeofNdMsg = 0xc + NDA_UNSPEC = 0x0 + NDA_DST = 0x1 + NDA_LLADDR = 0x2 + NDA_CACHEINFO = 0x3 + NDA_PROBES = 0x4 + NDA_VLAN = 0x5 + NDA_PORT = 0x6 + NDA_VNI = 0x7 + NDA_IFINDEX = 0x8 + NDA_MASTER = 0x9 + NDA_LINK_NETNSID = 0xa + NDA_SRC_VNI = 0xb + NTF_USE = 0x1 + NTF_SELF = 0x2 + NTF_MASTER = 0x4 + NTF_PROXY = 0x8 + NTF_EXT_LEARNED = 0x10 + NTF_OFFLOADED = 0x20 + NTF_ROUTER = 0x80 + NUD_INCOMPLETE = 0x1 + NUD_REACHABLE = 0x2 + NUD_STALE = 0x4 + NUD_DELAY = 0x8 + NUD_PROBE = 0x10 + NUD_FAILED = 0x20 + NUD_NOARP = 0x40 + NUD_PERMANENT = 0x80 + NUD_NONE = 0x0 + IFA_UNSPEC = 0x0 + IFA_ADDRESS = 0x1 + IFA_LOCAL = 0x2 + IFA_LABEL = 0x3 + IFA_BROADCAST = 0x4 + IFA_ANYCAST = 0x5 + IFA_CACHEINFO = 0x6 + IFA_MULTICAST = 0x7 + IFA_FLAGS = 0x8 + IFA_RT_PRIORITY = 0x9 + IFA_TARGET_NETNSID = 0xa + IFAL_LABEL = 0x2 + IFAL_ADDRESS = 0x1 + RT_SCOPE_UNIVERSE = 0x0 + RT_SCOPE_SITE = 0xc8 + RT_SCOPE_LINK = 0xfd + RT_SCOPE_HOST = 0xfe + RT_SCOPE_NOWHERE = 0xff + RT_TABLE_UNSPEC = 0x0 + RT_TABLE_COMPAT = 0xfc + RT_TABLE_DEFAULT = 0xfd + RT_TABLE_MAIN = 0xfe + RT_TABLE_LOCAL = 0xff + RT_TABLE_MAX = 0xffffffff + RTA_UNSPEC = 0x0 + RTA_DST = 0x1 + RTA_SRC = 0x2 + RTA_IIF = 0x3 + RTA_OIF = 0x4 + RTA_GATEWAY = 0x5 + RTA_PRIORITY = 0x6 + RTA_PREFSRC = 0x7 + RTA_METRICS = 0x8 + RTA_MULTIPATH = 0x9 + RTA_FLOW = 0xb + RTA_CACHEINFO = 0xc + RTA_TABLE = 0xf + RTA_MARK = 0x10 + RTA_MFC_STATS = 0x11 + RTA_VIA = 0x12 + RTA_NEWDST = 0x13 + RTA_PREF = 0x14 + RTA_ENCAP_TYPE = 0x15 + RTA_ENCAP = 0x16 + RTA_EXPIRES = 0x17 + RTA_PAD = 0x18 + RTA_UID = 0x19 + RTA_TTL_PROPAGATE = 0x1a + RTA_IP_PROTO = 0x1b + RTA_SPORT = 0x1c + RTA_DPORT = 0x1d + RTN_UNSPEC = 0x0 + RTN_UNICAST = 0x1 + RTN_LOCAL = 0x2 + RTN_BROADCAST = 0x3 + RTN_ANYCAST = 0x4 + RTN_MULTICAST = 0x5 + RTN_BLACKHOLE = 0x6 + RTN_UNREACHABLE = 0x7 + RTN_PROHIBIT = 0x8 + RTN_THROW = 0x9 + RTN_NAT = 0xa + RTN_XRESOLVE = 0xb + PREFIX_UNSPEC = 0x0 + PREFIX_ADDRESS = 0x1 + PREFIX_CACHEINFO = 0x2 + SizeofNlMsghdr = 0x10 + SizeofNlMsgerr = 0x14 + SizeofRtGenmsg = 0x1 + SizeofNlAttr = 0x4 + SizeofRtAttr = 0x4 + SizeofIfInfomsg = 0x10 + SizeofPrefixmsg = 0xc + SizeofPrefixCacheinfo = 0x8 + SizeofIfAddrmsg = 0x8 + SizeofIfAddrlblmsg = 0xc + SizeofIfaCacheinfo = 0x10 + SizeofRtMsg = 0xc + SizeofRtNexthop = 0x8 + SizeofNdUseroptmsg = 0x10 + SizeofNdMsg = 0xc ) type NlMsghdr struct { @@ -735,6 +740,22 @@ type IfInfomsg struct { Change uint32 } +type Prefixmsg struct { + Family uint8 + Pad1 uint8 + Pad2 uint16 + Ifindex int32 + Type uint8 + Len uint8 + Flags uint8 + Pad3 uint8 +} + +type PrefixCacheinfo struct { + Preferred_time uint32 + Valid_time uint32 +} + type IfAddrmsg struct { Family uint8 Prefixlen uint8 diff --git a/vendor/golang.org/x/sys/windows/aliases.go b/vendor/golang.org/x/sys/windows/aliases.go index 16f90560a..96317966e 100644 --- a/vendor/golang.org/x/sys/windows/aliases.go +++ b/vendor/golang.org/x/sys/windows/aliases.go @@ -8,5 +8,6 @@ package windows import "syscall" +type Signal = syscall.Signal type Errno = syscall.Errno type SysProcAttr = syscall.SysProcAttr diff --git a/vendor/golang.org/x/sys/windows/syscall_windows.go b/vendor/golang.org/x/sys/windows/syscall_windows.go index 738a9f212..d76643658 100644 --- a/vendor/golang.org/x/sys/windows/syscall_windows.go +++ b/vendor/golang.org/x/sys/windows/syscall_windows.go @@ -1490,20 +1490,6 @@ func Getgid() (gid int) { return -1 } func Getegid() (egid int) { return -1 } func Getgroups() (gids []int, err error) { return nil, syscall.EWINDOWS } -type Signal int - -func (s Signal) Signal() {} - -func (s Signal) String() string { - if 0 <= s && int(s) < len(signals) { - str := signals[s] - if str != "" { - return str - } - } - return "signal " + itoa(int(s)) -} - func LoadCreateSymbolicLink() error { return procCreateSymbolicLinkW.Find() } diff --git a/vendor/modules.txt b/vendor/modules.txt index b0e654f5b..41338b223 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -12,13 +12,13 @@ github.com/Microsoft/go-winio/internal/fs github.com/Microsoft/go-winio/internal/socket github.com/Microsoft/go-winio/internal/stringbuffer github.com/Microsoft/go-winio/pkg/guid -# github.com/VictoriaMetrics/metrics v1.41.2 +# github.com/VictoriaMetrics/metrics v1.42.0 ## explicit; go 1.24.0 github.com/VictoriaMetrics/metrics # github.com/bitfield/script v0.24.1 ## explicit; go 1.18 github.com/bitfield/script -# github.com/bytedance/gopkg v0.1.3 +# github.com/bytedance/gopkg v0.1.4 ## explicit; go 1.18 github.com/bytedance/gopkg/lang/dirtmake # github.com/bytedance/sonic v1.15.0 @@ -140,8 +140,8 @@ github.com/docker/go-connections/tlsconfig # github.com/docker/go-units v0.4.0 ## explicit github.com/docker/go-units -# github.com/fatih/color v1.18.0 -## explicit; go 1.17 +# github.com/fatih/color v1.19.0 +## explicit; go 1.25.0 github.com/fatih/color # github.com/felixge/httpsnoop v1.0.4 ## explicit; go 1.13 @@ -197,7 +197,7 @@ github.com/go-redis/redis/v8/internal/pool github.com/go-redis/redis/v8/internal/proto github.com/go-redis/redis/v8/internal/rand github.com/go-redis/redis/v8/internal/util -# github.com/goccy/go-json v0.10.5 +# github.com/goccy/go-json v0.10.6 ## explicit; go 1.19 github.com/goccy/go-json github.com/goccy/go-json/internal/decoder @@ -321,16 +321,17 @@ github.com/sirupsen/logrus/hooks/syslog # github.com/skycoin/noise v0.0.0-20180327030543-2492fe189ae6 ## explicit github.com/skycoin/noise -# github.com/skycoin/skycoin v0.28.3 -## explicit; go 1.25.4 +# github.com/skycoin/skycoin v0.28.5-alpha1.0.20260323015226-90b668188f85 +## explicit; go 1.26.1 github.com/skycoin/skycoin/src/cipher github.com/skycoin/skycoin/src/cipher/base58 +github.com/skycoin/skycoin/src/cipher/bech32 github.com/skycoin/skycoin/src/cipher/ripemd160 github.com/skycoin/skycoin/src/cipher/secp256k1-go github.com/skycoin/skycoin/src/cipher/secp256k1-go/secp256k1-go2 github.com/skycoin/skycoin/src/util/logging -# github.com/skycoin/skywire v1.3.35-0.20260222235451-f11c46808634 -## explicit; go 1.25.6 +# github.com/skycoin/skywire v1.3.37 +## explicit; go 1.26.1 github.com/skycoin/skywire/deployment github.com/skycoin/skywire/pkg/skywire-utilities/pkg/buildinfo github.com/skycoin/skywire/pkg/skywire-utilities/pkg/calvin @@ -384,7 +385,7 @@ github.com/valyala/fastrand # github.com/valyala/histogram v1.2.0 ## explicit; go 1.12 github.com/valyala/histogram -# github.com/xtaci/smux v1.5.56 +# github.com/xtaci/smux v1.5.57 ## explicit; go 1.18 github.com/xtaci/smux # go.mongodb.org/mongo-driver/v2 v2.5.0 @@ -433,11 +434,11 @@ go.opentelemetry.io/otel/trace go.opentelemetry.io/otel/trace/embedded go.opentelemetry.io/otel/trace/internal/telemetry go.opentelemetry.io/otel/trace/noop -# golang.org/x/arch v0.24.0 -## explicit; go 1.24.0 +# golang.org/x/arch v0.25.0 +## explicit; go 1.25.0 golang.org/x/arch/x86/x86asm -# golang.org/x/crypto v0.48.0 -## explicit; go 1.24.0 +# golang.org/x/crypto v0.49.0 +## explicit; go 1.25.0 golang.org/x/crypto/blake2b golang.org/x/crypto/blake2s golang.org/x/crypto/chacha20 @@ -447,7 +448,7 @@ golang.org/x/crypto/hkdf golang.org/x/crypto/internal/alias golang.org/x/crypto/internal/poly1305 golang.org/x/crypto/sha3 -# golang.org/x/net v0.51.0 +# golang.org/x/net v0.52.0 ## explicit; go 1.25.0 golang.org/x/net/bpf golang.org/x/net/context @@ -465,17 +466,17 @@ golang.org/x/net/ipv4 golang.org/x/net/ipv6 golang.org/x/net/nettest golang.org/x/net/proxy -# golang.org/x/sys v0.41.0 -## explicit; go 1.24.0 +# golang.org/x/sys v0.42.0 +## explicit; go 1.25.0 golang.org/x/sys/cpu golang.org/x/sys/plan9 golang.org/x/sys/unix golang.org/x/sys/windows -# golang.org/x/term v0.40.0 -## explicit; go 1.24.0 +# golang.org/x/term v0.41.0 +## explicit; go 1.25.0 golang.org/x/term -# golang.org/x/text v0.34.0 -## explicit; go 1.24.0 +# golang.org/x/text v0.35.0 +## explicit; go 1.25.0 golang.org/x/text/internal/language golang.org/x/text/internal/language/compact golang.org/x/text/internal/tag @@ -484,8 +485,6 @@ golang.org/x/text/secure/bidirule golang.org/x/text/transform golang.org/x/text/unicode/bidi golang.org/x/text/unicode/norm -# google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d -## explicit; go 1.25.0 # google.golang.org/protobuf v1.36.11 ## explicit; go 1.23 google.golang.org/protobuf/encoding/protowire @@ -506,10 +505,11 @@ google.golang.org/protobuf/runtime/protoiface gopkg.in/yaml.v3 # gotest.tools/v3 v3.5.2 ## explicit; go 1.17 -# mvdan.cc/sh/v3 v3.12.0 -## explicit; go 1.23.0 +# mvdan.cc/sh/v3 v3.13.0 +## explicit; go 1.25.0 mvdan.cc/sh/v3/expand mvdan.cc/sh/v3/fileutil +mvdan.cc/sh/v3/internal mvdan.cc/sh/v3/pattern mvdan.cc/sh/v3/shell mvdan.cc/sh/v3/syntax diff --git a/vendor/mvdan.cc/sh/v3/expand/arith.go b/vendor/mvdan.cc/sh/v3/expand/arith.go index f01856136..6865a8840 100644 --- a/vendor/mvdan.cc/sh/v3/expand/arith.go +++ b/vendor/mvdan.cc/sh/v3/expand/arith.go @@ -67,8 +67,10 @@ func Arithm(cfg *Config, expr syntax.ArithmExpr) (int, error) { return ^val, nil case syntax.Plus: return val, nil - default: // syntax.Minus + case syntax.Minus: return -val, nil + default: + return 0, fmt.Errorf("unsupported unary arithmetic operator: %q", expr.Op) } case *syntax.BinaryArithm: switch expr.Op { @@ -218,8 +220,10 @@ func binArit(op syntax.BinAritOperator, x, y int) (int, error) { return oneIf(x != 0 && y != 0), nil case syntax.OrArit: return oneIf(x != 0 || y != 0), nil - default: // syntax.Comma + case syntax.Comma: // x is executed but its result discarded return y, nil + default: + return 0, fmt.Errorf("unsupported binary arithmetic operator: %q", op) } } diff --git a/vendor/mvdan.cc/sh/v3/expand/environ.go b/vendor/mvdan.cc/sh/v3/expand/environ.go index b7305d065..51fea7354 100644 --- a/vendor/mvdan.cc/sh/v3/expand/environ.go +++ b/vendor/mvdan.cc/sh/v3/expand/environ.go @@ -54,7 +54,7 @@ type WriteEnviron interface { Set(name string, vr Variable) error } -//go:generate stringer -type=ValueKind +//go:generate go tool stringer -type=ValueKind // ValueKind describes which kind of value the variable holds. // While most unset variables will have an [Unknown] kind, an unset variable may @@ -116,6 +116,27 @@ func (v Variable) Declared() bool { return v.Set || v.Local || v.Exported || v.ReadOnly || v.Kind != Unknown } +// Flags returns the variable's attribute flags in the order used by bash's +// declare builtin and ${var@a}: type (a/A/n), readonly (r), exported (x). +func (v Variable) Flags() string { + var flags []byte + switch v.Kind { + case Indexed: + flags = append(flags, 'a') + case Associative: + flags = append(flags, 'A') + case NameRef: + flags = append(flags, 'n') + } + if v.ReadOnly { + flags = append(flags, 'r') + } + if v.Exported { + flags = append(flags, 'x') + } + return string(flags) +} + // String returns the variable's value as a string. In general, this only makes // sense if the variable has a string value or no value at all. func (v Variable) String() string { @@ -179,23 +200,14 @@ func (f funcEnviron) Each(func(name string, vr Variable) bool) {} // On Windows, where environment variable names are case-insensitive, the // resulting variable names will all be uppercase. func ListEnviron(pairs ...string) Environ { - return listEnvironWithUpper(runtime.GOOS == "windows", pairs...) + return listEnviron_(runtime.GOOS == "windows", pairs...) } -// listEnvironWithUpper implements [ListEnviron], but letting the tests specify +// listEnviron_ implements [ListEnviron], but letting the tests specify // whether to uppercase all names or not. -func listEnvironWithUpper(upper bool, pairs ...string) Environ { +func listEnviron_(caseInsensitive bool, pairs ...string) Environ { list := slices.Clone(pairs) - if upper { - // Uppercase before sorting, so that we can remove duplicates - // without the need for linear search nor a map. - for i, s := range list { - if name, val, ok := strings.Cut(s, "="); ok { - list[i] = strings.ToUpper(name) + "=" + val - } - } - } - + env := listEnviron{caseInsensitive: caseInsensitive} slices.SortStableFunc(list, func(a, b string) int { isep := strings.IndexByte(a, '=') jsep := strings.IndexByte(b, '=') @@ -209,7 +221,7 @@ func listEnvironWithUpper(upper bool, pairs ...string) Environ { } else { jsep += 1 } - return strings.Compare(a[:isep], b[:jsep]) + return env.compare(a[:isep], b[:jsep]) }) last := "" @@ -220,7 +232,7 @@ func listEnvironWithUpper(upper bool, pairs ...string) Environ { list = slices.Delete(list, i, i+1) continue } - if last == name { + if env.compare(last, name) == 0 { // duplicate; the last one wins list = slices.Delete(list, i-1, i) continue @@ -228,36 +240,50 @@ func listEnvironWithUpper(upper bool, pairs ...string) Environ { last = name i++ } - return listEnviron(list) + env.pairs = list + return env } // listEnviron is a sorted list of "name=value" strings. -type listEnviron []string +type listEnviron struct { + caseInsensitive bool + pairs []string +} + +func (l listEnviron) compare(a, b string) int { + if l.caseInsensitive { + // This is not particularly efficient, but it does the job. + // If we had a cmp-compatible version of [strings.EqualFold], we'd use it. + a = strings.ToUpper(a) + b = strings.ToUpper(b) + } + return strings.Compare(a, b) +} func (l listEnviron) Get(name string) Variable { eqpos := len(name) endpos := len(name) + 1 - i, ok := slices.BinarySearchFunc(l, name, func(l, name string) int { - if len(l) < endpos { + i, ok := slices.BinarySearchFunc(l.pairs, name, func(pair, name string) int { + if len(pair) < endpos { // Too short; see if we are before or after the name. - return strings.Compare(l, name) + return l.compare(pair, name) } // Compare the name prefix, then the equal character. - c := strings.Compare(l[:eqpos], name) - eq := l[eqpos] + c := l.compare(pair[:eqpos], name) + eq := pair[eqpos] if c == 0 { return cmp.Compare(eq, '=') } return c }) if ok { - return Variable{Set: true, Exported: true, Kind: String, Str: l[i][endpos:]} + return Variable{Set: true, Exported: true, Kind: String, Str: l.pairs[i][endpos:]} } return Variable{} } func (l listEnviron) Each(fn func(name string, vr Variable) bool) { - for _, pair := range l { + for _, pair := range l.pairs { name, value, ok := strings.Cut(pair, "=") if !ok { // should never happen; see listEnvironWithUpper diff --git a/vendor/mvdan.cc/sh/v3/expand/expand.go b/vendor/mvdan.cc/sh/v3/expand/expand.go index 48ebfbfcb..b51c1b21f 100644 --- a/vendor/mvdan.cc/sh/v3/expand/expand.go +++ b/vendor/mvdan.cc/sh/v3/expand/expand.go @@ -20,6 +20,7 @@ import ( "strconv" "strings" + "mvdan.cc/sh/v3/internal" "mvdan.cc/sh/v3/pattern" "mvdan.cc/sh/v3/syntax" ) @@ -49,9 +50,6 @@ type Config struct { CmdSubst func(io.Writer, *syntax.CmdSubst) error // ProcSubst expands a process substitution node. - // - // Note that this feature is a work in progress, and the signature of - // this field might change until #451 is completely fixed. ProcSubst func(*syntax.ProcSubst) (string, error) // TODO(v4): replace ReadDir with ReadDir2. @@ -66,22 +64,29 @@ type Config struct { // Use [os.ReadDir] to use the filesystem directly. ReadDir2 func(string) ([]fs.DirEntry, error) - // GlobStar corresponds to the shell option that allows globbing with - // "**". + // GlobStar corresponds to the shell option which allows globbing with "**". GlobStar bool - // NoCaseGlob corresponds to the shell option that causes case-insensitive + // DotGlob corresponds to the shell option which allows filenames beginning + // with a dot to be matched by a pattern which does not begin with a dot. + DotGlob bool + + // NoCaseGlob corresponds to the shell option which causes case-insensitive // pattern matching in pathname expansion. NoCaseGlob bool - // NullGlob corresponds to the shell option that allows globbing + // NullGlob corresponds to the shell option which allows globbing // patterns which match nothing to result in zero fields. NullGlob bool - // NoUnset corresponds to the shell option that treats unset variables + // NoUnset corresponds to the shell option which treats unset variables // as errors. NoUnset bool + // ExtGlob corresponds to the shell option which allows using extended + // pattern matching features when performing pathname expansion (globbing). + ExtGlob bool + bufferAlloc strings.Builder fieldAlloc [4]fieldPart fieldsAlloc [4][]fieldPart @@ -233,6 +238,9 @@ func Pattern(cfg *Config, word *syntax.Word) (string, error) { // shell's format specifications. These include printf(1), among others. // // The resulting string is returned, along with the number of arguments used. +// Note that the resulting string may contain null bytes, for example +// if the format string used `\x00`. The caller should terminate the string +// at the first null byte if needed, such as when expanding for `$'foo\x00bar'`. // // The config specifies shell expansion options; nil behaves the same as an // empty config. @@ -252,13 +260,12 @@ func formatInto(sb *strings.Builder, format string, args []string) (int, error) var fmts []byte initialArgs := len(args) -formatLoop: for i := 0; i < len(format); i++ { // readDigits reads from 0 to max digits, either octal or // hexadecimal. readDigits := func(max int, hex bool) string { j := 0 - for ; j < max; j++ { + for ; j < max && i+j < len(format); j++ { c := format[i+j] if (c >= '0' && c <= '9') || (hex && c >= 'a' && c <= 'f') || @@ -276,6 +283,10 @@ formatLoop: switch { case c == '\\': // escaped i++ + if i >= len(format) { + sb.WriteByte('\\') + break + } switch c = format[i]; c { case 'a': // bell sb.WriteByte('\a') @@ -313,11 +324,6 @@ formatLoop: if len(digits) > 0 { // can't error n, _ := strconv.ParseUint(digits, 16, 32) - if n == 0 { - // If we're about to print \x00, - // stop the entire loop, like bash. - break formatLoop - } if c == 'x' { // always as a single byte sb.WriteByte(byte(n)) @@ -449,7 +455,7 @@ func Fields(cfg *Config, words ...*syntax.Word) ([]string, error) { // FieldsSeq expands a number of words as if they were arguments in a shell // command. This includes brace expansion, tilde expansion, parameter expansion, -// command substitution, arithmetic expansion, and quote removal. +// command substitution, arithmetic expansion, quote removal, and globbing. func FieldsSeq(cfg *Config, words ...*syntax.Word) iter.Seq2[string, error] { cfg = prepareConfig(cfg) dir := cfg.envGet("PWD") @@ -544,12 +550,13 @@ func (cfg *Config) wordField(wps []syntax.WordPart, ql quoteLevel) ([]fieldPart, } s = sb.String() } - s, _, _ = strings.Cut(s, "\x00") + s, _, _ = strings.Cut(s, "\x00") // TODO: why is this needed? field = append(field, fieldPart{val: s}) case *syntax.SglQuoted: fp := fieldPart{quote: quoteSingle, val: wp.Value} if wp.Dollar { fp.val, _, _ = Format(cfg, fp.val, nil) + fp.val, _, _ = strings.Cut(fp.val, "\x00") // cut the string if format included \x00 } field = append(field, fp) case *syntax.DblQuoted: @@ -586,7 +593,10 @@ func (cfg *Config) wordField(wps []syntax.WordPart, ql quoteLevel) ([]fieldPart, } field = append(field, fieldPart{val: path}) case *syntax.ExtGlob: - return nil, fmt.Errorf("extended globbing is not supported") + // Like how [Config.wordFields] deals with [syntax.ExtGlob], + // except that we allow these through even when [Config.ExtGlob] + // is false, as it only applies to pathname expansion. + field = append(field, fieldPart{val: wp.Op.String() + wp.Pattern.Value + ")"}) default: panic(fmt.Sprintf("unhandled word part: %T", wp)) } @@ -669,6 +679,7 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) { fp := fieldPart{quote: quoteSingle, val: wp.Value} if wp.Dollar { fp.val, _, _ = Format(cfg, fp.val, nil) + fp.val, _, _ = strings.Cut(fp.val, "\x00") // cut the string if format included \x00 } curField = append(curField, fp) case *syntax.DblQuoted: @@ -721,7 +732,17 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) { } splitAdd(path) case *syntax.ExtGlob: - return nil, fmt.Errorf("extended globbing is not supported") + if !cfg.ExtGlob { + return nil, fmt.Errorf("extended globbing operator used without the \"extglob\" option set") + } + // We don't translate or interpret the pattern here in any way; + // that's done later when globbing takes place via [pattern.Regexp]. + // Here, all we do is keep the extended globbing expression in string form. + // + // TODO(v4): perhaps the syntax parser should keep extended globbing expressions + // as plain literal strings, because a custom node is not particularly helpful. + // It's not like other globbing operators like `*` or `**` get their own nodes. + curField = append(curField, fieldPart{val: wp.Op.String() + wp.Pattern.Value + ")"}) default: panic(fmt.Sprintf("unhandled word part: %T", wp)) } @@ -736,7 +757,7 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) { // quotedElemFields returns the list of elements resulting from a quoted // parameter expansion that should be treated especially, like "${foo[@]}". func (cfg *Config) quotedElemFields(pe *syntax.ParamExp) []string { - if pe == nil || pe.Length || pe.Width { + if pe == nil || pe.Length || pe.Width || pe.IsSet { return nil } name := pe.Param.Value @@ -765,27 +786,74 @@ func (cfg *Config) quotedElemFields(pe *syntax.ParamExp) []string { return nil } switch name { - case "*": // "${*}" - return []string{cfg.ifsJoin(cfg.Env.Get(name).List)} - case "@": // "${@}" - return cfg.Env.Get(name).List + case "*": // "${*}" or "${*:offset:length}" + return []string{cfg.ifsJoin(cfg.sliceElems(pe, cfg.Env.Get(name).List, true))} + case "@": // "${@}" or "${@:offset:length}" + return cfg.sliceElems(pe, cfg.Env.Get(name).List, true) } switch nodeLit(pe.Index) { case "@": // "${name[@]}" - switch vr := cfg.Env.Get(name); vr.Kind { + vr := cfg.Env.Get(name) + switch vr.Kind { case Indexed: - return vr.List + return cfg.sliceElems(pe, vr.List, false) case Associative: return slices.Collect(maps.Values(vr.Map)) + case Unknown: + if !vr.IsSet() { + // An unset variable expanded as "${name[@]}" produces + // zero fields, just like an empty array. + return []string{} + } } case "*": // "${name[*]}" if vr := cfg.Env.Get(name); vr.Kind == Indexed { - return []string{cfg.ifsJoin(vr.List)} + return []string{cfg.ifsJoin(cfg.sliceElems(pe, vr.List, false))} } } return nil } +// sliceElems applies ${var:offset:length} slicing to a list of elements. +// When positional is true, $0 is prepended to the list before slicing. +// In bash, positional parameter offsets ($@ and $*) are 1-based and +// offset 0 includes $0 (the shell or script name). Negative offsets +// count from $# + 1, so $0 is reachable via large enough negative values. +func (cfg *Config) sliceElems(pe *syntax.ParamExp, elems []string, positional bool) []string { + if pe.Slice == nil { + return elems + } + if positional { + elems = append([]string{cfg.Env.Get("0").Str}, elems...) + } + slicePos := func(n int) int { + if n < 0 { + n = len(elems) + n + if n < 0 { + n = len(elems) + } + } else if n > len(elems) { + n = len(elems) + } + return n + } + if pe.Slice.Offset != nil { + offset, err := Arithm(cfg, pe.Slice.Offset) + if err != nil { + return elems + } + elems = elems[slicePos(offset):] + } + if pe.Slice.Length != nil { + length, err := Arithm(cfg, pe.Slice.Length) + if err != nil { + return elems + } + elems = elems[:slicePos(length)] + } + return elems +} + func (cfg *Config) expandUser(field string, moreFields bool) (prefix, rest string) { name, ok := strings.CutPrefix(field, "~") if !ok { @@ -843,7 +911,10 @@ func findAllIndex(pat, name string, n int) [][]int { return rx.FindAllStringIndex(name, n) } -var rxGlobStar = regexp.MustCompile(".*") +var ( + rxGlobStar = regexp.MustCompile(`^[^/.][^/]*$`) + rxGlobStarDotGlob = regexp.MustCompile(`^[^/]*$`) +) // pathJoin2 is a simpler version of [filepath.Join] without cleaning the result, // since that's needed for globbing. @@ -940,7 +1011,7 @@ func (cfg *Config) glob(base, pat string) ([]string, error) { stack := make([]string, 0, len(matches)) for _, match := range slices.Backward(matches) { // "a/**" should match "a/ a/b a/b/cfg ..."; - // note how the zero-match case has a trailing separator. + // note how the zero-match case there has a trailing separator. stack = append(stack, pathJoin2(match, "")) } matches = matches[:0] @@ -948,15 +1019,15 @@ func (cfg *Config) glob(base, pat string) ([]string, error) { for len(stack) > 0 { dir := stack[len(stack)-1] stack = stack[:len(stack)-1] - - // Don't include the original "" match as it's not a valid path. - if dir != "" { - matches = append(matches, dir) - } + matches = append(matches, dir) // If dir is not a directory, we keep the stack as-is and continue. newMatches = newMatches[:0] - newMatches, _ = cfg.globDir(base, dir, rxGlobStar, wantDir, newMatches) + rx := rxGlobStar.MatchString + if cfg.DotGlob { + rx = rxGlobStarDotGlob.MatchString + } + newMatches, _ = cfg.globDir(base, dir, rx, wantDir, newMatches) for _, match := range slices.Backward(newMatches) { stack = append(stack, match) } @@ -967,24 +1038,36 @@ func (cfg *Config) glob(base, pat string) ([]string, error) { if cfg.NoCaseGlob { mode |= pattern.NoGlobCase } - expr, err := pattern.Regexp(part, mode) + if cfg.DotGlob { + mode |= pattern.GlobLeadingDot + } + if cfg.ExtGlob { + mode |= pattern.ExtendedOperators + } + matcher, err := internal.ExtendedPatternMatcher(part, mode) if err != nil { return nil, err } - rx := regexp.MustCompile(expr) var newMatches []string for _, dir := range matches { - newMatches, err = cfg.globDir(base, dir, rx, wantDir, newMatches) + newMatches, err = cfg.globDir(base, dir, matcher, wantDir, newMatches) if err != nil { return nil, err } } matches = newMatches } + // Note that the results need to be sorted. + // TODO: above we do a BFS; if we did a DFS, the matches would already be sorted. + slices.Sort(matches) + // Remove any empty matches left behind from "**". + if len(matches) > 0 && matches[0] == "" { + matches = matches[1:] + } return matches, nil } -func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, wantDir bool, matches []string) ([]string, error) { +func (cfg *Config) globDir(base, dir string, matcher func(string) bool, wantDir bool, matches []string) ([]string, error) { fullDir := dir if !filepath.IsAbs(dir) { fullDir = filepath.Join(base, dir) @@ -1011,7 +1094,7 @@ func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, wantDir bool, ma // Not a symlink nor a directory. continue } - if rx.MatchString(name) { + if matcher(name) { matches = append(matches, pathJoin2(dir, name)) } } diff --git a/vendor/mvdan.cc/sh/v3/expand/param.go b/vendor/mvdan.cc/sh/v3/expand/param.go index 757a5dff0..c248d117b 100644 --- a/vendor/mvdan.cc/sh/v3/expand/param.go +++ b/vendor/mvdan.cc/sh/v3/expand/param.go @@ -24,6 +24,8 @@ func nodeLit(node syntax.Node) string { return "" } +// UnsetParameterError is returned when a parameter expansion encounters an +// unset variable and [Config.NoUnset] has been set. type UnsetParameterError struct { Node *syntax.ParamExp Message string @@ -113,24 +115,7 @@ func (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) { case Indexed: indexAllElements = true callVarInd = false - elems = vr.List - slicePos := func(n int) int { - if n < 0 { - n = len(elems) + n - if n < 0 { - n = len(elems) - } - } else if n > len(elems) { - n = len(elems) - } - return n - } - if pe.Slice != nil && pe.Slice.Offset != nil { - elems = elems[slicePos(sliceOffset):] - } - if pe.Slice != nil && pe.Slice.Length != nil { - elems = elems[:slicePos(sliceLen)] - } + elems = cfg.sliceElems(pe, vr.List, name == "@" || name == "*") str = strings.Join(elems, " ") } } @@ -179,6 +164,10 @@ func (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) { } slices.Sort(strs) str = strings.Join(strs, " ") + case pe.Width: + return "", fmt.Errorf("unsupported") + case pe.IsSet: + return "", fmt.Errorf("unsupported") case pe.Slice != nil: if callVarInd { slicePos := func(n int) int { @@ -328,8 +317,24 @@ func (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) { rns = append(rns, rn) } str = string(rns) - case "P", "A", "a": - panic(fmt.Sprintf("unhandled @%s param expansion", arg)) + case "a": + // ${var@a} returns variable attribute flags. + // We use orig (before nameref resolve) for the attributes. + str = orig.Flags() + case "A": + // ${var@A} returns a declare statement that recreates the variable. + flags := orig.Flags() + quoted, err := syntax.Quote(str, syntax.LangBash) + if err != nil { + return "", err + } + if flags == "" { + str = fmt.Sprintf("%s=%s", name, quoted) + } else { + str = fmt.Sprintf("declare -%s %s=%s", flags, name, quoted) + } + case "P": + // TODO: implement prompt expansion (\u, \h, \w, etc.). default: panic(fmt.Sprintf("unexpected @%s param expansion", arg)) } diff --git a/vendor/mvdan.cc/sh/v3/expand/valuekind_string.go b/vendor/mvdan.cc/sh/v3/expand/valuekind_string.go index b98f03ef3..f5a6d1095 100644 --- a/vendor/mvdan.cc/sh/v3/expand/valuekind_string.go +++ b/vendor/mvdan.cc/sh/v3/expand/valuekind_string.go @@ -21,8 +21,9 @@ const _ValueKind_name = "UnknownStringNameRefIndexedAssociativeKeepValue" var _ValueKind_index = [...]uint8{0, 7, 13, 20, 27, 38, 47} func (i ValueKind) String() string { - if i >= ValueKind(len(_ValueKind_index)-1) { + idx := int(i) - 0 + if i < 0 || idx >= len(_ValueKind_index)-1 { return "ValueKind(" + strconv.FormatInt(int64(i), 10) + ")" } - return _ValueKind_name[_ValueKind_index[i]:_ValueKind_index[i+1]] + return _ValueKind_name[_ValueKind_index[idx]:_ValueKind_index[idx+1]] } diff --git a/vendor/mvdan.cc/sh/v3/internal/pattern.go b/vendor/mvdan.cc/sh/v3/internal/pattern.go new file mode 100644 index 000000000..d25796753 --- /dev/null +++ b/vendor/mvdan.cc/sh/v3/internal/pattern.go @@ -0,0 +1,77 @@ +// Copyright (c) 2026, Daniel Martí +// See LICENSE for licensing information + +package internal + +import ( + "errors" + "fmt" + "regexp" + "strings" + + "mvdan.cc/sh/v3/pattern" +) + +// ExtendedPatternMatcher returns a [regexp.Regexp.MatchString]-like function +// to support !(pattern-list) extended patterns where possible. +// It can be used instead of [pattern.Regexp] for narrow use cases. +func ExtendedPatternMatcher(pat string, mode pattern.Mode) (func(string) bool, error) { + if mode&pattern.ExtendedOperators != 0 && mode&pattern.EntireString == 0 { + // In the future we could try to support !(pattern) without matching + // the entire input, ensuring we add enough test cases. + panic("ExtendedOperators is only supported with EntireString") + } + + // Extended pattern matching operators are always on outside of pathname expansion. + expr, err := pattern.Regexp(pat, mode) + if err != nil { + // Handle !(pattern-list) negation: when Regexp returns NegExtglobError, + // match the inner pattern and negate the result. + var negErr *pattern.NegExtGlobError + if !errors.As(err, &negErr) { + return nil, err + } + return extNegatedMatcher(pat, negErr.Groups) + } + rx := regexp.MustCompile(expr) + return rx.MatchString, nil +} + +// extNegatedMatcher handles !(pattern-list) extglob negation. +// Only a single !(...) group with fixed-string prefix and suffix is supported. +func extNegatedMatcher(pat string, groups []pattern.NegExtGlobGroup) (func(string) bool, error) { + if len(groups) != 1 { + return nil, fmt.Errorf("multiple extglob !(...) groups are not supported yet") + } + g := groups[0] + prefix := pat[:g.Start] + suffix := pat[g.End:] + + if pattern.HasMeta(prefix, 0) || pattern.HasMeta(suffix, 0) { + return nil, fmt.Errorf("extglob !(...) is only supported with a fixed prefix and suffix") + } + + // Use @(inner) to compile the pattern list, then negate the match. + inner := pat[g.Start+len("!(") : g.End-len(")")] + expr, err := pattern.Regexp("@("+inner+")", pattern.EntireString|pattern.ExtendedOperators) + if err != nil { + return nil, err + } + rx := regexp.MustCompile(expr) + + return func(name string) bool { + if !strings.HasPrefix(name, prefix) { + return false + } + if !strings.HasSuffix(name, suffix) { + return false + } + end := len(name) - len(suffix) + if end < len(prefix) { + return false // prefix and suffix overlap in name + } + middle := name[len(prefix):end] + + return !rx.MatchString(middle) + }, nil +} diff --git a/vendor/mvdan.cc/sh/v3/internal/testing.go b/vendor/mvdan.cc/sh/v3/internal/testing.go new file mode 100644 index 000000000..d9907c809 --- /dev/null +++ b/vendor/mvdan.cc/sh/v3/internal/testing.go @@ -0,0 +1,52 @@ +// Copyright (c) 2026, Daniel Martí +// See LICENSE for licensing information + +package internal + +import ( + "os" + "os/exec" + "path/filepath" + "strings" +) + +// TestMainSetup is used by the integration tests running shell scripts +// either via our interpreter or via real shells, +// to ensure a reasonably clean and consistent environment. +func TestMainSetup() { + // Set the locale to computer-friendly English and UTF-8. + // Some systems like macOS miss C.UTF8, so fall back to the US English locale. + if out, _ := exec.Command("locale", "-a").Output(); strings.Contains( + strings.ToLower(string(out)), "c.utf", + ) { + os.Setenv("LANGUAGE", "C.UTF-8") + os.Setenv("LC_ALL", "C.UTF-8") + } else { + os.Setenv("LANGUAGE", "en_US.UTF-8") + os.Setenv("LC_ALL", "en_US.UTF-8") + } + + // Bash prints the pwd after changing directories when CDPATH is set. + os.Unsetenv("CDPATH") + + pathDir, err := os.MkdirTemp("", "interp-bin-") + if err != nil { + panic(err) + } + + // These short names are commonly used as variables. + // Ensure they are unset as env vars. + // We can't easily remove names from $PATH, + // so do the next best thing: override each name with a failing script. + for _, s := range []string{ + "a", "b", "c", "d", "e", "f", "foo", "bar", + } { + os.Unsetenv(s) + pathFile := filepath.Join(pathDir, s) + if err := os.WriteFile(pathFile, []byte("#!/bin/sh\necho NO_SUCH_COMMAND; exit 1"), 0o777); err != nil { + panic(err) + } + } + + os.Setenv("PATH", pathDir+string(os.PathListSeparator)+os.Getenv("PATH")) +} diff --git a/vendor/mvdan.cc/sh/v3/pattern/pattern.go b/vendor/mvdan.cc/sh/v3/pattern/pattern.go index 6c8847673..51118b8b5 100644 --- a/vendor/mvdan.cc/sh/v3/pattern/pattern.go +++ b/vendor/mvdan.cc/sh/v3/pattern/pattern.go @@ -29,16 +29,36 @@ func (e SyntaxError) Error() string { return e.msg } func (e SyntaxError) Unwrap() error { return e.err } +// NegExtGlobGroup represents the byte offset range of a single !(expr) group +// within a pattern string. Start is the offset of '!', End is one past ')'. +type NegExtGlobGroup struct { + Start, End int +} + +// NegExtGlobError is returned by [Regexp] when an extglob negation operator +// !(pattern-list) is encountered, as Go's [regexp] package does not support +// negative lookahead. Callers can handle this by negating the result of +// matching the inner pattern. +type NegExtGlobError struct { + Groups []NegExtGlobGroup +} + +func (e *NegExtGlobError) Error() string { + return "extglob !(...) is not supported in this scenario" +} + // TODO(v4): flip NoGlobStar to be opt-in via GlobStar, matching bash // TODO(v4): flip EntireString to be opt-out via PartialMatch, as EntireString causes subtle bugs when forgotten // TODO(v4): rename NoGlobCase to CaseInsensitive for readability const ( - Shortest Mode = 1 << iota // prefer the shortest match. - Filenames // "*" and "?" don't match slashes; only "**" does - EntireString // match the entire string using ^$ delimiters - NoGlobCase // Do case-insensitive match (that is, use (?i) in the regexp) - NoGlobStar // Do not support "**" + Shortest Mode = 1 << iota // prefer the shortest match. + Filenames // "*" and "?" don't match slashes; only "**" does; only makes sense with EntireString too + EntireString // match the entire string using ^$ delimiters + NoGlobCase // do case-insensitive match (that is, use (?i) in the regexp); shopt "nocaseglob" + NoGlobStar // do not support "**"; negated shopt "globstar" + GlobLeadingDot // let wildcards match leading dots in filenames; shopt "dotglob" + ExtendedOperators // support extended pattern matching operators; shopt "extglob" for pathname expansion ) // Regexp turns a shell pattern into a regular expression that can be used with @@ -73,27 +93,35 @@ func Regexp(pat string, mode Mode) (string, error) { } var sb strings.Builder // Enable matching `\n` with the `.` metacharacter as globs match `\n` - sb.WriteString("(?s") + sb.WriteString(`(?s`) if mode&NoGlobCase != 0 { - sb.WriteString("i") + sb.WriteString(`i`) } if mode&Shortest != 0 { - sb.WriteString("U") + sb.WriteString(`U`) } - sb.WriteString(")") + sb.WriteString(`)`) if mode&EntireString != 0 { - sb.WriteString("^") + sb.WriteString(`^`) } sl := stringLexer{s: pat} + var negGroups []NegExtGlobGroup for { if err := regexpNext(&sb, &sl, mode); err == io.EOF { break } else if err != nil { - return "", err + negErr, ok := err.(*NegExtGlobError) + if !ok { + return "", err + } + negGroups = append(negGroups, negErr.Groups...) } } + if len(negGroups) > 0 { + return "", &NegExtGlobError{Groups: negGroups} + } if mode&EntireString != 0 { - sb.WriteString("$") + sb.WriteString(`$`) } return sb.String(), nil } @@ -134,13 +162,53 @@ func (sl *stringLexer) peekRest() string { } func regexpNext(sb *strings.Builder, sl *stringLexer, mode Mode) error { - switch c := sl.next(); c { + c := sl.next() + if mode&ExtendedOperators != 0 { + // Handle extended pattern matching operators separately, + // given that they can be one of many two-character prefixes. + // Note that we recurse into the same function in a loop, + // as each of the patterns in the list separated by '|' is a regular pattern. + switch op := c; op { + case '!', '?', '*', '+', '@': + if sl.peekNext() != '(' { + break + } + start := sl.i - 1 // position of the operator + sb.WriteByte(sl.next()) // ( + nestedLoop: + for { + switch sl.peekNext() { + case ')': + break nestedLoop + case '|': + // extended operators support a list of "or" separated expressions + sb.WriteByte(sl.next()) + continue + } + if err := regexpNext(sb, sl, mode); err == io.EOF { + break + } else if err != nil { + return err + } + } + sb.WriteByte(sl.next()) // ) + if op == '!' { + return &NegExtGlobError{Groups: []NegExtGlobGroup{{Start: start, End: sl.i}}} + } + if op != '@' { + // @( is [syntax.GlobOne] for matching once; no suffix needed + sb.WriteByte(op) + } + return nil + } + } + switch c { case '\x00': return io.EOF case '*': if mode&Filenames == 0 { // * - matches anything when not in filename mode - sb.WriteString(".*") + sb.WriteString(`.*`) break } // "**" only acts as globstar if it is alone as a path element. @@ -149,27 +217,37 @@ func regexpNext(sb *strings.Builder, sl *stringLexer, mode Mode) error { sl.i++ singleAfter := sl.i == len(sl.s) || sl.peekNext() == '/' if mode&NoGlobStar == 0 && singleBefore && singleAfter { - if sl.peekNext() == '/' { + // ** - match any number of slashes or "*" path elements + slashSuffix := sl.peekNext() == '/' + if slashSuffix { // **/ - like "**" but requiring a trailing slash when matching sl.i++ - sb.WriteString("((/|[^/.][^/]*)*/)?") + // wrap the expression to ensure that any match has a slash suffix + sb.WriteString(`(`) + } + if mode&GlobLeadingDot == 0 { + sb.WriteString(`(/|[^/.][^/]*)*`) } else { - // ** - match any number of slashes or "*" path elements - sb.WriteString("(/|[^/.][^/]*)*") + // with GlobLeadingDot (dotglob), match anything at all + sb.WriteString(`.*`) + } + if slashSuffix { + sb.WriteString(`/)?`) } break } // foo**, **bar, or NoGlobStar - behaves like "*" below } // * - matches anything except slashes and leading dots - if singleBefore { - sb.WriteString("([^/.][^/]*)?") + if singleBefore && mode&GlobLeadingDot == 0 { + sb.WriteString(`([^/.][^/]*)?`) } else { - sb.WriteString("[^/]*") + // with GlobLeadingDot (dotglob), match anything except slashes + sb.WriteString(`[^/]*`) } case '?': if mode&Filenames != 0 { - sb.WriteString("[^/]") + sb.WriteString(`[^/]`) } else { sb.WriteByte('.') } @@ -190,11 +268,11 @@ func regexpNext(sb *strings.Builder, sl *stringLexer, mode Mode) error { break } if mode&Filenames != 0 { - for _, c := range sl.peekRest() { - if c == ']' { + for i, c := range sl.peekRest() { + if i > 0 && c == ']' { break } else if c == '/' { - sb.WriteString("\\[") + sb.WriteString(`\[`) return nil } } diff --git a/vendor/mvdan.cc/sh/v3/shell/expand.go b/vendor/mvdan.cc/sh/v3/shell/expand.go index d0114ba6f..f8468cbf8 100644 --- a/vendor/mvdan.cc/sh/v3/shell/expand.go +++ b/vendor/mvdan.cc/sh/v3/shell/expand.go @@ -17,10 +17,10 @@ import ( // // If env is nil, the current environment variables are used. Empty variables // are treated as unset; to support variables which are set but empty, use the -// expand package directly. +// [expand] package directly. // -// Command substitutions like $(echo foo) aren't supported to avoid running -// arbitrary code. To support those, use an interpreter with the expand package. +// Other forms of expansion are not supported in this simple API, such as +// command substitutions like $(echo foo). To support them, use the [expand] package. // // An error will be reported if the input string had invalid syntax. func Expand(s string, env func(string) string) (string, error) { @@ -38,11 +38,15 @@ func Expand(s string, env func(string) string) (string, error) { // Fields performs shell expansion on s as if it were a command's arguments, // using env to resolve variables. It is similar to Expand, but includes brace -// expansion, tilde expansion, and globbing. +// expansion, tilde expansion, and word splitting. // // If env is nil, the current environment variables are used. Empty variables // are treated as unset; to support variables which are set but empty, use the -// expand package directly. +// [expand] package directly. +// +// Other forms of expansion are not supported in this simple API, such as +// globbing and command substitutions like $(echo foo). +// To support them, use the [expand] package. // // An error will be reported if the input string had invalid syntax. func Fields(s string, env func(string) string) ([]string, error) { diff --git a/vendor/mvdan.cc/sh/v3/syntax/braces.go b/vendor/mvdan.cc/sh/v3/syntax/braces.go index 0d32199fb..743b188d1 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/braces.go +++ b/vendor/mvdan.cc/sh/v3/syntax/braces.go @@ -4,6 +4,7 @@ package syntax import ( + "slices" "strconv" "strings" ) @@ -15,10 +16,10 @@ var ( litRightBrace = &Lit{Value: "}"} ) -// SplitBraces parses brace expansions within a word's literal parts. If any -// valid brace expansions are found, they are replaced with BraceExp nodes, and -// the function returns true. Otherwise, the word is left untouched and the -// function returns false. +// SplitBraces parses brace expansions within a word's literal parts. +// If any valid brace expansions are found, they are replaced with BraceExp nodes, +// and the function returns true. +// Otherwise, the word is left untouched and the function returns false. // // For example, a literal word "foo{bar,baz}" will result in a word containing // the literal "foo", and a brace expansion with the elements "bar" and "baz". @@ -26,7 +27,10 @@ var ( // It does not return an error; malformed brace expansions are simply skipped. // For example, the literal word "a{b" is left unchanged. func SplitBraces(word *Word) bool { - if !strings.Contains(word.Lit(), "{") { + if !slices.ContainsFunc(word.Parts, func(part WordPart) bool { + lit, ok := part.(*Lit) + return ok && strings.Contains(lit.Value, "{") + }) { // In the common case where a word has no braces, skip any allocs. return false } @@ -114,9 +118,7 @@ func SplitBraces(word *Word) bool { for i, elem := range br.Elems[:2] { val := elem.Lit() if _, err := strconv.Atoi(val); err == nil { - } else if len(val) == 1 && - (('a' <= val[0] && val[0] <= 'z') || - ('A' <= val[0] && val[0] <= 'Z')) { + } else if len(val) == 1 && asciiLetter(val[0]) { chars[i] = true } else { broken = true diff --git a/vendor/mvdan.cc/sh/v3/syntax/lexer.go b/vendor/mvdan.cc/sh/v3/syntax/lexer.go index f518cf199..3ac72bdff 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/lexer.go +++ b/vendor/mvdan.cc/sh/v3/syntax/lexer.go @@ -9,6 +9,14 @@ import ( "unicode/utf8" ) +func asciiLetter[T rune | byte](r T) bool { + return ('a' <= r && r <= 'z') || ('A' <= r && r <= 'Z') +} + +func asciiDigit[T rune | byte](r T) bool { + return r >= '0' && r <= '9' +} + // bytes that form or start a token func regOps(r rune) bool { switch r { @@ -28,20 +36,11 @@ func paramOps(r rune) bool { return false } -// these start a parameter expansion name -func paramNameOp(r rune) bool { - switch r { - case '}', ':', '+', '=', '%', '[', ']', '/', '^', ',': - return false - } - return true -} - // tokenize these inside arithmetic expansions func arithmOps(r rune) bool { switch r { case '+', '-', '!', '~', '*', '/', '%', '(', ')', '^', '<', '>', ':', '=', - ',', '?', '|', '&', '[', ']', '#': + ',', '?', '|', '&', '[', ']', '#', '.': return true } return false @@ -67,80 +66,91 @@ func (p *Parser) rune() rune { p.col += int64(p.w) bquotes := 0 retry: - if p.bsp < uint(len(p.bs)) { - if b := p.bs[p.bsp]; b < utf8.RuneSelf { - p.bsp++ - switch b { - case '\x00': - // Ignore null bytes while parsing, like bash. + if p.bsp >= uint(len(p.bs)) && p.fill() == 0 { + if len(p.bs) == 0 { + // Necessary for the last position to be correct. + // TODO: this is not exactly intuitive; figure out a better way. + p.bsp = 1 + } + p.r = utf8.RuneSelf + p.w = 1 + return p.r + } + if b := p.bs[p.bsp]; b < utf8.RuneSelf { + p.bsp++ + switch b { + case '\x00': + // Ignore null bytes while parsing, like bash. + p.col++ + goto retry + case '\r': + if p.peek() == '\n' { // \r\n turns into \n p.col++ goto retry - case '\r': - if p.peekByte('\n') { // \r\n turns into \n - p.col++ - goto retry - } - case '\\': - if p.r == '\\' { - } else if p.peekByte('\n') { - p.bsp++ - p.w, p.r = 1, escNewl - return escNewl - } else if p.peekBytes("\r\n") { // \\\r\n turns into \\\n - p.col++ - p.bsp += 2 - p.w, p.r = 2, escNewl - return escNewl - } - if p.openBquotes > 0 && bquotes < p.openBquotes && - p.bsp < uint(len(p.bs)) && bquoteEscaped(p.bs[p.bsp]) { - // We turn backquote command substitutions into $(), - // so we remove the extra backslashes needed by the backquotes. - bquotes++ - p.col++ - goto retry - } } - if b == '`' { - p.lastBquoteEsc = bquotes + case '\\': + if p.r == '\\' { + } else if p.peek() == '\n' { + p.bsp++ + p.w, p.r = 1, escNewl + return escNewl + } else if p1, p2 := p.peekTwo(); p1 == '\r' && p2 == '\n' { // \\\r\n turns into \\\n + p.col++ + p.bsp += 2 + p.w, p.r = 2, escNewl + return escNewl } - if p.litBs != nil { - p.litBs = append(p.litBs, b) + // TODO: why is this necessary to ensure correct position info? + p.readEOF = false + if p.openBquotes > 0 && bquotes < p.openBquotes && + p.bsp < uint(len(p.bs)) && bquoteEscaped(p.bs[p.bsp]) { + // We turn backquote command substitutions into $(), + // so we remove the extra backslashes needed by the backquotes. + bquotes++ + p.col++ + goto retry } - p.w, p.r = 1, rune(b) - return p.r } - if !utf8.FullRune(p.bs[p.bsp:]) { - // we need more bytes to read a full non-ascii rune - p.fill() + if b == '`' { + p.lastBquoteEsc = bquotes } - var w int - p.r, w = utf8.DecodeRune(p.bs[p.bsp:]) if p.litBs != nil { - p.litBs = append(p.litBs, p.bs[p.bsp:p.bsp+uint(w)]...) + p.litBs = append(p.litBs, b) } - p.bsp += uint(w) - if p.r == utf8.RuneError && w == 1 { - p.posErr(p.nextPos(), "invalid UTF-8 encoding") - } - p.w = w - } else { - if p.r == utf8.RuneSelf { - } else if p.fill(); p.bs == nil { - p.bsp++ - p.r = utf8.RuneSelf - p.w = 1 - } else { - goto retry + p.w, p.r = 1, rune(b) + return p.r + } +decodeRune: + var w int + p.r, w = utf8.DecodeRune(p.bs[p.bsp:]) + if p.r == utf8.RuneError && !utf8.FullRune(p.bs[p.bsp:]) { + // we need more bytes to read a full non-ascii rune + if p.fill() > 0 { + goto decodeRune } } + if p.litBs != nil { + p.litBs = append(p.litBs, p.bs[p.bsp:p.bsp+uint(w)]...) + } + p.bsp += uint(w) + if p.r == utf8.RuneError && w == 1 { + p.posErr(p.nextPos(), "invalid UTF-8 encoding") + } + p.w = w return p.r } -// fill reads more bytes from the input src into readBuf. Any bytes that -// had not yet been used at the end of the buffer are slid into the -// beginning of the buffer. -func (p *Parser) fill() { +// fill reads more bytes from the input src into readBuf. +// Any bytes that had not yet been used at the end of the buffer +// are slid into the beginning of the buffer. +// The number of read bytes is returned, which is at least one +// unless a read error occurred, such as [io.EOF]. +func (p *Parser) fill() (n int) { + if p.readEOF || p.r == utf8.RuneSelf { + // If the reader already gave us [io.EOF], do not try again. + // If we decided to stop for any reason, do not bother reading either. + return 0 + } p.offs += int64(p.bsp) left := len(p.bs) - int(p.bsp) copy(p.readBuf[:left], p.readBuf[p.bsp:]) @@ -149,6 +159,9 @@ readAgain: if err == nil { n, err = p.src.Read(p.readBuf[left:]) p.readErr = err + if err == io.EOF { + p.readEOF = true + } } if n == 0 { if err == nil { @@ -167,28 +180,21 @@ readAgain: p.bs = p.readBuf[:left+n] } p.bsp = 0 + return n } func (p *Parser) nextKeepSpaces() { r := p.r if p.quote != hdocBody && p.quote != hdocBodyTabs { - // Heredocs handle escaped newlines in a special way, but others - // do not. + // Heredocs handle escaped newlines in a special way, but others do not. for r == escNewl { r = p.rune() } } p.pos = p.nextPos() switch p.quote { - case paramExpRepl: - switch r { - case '}', '/': - p.tok = p.paramToken(r) - case '`', '"', '$', '\'': - p.tok = p.regToken(r) - default: - p.advanceLitOther(r) - } + case runeByRune: + p.tok = illegalTok case dblQuotes: switch r { case '`', '"', '$': @@ -203,7 +209,14 @@ func (p *Parser) nextKeepSpaces() { default: p.advanceLitHdoc(r) } - default: // paramExpExp: + case paramExpRepl: + if r == '/' { + p.rune() + p.tok = slash + break + } + fallthrough + case paramExpExp: switch r { case '}': p.tok = p.paramToken(r) @@ -213,7 +226,7 @@ func (p *Parser) nextKeepSpaces() { p.advanceLitOther(r) } } - if p.err != nil && p.tok != _EOF { + if p.err != nil { p.tok = _EOF } } @@ -273,19 +286,17 @@ skipSpace: case p.quote&allRegTokens != 0: switch r { case ';', '"', '\'', '(', ')', '$', '|', '&', '>', '<', '`': + if r == '<' && p.lang.in(LangZsh) && p.zshNumRange() { + p.advanceLitNone(r) + return + } p.tok = p.regToken(r) case '#': // If we're parsing $foo#bar, ${foo}#bar, 'foo'#bar, or "foo"#bar, // #bar is a continuation of the same word, not a comment. - // TODO: support $(foo)#bar and `foo`#bar as well, which is slightly tricky, - // as we can't easily tell them apart from (foo)#bar and `#bar`, - // where #bar should remain a comment. - if !p.spaced { - switch p.tok { - case _LitWord, rightBrace, sglQuote, dblQuote: - p.advanceLitNone(r) - return - } + if p.quote == unquotedWordCont && !p.spaced { + p.advanceLitNone(r) + return } r = p.rune() p.newLit(r) @@ -313,9 +324,21 @@ skipSpace: p.litBs = nil } p.next() - case '[', '=': + case '[': if p.quote == arrayElems { - p.tok = p.paramToken(r) + p.rune() + p.tok = leftBrack + } else { + p.advanceLitNone(r) + } + case '=': + if p.peek() == '(' { + p.rune() + p.rune() + p.tok = assgnParen + } else if p.quote == arrayElems { + p.rune() + p.tok = assgn } else { p.advanceLitNone(r) } @@ -330,7 +353,7 @@ skipSpace: p.tok = globPlus case '@': p.tok = globAt - default: // '!' + case '!': p.tok = globExcl } p.rune() @@ -347,7 +370,7 @@ skipSpace: p.tok = p.paramToken(r) case p.quote == testExprRegexp: if !p.rxFirstPart && p.spaced { - p.quote = noState + p.quote = testExpr goto skipSpace } p.rxFirstPart = false @@ -360,7 +383,7 @@ skipSpace: p.advanceLitRe(r) } else { p.tok = rightParen - p.quote = noState + p.quote = testExpr p.rune() // we are tokenizing manually } default: // including '(', '|' @@ -371,7 +394,7 @@ skipSpace: default: p.advanceLitOther(r) } - if p.err != nil && p.tok != _EOF { + if p.err != nil { p.tok = _EOF } } @@ -379,36 +402,53 @@ skipSpace: // extendedGlob determines whether we're parsing a Bash extended globbing expression. // For example, whether `*` or `@` are followed by `(` to form `@(foo)`. func (p *Parser) extendedGlob() bool { + if p.lang.in(LangZsh) { + // Zsh supports Bash extended globs via the KSH_GLOB option. + // In Bash we would parse extended globs as [ExtGlob] nodes, + // but trying to do that in Zsh would cause ambiguity with glob qualifiers. + // Just like glob qualifiers, parse extended globs as literals in Zsh. + return false + } if p.val == "function" { + // We don't support e.g. `function @() { ... }` at the moment, but we could. return false } - if p.peekByte('(') { + if p.peek() == '(' { // NOTE: empty pattern list is a valid globbing syntax like `@()`, // but we'll operate on the "likelihood" that it is a function; // only tokenize if its a non-empty pattern list. // We do this after peeking for just one byte, so that the input `echo *` // followed by a newline does not hang an interactive shell parser until // another byte is input. - return !p.peekBytes("()") + _, p2 := p.peekTwo() + return p2 != ')' } return false } -func (p *Parser) peekBytes(s string) bool { - peekEnd := int(p.bsp) + len(s) - // TODO: This should loop for slow readers, e.g. those providing one byte at - // a time. Use a loop and test it with [testing/iotest.OneByteReader]. - if peekEnd > len(p.bs) { +func (p *Parser) peek() byte { + if int(p.bsp) >= len(p.bs) { p.fill() } - return peekEnd <= len(p.bs) && bytes.HasPrefix(p.bs[p.bsp:peekEnd], []byte(s)) + if int(p.bsp) >= len(p.bs) { + return utf8.RuneSelf + } + return p.bs[p.bsp] } -func (p *Parser) peekByte(b byte) bool { - if p.bsp == uint(len(p.bs)) { +func (p *Parser) peekTwo() (byte, byte) { + // TODO: This should loop for slow readers, e.g. those providing one byte at + // a time. Use a loop and test it with [testing/iotest.OneByteReader]. + if int(p.bsp+1) >= len(p.bs) { p.fill() } - return p.bsp < uint(len(p.bs)) && p.bs[p.bsp] == b + if int(p.bsp) >= len(p.bs) { + return utf8.RuneSelf, utf8.RuneSelf + } + if int(p.bsp+1) >= len(p.bs) { + return p.bs[p.bsp], utf8.RuneSelf + } + return p.bs[p.bsp], p.bs[p.bsp+1] } func (p *Parser) regToken(r rune) token { @@ -429,11 +469,39 @@ func (p *Parser) regToken(r rune) token { p.rune() return andAnd case '>': - if p.rune() == '>' { + switch p.rune() { + case '|': p.rune() + return rdrAllClob + case '!': + if p.lang.in(LangZsh) { + p.rune() + return rdrAllClob + } + case '>': + switch p.rune() { + case '|': + p.rune() + return appAllClob + case '!': + if p.lang.in(LangZsh) { + p.rune() + return appAllClob + } + } return appAll } return rdrAll + case '|': + if p.lang.in(LangZsh) { + p.rune() + return andPipe + } + case '!': + if p.lang.in(LangZsh) { + p.rune() + return andBang + } } return and case '|': @@ -442,7 +510,7 @@ func (p *Parser) regToken(r rune) token { p.rune() return orOr case '&': - if p.lang == LangPOSIX { + if !p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { break } p.rune() @@ -452,13 +520,13 @@ func (p *Parser) regToken(r rune) token { case '$': switch p.rune() { case '\'': - if p.lang == LangPOSIX { + if !p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { break } p.rune() return dollSglQuote case '"': - if p.lang == LangPOSIX { + if !p.lang.in(langBashLike | LangMirBSDKorn) { break } p.rune() @@ -467,7 +535,7 @@ func (p *Parser) regToken(r rune) token { p.rune() return dollBrace case '[': - if !p.lang.isBash() || p.quote == paramExpName { + if !p.lang.in(langBashLike) { // latter to not tokenise ${$[@]} as $[ break } @@ -482,7 +550,7 @@ func (p *Parser) regToken(r rune) token { } return dollar case '(': - if p.rune() == '(' && p.lang != LangPOSIX && p.quote != testExpr { + if p.rune() == '(' && p.lang.in(langBashLike|LangMirBSDKorn|LangZsh) && p.quote != testExpr { p.rune() return dblLeftParen } @@ -493,19 +561,19 @@ func (p *Parser) regToken(r rune) token { case ';': switch p.rune() { case ';': - if p.rune() == '&' && p.lang.isBash() { + if p.rune() == '&' && p.lang.in(langBashLike) { p.rune() return dblSemiAnd } return dblSemicolon case '&': - if p.lang == LangPOSIX { + if !p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { break } p.rune() return semiAnd case '|': - if p.lang != LangMirBSDKorn { + if !p.lang.in(LangMirBSDKorn) { break } p.rune() @@ -515,10 +583,11 @@ func (p *Parser) regToken(r rune) token { case '<': switch p.rune() { case '<': - if r = p.rune(); r == '-' { + switch p.rune() { + case '-': p.rune() return dashHdoc - } else if r == '<' { + case '<': p.rune() return wordHdoc } @@ -530,26 +599,63 @@ func (p *Parser) regToken(r rune) token { p.rune() return dplIn case '(': - if !p.lang.isBash() { + if !p.lang.in(langBashLike | LangZsh) { break } p.rune() return cmdIn } return rdrIn - default: // '>' + case '>': switch p.rune() { case '>': - p.rune() + switch p.rune() { + case '|': + p.rune() + return appClob + case '!': + if p.lang.in(LangZsh) { + p.rune() + return appClob + } + case '&': + if !p.lang.in(LangZsh) { + break + } + switch p.rune() { + case '|': + p.rune() + return appAllClob // >>&| is an alias for &>>| + case '!': + p.rune() + return appAllClob // >>&! is an alias for &>>| + } + return appAll // >>& is an alias for &>> + } return appOut case '&': - p.rune() + r = p.rune() + if p.lang.in(LangZsh) { + switch r { + case '|': + p.rune() + return rdrAllClob // >&| is an alias for &>| + case '!': + p.rune() + return rdrAllClob // >&! is an alias for &>| + } + } return dplOut case '|': p.rune() - return clbOut + return rdrClob + case '!': + if p.lang.in(LangZsh) { + p.rune() + return rdrClob + } case '(': - if !p.lang.isBash() { + if !p.lang.in(langBashLike | LangZsh) { break } p.rune() @@ -557,6 +663,7 @@ func (p *Parser) regToken(r rune) token { } return rdrOut } + panic("unreachable") } func (p *Parser) dqToken(r rune) token { @@ -568,13 +675,13 @@ func (p *Parser) dqToken(r rune) token { // Don't call p.rune, as we need to work out p.openBquotes to // properly handle backslashes in the lexer. return bckQuote - default: // '$' + case '$': switch p.rune() { case '{': p.rune() return dollBrace case '[': - if !p.lang.isBash() { + if !p.lang.in(langBashLike) { break } p.rune() @@ -588,6 +695,7 @@ func (p *Parser) dqToken(r rune) token { } return dollar } + panic("unreachable") } func (p *Parser) paramToken(r rune) token { @@ -609,6 +717,15 @@ func (p *Parser) paramToken(r rune) token { case '=': p.rune() return colAssgn + case '#': + p.rune() + return colHash + case '|': + p.rune() + return colPipe + case '*': + p.rune() + return colStar } return colon case '+': @@ -638,14 +755,11 @@ func (p *Parser) paramToken(r rune) token { case '!': p.rune() return exclMark - case '[': - p.rune() - return leftBrack case ']': p.rune() return rightBrack case '/': - if p.rune() == '/' && p.quote != paramExpRepl { + if p.rune() == '/' { p.rune() return dblSlash } @@ -665,9 +779,16 @@ func (p *Parser) paramToken(r rune) token { case '@': p.rune() return at - default: // '*' + case '*': p.rune() return star + + // This func gets called by the parser in [runeByRune] mode; + // we need to handle EOF and unexpected runes. + case utf8.RuneSelf: + return _EOF + default: + return illegalTok } } @@ -697,7 +818,10 @@ func (p *Parser) arithmToken(r rune) token { case '&': switch p.rune() { case '&': - p.rune() + if p.rune() == '=' && p.lang.in(LangZsh) { + p.rune() + return andBoolAssgn + } return andAnd case '=': p.rune() @@ -707,7 +831,10 @@ func (p *Parser) arithmToken(r rune) token { case '|': switch p.rune() { case '|': - p.rune() + if p.rune() == '=' && p.lang.in(LangZsh) { + p.rune() + return orBoolAssgn + } return orOr case '=': p.rune() @@ -769,7 +896,10 @@ func (p *Parser) arithmToken(r rune) token { case '*': switch p.rune() { case '*': - p.rune() + if p.rune() == '=' && p.lang.in(LangZsh) { + p.rune() + return powAssgn + } return power case '=': p.rune() @@ -783,7 +913,14 @@ func (p *Parser) arithmToken(r rune) token { } return slash case '^': - if p.rune() == '=' { + switch p.rune() { + case '^': + if p.rune() == '=' && p.lang.in(LangZsh) { + p.rune() + return xorBoolAssgn + } + return dblCaret + case '=': p.rune() return xorAssgn } @@ -803,10 +940,14 @@ func (p *Parser) arithmToken(r rune) token { case ':': p.rune() return colon - default: // '#' + case '#': p.rune() return hash + case '.': + p.rune() + return period } + panic("unreachable") } func (p *Parser) newLit(r rune) { @@ -831,7 +972,7 @@ func (p *Parser) endLit() (s string) { s = string(p.litBs[:len(p.litBs)-p.w]) } p.litBs = nil - return + return s } func (p *Parser) isLitRedir() bool { @@ -839,31 +980,20 @@ func (p *Parser) isLitRedir() bool { if lit[0] == '{' && lit[len(lit)-1] == '}' { return ValidName(string(lit[1 : len(lit)-1])) } - for _, b := range lit { - switch b { - case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': - default: - return false - } - } - return true + return numberLiteral(lit) } -func (p *Parser) advanceNameCont(r rune) { - // we know that r is a letter or underscore -loop: - for p.newLit(r); r != utf8.RuneSelf; r = p.rune() { - switch { - case 'a' <= r && r <= 'z': - case 'A' <= r && r <= 'Z': - case r == '_': - case '0' <= r && r <= '9': - case r == escNewl: - default: - break loop - } +func singleRuneParam[T rune | byte](r T) bool { + switch r { + case '@', '*', '#', '$', '?', '!', '-', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + return true } - p.tok, p.val = _LitWord, p.endLit() + return false +} + +func paramNameRune[T rune | byte](r T) bool { + return asciiLetter(r) || asciiDigit(r) || r == '_' } func (p *Parser) advanceLitOther(r rune) { @@ -885,18 +1015,18 @@ loop: break loop } case ':', '=', '%', '^', ',', '?', '!', '~', '*': - if p.quote&allArithmExpr != 0 || p.quote == paramExpName { + if p.quote&allArithmExpr != 0 { break loop } - case '[', ']': - if p.lang != LangPOSIX && p.quote&allArithmExpr != 0 { + case '.': + if !p.lang.in(LangZsh) && p.quote&allArithmExpr != 0 { break loop } - fallthrough - case '#', '@': - if p.quote&allParamReg != 0 { + case '[', ']': + if p.lang.in(langBashLike|LangMirBSDKorn|LangZsh) && p.quote&allArithmExpr != 0 { break loop } + fallthrough case '+', '-', ' ', '\t', ';', '&', '>', '<', '|', '(', ')', '\n', '\r': if p.quote&allKeepSpaces == 0 { break loop @@ -906,18 +1036,52 @@ loop: p.tok, p.val = tok, p.endLit() } +// zshNumRange peeks at the bytes after '<' to check for a zsh numeric +// range glob pattern like <->, <5->, <-10>, or <5-10>. +func (p *Parser) zshNumRange() bool { + // Peeking a handful of bytes here should be enough. + // TODO: This should loop for slow readers, e.g. those providing one byte at + // a time. Use a loop and test it with [testing/iotest.OneByteReader]. + if int(p.bsp) >= len(p.bs) { + p.fill() + } + rest := p.bs[p.bsp:] + for len(rest) > 0 && rest[0] >= '0' && rest[0] <= '9' { + rest = rest[1:] + } + if len(rest) == 0 || rest[0] != '-' { + return false + } + rest = rest[1:] + for len(rest) > 0 && rest[0] >= '0' && rest[0] <= '9' { + rest = rest[1:] + } + return len(rest) > 0 && rest[0] == '>' +} + func (p *Parser) advanceLitNone(r rune) { p.eqlOffs = -1 tok := _LitWord loop: for p.newLit(r); r != utf8.RuneSelf; r = p.rune() { switch r { - case ' ', '\t', '\n', '\r', '&', '|', ';', '(', ')': + case ' ', '\t', '\n', '\r', '&', '|', ';', ')': + break loop + case '(': break loop case '\\': // escaped byte follows p.rune() case '>', '<': - if p.peekByte('(') { + if r == '<' && p.lang.in(LangZsh) && p.zshNumRange() { + // Zsh numeric range glob like <-> or <1-100>; consume until '>'. + for { + if r = p.rune(); r == '>' || r == utf8.RuneSelf { + break + } + } + continue + } + if p.peek() == '(' { tok = _Lit } else if p.isLitRedir() { tok = _LitRedir @@ -941,7 +1105,7 @@ loop: p.eqlOffs = len(p.litBs) - 1 } case '[': - if p.lang != LangPOSIX && len(p.litBs) > 1 && p.litBs[0] != '[' { + if p.lang.in(langBashLike|LangMirBSDKorn|LangZsh) && len(p.litBs) > 1 && p.litBs[0] != '[' { tok = _Lit break loop } @@ -980,10 +1144,8 @@ func (p *Parser) advanceLitHdoc(r rune) { p.tok = _Lit p.newLit(r) - if p.quote == hdocBodyTabs { - for r == '\t' { - r = p.rune() - } + for p.quote == hdocBodyTabs && r == '\t' { + r = p.rune() } lStart := len(p.litBs) - 1 stop := p.hdocStops[len(p.hdocStops)-1] @@ -1029,10 +1191,8 @@ func (p *Parser) advanceLitHdoc(r rune) { if r != '\n' { return // hit an unexpected EOF or closing backquote } - if p.quote == hdocBodyTabs { - for p.peekByte('\t') { - p.rune() - } + for p.quote == hdocBodyTabs && p.peek() == '\t' { + p.rune() } lStart = len(p.litBs) } @@ -1048,10 +1208,8 @@ func (p *Parser) quotedHdocWord() *Word { if r == utf8.RuneSelf { return nil } - if p.quote == hdocBodyTabs { - for r == '\t' { - r = p.rune() - } + for p.quote == hdocBodyTabs && r == '\t' { + r = p.rune() } lStart := len(p.litBs) - 1 runeLoop: @@ -1098,13 +1256,13 @@ func (p *Parser) advanceLitRe(r rune) { case ')': if p.rxOpenParens--; p.rxOpenParens < 0 { p.tok, p.val = _LitWord, p.endLit() - p.quote = noState + p.quote = testExpr return } case ' ', '\t', '\r', '\n', ';', '&', '>', '<': if p.rxOpenParens <= 0 { p.tok, p.val = _LitWord, p.endLit() - p.quote = noState + p.quote = testExpr return } case '"', '\'', '$', '`': diff --git a/vendor/mvdan.cc/sh/v3/syntax/nodes.go b/vendor/mvdan.cc/sh/v3/syntax/nodes.go index 902e32509..e50dc8246 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/nodes.go +++ b/vendor/mvdan.cc/sh/v3/syntax/nodes.go @@ -219,6 +219,7 @@ type Stmt struct { Negated bool // ! stmt Background bool // stmt & Coprocess bool // mksh's |& + Disown bool // zsh's &| or &! Redirs []*Redirect // stmt >a w.lastLine { - if w.Incomplete() { + // by [Parser.Stmts]. + if (w.p.r == '\n' || w.p.r == escNewl) && w.p.line > w.lastLine { + if w.p.Incomplete() { // Incomplete statement; call back to print "> ". - if !w.fn(w.accumulated) { + if !w.yield(w.accumulated, w.p.err) { return 0, io.EOF } } else if len(w.accumulated) == 0 { // Nothing was parsed; call back to print another "$ ". - if !w.fn(nil) { + if !w.yield(nil, w.p.err) { return 0, io.EOF } } - w.lastLine = w.line + w.lastLine = w.p.line } - return w.Reader.Read(p) + return w.rd.Read(p) } -// Interactive implements what is necessary to parse statements in an +// Interactive is a pre-iterators API which now wraps [Parser.InteractiveSeq]. +// +// Deprecated: use [Parser.InteractiveSeq]. +func (p *Parser) Interactive(r io.Reader, fn func([]*Stmt) bool) error { + for stmts, err := range p.InteractiveSeq(r) { + if err != nil { + return err + } + if !fn(stmts) { + break + } + } + return nil +} + +// InteractiveSeq implements what is necessary to parse statements in an // interactive shell. The parser will call the given function under two // circumstances outlined below. // @@ -268,28 +362,39 @@ func (w *wrappedReader) Read(p []byte) (n int, err error) { // // If the callback function returns false, parsing is stopped and the function // is not called again. -func (p *Parser) Interactive(r io.Reader, fn func([]*Stmt) bool) error { - w := wrappedReader{Parser: p, Reader: r, fn: fn} - return p.Stmts(&w, func(stmt *Stmt) bool { - w.accumulated = append(w.accumulated, stmt) - // We finished parsing a statement and we're at a newline token, - // so we finished fully parsing a number of statements. Call - // back to run the statements and print "$ ". - if p.tok == _Newl { - if !fn(w.accumulated) { - return false +func (p *Parser) InteractiveSeq(r io.Reader) iter.Seq2[[]*Stmt, error] { + return func(yield func([]*Stmt, error) bool) { + w := wrappedReader{p: p, rd: r, yield: yield} + for stmts, err := range p.StmtsSeq(&w) { + w.accumulated = append(w.accumulated, stmts) + if err != nil { + if !yield(w.accumulated, err) { + break + } + // If the caller wishes, they can continue in the presence of parse errors. + // TODO: does this even work? Write tests for it. This only came up + continue + } + // We finished parsing a statement and we're at a newline token, + // so we finished fully parsing a number of statements. Call + // back to run the statements and print "$ ". + if p.tok == _Newl { + if !yield(w.accumulated, nil) { + break + } + w.accumulated = w.accumulated[:0] + // The callback above would already print "$ ", so we + // don't want the subsequent wrappedReader.Read to cause + // another "$ " print thinking that nothing was parsed. + w.lastLine = w.p.line + 1 } - w.accumulated = w.accumulated[:0] - // The callback above would already print "$ ", so we - // don't want the subsequent wrappedReader.Read to cause - // another "$ " print thinking that nothing was parsed. - w.lastLine = w.line + 1 } - return true - }) + } } // Words is a pre-iterators API which now wraps [Parser.WordsSeq]. +// +// Deprecated: use [Parser.WordsSeq]. func (p *Parser) Words(r io.Reader, fn func(*Word) bool) error { for w, err := range p.WordsSeq(r) { if err != nil { @@ -323,7 +428,7 @@ func (p *Parser) WordsSeq(r io.Reader) iter.Seq2[*Word, error] { w := p.getWord() if w == nil { if p.tok != _EOF { - p.curErr("%s is not a valid word", p.tok) + p.curErr("%#q is not a valid word", p.tok) } if p.err != nil { yield(nil, p.err) @@ -375,27 +480,28 @@ func (p *Parser) Arithmetic(r io.Reader) (ArithmExpr, error) { type Parser struct { src io.Reader bs []byte // current chunk of read bytes - bsp uint // pos within chunk for the rune after r; uint helps eliminate bounds checks - r rune // next rune - w int // width of r + bsp uint // offset within [Parser.bs] for the rune after [Parser.r] + r rune // next rune; [utf8.RuneSelf] when it went past EOF, or we stopped + w int // width of [Parser.r] f *File - spaced bool // whether tok has whitespace on its left + spaced bool // whether [Parser.tok] has whitespace on its left err error // lexer/parser error readErr error // got a read error, but bytes left + readEOF bool // [Parser.src] already gave us an [io.EOF] error tok token // current token val string // current value (valid if tok is _Lit*) - // position of r, to be converted to Parser.pos later + // position of [Parser.r], to be converted to [Parser.pos] later offs, line, col int64 pos Pos // position of tok quote quoteState // current lexer state - eqlOffs int // position of '=' in val (a literal) + eqlOffs int // position of '=' in [Parser.val] (a literal) keepComments bool lang LangVariant @@ -413,7 +519,7 @@ type Parser struct { hdocStops [][]byte // stack of end words for open heredocs - parsingDoc bool // true if using Parser.Document + parsingDoc bool // true if using [Parser.Document] // openNodes tracks how many entire statements or words we're currently parsing. // A non-zero number means that we require certain tokens or words before @@ -458,7 +564,7 @@ func (p *Parser) reset() { p.bs, p.bsp = nil, 0 p.offs, p.line, p.col = 0, 1, 1 p.r, p.w = 0, 0 - p.err, p.readErr = nil, nil + p.err, p.readErr, p.readEOF = nil, nil, false p.quote, p.forbidNested = noState, false p.openNodes = 0 p.recoveredErrors = 0 @@ -473,6 +579,7 @@ func (p *Parser) reset() { p.litBs = nil } +// nextPos returns the position of the next rune, [Parser.r]. func (p *Parser) nextPos() Pos { // Basic protection against offset overflow; // note that an offset of 0 is valid, so we leave the maximum. @@ -538,12 +645,20 @@ func (p *Parser) call(w *Word) *CallExpr { return ce } -//go:generate stringer -type=quoteState - type quoteState uint32 const ( + // The initial state of the parser. noState quoteState = 1 << iota + + // Used when parsing parameter expansions; use with [Parser.rune], + // [Parser.next] always returns [illegalTok]. + runeByRune + + // unquotedWordCont exists purely so that the '#' in $foo#bar does not + // get parsed as a comment; it's a tiny variation on [noState]. + unquotedWordCont + subCmd subCmdBckquo dblQuotes @@ -553,24 +668,20 @@ const ( arithmExpr arithmExprLet arithmExprCmd - arithmExprBrack testExpr testExprRegexp switchCase - paramExpName - paramExpSlice + paramExpArithm paramExpRepl paramExpExp arrayElems - allKeepSpaces = paramExpRepl | dblQuotes | hdocBody | - hdocBodyTabs | paramExpExp - allRegTokens = noState | subCmd | subCmdBckquo | hdocWord | + allKeepSpaces = runeByRune | paramExpRepl | dblQuotes | hdocBody | + hdocBodyTabs | paramExpRepl | paramExpExp + allRegTokens = noState | unquotedWordCont | subCmd | subCmdBckquo | hdocWord | switchCase | arrayElems | testExpr - allArithmExpr = arithmExpr | arithmExprLet | arithmExprCmd | - arithmExprBrack | paramExpSlice - allParamReg = paramExpName | paramExpSlice - allParamExp = allParamReg | paramExpRepl | paramExpExp | arithmExprBrack + allArithmExpr = arithmExpr | arithmExprLet | arithmExprCmd | paramExpArithm + allParamExp = paramExpArithm | paramExpRepl | paramExpExp ) type saveState struct { @@ -581,7 +692,7 @@ type saveState struct { func (p *Parser) preNested(quote quoteState) (s saveState) { s.quote, s.buriedHdocs = p.quote, p.buriedHdocs p.buriedHdocs, p.quote = len(p.heredocs), quote - return + return s } func (p *Parser) postNested(s saveState) { @@ -644,28 +755,14 @@ func (p *Parser) doHeredocs() { if i > 0 && p.r == '\n' { p.rune() } - lastLine := p.line if quoted { r.Hdoc = p.quotedHdocWord() } else { p.next() r.Hdoc = p.getWord() } - if r.Hdoc != nil { - lastLine = int64(r.Hdoc.End().Line()) - } - if lastLine < p.line { - // TODO: It seems like this triggers more often than it - // should. Look into it. - l := p.lit(p.nextPos(), "") - if r.Hdoc == nil { - r.Hdoc = p.wordOne(l) - } else { - r.Hdoc.Parts = append(r.Hdoc.Parts, l) - } - } if stop := p.hdocStops[len(p.hdocStops)-1]; stop != nil { - p.posErr(r.Pos(), "unclosed here-document '%s'", stop) + p.posErr(r.Pos(), "unclosed here-document %#q", stop) } p.hdocStops = p.hdocStops[:len(p.hdocStops)-1] } @@ -697,26 +794,33 @@ func (p *Parser) recoverError() bool { return false } -func readableStr(s string) string { - // don't quote tokens like & or } - if s != "" && s[0] >= 'a' && s[0] <= 'z' { - return strconv.Quote(s) +type noQuote string + +func (s noQuote) Format(f fmt.State, verb rune) { + f.Write([]byte(s)) +} + +func (t token) Format(f fmt.State, verb rune) { + if t < _realTokenBoundary && verb == 'q' { + // EOF, Lit and the others should not be quoted in error messages + // as they are not real shell syntax like `if` or `{`. + f.Write([]byte(t.String())) + } else { + fmt.Fprintf(f, fmt.FormatString(f, verb), t.String()) } - return s } -func (p *Parser) followErr(pos Pos, left, right string) { - leftStr := readableStr(left) - p.posErr(pos, "%s must be followed by %s", leftStr, right) +func (p *Parser) followErr(pos Pos, left, right any) { + p.posErr(pos, "%#q must be followed by %#q", left, right) } -func (p *Parser) followErrExp(pos Pos, left string) { - p.followErr(pos, left, "an expression") +func (p *Parser) followErrExp(pos Pos, left any) { + p.followErr(pos, left, noQuote("an expression")) } func (p *Parser) follow(lpos Pos, left string, tok token) { if !p.got(tok) { - p.followErr(lpos, left, tok.String()) + p.followErr(lpos, left, tok) } } @@ -726,22 +830,36 @@ func (p *Parser) followRsrv(lpos Pos, left, val string) Pos { if p.recoverError() { return recoveredPos } - p.followErr(lpos, left, fmt.Sprintf("%q", val)) + p.followErr(lpos, left, val) } return pos } func (p *Parser) followStmts(left string, lpos Pos, stops ...string) ([]*Stmt, []Comment) { + // Language variants disallowing empty command lists: + // * [LangPOSIX]: "A list is a sequence of one or more AND-OR lists...". + // * [LangBash]: "A list is a sequence of one or more pipelines..." + // + // Language variants allowing empty command lists: + // * [LangZsh]: "A list is a sequence of zero or more sublists...". + // * [LangMirBSDKorn]: "Lists of commands can be created by separating pipelines..."; + // note that the man page is not explicit, but the shell clearly allows e.g. `{ }`. if p.got(semicolon) { + if p.lang.in(LangZsh | LangMirBSDKorn) { + return nil, nil // allow an empty list + } + p.followErr(lpos, left, noQuote("a statement list")) return nil, nil } - newLine := p.got(_Newl) stmts, last := p.stmtList(stops...) - if len(stmts) < 1 && !newLine { + if len(stmts) < 1 { + if p.lang.in(LangZsh | LangMirBSDKorn) { + return nil, nil // allow an empty list + } if p.recoverError() { return []*Stmt{{Position: recoveredPos}}, nil } - p.followErr(lpos, left, "a statement list") + p.followErr(lpos, left, noQuote("a statement list")) } return stmts, last } @@ -752,7 +870,7 @@ func (p *Parser) followWordTok(tok token, pos Pos) *Word { if p.recoverError() { return p.wordOne(&Lit{ValuePos: recoveredPos}) } - p.followErr(pos, tok.String(), "a word") + p.followErr(pos, tok, noQuote("a word")) } return w } @@ -763,19 +881,17 @@ func (p *Parser) stmtEnd(n Node, start, end string) Pos { if p.recoverError() { return recoveredPos } - p.posErr(n.Pos(), "%s statement must end with %q", start, end) + p.posErr(n.Pos(), "%#q statement must end with %#q", start, end) } return pos } func (p *Parser) quoteErr(lpos Pos, quote token) { - p.posErr(lpos, "reached %s without closing quote %s", - p.tok.String(), quote) + p.posErr(lpos, "reached %#q without closing quote %#q", p.tok, quote) } -func (p *Parser) matchingErr(lpos Pos, left, right any) { - p.posErr(lpos, "reached %s without matching %s with %s", - p.tok.String(), left, right) +func (p *Parser) matchingErr(lpos Pos, left, right token) { + p.posErr(lpos, "reached %#q without matching %#q with %#q", p.tok, left, right) } func (p *Parser) matched(lpos Pos, left, right token) Pos { @@ -807,9 +923,14 @@ func IsIncomplete(err error) bool { return ok && perr.Incomplete } -// IsKeyword returns true if the given word is part of the language keywords. +// TODO: probably redo with a [LangVariant] argument. +// Perhaps offer an iterator version as well. + +// IsKeyword returns true if the given word is a language keyword +// in POSIX Shell or Bash. func IsKeyword(word string) bool { // This list has been copied from the bash 5.1 source code, file y.tab.c +4460 + // TODO: should we include entries for zsh here? e.g. "{}", "repeat", "always", ... switch word { case "!", @@ -850,9 +971,9 @@ type ParseError struct { func (e ParseError) Error() string { if e.Filename == "" { - return fmt.Sprintf("%s: %s", e.Pos.String(), e.Text) + return fmt.Sprintf("%s: %s", e.Pos, e.Text) } - return fmt.Sprintf("%s:%s: %s", e.Filename, e.Pos.String(), e.Text) + return fmt.Sprintf("%s:%s: %s", e.Filename, e.Pos, e.Text) } // LangError is returned when the parser encounters code that is only valid in @@ -862,6 +983,8 @@ type LangError struct { Filename string Pos Pos + // TODO: consider replacing the Langs slice with a bitset. + // Feature briefly describes which language feature caused the error. Feature string // Langs lists some of the language variants which support the feature. @@ -873,9 +996,11 @@ type LangError struct { func (e LangError) Error() string { var sb strings.Builder if e.Filename != "" { - sb.WriteString(e.Filename + ":") + sb.WriteString(e.Filename) + sb.WriteString(":") } - sb.WriteString(e.Pos.String() + ": ") + sb.WriteString(e.Pos.String()) + sb.WriteString(": ") sb.WriteString(e.Feature) if strings.HasSuffix(e.Feature, "s") { sb.WriteString(" are a ") @@ -893,30 +1018,43 @@ func (e LangError) Error() string { return sb.String() } -func (p *Parser) posErr(pos Pos, format string, a ...any) { +func (p *Parser) posErr(pos Pos, format string, args ...any) { + // for i, arg := range args { + // if arg, ok := arg.(fmt.Stringer); ok && arg != _EOF { + // args[i] = quotedToken(arg) + // } + // } p.errPass(ParseError{ Filename: p.f.Name, Pos: pos, - Text: fmt.Sprintf(format, a...), + Text: fmt.Sprintf(format, args...), Incomplete: p.tok == _EOF && p.Incomplete(), }) } -func (p *Parser) curErr(format string, a ...any) { - p.posErr(p.pos, format, a...) +func (p *Parser) curErr(format string, args ...any) { + p.posErr(p.pos, format, args...) } -func (p *Parser) langErr(pos Pos, feature string, langs ...LangVariant) { +func (p *Parser) checkLang(pos Pos, langSet LangVariant, format string, a ...any) { + if p.lang.in(langSet) { + return + } + if langBashLike.in(langSet) { + // If we're reporting an error because a feature is for bash-like funcs, + // just mention "bash" rather than "bash/bats" for the sake of clarity. + langSet &^= LangBats + } p.errPass(LangError{ Filename: p.f.Name, Pos: pos, - Feature: feature, - Langs: langs, + Feature: fmt.Sprintf(format, a...), + Langs: slices.Collect(langSet.bits()), LangUsed: p.lang, }) } -func (p *Parser) stmts(fn func(*Stmt) bool, stops ...string) { +func (p *Parser) stmts(yield func(*Stmt, error) bool, stops ...string) { gotEnd := true loop: for p.tok != _EOF { @@ -928,6 +1066,9 @@ loop: break loop } } + if p.val == "}" { + p.curErr(`%#q can only be used to close a block`, rightBrace) + } case rightParen: if p.quote == subCmd { break loop @@ -940,7 +1081,7 @@ loop: if p.quote == switchCase { break loop } - p.curErr("%s can only be used in a case clause", p.tok) + p.curErr("%#q can only be used in a case clause", p.tok) } if !newLine && !gotEnd { p.curErr("statements must be separated by &, ; or a newline") @@ -956,7 +1097,7 @@ loop: break } gotEnd = s.Semicolon.IsValid() - if !fn(s) { + if !yield(s, p.err) { break } } @@ -965,7 +1106,7 @@ loop: func (p *Parser) stmtList(stops ...string) ([]*Stmt, []Comment) { var stmts []*Stmt var last []Comment - fn := func(s *Stmt) bool { + fn := func(s *Stmt, err error) bool { stmts = append(stmts, s) return true } @@ -998,12 +1139,12 @@ func (p *Parser) stmtList(stops ...string) ([]*Stmt, []Comment) { func (p *Parser) invalidStmtStart() { switch p.tok { - case semicolon, and, or, andAnd, orOr: - p.curErr("%s can only immediately follow a statement", p.tok) + case semicolon, and, or, andAnd, orOr, andPipe, andBang: + p.curErr("%#q can only immediately follow a statement", p.tok) case rightParen: - p.curErr("%s can only be used to close a subshell", p.tok) + p.curErr("%#q can only be used to close a subshell", p.tok) default: - p.curErr("%s is not a valid start for a statement", p.tok) + p.curErr("%#q is not a valid start for a statement", p.tok) } } @@ -1025,6 +1166,10 @@ func (p *Parser) getLit() *Lit { } func (p *Parser) wordParts(wps []WordPart) []WordPart { + if p.quote == noState { + p.quote = unquotedWordCont + defer func() { p.quote = noState }() + } for { p.openNodes++ n := p.wordPart() @@ -1042,9 +1187,9 @@ func (p *Parser) wordParts(wps []WordPart) []WordPart { } } -func (p *Parser) ensureNoNested() { +func (p *Parser) ensureNoNested(pos Pos) { if p.forbidNested { - p.curErr("expansions not allowed in heredoc words") + p.posErr(pos, "expansions not allowed in heredoc words") } } @@ -1055,17 +1200,13 @@ func (p *Parser) wordPart() WordPart { p.next() return l case dollBrace: - p.ensureNoNested() + p.ensureNoNested(p.pos) switch p.r { case '|': - if p.lang != LangMirBSDKorn { - p.langErr(p.pos, `"${|stmts;}"`, LangMirBSDKorn) - } + p.checkLang(p.pos, langBashLike|LangMirBSDKorn, "`${|stmts;}`") fallthrough case ' ', '\t', '\n': - if p.lang != LangMirBSDKorn { - p.langErr(p.pos, `"${ stmts;}"`, LangMirBSDKorn) - } + p.checkLang(p.pos, langBashLike|LangMirBSDKorn, "`${ stmts;}`") cs := &CmdSubst{ Left: p.pos, TempFile: p.r != '|', @@ -1078,7 +1219,7 @@ func (p *Parser) wordPart() WordPart { p.postNested(old) pos, ok := p.gotRsrv("}") if !ok { - p.matchingErr(cs.Left, "${", "}") + p.matchingErr(cs.Left, dollBrace, rightBrace) } cs.Right = pos return cs @@ -1086,20 +1227,13 @@ func (p *Parser) wordPart() WordPart { return p.paramExp() } case dollDblParen, dollBrack: - p.ensureNoNested() + p.ensureNoNested(p.pos) left := p.tok ar := &ArithmExp{Left: p.pos, Bracket: left == dollBrack} - var old saveState - if ar.Bracket { - old = p.preNested(arithmExprBrack) - } else { - old = p.preNested(arithmExpr) - } + old := p.preNested(arithmExpr) p.next() if p.got(hash) { - if p.lang != LangMirBSDKorn { - p.langErr(ar.Pos(), "unsigned expressions", LangMirBSDKorn) - } + p.checkLang(ar.Pos(), LangMirBSDKorn, "unsigned expressions") ar.Unsigned = true } ar.X = p.followArithm(left, ar.Left) @@ -1115,42 +1249,22 @@ func (p *Parser) wordPart() WordPart { } return ar case dollParen: - p.ensureNoNested() - cs := &CmdSubst{Left: p.pos} - old := p.preNested(subCmd) - p.next() - cs.Stmts, cs.Last = p.stmtList() - p.postNested(old) - cs.Right = p.matched(cs.Left, leftParen, rightParen) - return cs + p.ensureNoNested(p.pos) + return p.cmdSubst() case dollar: - r := p.r - switch { - case singleRuneParam(r): - p.tok, p.val = _LitWord, string(r) - p.rune() - case 'a' <= r && r <= 'z', 'A' <= r && r <= 'Z', - '0' <= r && r <= '9', r == '_', r == '\\': - p.advanceNameCont(r) - default: + pe := p.paramExp() + if pe == nil { // was not actually a parameter expansion, like: "foo$" l := p.lit(p.pos, "$") p.next() return l } - p.ensureNoNested() - pe := &ParamExp{Dollar: p.pos, Short: true} - p.pos = posAddCol(p.pos, 1) - pe.Param = p.getLit() - if pe.Param != nil && pe.Param.Value == "" { - l := p.lit(pe.Dollar, "$") - // e.g. "$\\\"" within double quotes, so we must - // keep the rest of the literal characters. - l.ValueEnd = posAddCol(l.ValuePos, 1) - return l - } + p.ensureNoNested(pe.Dollar) return pe + case assgnParen: + p.checkLang(p.pos, LangZsh, `%#q process substitutions`, p.tok) + fallthrough case cmdIn, cmdOut: - p.ensureNoNested() + p.ensureNoNested(p.pos) ps := &ProcSubst{Op: ProcOperator(p.tok), OpPos: p.pos} old := p.preNested(subCmd) p.next() @@ -1196,7 +1310,7 @@ func (p *Parser) wordPart() WordPart { if p.backquoteEnd() { return nil } - p.ensureNoNested() + p.ensureNoNested(p.pos) cs := &CmdSubst{Left: p.pos, Backquotes: true} old := p.preNested(subCmdBckquo) p.openBquotes++ @@ -1226,10 +1340,28 @@ func (p *Parser) wordPart() WordPart { } } return cs - case globQuest, globStar, globPlus, globAt, globExcl: - if p.lang == LangPOSIX { - p.langErr(p.pos, "extended globs", LangBash, LangMirBSDKorn) + case leftParen: + if p.lang.in(LangZsh) && p.r != ')' { + // Zsh glob qualifier like *(N) or .(:a); the only case where + // ( immediately after a word is not a glob qualifier is () + // for a function declaration, which the parser handles earlier. + pos := p.pos + p.pos = p.nextPos() + for p.newLit(p.r); p.r != utf8.RuneSelf && p.r != ')'; p.rune() { + } + if p.r != ')' { + p.tok = _EOF // we can only get here due to EOF + p.matchingErr(pos, leftParen, rightParen) + } + p.rune() + p.val = p.endLit() + l := p.lit(pos, "("+p.val) + p.next() + return l } + return nil + case globQuest, globStar, globPlus, globAt, globExcl: + p.checkLang(p.pos, langBashLike|LangMirBSDKorn, "extended globs") eg := &ExtGlob{Op: GlobOperator(p.tok), OpPos: p.pos} lparens := 1 r := p.r @@ -1250,7 +1382,7 @@ func (p *Parser) wordPart() WordPart { p.rune() p.next() if lparens != 0 { - p.matchingErr(eg.OpPos, eg.Op, rightParen) + p.matchingErr(eg.OpPos, token(eg.Op), rightParen) } return eg default: @@ -1258,6 +1390,16 @@ func (p *Parser) wordPart() WordPart { } } +func (p *Parser) cmdSubst() *CmdSubst { + cs := &CmdSubst{Left: p.pos} + old := p.preNested(subCmd) + p.next() + cs.Stmts, cs.Last = p.stmtList() + p.postNested(old) + cs.Right = p.matched(cs.Left, dollParen, rightParen) + return cs +} + func (p *Parser) dblQuoted() *DblQuoted { alloc := &struct { quoted DblQuoted @@ -1282,104 +1424,107 @@ func (p *Parser) dblQuoted() *DblQuoted { return q } -func singleRuneParam(r rune) bool { - switch r { - case '@', '*', '#', '$', '?', '!', '-', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': - return true - } - return false -} - +// paramExp parses a short or full parameter expansion, depending on whether +// [Parser.tok] is [dollar] or [dollBrace]. It returns nil if a [dollar] token +// does not form a valid parameter expansion, in which case it should be parsed +// as a literal. func (p *Parser) paramExp() *ParamExp { - pe := &ParamExp{Dollar: p.pos} old := p.quote - p.quote = paramExpName - if p.r == '#' { - p.tok = hash - p.pos = p.nextPos() + p.quote = runeByRune + // [ParamExp.Short] means we are parsing $exp rather than ${exp}. + pe := &ParamExp{ + Dollar: p.pos, + Short: p.tok == dollar, + } + if !pe.Short && p.r == '(' { + p.checkLang(pe.Pos(), LangZsh, `parameter expansion flags`) + // For now, for simplicity, we parse flags as just a literal. + // In the future, parsing as a word is better for cases like + // `${(ps.$sep.)val}`. + lparen := p.nextPos() p.rune() - } else { - p.next() - } - switch p.tok { - case hash: - if paramNameOp(p.r) { - pe.Length = true - p.next() - } - case perc: - if p.lang != LangMirBSDKorn { - p.langErr(pe.Pos(), `"${%foo}"`, LangMirBSDKorn) - } - if paramNameOp(p.r) { - pe.Width = true - p.next() + p.pos = p.nextPos() + for p.newLit(p.r); p.r != utf8.RuneSelf && p.r != ')'; p.rune() { } - case exclMark: - if paramNameOp(p.r) { - pe.Excl = true - p.next() + p.val = p.endLit() + if p.r != ')' { + p.tok = _EOF // we can only get here due to EOF + p.matchingErr(lparen, leftParen, rightParen) } + pe.Flags = p.lit(p.pos, p.val) + p.rune() } - op := p.tok - switch p.tok { - case _Lit, _LitWord: - if !numberLiteral(p.val) && !ValidName(p.val) { - p.curErr("invalid parameter name") - } - pe.Param = p.lit(p.pos, p.val) - p.next() - case quest, minus: - if pe.Length && p.r != '}' { - // actually ${#-default}, not ${#-}; fix the ambiguity - pe.Length = false - pe.Param = p.lit(posAddCol(p.pos, -1), "#") - pe.Param.ValueEnd = p.pos - break + if !pe.Short || p.lang.in(LangZsh) { + // Prefixes, like ${#name} to get the length of a variable. + // Note that in Zsh, the short form like $#name is allowed too. + switch p.r { + case '#': + if r := p.peek(); r == utf8.RuneSelf || singleRuneParam(r) || paramNameRune(r) || r == '"' { + pe.Length = true + p.rune() + } + case '%': + if r := p.peek(); r == utf8.RuneSelf || singleRuneParam(r) || paramNameRune(r) || r == '"' { + p.checkLang(pe.Pos(), LangMirBSDKorn, "`${%%foo}`") + pe.Width = true + p.rune() + } + case '!': + if r := p.peek(); r == utf8.RuneSelf || singleRuneParam(r) || paramNameRune(r) || r == '"' { + p.checkLang(pe.Pos(), langBashLike|LangMirBSDKorn, "`${!foo}`") + pe.Excl = true + p.rune() + } + case '+': + if r := p.peek(); r == utf8.RuneSelf || singleRuneParam(r) || paramNameRune(r) || r == '"' { + p.checkLang(pe.Pos(), LangZsh, "`${+foo}`") + pe.IsSet = true + p.rune() + } } - fallthrough - case at, star, hash, exclMark, dollar: - pe.Param = p.lit(p.pos, p.tok.String()) - p.next() - default: - p.curErr("parameter expansion requires a literal") } - switch p.tok { - case _Lit, _LitWord: - p.curErr("%s cannot be followed by a word", op) - case rightBrace: - if pe.Excl && p.lang == LangPOSIX { - p.langErr(pe.Pos(), `"${!foo}"`, LangBash, LangMirBSDKorn) + if pe = p.paramExpParameter(pe); pe == nil { + p.quote = old + return nil // just "$" + } + // In short mode, any indexing or suffixes is not allowed, and we don't require '}'. + // Zsh is an exception: $foo[1] and $foo[1,3] are valid. + if pe.Short { + if p.lang.in(LangZsh) && p.r == '[' { + p.pos = p.nextPos() + p.rune() + pe.Index = p.eitherIndex() } - pe.Rbrace = p.pos p.quote = old p.next() return pe - case leftBrack: - if p.lang == LangPOSIX { - p.langErr(p.pos, "arrays", LangBash, LangMirBSDKorn) - } - if !ValidName(pe.Param.Value) { - p.curErr("cannot index a special parameter name") + } + // Index expressions like ${foo[1]}. Note that expansion suffixes can be combined, + // like ${foo[@]//replace/with}. + if p.r == '[' { + p.checkLang(p.nextPos(), langBashLike|LangMirBSDKorn|LangZsh, "arrays") + if pe.Param != nil && !ValidName(pe.Param.Value) { + p.posErr(p.nextPos(), "cannot index a special parameter name") } + p.pos = p.nextPos() + p.rune() pe.Index = p.eitherIndex() } + tokRune := p.r + p.pos = p.nextPos() + p.tok = p.paramToken(p.r) if p.tok == rightBrace { pe.Rbrace = p.pos p.quote = old p.next() return pe } - if p.tok != _EOF && (pe.Length || pe.Width) { + if p.tok != _EOF && (pe.Length || pe.Width || pe.IsSet) { p.curErr("cannot combine multiple parameter expansion operators") } switch p.tok { - case slash, dblSlash: - // pattern search and replace - if p.lang == LangPOSIX { - p.langErr(p.pos, "search and replace", LangBash, LangMirBSDKorn) - } + case slash, dblSlash: // pattern search and replace + p.checkLang(p.pos, langBashLike|LangMirBSDKorn|LangZsh, "search and replace") pe.Repl = &Replace{All: p.tok == dblSlash} p.quote = paramExpRepl p.next() @@ -1388,14 +1533,36 @@ func (p *Parser) paramExp() *ParamExp { if p.got(slash) { pe.Repl.With = p.getWord() } - case colon: - // slicing - if p.lang == LangPOSIX { - p.langErr(p.pos, "slicing", LangBash, LangMirBSDKorn) + case colon: // slicing + if p.lang.in(LangZsh) && (p.r == '&' || asciiLetter(p.r)) { + pos := p.pos + loop: + for p.newLit(p.r); ; p.rune() { + switch p.r { + case utf8.RuneSelf: + p.tok = _EOF + p.matchingErr(pe.Dollar, dollBrace, rightBrace) + break loop + case '}': + pe.Modifiers = append(pe.Modifiers, p.lit(pos, p.endLit())) + pe.Rbrace = p.nextPos() + p.rune() + break loop + case ':': + pe.Modifiers = append(pe.Modifiers, p.lit(pos, p.endLit())) + p.rune() + pos = p.nextPos() + p.newLit(p.r) + } + } + p.quote = old + p.next() + return pe } + p.checkLang(p.pos, langBashLike|LangMirBSDKorn|LangZsh, "slicing") pe.Slice = &Slice{} colonPos := p.pos - p.quote = paramExpSlice + p.quote = paramExpArithm if p.next(); p.tok != colon { pe.Slice.Offset = p.followArithm(colon, colonPos) } @@ -1409,41 +1576,159 @@ func (p *Parser) paramExp() *ParamExp { pe.Rbrace = p.pos p.matchedArithm(pe.Dollar, dollBrace, rightBrace) return pe - case caret, dblCaret, comma, dblComma: - // upper/lower case - if !p.lang.isBash() { - p.langErr(p.pos, "this expansion operator", LangBash) - } + case caret, dblCaret, comma, dblComma: // upper/lower case + p.checkLang(p.pos, langBashLike, "this expansion operator") pe.Exp = p.paramExpExp() case at, star: switch { - case p.tok == at && p.lang == LangPOSIX: - p.langErr(p.pos, "this expansion operator", LangBash, LangMirBSDKorn) case p.tok == star && !pe.Excl: - p.curErr("not a valid parameter expansion operator: %v", p.tok) + p.curErr("not a valid parameter expansion operator: %#q", p.tok) case pe.Excl && p.r == '}': - if !p.lang.isBash() { - p.langErr(pe.Pos(), fmt.Sprintf(`"${!foo%s}"`, p.tok), LangBash) - } + p.checkLang(pe.Pos(), langBashLike, "`${!foo%s}`", p.tok) pe.Names = ParNamesOperator(p.tok) p.next() + case p.tok == at: + p.checkLang(p.pos, langBashLike|LangMirBSDKorn, "this expansion operator") + fallthrough default: pe.Exp = p.paramExpExp() } case plus, colPlus, minus, colMinus, quest, colQuest, assgn, colAssgn, - perc, dblPerc, hash, dblHash: + perc, dblPerc, hash, dblHash, colHash, colPipe, colStar: pe.Exp = p.paramExpExp() case _EOF: default: - p.curErr("not a valid parameter expansion operator: %v", p.tok) + if paramNameRune(tokRune) { + if pe.Param != nil { + p.curErr("%#q cannot be followed by a word", pe.Param.Value) + } else { + p.curErr("nested parameter expansion cannot be followed by a word") + } + } else { + p.curErr("not a valid parameter expansion operator: %#q", string(tokRune)) + } + } + if p.tok != _EOF && p.tok != rightBrace { + p.tok = p.paramToken(p.r) } p.quote = old pe.Rbrace = p.matched(pe.Dollar, dollBrace, rightBrace) return pe } +func (p *Parser) nestedParameterStart(pe *ParamExp) (left token, quotePos Pos) { + if pe.Short { + return illegalTok, Pos{} + } + if p.r == '"' { + quotePos = p.nextPos() + p.rune() + } + if p.r != '$' { + if quotePos.IsValid() { + return dollar, quotePos + } + return illegalTok, Pos{} + } + switch p1 := p.peek(); p1 { + case '{', '(': + p.pos = p.nextPos() + p.checkLang(p.pos, LangZsh, "nested parameter expansions") + if p.err != nil { + return illegalTok, Pos{} // xxx given that we overwrite p.tok below + } + p.rune() + p.rune() + if p1 == '{' { + left = dollBrace + } else { // '(' + left = dollParen + } + } + return left, quotePos +} + +func (p *Parser) paramExpParameter(pe *ParamExp) *ParamExp { + // Check for Zsh nested parameter expressions like ${(f)"$(foo)"}. + if left, quotePos := p.nestedParameterStart(pe); left != illegalTok { + var wp WordPart + switch p.tok = left; p.tok { + case dollBrace: // ${#${nested parameter}} + p.tok = dollBrace + wp = p.paramExp() + case dollParen: // ${#$(nested command)} + wp = p.cmdSubst() + default: // dollar + p.posErr(pe.Pos(), "invalid nested parameter expansion") + } + if quotePos.IsValid() { + if p.r != '"' { + p.tok = p.paramToken(p.r) + if p.tok == illegalTok { + p.posErr(pe.Pos(), "invalid nested parameter expansion") + } else { + p.quoteErr(quotePos, dblQuote) + } + } + pe.NestedParam = &DblQuoted{ + Left: quotePos, + Right: p.nextPos(), + Parts: []WordPart{wp}, + } + p.rune() + } else { + pe.NestedParam = wp + } + return pe + } + // The parameter name itself, like $foo or $?. + switch p.r { + case '?', '-': + if pe.Length && p.peek() != '}' { + // actually ${#-default}, not ${#-}; fix the ambiguity + pe.Length = false + pos := p.nextPos() + pe.Param = p.lit(posAddCol(pos, -1), "#") + pe.Param.ValueEnd = pos + break + } + fallthrough + case '@', '*', '#', '!', '$': + r, pos := p.r, p.nextPos() + p.rune() + pe.Param = p.lit(pos, string(r)) + default: + // Note that $1a is equivalent to ${1}a, but ${1a} is not. + // POSIX Shell says the latter is unspecified behavior, so match Bash's behavior. + pos := p.nextPos() + if pe.Short && singleRuneParam(p.r) { + p.val = string(p.r) + p.rune() + } else { + for p.newLit(p.r); p.r != utf8.RuneSelf; p.rune() { + if !paramNameRune(p.r) && p.r != escNewl { + break + } + } + p.val = p.endLit() + if !numberLiteral(p.val) && !ValidName(p.val) { + if pe.Short { + return nil // just "$" + } + p.posErr(pos, "invalid parameter name") + } + } + pe.Param = p.lit(pos, p.val) + } + return pe +} + func (p *Parser) paramExpExp() *Expansion { op := ParExpOperator(p.tok) + switch op { + case MatchEmpty, ArrayExclude, ArrayIntersect: + p.checkLang(p.pos, LangZsh, "${name%sarg}", op) + } p.quote = paramExpExp p.next() if op == OtherParamOps { @@ -1454,16 +1739,12 @@ func (p *Parser) paramExpExp() *Expansion { } switch p.val { case "a", "k", "u", "A", "E", "K", "L", "P", "U": - if !p.lang.isBash() { - p.langErr(p.pos, "this expansion operator", LangBash) - } + p.checkLang(p.pos, langBashLike, "this expansion operator") case "#": - if p.lang != LangMirBSDKorn { - p.langErr(p.pos, "this expansion operator", LangMirBSDKorn) - } + p.checkLang(p.pos, LangMirBSDKorn, "this expansion operator") case "Q": default: - p.curErr("invalid @ expansion operator %q", p.val) + p.curErr("invalid @ expansion operator %#q", p.val) } } return &Expansion{Op: op, Word: p.getWord()} @@ -1472,7 +1753,7 @@ func (p *Parser) paramExpExp() *Expansion { func (p *Parser) eitherIndex() ArithmExpr { old := p.quote lpos := p.pos - p.quote = arithmExprBrack + p.quote = paramExpArithm p.next() if p.tok == star || p.tok == at { p.tok, p.val = _LitWord, p.tok.String() @@ -1483,10 +1764,36 @@ func (p *Parser) eitherIndex() ArithmExpr { return expr } +func (p *Parser) zshSubFlags() *FlagsArithm { + zf := &FlagsArithm{} + // Lex flags as raw text, like paramExp does for ${(flags)...}. + lparen := p.pos + old := p.quote + p.quote = runeByRune + p.pos = p.nextPos() + for p.newLit(p.r); p.r != utf8.RuneSelf && p.r != ')'; p.rune() { + } + p.val = p.endLit() + if p.r != ')' { + p.tok = _EOF + p.matchingErr(lparen, leftParen, rightParen) + } + zf.Flags = p.lit(p.pos, p.val) + p.rune() + p.quote = old + // Parse the expression; use arithmExprAssign so commas are left for ranges. + p.next() + if p.tok == star || p.tok == at { + p.tok, p.val = _LitWord, p.tok.String() + } + zf.X = p.arithmExprAssign(false) + return zf +} + func (p *Parser) stopToken() bool { switch p.tok { - case _EOF, _Newl, semicolon, and, or, andAnd, orOr, orAnd, dblSemicolon, - semiAnd, dblSemiAnd, semiOr, rightParen: + case _EOF, _Newl, semicolon, and, or, andAnd, orOr, orAnd, andPipe, andBang, + dblSemicolon, semiAnd, dblSemiAnd, semiOr, rightParen: return true case bckQuote: return p.backquoteEnd() @@ -1505,10 +1812,8 @@ func ValidName(val string) bool { } for i, r := range val { switch { - case 'a' <= r && r <= 'z': - case 'A' <= r && r <= 'Z': - case r == '_': - case i > 0 && '0' <= r && r <= '9': + case asciiLetter(r), r == '_': + case i > 0 && asciiDigit(r): default: return false } @@ -1516,9 +1821,12 @@ func ValidName(val string) bool { return true } -func numberLiteral(val string) bool { - for _, r := range val { - if '0' > r || r > '9' { +func numberLiteral[T string | []byte](val T) bool { + if len(val) == 0 { + return false + } + for _, r := range string(val) { + if !asciiDigit(r) { return false } } @@ -1530,7 +1838,7 @@ func (p *Parser) hasValidIdent() bool { return false } if end := p.eqlOffs; end > 0 { - if p.val[end-1] == '+' && p.lang != LangPOSIX { + if p.val[end-1] == '+' && p.lang.in(langBashLike|LangMirBSDKorn|LangZsh) { end-- // a+=x } if ValidName(p.val[:end]) { @@ -1546,7 +1854,7 @@ func (p *Parser) getAssign(needEqual bool) *Assign { as := &Assign{} if p.eqlOffs > 0 { // foo=bar nameEnd := p.eqlOffs - if p.lang != LangPOSIX && p.val[p.eqlOffs-1] == '+' { + if p.lang.in(langBashLike|LangMirBSDKorn|LangZsh) && p.val[p.eqlOffs-1] == '+' { // a+=b as.Append = true nameEnd-- @@ -1568,44 +1876,51 @@ func (p *Parser) getAssign(needEqual bool) *Assign { as.Index = p.eitherIndex() if p.spaced || p.stopToken() { if needEqual { - p.followErr(as.Pos(), "a[b]", "=") + p.followErr(as.Pos(), "a[b]", assgn) } else { as.Naked = true return as } } - if len(p.val) > 0 && p.val[0] == '+' { - as.Append = true - p.val = p.val[1:] + if p.tok == assgnParen { + if !p.lang.in(LangZsh) { + p.curErr("arrays cannot be nested") + return nil + } + // zsh allows a[i]=(values...). + // assgnParen consumed both '=' and '(', + // so rewrite as leftParen for array parsing below. + p.tok = leftParen p.pos = posAddCol(p.pos, 1) - } - if len(p.val) < 1 || p.val[0] != '=' { - if as.Append { - p.followErr(as.Pos(), "a[b]+", "=") - } else { - p.followErr(as.Pos(), "a[b]", "=") + } else { + if len(p.val) > 0 && p.val[0] == '+' { + as.Append = true + p.val = p.val[1:] + p.pos = posAddCol(p.pos, 1) + } + if len(p.val) < 1 || p.val[0] != '=' { + if as.Append { + p.followErr(as.Pos(), "a[b]+", assgn) + } else { + p.followErr(as.Pos(), "a[b]", assgn) + } + return nil + } + p.pos = posAddCol(p.pos, 1) + p.val = p.val[1:] + if p.val == "" { + p.next() } - return nil - } - p.pos = posAddCol(p.pos, 1) - p.val = p.val[1:] - if p.val == "" { - p.next() } } if p.spaced || p.stopToken() { return as } if as.Value == nil && p.tok == leftParen { - if p.lang == LangPOSIX { - p.langErr(p.pos, "arrays", LangBash, LangMirBSDKorn) - } - if as.Index != nil { - p.curErr("arrays cannot be nested") - } + p.checkLang(p.pos, langBashLike|LangMirBSDKorn|LangZsh, "arrays") as.Array = &ArrayExpr{Lparen: p.pos} newQuote := p.quote - if p.lang.isBash() { + if p.lang.in(langBashLike | LangZsh) { newQuote = arrayElems } old := p.preNested(newQuote) @@ -1617,13 +1932,14 @@ func (p *Parser) getAssign(needEqual bool) *Assign { if p.tok == leftBrack { left := p.pos ae.Index = p.eitherIndex() - p.follow(left, `"[x]"`, assgn) + if p.tok == assgnParen { + p.curErr("arrays cannot be nested") + return nil + } + p.follow(left, `[x]`, assgn) } if ae.Value = p.getWord(); ae.Value == nil { switch p.tok { - case leftParen: - p.curErr("arrays cannot be nested") - return nil case _Newl, rightParen, leftBrack: // TODO: support [index]=[ default: @@ -1656,8 +1972,9 @@ func (p *Parser) getAssign(needEqual bool) *Assign { func (p *Parser) peekRedir() bool { switch p.tok { - case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, - hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: + case _LitRedir, rdrOut, appOut, rdrIn, rdrInOut, dplIn, dplOut, + rdrClob, appClob, hdoc, dashHdoc, wordHdoc, + rdrAll, rdrAllClob, appAll, appAllClob: return true } return false @@ -1678,13 +1995,16 @@ func (p *Parser) doRedirect(s *Stmt) { s.Redirs = append(s.Redirs, r) } r.N = p.getLit() - if !p.lang.isBash() && r.N != nil && r.N.Value[0] == '{' { - p.langErr(r.N.Pos(), "{varname} redirects", LangBash) - } - if p.lang == LangPOSIX && (p.tok == rdrAll || p.tok == appAll) { - p.langErr(p.pos, "&> redirects", LangBash, LangMirBSDKorn) + if r.N != nil && r.N.Value[0] == '{' { + p.checkLang(r.N.Pos(), langBashLike, "`{varname}` redirects") } r.Op, r.OpPos = RedirOperator(p.tok), p.pos + switch r.Op { + case RdrAll, AppAll: + p.checkLang(p.pos, langBashLike|LangMirBSDKorn|LangZsh, "%#q redirects", r.Op) + case AppClob, RdrAllClob, AppAllClob: + p.checkLang(p.pos, LangZsh, "%#q redirects", r.Op) + } p.next() switch r.Op { case Hdoc, DashHdoc: @@ -1704,9 +2024,7 @@ func (p *Parser) doRedirect(s *Stmt) { p.doHeredocs() } case WordHdoc: - if p.lang == LangPOSIX { - p.langErr(r.OpPos, "herestrings", LangBash, LangMirBSDKorn) - } + p.checkLang(r.OpPos, langBashLike|LangMirBSDKorn|LangZsh, "herestrings") fallthrough default: r.Word = p.followWordTok(token(r.Op), r.OpPos) @@ -1719,7 +2037,7 @@ func (p *Parser) getStmt(readEnd, binCmd, fnBody bool) *Stmt { if ok { s.Negated = true if p.stopToken() { - p.posErr(s.Pos(), `"!" cannot form a statement alone`) + p.posErr(s.Pos(), `%#q cannot form a statement alone`, exclMark) } if _, ok := p.gotRsrv("!"); ok { p.posErr(s.Pos(), `cannot negate a command multiple times`) @@ -1747,7 +2065,7 @@ func (p *Parser) getStmt(readEnd, binCmd, fnBody bool) *Stmt { if p.recoverError() { b.Y = &Stmt{Position: recoveredPos} } else { - p.followErr(b.OpPos, b.Op.String(), "a statement") + p.followErr(b.OpPos, b.Op, noQuote("a statement")) return nil } } @@ -1768,6 +2086,10 @@ func (p *Parser) getStmt(readEnd, binCmd, fnBody bool) *Stmt { s.Semicolon = p.pos p.next() s.Coprocess = true + case andPipe, andBang: + s.Semicolon = p.pos + p.next() + s.Disown = true } } if len(p.accComs) > 0 && !binCmd && !fnBody { @@ -1782,76 +2104,86 @@ func (p *Parser) getStmt(readEnd, binCmd, fnBody bool) *Stmt { func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { s.Comments, p.accComs = p.accComs, nil + for p.peekRedir() { + p.doRedirect(s) + } + redirsStart := len(s.Redirs) switch p.tok { case _LitWord: switch p.val { case "{": p.block(s) + case "{}": + // Zsh treats closing braces in a special way, allowing this. + if p.lang.in(LangZsh) { + s.Cmd = &Block{Lbrace: p.pos, Rbrace: posAddCol(p.pos, 1)} + p.next() + } case "if": p.ifClause(s) case "while", "until": + // TODO(zsh): "repeat" p.whileClause(s, p.val == "until") case "for": p.forClause(s) case "case": p.caseClause(s) + // TODO(zsh): { try-list } "always" { always-list } case "}": - p.curErr(`%q can only be used to close a block`, p.val) - case "then": - p.curErr(`%q can only be used in an if`, p.val) - case "elif": - p.curErr(`%q can only be used in an if`, p.val) + p.curErr(`%#q can only be used to close a block`, rightBrace) + case "then", "elif": + p.curErr("%#q can only be used in an `if`", p.val) case "fi": - p.curErr(`%q can only be used to end an if`, p.val) + p.curErr("%#q can only be used to end an `if`", p.val) case "do": - p.curErr(`%q can only be used in a loop`, p.val) + p.curErr(`%#q can only be used in a loop`, p.val) case "done": - p.curErr(`%q can only be used to end a loop`, p.val) + p.curErr(`%#q can only be used to end a loop`, p.val) case "esac": - p.curErr(`%q can only be used to end a case`, p.val) + p.curErr("%#q can only be used to end a `case`", p.val) case "!": if !s.Negated { - p.curErr(`"!" can only be used in full statements`) + p.curErr(`%#q can only be used in full statements`, exclMark) break } case "[[": - if p.lang != LangPOSIX { + if p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { p.testClause(s) } case "]]": - if p.lang != LangPOSIX { - p.curErr(`%q can only be used to close a test`, p.val) + if p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { + p.curErr(`%#q can only be used to close a test`, dblRightBrack) } case "let": - if p.lang != LangPOSIX { + if p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { p.letClause(s) } case "function": - if p.lang != LangPOSIX { + if p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { p.bashFuncDecl(s) } case "declare": - if p.lang.isBash() { // Note that mksh lacks this one. + if p.lang.in(langBashLike | LangZsh) { // Note that mksh lacks this one. p.declClause(s) } case "local", "export", "readonly", "typeset", "nameref": - if p.lang != LangPOSIX { + if p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { p.declClause(s) } case "time": - if p.lang != LangPOSIX { + if p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { p.timeClause(s) } case "coproc": - if p.lang.isBash() { // Note that mksh lacks this one. + if p.lang.in(langBashLike) { // Note that mksh lacks this one. p.coprocClause(s) } case "select": - if p.lang != LangPOSIX { + if p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { p.selectClause(s) } case "@test": - if p.lang == LangBats { + if p.lang.in(LangBats) { p.testDecl(s) } } @@ -1863,25 +2195,29 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { break } name := p.lit(p.pos, p.val) - if p.next(); p.got(leftParen) { + p.next() + // In zsh, ( after a word is a glob qualifier unless followed + // immediately by ), which is the func declaration syntax. + if p.tok == leftParen && (!p.lang.in(LangZsh) || p.r == ')') { + p.next() p.follow(name.ValuePos, "foo(", rightParen) - if p.lang == LangPOSIX && !ValidName(name.Value) { + if p.lang.in(LangPOSIX) && !ValidName(name.Value) { p.posErr(name.Pos(), "invalid func name") } - p.funcDecl(s, name, name.ValuePos, true) + p.funcDecl(s, name.ValuePos, false, true, name) } else { - p.callExpr(s, p.wordOne(name), false) + w := p.wordOne(name) + if p.lang.in(LangZsh) && !p.spaced { + w.Parts = append(w.Parts, p.wordParts(nil)...) + } + p.callExpr(s, w, false) } - case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, - hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: - p.doRedirect(s) - p.callExpr(s, nil, false) case bckQuote: if p.backquoteEnd() { - return nil + break } fallthrough - case _Lit, dollBrace, dollDblParen, dollParen, dollar, cmdIn, cmdOut, + case _Lit, dollBrace, dollDblParen, dollParen, dollar, cmdIn, assgnParen, cmdOut, sglQuote, dollSglQuote, dblQuote, dollDblQuote, dollBrack, globQuest, globStar, globPlus, globAt, globExcl: if p.hasValidIdent() { @@ -1894,12 +2230,26 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { } p.callExpr(s, w, false) case leftParen: + if p.r == ')' { + p.rune() + fpos := p.pos + p.next() + if p.tok == _LitWord && p.val == "{" { + p.checkLang(fpos, LangZsh, "anonymous functions") + } + p.funcDecl(s, fpos, false, true) + break + } p.subshell(s) case dblLeftParen: p.arithmExpCmd(s) - default: - if len(s.Redirs) == 0 { - return nil + } + if s.Cmd == nil && len(s.Redirs) == 0 { + return nil // no statement found + } + if redirsStart > 0 && s.Cmd != nil { + if _, ok := s.Cmd.(*CallExpr); !ok { + p.checkLang(s.Pos(), LangZsh, "redirects before compound commands") } } for p.peekRedir() { @@ -1912,7 +2262,7 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { // right recursion should only read a single element return s } - if p.tok == orAnd && p.lang == LangMirBSDKorn { + if p.tok == orAnd && p.lang.in(LangMirBSDKorn) { // No need to check for LangPOSIX, as on that language // we parse |& as two tokens. break @@ -1924,7 +2274,7 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { if p.recoverError() { b.Y = &Stmt{Position: recoveredPos} } else { - p.followErr(b.OpPos, b.Op.String(), "a statement") + p.followErr(b.OpPos, b.Op, noQuote("a statement")) break } } @@ -1942,7 +2292,7 @@ func (p *Parser) subshell(s *Stmt) { sub := &Subshell{Lparen: p.pos} old := p.preNested(subCmd) p.next() - sub.Stmts, sub.Last = p.stmtList() + sub.Stmts, sub.Last = p.followStmts("(", sub.Lparen) p.postNested(old) sub.Rparen = p.matched(sub.Lparen, leftParen, rightParen) s.Cmd = sub @@ -1953,9 +2303,7 @@ func (p *Parser) arithmExpCmd(s *Stmt) { old := p.preNested(arithmExprCmd) p.next() if p.got(hash) { - if p.lang != LangMirBSDKorn { - p.langErr(ar.Pos(), "unsigned expressions", LangMirBSDKorn) - } + p.checkLang(ar.Pos(), LangMirBSDKorn, "unsigned expressions") ar.Unsigned = true } ar.X = p.followArithm(dblLeftParen, ar.Left) @@ -1966,13 +2314,13 @@ func (p *Parser) arithmExpCmd(s *Stmt) { func (p *Parser) block(s *Stmt) { b := &Block{Lbrace: p.pos} p.next() - b.Stmts, b.Last = p.stmtList("}") + b.Stmts, b.Last = p.followStmts("{", b.Lbrace, "}") if pos, ok := p.gotRsrv("}"); ok { b.Rbrace = pos } else if p.recoverError() { b.Rbrace = recoveredPos } else { - p.matchingErr(b.Lbrace, "{", "}") + p.matchingErr(b.Lbrace, leftBrace, rightBrace) } s.Cmd = b } @@ -2036,9 +2384,7 @@ func (p *Parser) forClause(s *Stmt) { start, end := "do", "done" if pos, ok := p.gotRsrv("{"); ok { - if p.lang == LangPOSIX { - p.langErr(pos, "for loops with braces", LangBash, LangMirBSDKorn) - } + p.checkLang(pos, langBashLike|LangMirBSDKorn, "for loops with braces") fc.DoPos = pos fc.Braces = true start, end = "{", "}" @@ -2054,11 +2400,9 @@ func (p *Parser) forClause(s *Stmt) { } func (p *Parser) loop(fpos Pos) Loop { - if !p.lang.isBash() { - switch p.tok { - case leftParen, dblLeftParen: - p.langErr(p.pos, "c-style fors", LangBash) - } + switch p.tok { + case leftParen, dblLeftParen: + p.checkLang(p.pos, langBashLike|LangZsh, "c-style fors") } if p.tok == dblLeftParen { cl := &CStyleLoop{Lparen: p.pos} @@ -2082,7 +2426,7 @@ func (p *Parser) loop(fpos Pos) Loop { func (p *Parser) wordIter(ftok string, fpos Pos) *WordIter { wi := &WordIter{} if wi.Name = p.getLit(); wi.Name == nil { - p.followErr(fpos, ftok, "a literal") + p.followErr(fpos, ftok, noQuote("a literal")) } if p.got(semicolon) { p.got(_Newl) @@ -2102,7 +2446,7 @@ func (p *Parser) wordIter(ftok string, fpos Pos) *WordIter { p.got(_Newl) } else if p.tok == _LitWord && p.val == "do" { } else { - p.followErr(fpos, ftok+" foo", `"in", "do", ;, or a newline`) + p.followErr(fpos, ftok+" foo", noQuote("`in`, `do`, `;`, or a newline")) } return wi } @@ -2122,16 +2466,14 @@ func (p *Parser) caseClause(s *Stmt) { p.next() cc.Word = p.getWord() if cc.Word == nil { - p.followErr(cc.Case, "case", "a word") + p.followErr(cc.Case, "case", noQuote("a word")) } end := "esac" p.got(_Newl) if pos, ok := p.gotRsrv("{"); ok { cc.In = pos cc.Braces = true - if p.lang != LangMirBSDKorn { - p.langErr(cc.Pos(), `"case i {"`, LangMirBSDKorn) - } + p.checkLang(cc.Pos(), LangMirBSDKorn, "`case i {`") end = "}" } else { cc.In = p.followRsrv(cc.Case, "case x", "in") @@ -2158,7 +2500,7 @@ func (p *Parser) caseItems(stop string) (items []*CaseItem) { break } if !p.got(or) { - p.curErr("case patterns must be separated with |") + p.curErr("case patterns must be separated with %#q", or) } } old := p.preNested(switchCase) @@ -2170,7 +2512,7 @@ func (p *Parser) caseItems(stop string) (items []*CaseItem) { default: ci.Op = Break items = append(items, ci) - return + return items } ci.Last = append(ci.Last, p.accComs...) p.accComs = nil @@ -2201,35 +2543,31 @@ func (p *Parser) caseItems(stop string) (items []*CaseItem) { items = append(items, ci) } - return + return items } func (p *Parser) testClause(s *Stmt) { tc := &TestClause{Left: p.pos} old := p.preNested(testExpr) p.next() - if _, ok := p.gotRsrv("]]"); ok || p.tok == _EOF { - p.posErr(tc.Left, "test clause requires at least one expression") - } - tc.X = p.testExpr(false) - if tc.X == nil { - p.followErrExp(tc.Left, "[[") + if tc.X = p.testExprBinary(false); tc.X == nil { + p.followErrExp(tc.Left, dblLeftBrack) } tc.Right = p.pos if _, ok := p.gotRsrv("]]"); !ok { - p.matchingErr(tc.Left, "[[", "]]") + p.matchingErr(tc.Left, dblLeftBrack, dblRightBrack) } p.postNested(old) s.Cmd = tc } -func (p *Parser) testExpr(pastAndOr bool) TestExpr { +func (p *Parser) testExprBinary(pastAndOr bool) TestExpr { p.got(_Newl) var left TestExpr if pastAndOr { - left = p.testExprBase() + left = p.testExprUnary() } else { - left = p.testExpr(true) + left = p.testExprBinary(true) } if left == nil { return left @@ -2242,7 +2580,7 @@ func (p *Parser) testExpr(pastAndOr bool) TestExpr { return left } if p.tok = token(testBinaryOp(p.val)); p.tok == illegalTok { - p.curErr("not a valid test operator: %s", p.val) + p.curErr("not a valid test operator: %#q", p.val) } case rdrIn, rdrOut: case _EOF, rightParen: @@ -2250,26 +2588,21 @@ func (p *Parser) testExpr(pastAndOr bool) TestExpr { case _Lit: p.curErr("test operator words must consist of a single literal") default: - p.curErr("not a valid test operator: %v", p.tok) + p.curErr("not a valid test operator: %#q", p.tok) } b := &BinaryTest{ OpPos: p.pos, Op: BinTestOperator(p.tok), X: left, } - // Save the previous quoteState, since we change it in TsReMatch. - oldQuote := p.quote - switch b.Op { case AndTest, OrTest: p.next() - if b.Y = p.testExpr(false); b.Y == nil { - p.followErrExp(b.OpPos, b.Op.String()) + if b.Y = p.testExprBinary(false); b.Y == nil { + p.followErrExp(b.OpPos, b.Op) } case TsReMatch: - if !p.lang.isBash() { - p.langErr(p.pos, "regex tests", LangBash) - } + p.checkLang(p.pos, langBashLike|LangZsh, "regex tests") p.rxOpenParens = 0 p.rxFirstPart = true // TODO(mvdan): Using nested states within a regex will break in @@ -2279,17 +2612,16 @@ func (p *Parser) testExpr(pastAndOr bool) TestExpr { fallthrough default: if _, ok := b.X.(*Word); !ok { - p.posErr(b.OpPos, "expected %s, %s or %s after complex expr", - AndTest, OrTest, "]]") + p.posErr(b.OpPos, "expected %#q, %#q or %#q after complex expr", + AndTest, OrTest, dblRightBrack) } p.next() b.Y = p.followWordTok(token(b.Op), b.OpPos) } - p.quote = oldQuote return b } -func (p *Parser) testExprBase() TestExpr { +func (p *Parser) testExprUnary() TestExpr { switch p.tok { case _EOF, rightParen: return nil @@ -2298,7 +2630,7 @@ func (p *Parser) testExprBase() TestExpr { switch op { case illegalTok: case tsRefVar, tsModif: // not available in mksh - if p.lang.isBash() { + if p.lang.in(langBashLike) { p.tok = op } default: @@ -2309,8 +2641,8 @@ func (p *Parser) testExprBase() TestExpr { case exclMark: u := &UnaryTest{OpPos: p.pos, Op: TsNot} p.next() - if u.X = p.testExpr(false); u.X == nil { - p.followErrExp(u.OpPos, u.Op.String()) + if u.X = p.testExprBinary(false); u.X == nil { + p.followErrExp(u.OpPos, u.Op) } return u case tsExists, tsRegFile, tsDirect, tsCharSp, tsBlckSp, tsNmPipe, @@ -2324,8 +2656,8 @@ func (p *Parser) testExprBase() TestExpr { case leftParen: pe := &ParenTest{Lparen: p.pos} p.next() - if pe.X = p.testExpr(false); pe.X == nil { - p.followErrExp(pe.Lparen, "(") + if pe.X = p.testExprBinary(false); pe.X == nil { + p.followErrExp(pe.Lparen, leftParen) } pe.Rparen = p.matched(pe.Lparen, leftParen, rightParen) return pe @@ -2349,7 +2681,7 @@ func (p *Parser) declClause(s *Stmt) { for !p.stopToken() && !p.peekRedir() { if p.hasValidIdent() { ds.Args = append(ds.Args, p.getAssign(false)) - } else if p.eqlOffs > 0 { + } else if p.eqlOffs > 0 && !strings.Contains(p.val[:p.eqlOffs], "{") { p.curErr("invalid var name") } else if p.tok == _LitWord && ValidName(p.val) { ds.Args = append(ds.Args, &Assign{ @@ -2362,7 +2694,7 @@ func (p *Parser) declClause(s *Stmt) { Value: w, }) } else { - p.followErr(p.pos, ds.Variant.Value, "names or assignments") + p.followErr(p.pos, ds.Variant.Value, noQuote("names or assignments")) } } s.Cmd = ds @@ -2442,26 +2774,40 @@ func (p *Parser) letClause(s *Stmt) { func (p *Parser) bashFuncDecl(s *Stmt) { fpos := p.pos - if p.next(); p.tok != _LitWord { - p.followErr(fpos, "function", "a name") + p.next() + names := make([]*Lit, 0, 1) + for p.tok == _LitWord && p.val != "{" { + names = append(names, p.lit(p.pos, p.val)) + p.next() } - name := p.lit(p.pos, p.val) - hasParens := false - if p.next(); p.got(leftParen) { - hasParens = true - p.follow(name.ValuePos, "foo(", rightParen) + hasParens := p.got(leftParen) + switch len(names) { + case 0: + if hasParens || (p.tok == _LitWord && p.val == "{") { + p.checkLang(fpos, LangZsh, "anonymous functions") + } else if !p.lang.in(LangZsh) { + p.followErr(fpos, "function", noQuote("a name")) + } + names = nil // avoid non-nil zero-length slices + case 1: + // allowed in all variants + default: + p.checkLang(fpos, LangZsh, "multi-name functions") } - p.funcDecl(s, name, fpos, hasParens) + if hasParens { + p.follow(fpos, "function foo(", rightParen) + } + p.funcDecl(s, fpos, true, hasParens, names...) } func (p *Parser) testDecl(s *Stmt) { td := &TestDecl{Position: p.pos} p.next() if td.Description = p.getWord(); td.Description == nil { - p.followErr(td.Position, "@test", "a description word") + p.followErr(td.Position, "@test", noQuote("a description word")) } if td.Body = p.getStmt(false, false, true); td.Body == nil { - p.followErr(td.Position, `@test "desc"`, "a statement") + p.followErr(td.Position, `@test "desc"`, noQuote("a statement")) } s.Cmd = td } @@ -2477,7 +2823,7 @@ func (p *Parser) callExpr(s *Stmt, w *Word, assign bool) { loop: for { switch p.tok { - case _EOF, _Newl, semicolon, and, or, andAnd, orOr, orAnd, + case _EOF, _Newl, semicolon, and, or, andAnd, orOr, orAnd, andPipe, andBang, dblSemicolon, semiAnd, dblSemiAnd, semiOr: break loop case _LitWord: @@ -2486,11 +2832,19 @@ loop: break } // Avoid failing later with the confusing "} can only be used to close a block". - if p.lang == LangPOSIX && p.val == "{" && w != nil && w.Lit() == "function" { - p.langErr(p.pos, `the "function" builtin`, LangBash) + if p.val == "{" && w != nil && w.Lit() == "function" { + p.checkLang(p.pos, langBashLike, `the "function" builtin`) + } + // Zsh does not require a semicolon to close a block. + if p.lang.in(LangZsh) && p.val == "}" { + break loop } - ce.Args = append(ce.Args, p.wordOne(p.lit(p.pos, p.val))) + w := p.wordOne(p.lit(p.pos, p.val)) p.next() + if p.lang.in(LangZsh) && !p.spaced { + w.Parts = append(w.Parts, p.wordParts(nil)...) + } + ce.Args = append(ce.Args, w) case _Lit: if len(ce.Args) == 0 && p.hasValidIdent() { ce.Assigns = append(ce.Assigns, p.getAssign(true)) @@ -2502,33 +2856,31 @@ loop: break loop } fallthrough - case dollBrace, dollDblParen, dollParen, dollar, cmdIn, cmdOut, + case dollBrace, dollDblParen, dollParen, dollar, cmdIn, assgnParen, cmdOut, sglQuote, dollSglQuote, dblQuote, dollDblQuote, dollBrack, globQuest, globStar, globPlus, globAt, globExcl: ce.Args = append(ce.Args, p.wordAnyNumber()) - case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, - hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: - p.doRedirect(s) case dblLeftParen: - p.curErr("%s can only be used to open an arithmetic cmd", p.tok) + p.curErr("%#q can only be used to open an arithmetic cmd", p.tok) case rightParen: if p.quote == subCmd { break loop } fallthrough default: + if p.peekRedir() { + p.doRedirect(s) + continue + } // Note that we'll only keep the first error that happens. if len(ce.Args) > 0 { - if cmd := ce.Args[0].Lit(); p.lang == LangPOSIX && isBashCompoundCommand(_LitWord, cmd) { - p.langErr(p.pos, fmt.Sprintf("the %q builtin", cmd), LangBash) + if cmd := ce.Args[0].Lit(); isBashCompoundCommand(_LitWord, cmd) { + p.checkLang(p.pos, langBashLike, "the %#q builtin", cmd) } } - p.curErr("a command can only contain words and redirects; encountered %s", p.tok) + p.curErr("a command can only contain words and redirects; encountered %#q", p.tok) } } - if len(ce.Assigns) == 0 && len(ce.Args) == 0 { - return - } if len(ce.Args) == 0 { ce.Args = nil } else { @@ -2541,16 +2893,21 @@ loop: s.Cmd = ce } -func (p *Parser) funcDecl(s *Stmt, name *Lit, pos Pos, withParens bool) { +func (p *Parser) funcDecl(s *Stmt, pos Pos, long, withParens bool, names ...*Lit) { fd := &FuncDecl{ Position: pos, - RsrvWord: pos != name.ValuePos, + RsrvWord: long, Parens: withParens, - Name: name, + } + if len(names) == 1 { + fd.Name = names[0] + } else { + fd.Names = names } p.got(_Newl) + // TODO: reject any body which isn't a compound command, like a quoted word if fd.Body = p.getStmt(false, false, true); fd.Body == nil { - p.followErr(fd.Pos(), "foo()", "a statement") + p.followErr(fd.Pos(), "foo()", noQuote("a statement")) } s.Cmd = fd } diff --git a/vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go b/vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go index d04f3d896..970b6bfaf 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go +++ b/vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go @@ -18,19 +18,20 @@ func (p *Parser) arithmExprAssign(compact bool) ArithmExpr { value := p.arithmExprTernary(compact) switch BinAritOperator(p.tok) { case AddAssgn, SubAssgn, MulAssgn, QuoAssgn, RemAssgn, AndAssgn, - OrAssgn, XorAssgn, ShlAssgn, ShrAssgn, Assgn: + OrAssgn, XorAssgn, ShlAssgn, ShrAssgn, Assgn, + AndBoolAssgn, OrBoolAssgn, XorBoolAssgn, PowAssgn: if compact && p.spaced { return value } if !isArithName(value) { - p.posErr(p.pos, "%s must follow a name", p.tok.String()) + p.posErr(p.pos, "%#q must follow a name", p.tok) } pos := p.pos tok := p.tok p.nextArithOp(compact) y := p.arithmExprAssign(compact) if y == nil { - p.followErrExp(pos, tok.String()) + p.followErrExp(pos, tok) } return &BinaryArithm{ OpPos: pos, @@ -49,25 +50,25 @@ func (p *Parser) arithmExprTernary(compact bool) ArithmExpr { } if value == nil { - p.curErr("%s must follow an expression", p.tok.String()) + p.curErr("%#q must follow an expression", p.tok) } questPos := p.pos p.nextArithOp(compact) if BinAritOperator(p.tok) == TernColon { - p.followErrExp(questPos, TernQuest.String()) + p.followErrExp(questPos, TernQuest) } trueExpr := p.arithmExpr(compact) if trueExpr == nil { - p.followErrExp(questPos, TernQuest.String()) + p.followErrExp(questPos, TernQuest) } if BinAritOperator(p.tok) != TernColon { - p.posErr(questPos, "ternary operator missing : after ?") + p.posErr(questPos, "ternary operator missing %#q after %#q", colon, quest) } colonPos := p.pos p.nextArithOp(compact) falseExpr := p.arithmExprTernary(compact) if falseExpr == nil { - p.followErrExp(colonPos, TernColon.String()) + p.followErrExp(colonPos, TernColon) } return &BinaryArithm{ OpPos: questPos, @@ -83,7 +84,7 @@ func (p *Parser) arithmExprTernary(compact bool) ArithmExpr { } func (p *Parser) arithmExprLor(compact bool) ArithmExpr { - return p.arithmExprBinary(compact, p.arithmExprLand, OrArit) + return p.arithmExprBinary(compact, p.arithmExprLand, OrArit, XorBool) } func (p *Parser) arithmExprLand(compact bool) ArithmExpr { @@ -130,7 +131,7 @@ func (p *Parser) arithmExprPower(compact bool) ArithmExpr { } if value == nil { - p.curErr("%s must follow an expression", p.tok.String()) + p.curErr("%#q must follow an expression", p.tok) } op := p.tok @@ -138,7 +139,7 @@ func (p *Parser) arithmExprPower(compact bool) ArithmExpr { p.nextArithOp(compact) y := p.arithmExprPower(compact) if y == nil { - p.followErrExp(pos, op.String()) + p.followErrExp(pos, op) } return &BinaryArithm{ OpPos: pos, @@ -158,7 +159,7 @@ func (p *Parser) arithmExprUnary(compact bool) ArithmExpr { ue := &UnaryArithm{OpPos: p.pos, Op: UnAritOperator(p.tok)} p.nextArithOp(compact) if ue.X = p.arithmExprUnary(compact); ue.X == nil { - p.followErrExp(ue.OpPos, ue.Op.String()) + p.followErrExp(ue.OpPos, ue.Op) } return ue } @@ -172,27 +173,34 @@ func (p *Parser) arithmExprValue(compact bool) ArithmExpr { ue := &UnaryArithm{OpPos: p.pos, Op: UnAritOperator(p.tok)} p.nextArith(compact) if p.tok != _LitWord { - p.followErr(ue.OpPos, token(ue.Op).String(), "a literal") + p.followErr(ue.OpPos, ue.Op, noQuote("a literal")) } ue.X = p.arithmExprValue(compact) return ue case leftParen: + if p.quote == paramExpArithm && p.lang.in(LangZsh) { + x = p.zshSubFlags() + break + } pe := &ParenArithm{Lparen: p.pos} p.nextArithOp(compact) pe.X = p.followArithm(leftParen, pe.Lparen) pe.Rparen = p.matched(pe.Lparen, leftParen, rightParen) + if p.quote == paramExpArithm && p.tok == _LitWord { + p.checkLang(pe.Lparen, LangZsh, "subscript flags") + } x = pe case leftBrack: - p.curErr("[ must follow a name") + p.curErr("%#q must follow a name", p.tok) case colon: - p.curErr("ternary operator missing ? before :") + p.curErr("ternary operator missing %#q before %#q", quest, colon) case _LitWord: l := p.getLit() if p.tok != leftBrack { x = p.wordOne(l) break } - pe := &ParamExp{Dollar: l.ValuePos, Short: true, Param: l} + pe := &ParamExp{Short: true, Param: l} pe.Index = p.eitherIndex() x = p.wordOne(pe) case bckQuote: @@ -219,7 +227,7 @@ func (p *Parser) arithmExprValue(compact bool) ArithmExpr { // sets the type to non-nil and then x != nil if p.tok == addAdd || p.tok == subSub { if !isArithName(x) { - p.curErr("%s must follow a name", p.tok.String()) + p.curErr("%#q must follow a name", p.tok) } u := &UnaryArithm{ Post: true, @@ -250,7 +258,7 @@ func (p *Parser) nextArithOp(compact bool) { pos := p.pos tok := p.tok if p.nextArith(compact) { - p.followErrExp(pos, tok.String()) + p.followErrExp(pos, tok) } } @@ -271,14 +279,14 @@ func (p *Parser) arithmExprBinary(compact bool, nextOp func(bool) ArithmExpr, op } if value == nil { - p.curErr("%s must follow an expression", p.tok.String()) + p.curErr("%#q must follow an expression", p.tok) } pos := p.pos p.nextArithOp(compact) y := nextOp(compact) if y == nil { - p.followErrExp(pos, foundOp.String()) + p.followErrExp(pos, foundOp) } value = &BinaryArithm{ @@ -308,7 +316,7 @@ func isArithName(left ArithmExpr) bool { func (p *Parser) followArithm(ftok token, fpos Pos) ArithmExpr { x := p.arithmExpr(false) if x == nil { - p.followErrExp(fpos, ftok.String()) + p.followErrExp(fpos, ftok) } return x } @@ -320,16 +328,18 @@ func (p *Parser) peekArithmEnd() bool { func (p *Parser) arithmMatchingErr(pos Pos, left, right token) { switch p.tok { case _Lit, _LitWord: - p.curErr("not a valid arithmetic operator: %s", p.val) + p.curErr("not a valid arithmetic operator: %#q", p.val) case leftBrack: - p.curErr("[ must follow a name") + p.curErr("%#q must follow a name", leftBrack) case colon: - p.curErr("ternary operator missing ? before :") + p.curErr("ternary operator missing %#q before %#q", quest, colon) case rightParen, _EOF: p.matchingErr(pos, left, right) + case period: + p.checkLang(p.pos, LangZsh, `floating point arithmetic`) default: - if p.quote == arithmExpr { - p.curErr("not a valid arithmetic operator: %v", p.tok) + if p.quote&allArithmExpr != 0 { + p.curErr("not a valid arithmetic operator: %#q", p.tok) } p.matchingErr(pos, left, right) } diff --git a/vendor/mvdan.cc/sh/v3/syntax/printer.go b/vendor/mvdan.cc/sh/v3/syntax/printer.go index 74675ad9c..1f0727d23 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/printer.go +++ b/vendor/mvdan.cc/sh/v3/syntax/printer.go @@ -65,13 +65,13 @@ func KeepPadding(enabled bool) PrinterOption { if enabled && !p.keepPadding { // Enable the flag, and set up the writer wrapper. p.keepPadding = true - p.cols.Writer = p.bufWriter.(*bufio.Writer) - p.bufWriter = &p.cols + p.cols.Writer = p.w.(*bufio.Writer) + p.w = &p.cols } else if !enabled && p.keepPadding { // Ensure we reset the state to that of NewPrinter. p.keepPadding = false - p.bufWriter = p.cols.Writer + p.w = p.cols.Writer p.cols = colCounter{} } } @@ -102,7 +102,7 @@ func FunctionNextLine(enabled bool) PrinterOption { // NewPrinter allocates a new Printer and applies any number of options. func NewPrinter(opts ...PrinterOption) *Printer { p := &Printer{ - bufWriter: bufio.NewWriter(nil), + w: bufio.NewWriter(nil), tabWriter: new(tabwriter.Writer), } for _, opt := range opts { @@ -138,7 +138,7 @@ func (p *Printer) Print(w io.Writer, node Node) error { p.tabWriter.Init(w, 0, tabwidth, 1, ' ', twmode) w = p.tabWriter - p.bufWriter.Reset(w) + p.w.Reset(w) switch node := node.(type) { case *File: p.stmtList(node.Stmts, node.Last) @@ -163,7 +163,7 @@ func (p *Printer) Print(w io.Writer, node Node) error { p.flushComments() // flush the writers - if err := p.bufWriter.Flush(); err != nil { + if err := p.w.Flush(); err != nil { return err } if tw, _ := w.(*tabwriter.Writer); tw != nil { @@ -221,9 +221,9 @@ func (c *colCounter) Reset(w io.Writer) { // Printer holds the internal state of the printing mechanism of a // program. type Printer struct { - bufWriter // TODO: embedding this makes the methods part of the API, which we did not intend + w bufWriter tabWriter *tabwriter.Writer - cols colCounter + cols colCounter // used for [KeepPadding] indentSpaces uint binNextLine bool @@ -286,12 +286,12 @@ func (p *Printer) reset() { func (p *Printer) spaces(n uint) { for range n { - p.WriteByte(' ') + p.w.WriteByte(' ') } } func (p *Printer) space() { - p.WriteByte(' ') + p.w.WriteByte(' ') p.wantSpace = spaceWritten } @@ -302,11 +302,11 @@ func (p *Printer) spacePad(pos Pos) { return } if p.wantSpace == spaceRequired { - p.WriteByte(' ') + p.w.WriteByte(' ') p.wantSpace = spaceWritten } for p.cols.column > 0 && p.cols.column < int(pos.Col()) { - p.WriteByte(' ') + p.w.WriteByte(' ') } } @@ -336,25 +336,25 @@ func (p *Printer) bslashNewl() { if p.wantSpace == spaceRequired { p.space() } - p.WriteString("\\\n") + p.w.WriteString("\\\n") p.line++ p.indent() } func (p *Printer) spacedString(s string, pos Pos) { p.spacePad(pos) - p.WriteString(s) + p.w.WriteString(s) p.wantSpace = spaceRequired } func (p *Printer) spacedToken(s string, pos Pos) { if p.minify { - p.WriteString(s) + p.w.WriteString(s) p.wantSpace = spaceNotRequired return } p.spacePad(pos) - p.WriteString(s) + p.w.WriteString(s) p.wantSpace = spaceRequired } @@ -364,14 +364,14 @@ func (p *Printer) semiOrNewl(s string, pos Pos) { p.indent() } else { if !p.wroteSemi { - p.WriteByte(';') + p.w.WriteByte(';') } if !p.minify { p.space() } p.advanceLine(pos.Line()) } - p.WriteString(s) + p.w.WriteString(s) p.wantSpace = spaceRequired } @@ -380,10 +380,10 @@ func (p *Printer) writeLit(s string) { // <<- heredoc bodies, so the parent printer will add the escape bytes // later. if p.tabWriter != nil && strings.Contains(s, "\t") { - p.WriteByte(tabwriter.Escape) - defer p.WriteByte(tabwriter.Escape) + p.w.WriteByte(tabwriter.Escape) + defer p.w.WriteByte(tabwriter.Escape) } - p.WriteString(s) + p.w.WriteString(s) } func (p *Printer) incLevel() { @@ -413,11 +413,11 @@ func (p *Printer) indent() { switch { case p.level == 0: case p.indentSpaces == 0: - p.WriteByte(tabwriter.Escape) + p.w.WriteByte(tabwriter.Escape) for i := uint(0); i < p.level; i++ { - p.WriteByte('\t') + p.w.WriteByte('\t') } - p.WriteByte(tabwriter.Escape) + p.w.WriteByte(tabwriter.Escape) default: p.spaces(p.indentSpaces * p.level) } @@ -429,7 +429,7 @@ func (p *Printer) indent() { func (p *Printer) newline(pos Pos) { p.flushHeredocs() p.flushComments() - p.WriteByte('\n') + p.w.WriteByte('\n') p.wantSpace = spaceWritten p.wantNewline, p.mustNewline = false, false p.advanceLine(pos.Line()) @@ -467,18 +467,18 @@ func (p *Printer) flushHeredocs() { for _, r := range hdocs { p.line++ - p.WriteByte('\n') + p.w.WriteByte('\n') p.wantSpace = spaceWritten p.wantNewline, p.wantNewline = false, false if r.Op == DashHdoc && p.indentSpaces == 0 && !p.minify { if r.Hdoc != nil { extra := extraIndenter{ - bufWriter: p.bufWriter, + bufWriter: p.w, baseIndent: int(p.level + 1), firstIndent: -1, } p.tabsPrinter = &Printer{ - bufWriter: &extra, + w: &extra, // The options need to persist. indentSpaces: p.indentSpaces, @@ -522,13 +522,13 @@ func (p *Printer) newlines(pos Pos) { } p.flushHeredocs() p.flushComments() - p.WriteByte('\n') + p.w.WriteByte('\n') p.wantSpace = spaceWritten p.wantNewline, p.mustNewline = false, false l := pos.Line() if l > p.line+1 && !p.minify { - p.WriteByte('\n') // preserve single empty lines + p.w.WriteByte('\n') // preserve single empty lines } p.advanceLine(l) p.indent() @@ -538,7 +538,7 @@ func (p *Printer) rightParen(pos Pos) { if len(p.pendingHdocs) > 0 || !p.minify { p.newlines(pos) } - p.WriteByte(')') + p.w.WriteByte(')') p.wantSpace = spaceRequired } @@ -547,13 +547,13 @@ func (p *Printer) semiRsrv(s string, pos Pos) { p.newlines(pos) } else { if !p.wroteSemi { - p.WriteByte(';') + p.w.WriteByte(';') } if !p.minify { p.spacePad(pos) } } - p.WriteString(s) + p.w.WriteString(s) p.wantSpace = spaceRequired } @@ -570,9 +570,9 @@ func (p *Printer) flushComments() { cline := c.Hash.Line() switch { case p.mustNewline, i > 0, cline > p.line && p.line > 0: - p.WriteByte('\n') + p.w.WriteByte('\n') if cline > p.line+1 { - p.WriteByte('\n') + p.w.WriteByte('\n') } p.indent() p.wantSpace = spaceWritten @@ -581,14 +581,14 @@ func (p *Printer) flushComments() { if p.keepPadding { p.spacePad(c.Pos()) } else { - p.WriteByte('\t') + p.w.WriteByte('\t') } case p.wantSpace != spaceWritten: p.space() } // don't go back one line, which may happen in some edge cases p.advanceLine(cline) - p.WriteByte('#') + p.w.WriteByte('#') p.writeLit(strings.TrimRightFunc(c.Text, unicode.IsSpace)) p.wantNewline = true p.mustNewline = true @@ -600,8 +600,8 @@ func (p *Printer) comments(comments ...Comment) { if p.minify { for _, c := range comments { if fileutil.Shebang([]byte("#"+c.Text)) != "" && c.Hash.Col() == 1 && c.Hash.Line() == 1 { - p.WriteString(strings.TrimRightFunc("#"+c.Text, unicode.IsSpace)) - p.WriteString("\n") + p.w.WriteString(strings.TrimRightFunc("#"+c.Text, unicode.IsSpace)) + p.w.WriteString("\n") p.line++ } } @@ -631,7 +631,7 @@ func (p *Printer) wordParts(wps []WordPart, quoted bool) { // Can't use p.wantsNewline here, since this is only about // escaped newlines. for quoted && !p.singleLine && wp.Pos().Line() > p.line { - p.WriteString("\\\n") + p.w.WriteString("\\\n") p.line++ } p.wordPart(wp, next) @@ -645,81 +645,52 @@ func (p *Printer) wordPart(wp, next WordPart) { p.writeLit(wp.Value) case *SglQuoted: if wp.Dollar { - p.WriteByte('$') + p.w.WriteByte('$') } - p.WriteByte('\'') + p.w.WriteByte('\'') p.writeLit(wp.Value) - p.WriteByte('\'') + p.w.WriteByte('\'') p.advanceLine(wp.End().Line()) case *DblQuoted: p.dblQuoted(wp) case *CmdSubst: p.advanceLine(wp.Pos().Line()) - switch { - case wp.TempFile: - p.WriteString("${") - p.wantSpace = spaceRequired - p.nestedStmts(wp.Stmts, wp.Last, wp.Right) - p.wantSpace = spaceNotRequired - p.semiRsrv("}", wp.Right) - case wp.ReplyVar: - p.WriteString("${|") - p.nestedStmts(wp.Stmts, wp.Last, wp.Right) - p.wantSpace = spaceNotRequired - p.semiRsrv("}", wp.Right) - // Special case: `# inline comment` - case wp.Backquotes && len(wp.Stmts) == 0 && - len(wp.Last) == 1 && wp.Right.Line() == p.line: - p.WriteString("`#") - p.WriteString(wp.Last[0].Text) - p.WriteString("`") - default: - p.WriteString("$(") - if len(wp.Stmts) > 0 && startsWithLparen(wp.Stmts[0]) { - p.wantSpace = spaceRequired - } else { - p.wantSpace = spaceNotRequired - } - p.nestedStmts(wp.Stmts, wp.Last, wp.Right) - p.rightParen(wp.Right) - } + p.cmdSubst(wp) case *ParamExp: litCont := ";" if nextLit, ok := next.(*Lit); ok && nextLit.Value != "" { litCont = nextLit.Value[:1] } - name := wp.Param.Value - switch { - case !p.minify: - case wp.Excl, wp.Length, wp.Width: - case wp.Index != nil, wp.Slice != nil: - case wp.Repl != nil, wp.Exp != nil: - case len(name) > 1 && !ValidName(name): // ${10} - case ValidName(name + litCont): // ${var}cont - default: - x2 := *wp - x2.Short = true - p.paramExp(&x2) - return + if p.minify && !wp.Short && wp.simple() { + name := wp.Param.Value + switch { + case len(name) > 1 && !ValidName(name): // ${10} + case ValidName(name + litCont): // ${var}cont + default: + x2 := *wp + x2.Short = true + p.paramExp(&x2) + return + } } p.paramExp(wp) case *ArithmExp: - p.WriteString("$((") + p.w.WriteString("$((") if wp.Unsigned { - p.WriteString("# ") + p.w.WriteString("# ") } p.arithmExpr(wp.X, false, false) - p.WriteString("))") + p.w.WriteString("))") case *ExtGlob: - p.WriteString(wp.Op.String()) + p.w.WriteString(wp.Op.String()) p.writeLit(wp.Pattern.Value) - p.WriteByte(')') + p.w.WriteByte(')') case *ProcSubst: // avoid conflict with << and others if p.wantSpace == spaceRequired { p.space() } - p.WriteString(wp.Op.String()) + p.w.WriteString(wp.Op.String()) p.nestedStmts(wp.Stmts, wp.Last, wp.Rparen) p.rightParen(wp.Rparen) } @@ -727,27 +698,29 @@ func (p *Printer) wordPart(wp, next WordPart) { func (p *Printer) dblQuoted(dq *DblQuoted) { if dq.Dollar { - p.WriteByte('$') + p.w.WriteByte('$') } - p.WriteByte('"') + p.w.WriteByte('"') if len(dq.Parts) > 0 { p.wordParts(dq.Parts, true) } // Add any trailing escaped newlines. for p.line < dq.Right.Line() { - p.WriteString("\\\n") + p.w.WriteString("\\\n") p.line++ } - p.WriteByte('"') + p.w.WriteByte('"') } func (p *Printer) wroteIndex(index ArithmExpr) bool { if index == nil { return false } - p.WriteByte('[') - p.arithmExpr(index, false, false) - p.WriteByte(']') + p.w.WriteByte('[') + // Note that e.g. foo[1,3]=$bar in Zsh does not allow any spaces around the comma, + // as that breaks the assignment word. + p.arithmExpr(index, true, false) + p.w.WriteByte(']') return true } @@ -757,52 +730,103 @@ func (p *Printer) paramExp(pe *ParamExp) { p.wroteIndex(pe.Index) return } - if pe.Short { // $var - p.WriteByte('$') - p.writeLit(pe.Param.Value) - return + p.w.WriteByte('$') + if !pe.Short { + p.w.WriteByte('{') + } + if pe.Flags != nil { + p.w.WriteByte('(') + p.writeLit(pe.Flags.Value) + p.w.WriteByte(')') } - // ${var...} - p.WriteString("${") switch { case pe.Length: - p.WriteByte('#') + p.w.WriteByte('#') case pe.Width: - p.WriteByte('%') + p.w.WriteByte('%') + case pe.IsSet: + p.w.WriteByte('+') case pe.Excl: - p.WriteByte('!') + p.w.WriteByte('!') + } + if pe.Param != nil { + p.writeLit(pe.Param.Value) + } else { + // Note that Zsh supports ${${nested}} but not ${$nested}, + // so we need to avoid that simplification here. + saved := p.minify + p.minify = false + p.wordPart(pe.NestedParam, nil) + p.minify = saved } - p.writeLit(pe.Param.Value) p.wroteIndex(pe.Index) switch { + case len(pe.Modifiers) > 0: + for _, lit := range pe.Modifiers { + p.w.WriteByte(':') + p.w.WriteString(lit.Value) + } case pe.Slice != nil: - p.WriteByte(':') + p.w.WriteByte(':') p.arithmExpr(pe.Slice.Offset, true, true) if pe.Slice.Length != nil { - p.WriteByte(':') + p.w.WriteByte(':') p.arithmExpr(pe.Slice.Length, true, false) } case pe.Repl != nil: if pe.Repl.All { - p.WriteByte('/') + p.w.WriteByte('/') } - p.WriteByte('/') + p.w.WriteByte('/') if pe.Repl.Orig != nil { p.word(pe.Repl.Orig) } - p.WriteByte('/') + p.w.WriteByte('/') if pe.Repl.With != nil { p.word(pe.Repl.With) } case pe.Names != 0: p.writeLit(pe.Names.String()) case pe.Exp != nil: - p.WriteString(pe.Exp.Op.String()) + p.w.WriteString(pe.Exp.Op.String()) if pe.Exp.Word != nil { p.word(pe.Exp.Word) } } - p.WriteByte('}') + if !pe.Short { + p.w.WriteByte('}') + } +} + +func (p *Printer) cmdSubst(cs *CmdSubst) { + switch { + case cs.TempFile: + p.w.WriteString("${") + p.wantSpace = spaceRequired + p.nestedStmts(cs.Stmts, cs.Last, cs.Right) + p.wantSpace = spaceNotRequired + p.semiRsrv("}", cs.Right) + case cs.ReplyVar: + p.w.WriteString("${|") + p.nestedStmts(cs.Stmts, cs.Last, cs.Right) + p.wantSpace = spaceNotRequired + p.semiRsrv("}", cs.Right) + // Special case: `# inline comment` + case cs.Backquotes && len(cs.Stmts) == 0 && + len(cs.Last) == 1 && cs.Right.Line() == p.line: + p.w.WriteString("`#") + p.w.WriteString(cs.Last[0].Text) + p.w.WriteString("`") + default: + p.w.WriteString("$(") + if len(cs.Stmts) > 0 && startsWithLparen(cs.Stmts[0]) { + p.wantSpace = spaceRequired + } else { + p.wantSpace = spaceNotRequired + } + p.nestedStmts(cs.Stmts, cs.Last, cs.Right) + p.rightParen(cs.Right) + } } func (p *Printer) loop(loop Loop) { @@ -814,16 +838,16 @@ func (p *Printer) loop(loop Loop) { p.wordJoin(loop.Items) } case *CStyleLoop: - p.WriteString("((") + p.w.WriteString("((") if loop.Init == nil { p.space() } p.arithmExpr(loop.Init, false, false) - p.WriteString("; ") + p.w.WriteString("; ") p.arithmExpr(loop.Cond, false, false) - p.WriteString("; ") + p.w.WriteString("; ") p.arithmExpr(loop.Post, false, false) - p.WriteString("))") + p.w.WriteString("))") } } @@ -831,27 +855,31 @@ func (p *Printer) arithmExpr(expr ArithmExpr, compact, spacePlusMinus bool) { if p.minify { compact = true } + p.arithmExprRecurse(expr, compact, spacePlusMinus) +} + +func (p *Printer) arithmExprRecurse(expr ArithmExpr, compact, spacePlusMinus bool) { switch expr := expr.(type) { case *Word: p.word(expr) case *BinaryArithm: if compact { - p.arithmExpr(expr.X, compact, spacePlusMinus) - p.WriteString(expr.Op.String()) - p.arithmExpr(expr.Y, compact, false) + p.arithmExprRecurse(expr.X, compact, spacePlusMinus) + p.w.WriteString(expr.Op.String()) + p.arithmExprRecurse(expr.Y, compact, false) } else { - p.arithmExpr(expr.X, compact, spacePlusMinus) + p.arithmExprRecurse(expr.X, compact, spacePlusMinus) if expr.Op != Comma { p.space() } - p.WriteString(expr.Op.String()) + p.w.WriteString(expr.Op.String()) p.space() - p.arithmExpr(expr.Y, compact, false) + p.arithmExprRecurse(expr.Y, compact, false) } case *UnaryArithm: if expr.Post { - p.arithmExpr(expr.X, compact, spacePlusMinus) - p.WriteString(expr.Op.String()) + p.arithmExprRecurse(expr.X, compact, spacePlusMinus) + p.w.WriteString(expr.Op.String()) } else { if spacePlusMinus { switch expr.Op { @@ -859,13 +887,20 @@ func (p *Printer) arithmExpr(expr ArithmExpr, compact, spacePlusMinus bool) { p.space() } } - p.WriteString(expr.Op.String()) - p.arithmExpr(expr.X, compact, false) + p.w.WriteString(expr.Op.String()) + p.arithmExprRecurse(expr.X, compact, false) } case *ParenArithm: - p.WriteByte('(') - p.arithmExpr(expr.X, false, false) - p.WriteByte(')') + p.w.WriteByte('(') + p.arithmExprRecurse(expr.X, false, false) + p.w.WriteByte(')') + case *FlagsArithm: + p.w.WriteByte('(') + p.w.WriteString(expr.Flags.Value) + p.w.WriteByte(')') + if expr.X != nil { + p.arithmExprRecurse(expr.X, compact, false) + } } } @@ -888,7 +923,7 @@ func (p *Printer) testExprSameLine(expr TestExpr) { case *BinaryTest: p.testExprSameLine(expr.X) p.space() - p.WriteString(expr.Op.String()) + p.w.WriteString(expr.Op.String()) switch expr.Op { case AndTest, OrTest: p.wantSpace = spaceRequired @@ -898,18 +933,18 @@ func (p *Printer) testExprSameLine(expr TestExpr) { p.testExprSameLine(expr.Y) } case *UnaryTest: - p.WriteString(expr.Op.String()) + p.w.WriteString(expr.Op.String()) p.space() p.testExprSameLine(expr.X) case *ParenTest: - p.WriteByte('(') + p.w.WriteByte('(') if startsWithLparen(expr.X) { p.wantSpace = spaceRequired } else { p.wantSpace = spaceNotRequired } p.testExpr(expr.X) - p.WriteByte(')') + p.w.WriteByte(')') } } @@ -929,10 +964,10 @@ func (p *Printer) unquotedWord(w *Word) { for i := 0; i < len(wp.Value); i++ { if b := wp.Value[i]; b == '\\' { if i++; i < len(wp.Value) { - p.WriteByte(wp.Value[i]) + p.w.WriteByte(wp.Value[i]) } } else { - p.WriteByte(b) + p.w.WriteByte(b) } } } @@ -960,6 +995,10 @@ func (p *Printer) wordJoin(ws []*Word) { func (p *Printer) casePatternJoin(pats []*Word) { anyNewline := false for i, w := range pats { + // Only valid situation for a literal 'esac' here is with a preceding left paran. + if i == 0 && w.Lit() == "esac" { + p.w.WriteString("(") + } if i > 0 { p.spacedToken("|", Pos{}) } @@ -998,7 +1037,7 @@ func (p *Printer) elemJoin(elems []*ArrayElem, last []Comment) { p.space() } if p.wroteIndex(el.Index) { - p.WriteByte('=') + p.w.WriteByte('=') } if el.Value != nil { p.word(el.Value) @@ -1032,7 +1071,7 @@ func (p *Printer) stmt(s *Stmt) { if r.N != nil { p.writeLit(r.N.Value) } - p.WriteString(r.Op.String()) + p.w.WriteString(r.Op.String()) if p.spaceRedirects && (r.Op != DplIn && r.Op != DplOut) { p.space() } else { @@ -1044,18 +1083,20 @@ func (p *Printer) stmt(s *Stmt) { } } sep := s.Semicolon.IsValid() && s.Semicolon.Line() > p.line && !p.singleLine - if sep || s.Background || s.Coprocess { + if sep || s.Background || s.Coprocess || s.Disown { if sep { p.bslashNewl() } else if !p.minify { p.space() } if s.Background { - p.WriteString("&") + p.w.WriteString("&") } else if s.Coprocess { - p.WriteString("|&") + p.w.WriteString("|&") + } else if s.Disown { + p.w.WriteString("&|") } else { - p.WriteString(";") + p.w.WriteString(";") } p.wroteSemi = true p.wantSpace = spaceRequired @@ -1074,7 +1115,7 @@ func (p *Printer) printRedirsUntil(redirs []*Redirect, startRedirs int, pos Pos) if r.N != nil { p.writeLit(r.N.Value) } - p.WriteString(r.Op.String()) + p.w.WriteString(r.Op.String()) if p.spaceRedirects && (r.Op != DplIn && r.Op != DplOut) { p.space() } else { @@ -1103,7 +1144,9 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { startRedirs = p.printRedirsUntil(redirs, startRedirs, cmd.Args[1].Pos()) p.wordJoin(cmd.Args[1:]) case *Block: - p.WriteByte('{') + p.w.WriteByte('{') + // avoid ; in an empty block + p.wroteSemi = true p.wantSpace = spaceRequired // Forbid "foo()\n{ bar; }" p.wantNewline = p.wantNewline || p.funcNextLine @@ -1112,7 +1155,7 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { case *IfClause: p.ifClause(cmd, false) case *Subshell: - p.WriteByte('(') + p.w.WriteByte('(') stmts := cmd.Stmts if len(stmts) > 0 && startsWithLparen(stmts[0]) { p.wantSpace = spaceRequired @@ -1125,6 +1168,10 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { p.mustNewline = true } } + } else if len(stmts) == 0 { + // Zsh allows empty subshells, but prevent `()` + // from looking like `() { anon-func; }`. + p.wantSpace = spaceRequired } else { p.wantSpace = spaceNotRequired } @@ -1146,9 +1193,9 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { p.semiRsrv("done", cmd.DonePos) case *ForClause: if cmd.Select { - p.WriteString("select ") + p.w.WriteString("select ") } else { - p.WriteString("for ") + p.w.WriteString("for ") } p.loop(cmd.Loop) p.semiOrNewl("do", cmd.DoPos) @@ -1196,11 +1243,18 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { p.nestedBinary = false case *FuncDecl: if cmd.RsrvWord { - p.WriteString("function ") + p.spacedString("function", Pos{}) + } + if cmd.Name != nil { + p.spacedString(cmd.Name.Value, Pos{}) + } else { + for _, name := range cmd.Names { + p.spacedString(name.Value, Pos{}) + } } - p.writeLit(cmd.Name.Value) if !cmd.RsrvWord || cmd.Parens { - p.WriteString("()") + p.w.WriteString("()") + p.wantSpace = spaceNotRequired } if p.funcNextLine { p.newline(Pos{}) @@ -1212,9 +1266,9 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { p.comments(cmd.Body.Comments...) p.stmt(cmd.Body) case *CaseClause: - p.WriteString("case ") + p.w.WriteString("case ") p.word(cmd.Word) - p.WriteString(" in") + p.w.WriteString(" in") p.advanceLine(cmd.In.Line()) p.wantSpace = spaceRequired if p.swtCaseIndent { @@ -1236,7 +1290,7 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { p.newlines(ci.Pos()) p.spacePad(ci.Pos()) p.casePatternJoin(ci.Patterns) - p.WriteByte(')') + p.w.WriteByte(')') if !p.minify { p.wantSpace = spaceRequired } else { @@ -1270,14 +1324,14 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { } p.semiRsrv("esac", cmd.Esac) case *ArithmCmd: - p.WriteString("((") + p.w.WriteString("((") if cmd.Unsigned { - p.WriteString("# ") + p.w.WriteString("# ") } p.arithmExpr(cmd.X, false, false) - p.WriteString("))") + p.w.WriteString("))") case *TestClause: - p.WriteString("[[ ") + p.w.WriteString("[[ ") p.incLevel() p.testExpr(cmd.X) p.decLevel() @@ -1363,7 +1417,7 @@ func (p *Printer) stmtList(stmts []*Stmt, last []Comment) { if i > 0 && p.singleLine && p.wantNewline && !p.wroteSemi { // In singleLine mode, ensure we use semicolons between // statements. - p.WriteByte(';') + p.w.WriteByte(';') p.wantSpace = spaceRequired } pos := s.Pos() @@ -1438,10 +1492,10 @@ func (p *Printer) assigns(assigns []*Assign) { p.writeLit(a.Name.Value) p.wroteIndex(a.Index) if a.Append { - p.WriteByte('+') + p.w.WriteByte('+') } if !a.Naked { - p.WriteByte('=') + p.w.WriteByte('=') } } if a.Value != nil { @@ -1452,7 +1506,7 @@ func (p *Printer) assigns(assigns []*Assign) { p.word(a.Value) } else if a.Array != nil { p.wantSpace = spaceNotRequired - p.WriteByte('(') + p.w.WriteByte('(') p.elemJoin(a.Array.Elems, a.Array.Last) p.rightParen(a.Array.Rparen) } diff --git a/vendor/mvdan.cc/sh/v3/syntax/quote.go b/vendor/mvdan.cc/sh/v3/syntax/quote.go index 6f27eba12..8f7f57eac 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/quote.go +++ b/vendor/mvdan.cc/sh/v3/syntax/quote.go @@ -78,7 +78,7 @@ func Quote(s string, lang LangVariant) (string, error) { return "", &QuoteError{ByteOffset: offs, Message: quoteErrNull} } if r == utf8.RuneError || !unicode.IsPrint(r) { - if lang == LangPOSIX { + if lang.in(LangPOSIX) { return "", &QuoteError{ByteOffset: offs, Message: quoteErrPOSIX} } nonPrintable = true @@ -132,13 +132,13 @@ func Quote(s string, lang LangVariant) (string, error) { fmt.Fprintf(&b, "\\x%02x", rem[0]) // Unfortunately, mksh allows \x to consume more hex characters. // Ensure that we don't allow it to read more than two. - if lang == LangMirBSDKorn { + if lang.in(LangMirBSDKorn) { nextRequoteIfHex = true } case r > utf8.MaxRune: // Not a valid Unicode code point? return "", &QuoteError{ByteOffset: offs, Message: quoteErrRange} - case lang == LangMirBSDKorn && r > 0xFFFD: + case lang.in(LangMirBSDKorn) && r > 0xFFFD: // From the CAVEATS section in R59's man page: // // mksh currently uses OPTU-16 internally, which is the same as @@ -180,6 +180,5 @@ func Quote(s string, lang LangVariant) (string, error) { func isHex(r rune) bool { return (r >= '0' && r <= '9') || - (r >= 'a' && r <= 'f') || - (r >= 'A' && r <= 'F') + (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') } diff --git a/vendor/mvdan.cc/sh/v3/syntax/quotestate_string.go b/vendor/mvdan.cc/sh/v3/syntax/quotestate_string.go deleted file mode 100644 index d43466f8f..000000000 --- a/vendor/mvdan.cc/sh/v3/syntax/quotestate_string.go +++ /dev/null @@ -1,61 +0,0 @@ -// Code generated by "stringer -type=quoteState"; DO NOT EDIT. - -package syntax - -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[noState-1] - _ = x[subCmd-2] - _ = x[subCmdBckquo-4] - _ = x[dblQuotes-8] - _ = x[hdocWord-16] - _ = x[hdocBody-32] - _ = x[hdocBodyTabs-64] - _ = x[arithmExpr-128] - _ = x[arithmExprLet-256] - _ = x[arithmExprCmd-512] - _ = x[arithmExprBrack-1024] - _ = x[testExpr-2048] - _ = x[testExprRegexp-4096] - _ = x[switchCase-8192] - _ = x[paramExpName-16384] - _ = x[paramExpSlice-32768] - _ = x[paramExpRepl-65536] - _ = x[paramExpExp-131072] - _ = x[arrayElems-262144] -} - -const _quoteState_name = "noStatesubCmdsubCmdBckquodblQuoteshdocWordhdocBodyhdocBodyTabsarithmExprarithmExprLetarithmExprCmdarithmExprBracktestExprtestExprRegexpswitchCaseparamExpNameparamExpSliceparamExpReplparamExpExparrayElems" - -var _quoteState_map = map[quoteState]string{ - 1: _quoteState_name[0:7], - 2: _quoteState_name[7:13], - 4: _quoteState_name[13:25], - 8: _quoteState_name[25:34], - 16: _quoteState_name[34:42], - 32: _quoteState_name[42:50], - 64: _quoteState_name[50:62], - 128: _quoteState_name[62:72], - 256: _quoteState_name[72:85], - 512: _quoteState_name[85:98], - 1024: _quoteState_name[98:113], - 2048: _quoteState_name[113:121], - 4096: _quoteState_name[121:135], - 8192: _quoteState_name[135:145], - 16384: _quoteState_name[145:157], - 32768: _quoteState_name[157:170], - 65536: _quoteState_name[170:182], - 131072: _quoteState_name[182:193], - 262144: _quoteState_name[193:203], -} - -func (i quoteState) String() string { - if str, ok := _quoteState_map[i]; ok { - return str - } - return "quoteState(" + strconv.FormatInt(int64(i), 10) + ")" -} diff --git a/vendor/mvdan.cc/sh/v3/syntax/simplify.go b/vendor/mvdan.cc/sh/v3/syntax/simplify.go index 7eef65ef2..3d44c3597 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/simplify.go +++ b/vendor/mvdan.cc/sh/v3/syntax/simplify.go @@ -156,8 +156,7 @@ func (s *simplifier) inlineSimpleParams(x ArithmExpr) ArithmExpr { // Not a parameter expansion, or not a valid name, like $3. return x } - if pe.Excl || pe.Length || pe.Width || pe.Slice != nil || - pe.Repl != nil || pe.Exp != nil || pe.Index != nil { + if !pe.simple() { // A complex parameter expansion can't be simplified. // // Note that index expressions can't generally be simplified @@ -172,7 +171,7 @@ func (s *simplifier) inlineSimpleParams(x ArithmExpr) ArithmExpr { func (s *simplifier) inlineSubshell(stmts []*Stmt) []*Stmt { for len(stmts) == 1 { st := stmts[0] - if st.Negated || st.Background || st.Coprocess || + if st.Negated || st.Background || st.Coprocess || st.Disown || len(st.Redirs) > 0 { break } diff --git a/vendor/mvdan.cc/sh/v3/syntax/token_string.go b/vendor/mvdan.cc/sh/v3/syntax/token_string.go index ab5c83aca..ef5c8c6f3 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/token_string.go +++ b/vendor/mvdan.cc/sh/v3/syntax/token_string.go @@ -14,136 +14,154 @@ func _() { _ = x[_Lit-3] _ = x[_LitWord-4] _ = x[_LitRedir-5] - _ = x[sglQuote-6] - _ = x[dblQuote-7] - _ = x[bckQuote-8] - _ = x[and-9] - _ = x[andAnd-10] - _ = x[orOr-11] - _ = x[or-12] - _ = x[orAnd-13] - _ = x[dollar-14] - _ = x[dollSglQuote-15] - _ = x[dollDblQuote-16] - _ = x[dollBrace-17] - _ = x[dollBrack-18] - _ = x[dollParen-19] - _ = x[dollDblParen-20] - _ = x[leftBrack-21] - _ = x[dblLeftBrack-22] - _ = x[leftParen-23] - _ = x[dblLeftParen-24] - _ = x[rightBrace-25] - _ = x[rightBrack-26] - _ = x[rightParen-27] - _ = x[dblRightParen-28] - _ = x[semicolon-29] - _ = x[dblSemicolon-30] - _ = x[semiAnd-31] - _ = x[dblSemiAnd-32] - _ = x[semiOr-33] - _ = x[exclMark-34] - _ = x[tilde-35] - _ = x[addAdd-36] - _ = x[subSub-37] - _ = x[star-38] - _ = x[power-39] - _ = x[equal-40] - _ = x[nequal-41] - _ = x[lequal-42] - _ = x[gequal-43] - _ = x[addAssgn-44] - _ = x[subAssgn-45] - _ = x[mulAssgn-46] - _ = x[quoAssgn-47] - _ = x[remAssgn-48] - _ = x[andAssgn-49] - _ = x[orAssgn-50] - _ = x[xorAssgn-51] - _ = x[shlAssgn-52] - _ = x[shrAssgn-53] - _ = x[rdrOut-54] - _ = x[appOut-55] - _ = x[rdrIn-56] - _ = x[rdrInOut-57] - _ = x[dplIn-58] - _ = x[dplOut-59] - _ = x[clbOut-60] - _ = x[hdoc-61] - _ = x[dashHdoc-62] - _ = x[wordHdoc-63] - _ = x[rdrAll-64] - _ = x[appAll-65] - _ = x[cmdIn-66] - _ = x[cmdOut-67] - _ = x[plus-68] - _ = x[colPlus-69] - _ = x[minus-70] - _ = x[colMinus-71] - _ = x[quest-72] - _ = x[colQuest-73] - _ = x[assgn-74] - _ = x[colAssgn-75] - _ = x[perc-76] - _ = x[dblPerc-77] - _ = x[hash-78] - _ = x[dblHash-79] - _ = x[caret-80] - _ = x[dblCaret-81] - _ = x[comma-82] - _ = x[dblComma-83] - _ = x[at-84] - _ = x[slash-85] - _ = x[dblSlash-86] - _ = x[colon-87] - _ = x[tsExists-88] - _ = x[tsRegFile-89] - _ = x[tsDirect-90] - _ = x[tsCharSp-91] - _ = x[tsBlckSp-92] - _ = x[tsNmPipe-93] - _ = x[tsSocket-94] - _ = x[tsSmbLink-95] - _ = x[tsSticky-96] - _ = x[tsGIDSet-97] - _ = x[tsUIDSet-98] - _ = x[tsGrpOwn-99] - _ = x[tsUsrOwn-100] - _ = x[tsModif-101] - _ = x[tsRead-102] - _ = x[tsWrite-103] - _ = x[tsExec-104] - _ = x[tsNoEmpty-105] - _ = x[tsFdTerm-106] - _ = x[tsEmpStr-107] - _ = x[tsNempStr-108] - _ = x[tsOptSet-109] - _ = x[tsVarSet-110] - _ = x[tsRefVar-111] - _ = x[tsReMatch-112] - _ = x[tsNewer-113] - _ = x[tsOlder-114] - _ = x[tsDevIno-115] - _ = x[tsEql-116] - _ = x[tsNeq-117] - _ = x[tsLeq-118] - _ = x[tsGeq-119] - _ = x[tsLss-120] - _ = x[tsGtr-121] - _ = x[globQuest-122] - _ = x[globStar-123] - _ = x[globPlus-124] - _ = x[globAt-125] - _ = x[globExcl-126] + _ = x[_realTokenBoundary-6] + _ = x[sglQuote-7] + _ = x[dblQuote-8] + _ = x[bckQuote-9] + _ = x[and-10] + _ = x[andAnd-11] + _ = x[orOr-12] + _ = x[or-13] + _ = x[orAnd-14] + _ = x[andPipe-15] + _ = x[andBang-16] + _ = x[dollar-17] + _ = x[dollSglQuote-18] + _ = x[dollDblQuote-19] + _ = x[dollBrace-20] + _ = x[dollBrack-21] + _ = x[dollParen-22] + _ = x[dollDblParen-23] + _ = x[leftBrace-24] + _ = x[leftBrack-25] + _ = x[dblLeftBrack-26] + _ = x[leftParen-27] + _ = x[dblLeftParen-28] + _ = x[rightBrace-29] + _ = x[rightBrack-30] + _ = x[dblRightBrack-31] + _ = x[rightParen-32] + _ = x[dblRightParen-33] + _ = x[semicolon-34] + _ = x[dblSemicolon-35] + _ = x[semiAnd-36] + _ = x[dblSemiAnd-37] + _ = x[semiOr-38] + _ = x[exclMark-39] + _ = x[tilde-40] + _ = x[addAdd-41] + _ = x[subSub-42] + _ = x[star-43] + _ = x[power-44] + _ = x[equal-45] + _ = x[nequal-46] + _ = x[lequal-47] + _ = x[gequal-48] + _ = x[addAssgn-49] + _ = x[subAssgn-50] + _ = x[mulAssgn-51] + _ = x[quoAssgn-52] + _ = x[remAssgn-53] + _ = x[andAssgn-54] + _ = x[orAssgn-55] + _ = x[xorAssgn-56] + _ = x[shlAssgn-57] + _ = x[shrAssgn-58] + _ = x[andBoolAssgn-59] + _ = x[orBoolAssgn-60] + _ = x[xorBoolAssgn-61] + _ = x[powAssgn-62] + _ = x[rdrOut-63] + _ = x[appOut-64] + _ = x[rdrIn-65] + _ = x[rdrInOut-66] + _ = x[dplIn-67] + _ = x[dplOut-68] + _ = x[rdrClob-69] + _ = x[appClob-70] + _ = x[hdoc-71] + _ = x[dashHdoc-72] + _ = x[wordHdoc-73] + _ = x[rdrAll-74] + _ = x[rdrAllClob-75] + _ = x[appAll-76] + _ = x[appAllClob-77] + _ = x[cmdIn-78] + _ = x[assgnParen-79] + _ = x[cmdOut-80] + _ = x[plus-81] + _ = x[colPlus-82] + _ = x[minus-83] + _ = x[colMinus-84] + _ = x[quest-85] + _ = x[colQuest-86] + _ = x[assgn-87] + _ = x[colAssgn-88] + _ = x[perc-89] + _ = x[dblPerc-90] + _ = x[hash-91] + _ = x[dblHash-92] + _ = x[colHash-93] + _ = x[colPipe-94] + _ = x[colStar-95] + _ = x[caret-96] + _ = x[dblCaret-97] + _ = x[comma-98] + _ = x[dblComma-99] + _ = x[at-100] + _ = x[slash-101] + _ = x[dblSlash-102] + _ = x[period-103] + _ = x[colon-104] + _ = x[tsExists-105] + _ = x[tsRegFile-106] + _ = x[tsDirect-107] + _ = x[tsCharSp-108] + _ = x[tsBlckSp-109] + _ = x[tsNmPipe-110] + _ = x[tsSocket-111] + _ = x[tsSmbLink-112] + _ = x[tsSticky-113] + _ = x[tsGIDSet-114] + _ = x[tsUIDSet-115] + _ = x[tsGrpOwn-116] + _ = x[tsUsrOwn-117] + _ = x[tsModif-118] + _ = x[tsRead-119] + _ = x[tsWrite-120] + _ = x[tsExec-121] + _ = x[tsNoEmpty-122] + _ = x[tsFdTerm-123] + _ = x[tsEmpStr-124] + _ = x[tsNempStr-125] + _ = x[tsOptSet-126] + _ = x[tsVarSet-127] + _ = x[tsRefVar-128] + _ = x[tsReMatch-129] + _ = x[tsNewer-130] + _ = x[tsOlder-131] + _ = x[tsDevIno-132] + _ = x[tsEql-133] + _ = x[tsNeq-134] + _ = x[tsLeq-135] + _ = x[tsGeq-136] + _ = x[tsLss-137] + _ = x[tsGtr-138] + _ = x[globQuest-139] + _ = x[globStar-140] + _ = x[globPlus-141] + _ = x[globAt-142] + _ = x[globExcl-143] } -const _token_name = "illegalTokEOFNewlLitLitWordLitRedir'\"`&&&||||&$$'$\"${$[$($(([[[(((}])));;;;&;;&;|!~++--***==!=<=>=+=-=*=/=%=&=|=^=<<=>>=>>><<><&>&>|<<<<-<<<&>&>><(>(+:+-:-?:?=:=%%%###^^^,,,@///:-e-f-d-c-b-p-S-L-k-g-u-G-O-N-r-w-x-s-t-z-n-o-v-R=~-nt-ot-ef-eq-ne-le-ge-lt-gt?(*(+(@(!(" +const _token_name = "illegalTokEOFNewlLitLitWordLitRedirrealTokenBoundary'\"`&&&||||&&|&!$$'$\"${$[$($(({[[[(((}]]])));;;;&;;&;|!~++--***==!=<=>=+=-=*=/=%=&=|=^=<<=>>=&&=||=^^=**=>>><<><&>&>|>>|<<<<-<<<&>&>|&>>&>>|<(=(>(+:+-:-?:?=:=%%%###:#:|:*^^^,,,@///.:-e-f-d-c-b-p-S-L-k-g-u-G-O-N-r-w-x-s-t-z-n-o-v-R=~-nt-ot-ef-eq-ne-le-ge-lt-gt?(*(+(@(!(" -var _token_index = [...]uint16{0, 10, 13, 17, 20, 27, 35, 36, 37, 38, 39, 41, 43, 44, 46, 47, 49, 51, 53, 55, 57, 60, 61, 63, 64, 66, 67, 68, 69, 71, 72, 74, 76, 79, 81, 82, 83, 85, 87, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 117, 120, 121, 123, 124, 126, 128, 130, 132, 134, 137, 140, 142, 145, 147, 149, 150, 152, 153, 155, 156, 158, 159, 161, 162, 164, 165, 167, 168, 170, 171, 173, 174, 175, 177, 178, 180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212, 214, 216, 218, 220, 222, 224, 226, 228, 231, 234, 237, 240, 243, 246, 249, 252, 255, 257, 259, 261, 263, 265} +var _token_index = [...]uint16{0, 10, 13, 17, 20, 27, 35, 52, 53, 54, 55, 56, 58, 60, 61, 63, 65, 67, 68, 70, 72, 74, 76, 78, 81, 82, 83, 85, 86, 88, 89, 90, 92, 93, 95, 96, 98, 100, 103, 105, 106, 107, 109, 111, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 141, 144, 147, 150, 153, 156, 157, 159, 160, 162, 164, 166, 168, 171, 173, 176, 179, 181, 184, 187, 191, 193, 195, 197, 198, 200, 201, 203, 204, 206, 207, 209, 210, 212, 213, 215, 217, 219, 221, 222, 224, 225, 227, 228, 229, 231, 232, 233, 235, 237, 239, 241, 243, 245, 247, 249, 251, 253, 255, 257, 259, 261, 263, 265, 267, 269, 271, 273, 275, 277, 279, 281, 283, 286, 289, 292, 295, 298, 301, 304, 307, 310, 312, 314, 316, 318, 320} func (i token) String() string { - if i >= token(len(_token_index)-1) { + idx := int(i) - 0 + if i < 0 || idx >= len(_token_index)-1 { return "token(" + strconv.FormatInt(int64(i), 10) + ")" } - return _token_name[_token_index[i]:_token_index[i+1]] + return _token_name[_token_index[idx]:_token_index[idx+1]] } diff --git a/vendor/mvdan.cc/sh/v3/syntax/tokens.go b/vendor/mvdan.cc/sh/v3/syntax/tokens.go index 97dec5433..51e9aadb8 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/tokens.go +++ b/vendor/mvdan.cc/sh/v3/syntax/tokens.go @@ -3,7 +3,7 @@ package syntax -//go:generate stringer -type token -linecomment -trimprefix _ +//go:generate go tool stringer -type token -linecomment -trimprefix _ type token uint32 @@ -17,15 +17,20 @@ const ( _LitWord _LitRedir + // Token values beyond this point stringify as exact source. + _realTokenBoundary + sglQuote // ' dblQuote // " bckQuote // ` - and // & - andAnd // && - orOr // || - or // | - orAnd // |& + and // & + andAnd // && + orOr // || + or // | + orAnd // |& + andPipe // &| + andBang // &! dollar // $ dollSglQuote // $' @@ -34,6 +39,7 @@ const ( dollBrack // $[ dollParen // $( dollDblParen // $(( + leftBrace // { leftBrack // [ dblLeftBrack // [[ leftParen // ( @@ -41,6 +47,7 @@ const ( rightBrace // } rightBrack // ] + dblRightBrack // ]] rightParen // ) dblRightParen // )) semicolon // ; @@ -61,32 +68,40 @@ const ( lequal // <= gequal // >= - addAssgn // += - subAssgn // -= - mulAssgn // *= - quoAssgn // /= - remAssgn // %= - andAssgn // &= - orAssgn // |= - xorAssgn // ^= - shlAssgn // <<= - shrAssgn // >>= - - rdrOut // > - appOut // >> - rdrIn // < - rdrInOut // <> - dplIn // <& - dplOut // >& - clbOut // >| - hdoc // << - dashHdoc // <<- - wordHdoc // <<< - rdrAll // &> - appAll // &>> - - cmdIn // <( - cmdOut // >( + addAssgn // += + subAssgn // -= + mulAssgn // *= + quoAssgn // /= + remAssgn // %= + andAssgn // &= + orAssgn // |= + xorAssgn // ^= + shlAssgn // <<= + shrAssgn // >>= + andBoolAssgn // &&= + orBoolAssgn // ||= + xorBoolAssgn // ^^= + powAssgn // **= + + rdrOut // > + appOut // >> + rdrIn // < + rdrInOut // <> + dplIn // <& + dplOut // >& + rdrClob // >| + appClob // >>| + hdoc // << + dashHdoc // <<- + wordHdoc // <<< + rdrAll // &> + rdrAllClob // &>| + appAll // &>> + appAllClob // &>>| + + cmdIn // <( + assgnParen // =( + cmdOut // >( plus // + colPlus // :+ @@ -100,6 +115,9 @@ const ( dblPerc // %% hash // # dblHash // ## + colHash // :# + colPipe // :| + colStar // :* caret // ^ dblCaret // ^^ comma // , @@ -107,6 +125,7 @@ const ( at // @ slash // / dblSlash // // + period // . colon // : tsExists // -e @@ -155,25 +174,34 @@ const ( type RedirOperator token const ( - RdrOut = RedirOperator(rdrOut) + iota // > - AppOut // >> - RdrIn // < - RdrInOut // <> - DplIn // <& - DplOut // >& - ClbOut // >| - Hdoc // << - DashHdoc // <<- - WordHdoc // <<< - RdrAll // &> - AppAll // &>> + RdrOut = RedirOperator(rdrOut) + iota // > + AppOut // >> + RdrIn // < + RdrInOut // <> + DplIn // <& + DplOut // >& + RdrClob // >| + AppClob // >>| with [LangZsh] + Hdoc // << + DashHdoc // <<- + WordHdoc // <<< + RdrAll // &> + RdrAllClob // &>| with [LangZsh] + AppAll // &>> + AppAllClob // &>>| with [LangZsh] + + // Deprecated: use [RdrClob] + // + //go:fix inline + ClbOut = RdrClob ) type ProcOperator token const ( - CmdIn = ProcOperator(cmdIn) + iota // <( - CmdOut // >( + CmdIn = ProcOperator(cmdIn) + iota // <( + CmdInTemp // =( + CmdOut // >( ) type GlobOperator token @@ -226,6 +254,9 @@ const ( RemLargeSuffix // %% RemSmallPrefix // # RemLargePrefix // ## + MatchEmpty // :# with [LangZsh] + ArrayExclude // :| with [LangZsh] + ArrayIntersect // :* with [LangZsh] UpperFirst // ^ UpperAll // ^^ LowerFirst // , @@ -265,23 +296,30 @@ const ( Shr = BinAritOperator(appOut) // >> Shl = BinAritOperator(hdoc) // << - AndArit = BinAritOperator(andAnd) // && - OrArit = BinAritOperator(orOr) // || - Comma = BinAritOperator(comma) // , - TernQuest = BinAritOperator(quest) // ? - TernColon = BinAritOperator(colon) // : - - Assgn = BinAritOperator(assgn) // = - AddAssgn = BinAritOperator(addAssgn) // += - SubAssgn = BinAritOperator(subAssgn) // -= - MulAssgn = BinAritOperator(mulAssgn) // *= - QuoAssgn = BinAritOperator(quoAssgn) // /= - RemAssgn = BinAritOperator(remAssgn) // %= - AndAssgn = BinAritOperator(andAssgn) // &= - OrAssgn = BinAritOperator(orAssgn) // |= - XorAssgn = BinAritOperator(xorAssgn) // ^= - ShlAssgn = BinAritOperator(shlAssgn) // <<= - ShrAssgn = BinAritOperator(shrAssgn) // >>= + // TODO: use "Bool" consistently for logical operators like AndArit and OrArit; use //go:fix inline? + + AndArit = BinAritOperator(andAnd) // && + OrArit = BinAritOperator(orOr) // || + XorBool = BinAritOperator(dblCaret) // ^^ + Comma = BinAritOperator(comma) // , + TernQuest = BinAritOperator(quest) // ? + TernColon = BinAritOperator(colon) // : + + Assgn = BinAritOperator(assgn) // = + AddAssgn = BinAritOperator(addAssgn) // += + SubAssgn = BinAritOperator(subAssgn) // -= + MulAssgn = BinAritOperator(mulAssgn) // *= + QuoAssgn = BinAritOperator(quoAssgn) // /= + RemAssgn = BinAritOperator(remAssgn) // %= + AndAssgn = BinAritOperator(andAssgn) // &= + OrAssgn = BinAritOperator(orAssgn) // |= + XorAssgn = BinAritOperator(xorAssgn) // ^= + ShlAssgn = BinAritOperator(shlAssgn) // <<= + ShrAssgn = BinAritOperator(shrAssgn) // >>= + AndBoolAssgn = BinAritOperator(andBoolAssgn) // &&= + OrBoolAssgn = BinAritOperator(orBoolAssgn) // ||= + XorBoolAssgn = BinAritOperator(xorBoolAssgn) // ^^= + PowAssgn = BinAritOperator(powAssgn) // **= ) type UnTestOperator token diff --git a/vendor/mvdan.cc/sh/v3/syntax/walk.go b/vendor/mvdan.cc/sh/v3/syntax/walk.go index 105f1ce0d..8a67dc55c 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/walk.go +++ b/vendor/mvdan.cc/sh/v3/syntax/walk.go @@ -79,7 +79,8 @@ func Walk(node Node, f func(Node) bool) { Walk(node.X, f) Walk(node.Y, f) case *FuncDecl: - Walk(node.Name, f) + walkNilable(node.Name, f) + walkList(node.Names, f) Walk(node.Body, f) case *Word: walkList(node.Parts, f) @@ -91,8 +92,14 @@ func Walk(node Node, f func(Node) bool) { walkList(node.Stmts, f) walkComments(node.Last, f) case *ParamExp: - Walk(node.Param, f) + walkNilable(node.Flags, f) + walkNilable(node.Param, f) + walkNilable(node.NestedParam, f) walkNilable(node.Index, f) + if node.Slice != nil { + walkNilable(node.Slice.Offset, f) + walkNilable(node.Slice.Length, f) + } if node.Repl != nil { walkNilable(node.Repl.Orig, f) walkNilable(node.Repl.With, f) @@ -116,6 +123,11 @@ func Walk(node Node, f func(Node) bool) { Walk(node.X, f) case *ParenArithm: Walk(node.X, f) + case *FlagsArithm: + Walk(node.Flags, f) + if node.X != nil { + Walk(node.X, f) + } case *ParenTest: Walk(node.X, f) case *CaseClause: @@ -234,7 +246,7 @@ func (p *debugPrinter) print(x reflect.Value) { return } p.print(x.Elem()) - case reflect.Ptr: + case reflect.Pointer: if x.IsNil() { p.printf("nil") return From e8b5d5b8fe116231e85df204c26f4cc4a2b7c5c5 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Sun, 22 Mar 2026 21:05:15 -0500 Subject: [PATCH 12/15] Update CI to Go 1.26.x and golangci-lint v2.11.4 - Bump go-version from 1.25.x to 1.26.x in CI workflow - Bump golangci-lint from v2.6.1 to v2.11.4 (built with go1.26) - Simplify Makefile lint target to single ./... pass --- .github/workflows/test.yml | 10 +++++----- Makefile | 4 ---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1cae4f957..00ecf23ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,13 +9,13 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: '1.25.x' + go-version: '1.26.x' cache: true cache-dependency-path: go.sum - uses: golangci/golangci-lint-action@v7 with: - version: v2.6.1 + version: v2.11.4 - name: Checking Format and Testing run: make check @@ -47,13 +47,13 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: '1.25.x' + go-version: '1.26.x' cache: true cache-dependency-path: go.sum - uses: golangci/golangci-lint-action@v7 with: - version: v2.6.1 + version: v2.11.4 - name: Checking Format and Testing run: make check @@ -68,7 +68,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: '1.25.x' + go-version: '1.26.x' cache: true cache-dependency-path: go.sum diff --git a/Makefile b/Makefile index 452b5ee0e..91c9ca9cc 100644 --- a/Makefile +++ b/Makefile @@ -55,11 +55,7 @@ check-windows: lint test-windows ## Run linters and tests on windows lint: ## Run linters. Use make install-linters first golangci-lint version - ${OPTS} golangci-lint run -c .golangci.yml ./cmd/... - ${OPTS} golangci-lint run -c .golangci.yml ./pkg/... - ${OPTS} golangci-lint run -c .golangci.yml ./internal/... ${OPTS} golangci-lint run -c .golangci.yml ./... - ${OPTS} golangci-lint run -c .golangci.yml . vendorcheck: ## Run vendorcheck GO111MODULE=off vendorcheck ./... From 5e160634ea324a04cf612cd6cc9d3cf2d6269a36 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Sun, 22 Mar 2026 21:21:56 -0500 Subject: [PATCH 13/15] Suppress new gosec G118/G115 rules from golangci-lint v2.11.4 --- cmd/dmsgweb/commands/dmsgwebsrv.go | 2 +- pkg/dmsghttp/http.go | 2 +- pkg/dmsgpty/whitelist_test.go | 2 +- pkg/dmsgtest/env.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/dmsgweb/commands/dmsgwebsrv.go b/cmd/dmsgweb/commands/dmsgwebsrv.go index 6bf775846..19c9dc9b1 100644 --- a/cmd/dmsgweb/commands/dmsgwebsrv.go +++ b/cmd/dmsgweb/commands/dmsgwebsrv.go @@ -195,7 +195,7 @@ func proxyHTTPConnections(ctx context.Context, localPort uint, listener net.List // Graceful shutdown on context cancellation go func() { <-ctx.Done() - if err := server.Shutdown(context.Background()); err != nil { + if err := server.Shutdown(context.Background()); err != nil { //nolint:gosec dlog.Errorf("HTTP server shutdown error: %v", err) } }() diff --git a/pkg/dmsghttp/http.go b/pkg/dmsghttp/http.go index 616fca4dc..e8433e0bd 100644 --- a/pkg/dmsghttp/http.go +++ b/pkg/dmsghttp/http.go @@ -37,7 +37,7 @@ func ListenAndServe(ctx context.Context, _ cipher.SecKey, a http.Handler, _ disc go func() { select { case <-ctx.Done(): - if err := srv.Shutdown(context.Background()); err != nil { + if err := srv.Shutdown(context.Background()); err != nil { //nolint:gosec log.WithError(err).Error() } case <-done: diff --git a/pkg/dmsgpty/whitelist_test.go b/pkg/dmsgpty/whitelist_test.go index 1e92b9e28..2af18ae12 100644 --- a/pkg/dmsgpty/whitelist_test.go +++ b/pkg/dmsgpty/whitelist_test.go @@ -356,7 +356,7 @@ func TestRPCUtil_RequestResponseRoundTrip(t *testing.T) { // Write length prefix + URI (same as writeRequest). uriStr := "dmsgpty/whitelist" buf := make([]byte, 0, 1+len(uriStr)) - buf = append(buf, byte(len(uriStr))) + buf = append(buf, byte(len(uriStr))) //nolint:gosec buf = append(buf, []byte(uriStr)...) _, err := connB.Write(buf) require.NoError(t, err) diff --git a/pkg/dmsgtest/env.go b/pkg/dmsgtest/env.go index 5f4cf2343..a7f208900 100644 --- a/pkg/dmsgtest/env.go +++ b/pkg/dmsgtest/env.go @@ -148,7 +148,7 @@ func (env *Env) newClientWithKeys(ctx context.Context, pk cipher.PubKey, sk ciph env.cWg.Add(1) go func() { - c.Serve(context.Background()) + c.Serve(context.Background()) //nolint:gosec env.mx.Lock() delete(env.c, pk) env.mx.Unlock() From 808ea1c44440f2019f1c7d49c6bb4f22640c30e8 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Sun, 22 Mar 2026 21:25:56 -0500 Subject: [PATCH 14/15] Move gosec G118 nolint to go func() line where error is reported --- cmd/dmsgweb/commands/dmsgwebsrv.go | 4 ++-- pkg/dmsghttp/http.go | 4 ++-- pkg/dmsgtest/env.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/dmsgweb/commands/dmsgwebsrv.go b/cmd/dmsgweb/commands/dmsgwebsrv.go index 19c9dc9b1..18d94c5ca 100644 --- a/cmd/dmsgweb/commands/dmsgwebsrv.go +++ b/cmd/dmsgweb/commands/dmsgwebsrv.go @@ -193,9 +193,9 @@ func proxyHTTPConnections(ctx context.Context, localPort uint, listener net.List } // Graceful shutdown on context cancellation - go func() { + go func() { //nolint:gosec <-ctx.Done() - if err := server.Shutdown(context.Background()); err != nil { //nolint:gosec + if err := server.Shutdown(context.Background()); err != nil { dlog.Errorf("HTTP server shutdown error: %v", err) } }() diff --git a/pkg/dmsghttp/http.go b/pkg/dmsghttp/http.go index e8433e0bd..d457b6a3b 100644 --- a/pkg/dmsghttp/http.go +++ b/pkg/dmsghttp/http.go @@ -34,10 +34,10 @@ func ListenAndServe(ctx context.Context, _ cipher.SecKey, a http.Handler, _ disc } done := make(chan struct{}) - go func() { + go func() { //nolint:gosec select { case <-ctx.Done(): - if err := srv.Shutdown(context.Background()); err != nil { //nolint:gosec + if err := srv.Shutdown(context.Background()); err != nil { log.WithError(err).Error() } case <-done: diff --git a/pkg/dmsgtest/env.go b/pkg/dmsgtest/env.go index a7f208900..291eb630a 100644 --- a/pkg/dmsgtest/env.go +++ b/pkg/dmsgtest/env.go @@ -147,8 +147,8 @@ func (env *Env) newClientWithKeys(ctx context.Context, pk cipher.PubKey, sk ciph env.c[pk] = c env.cWg.Add(1) - go func() { - c.Serve(context.Background()) //nolint:gosec + go func() { //nolint:gosec + c.Serve(context.Background()) env.mx.Lock() delete(env.c, pk) env.mx.Unlock() From c05b4751c10475d469d707905f37d12bc2bb604c Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Sun, 22 Mar 2026 21:32:35 -0500 Subject: [PATCH 15/15] Update Dockerfiles to Go 1.26 (matches go.mod) --- docker/images/dmsg-client/Dockerfile | 2 +- docker/images/dmsg-discovery/Dockerfile | 2 +- docker/images/dmsg-server/Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/images/dmsg-client/Dockerfile b/docker/images/dmsg-client/Dockerfile index deafe6a15..06f2207b1 100644 --- a/docker/images/dmsg-client/Dockerfile +++ b/docker/images/dmsg-client/Dockerfile @@ -1,5 +1,5 @@ # Builder -ARG base_image=golang:1.25-alpine +ARG base_image=golang:1.26-alpine FROM ${base_image} AS builder ARG CGO_ENABLED=0 diff --git a/docker/images/dmsg-discovery/Dockerfile b/docker/images/dmsg-discovery/Dockerfile index 11fd30221..08ec11c04 100755 --- a/docker/images/dmsg-discovery/Dockerfile +++ b/docker/images/dmsg-discovery/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.25-alpine AS builder +FROM golang:1.26-alpine AS builder ARG CGO_ENABLED=0 ENV CGO_ENABLED=${CGO_ENABLED} \ diff --git a/docker/images/dmsg-server/Dockerfile b/docker/images/dmsg-server/Dockerfile index e4b1c68fe..017b9920a 100755 --- a/docker/images/dmsg-server/Dockerfile +++ b/docker/images/dmsg-server/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.25-alpine AS builder +FROM golang:1.26-alpine AS builder ARG CGO_ENABLED=0 ENV CGO_ENABLED=${CGO_ENABLED} \