Compare commits

..
Author SHA1 Message Date
sixsmith 1f8ead2131 add --pindigests flag 2026-08-28 12:08:58 -04:00
sixsmith 5694d6c387 add regclient to fetch image manifest/digest
* also run `go mod vendor`
2026-08-28 12:08:58 -04:00
sixsmith a8d9c3984e add docs and test 2026-08-28 12:08:45 -04:00
352 changed files with 31485 additions and 6915 deletions
-1
View File
@@ -1 +0,0 @@
vendor/** linguist-generated=true
+2 -8
View File
@@ -17,7 +17,7 @@ export GOPRIVATE=coopcloud.tech
all: format check build
run:
@go run -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA) $(ARGS)
@go run -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA)
install:
@go install -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA)
@@ -40,17 +40,11 @@ check:
(echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
test:
@go test ./... -short -cover -v -p 1
integration-test:
@go test ./... -short=false -failfast -cover -v -p 1 -count=1
@go test ./... -cover -v -p 1
find-tests:
@find . -name "*_test.go"
find-integration-test:
@find . -iname "*_e2e_test.go"
loc:
@find . -name "*.go" | xargs wc -l
+1
View File
@@ -19,5 +19,6 @@ var (
Minor bool
NoDomainChecks bool
Patch bool
PinDigests bool
ShowUnchanged bool
)
+29 -2
View File
@@ -293,12 +293,31 @@ interface.`),
}
}
if upgradeTag != "skip" {
ok, err := recipe.UpdateTag(image, upgradeTag)
var ok bool
var err error
var resolvedDigest string
if internal.PinDigests {
digest, dErr := client.GetImageDigest(img, upgradeTag)
if dErr != nil {
log.Fatal(i18n.G("failed to resolve digest for %s:%s: %s", image, upgradeTag, dErr.Error()))
}
resolvedDigest = digest
ok, err = recipe.UpdatePinnedTag(image, upgradeTag, digest)
} else {
ok, err = recipe.UpdateTag(image, upgradeTag)
}
if err != nil {
log.Fatal(err)
}
if ok {
log.Info(i18n.G("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image))
if internal.PinDigests {
log.Info(i18n.G("tag upgraded and pinned from %s to %s@%s for %s", tag.String(), upgradeTag, resolvedDigest, image))
} else {
log.Info(i18n.G("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image))
}
}
} else {
if !internal.NoInput {
@@ -417,4 +436,12 @@ func init() {
false,
i18n.G("commit changes"),
)
RecipeUpgradeCommand.Flags().BoolVarP(
&internal.PinDigests,
i18n.G("pindigests"),
i18n.GC("p", "pin container image by digest"),
false,
i18n.G("pin the container image version by manifest digest (image:tag@sha256:...)"),
)
}
-5
View File
@@ -30,11 +30,6 @@
packages = with pkgs; [
go_1_26
gnumake
(bats.withLibraries (libraries: [
libraries.bats-support
libraries.bats-assert
libraries.bats-file
]))
];
};
}
+13 -10
View File
@@ -13,14 +13,15 @@ require (
github.com/docker/cli v28.4.0+incompatible
github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-units v0.5.0
github.com/go-git/go-git/v5 v5.19.3
github.com/go-git/go-git/v5 v5.19.2
github.com/google/go-cmp v0.7.0
github.com/leonelquinteros/gotext v1.7.2
github.com/moby/sys/signal v0.7.1
github.com/moby/term v0.5.2
github.com/pkg/errors v0.9.1
github.com/regclient/regclient v0.11.5
github.com/schollz/progressbar/v3 v3.19.1
golang.org/x/term v0.47.0
golang.org/x/term v0.45.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.2
)
@@ -59,7 +60,7 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.2 // indirect
github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@@ -72,7 +73,8 @@ require (
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
@@ -94,7 +96,7 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/runc v1.1.13 // indirect
github.com/opencontainers/runtime-spec v1.1.0 // indirect
github.com/pjbgf/sha1cd v0.7.0 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
@@ -103,6 +105,7 @@ require (
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
@@ -121,10 +124,10 @@ require (
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto v0.56.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/text v0.39.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
@@ -148,11 +151,11 @@ require (
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/spf13/cobra v1.10.1
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.12.1
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
golang.org/x/sys v0.49.0
golang.org/x/sys v0.47.0
)
replace github.com/docker/cli v28.4.0+incompatible => git.coopcloud.tech/toolshed/docker-cli v28.5.3-0.20260202112816-30df2d0b3a00+incompatible
+30 -18
View File
@@ -388,12 +388,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.9.2 h1:OXFSRyz4g20upsGDJgQG9Bak1l/ZEv8GHVYB52O71sE=
github.com/go-git/go-billy/v5 v5.9.2/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.19.3 h1:qHttbZ+Am7wEjdTpR1XMQ4rl42FK9SIXzZ4o9x7s6M4=
github.com/go-git/go-git/v5 v5.19.3/go.mod h1:Ye4C8dVigkqrv9UsB4NMdSLV4yAIkLiIhW61gcOyhl4=
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -426,6 +426,8 @@ github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
@@ -583,8 +585,10 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -696,6 +700,8 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/olareg/olareg v0.2.1 h1:RPHGIaqlVWPbKAsOYUj7e2WfEEW5M7F8I6hKMrwd4jU=
github.com/olareg/olareg v0.2.1/go.mod h1:dhr8QetC7U7jJ2m93oxDhEEOKCRbPgOK1oGyKfB4QNo=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -750,8 +756,8 @@ github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuh
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pjbgf/sha1cd v0.7.0 h1:ZRNPKHj+gfkLBf0KJv/p1Hmz+7bqCT5o311YX0nq+DA=
github.com/pjbgf/sha1cd v0.7.0/go.mod h1:pKR5Li+qTCo+ebqITZfwPlJGoCFCvSO00qxjbNtTUFQ=
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -792,6 +798,8 @@ github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/regclient/regclient v0.11.5 h1:OHRsXO0F3qHGfa4HEUv+EkMH9NXNcCTBKjNzyC/UhIA=
github.com/regclient/regclient v0.11.5/go.mod h1:DZUOfIT14WFTK2Pj4vjd93avy9O4Fdpjrf9ir23TbRE=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
@@ -855,6 +863,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/sudo-bmitch/oci-digest v0.1.2 h1:are0qzWTsFZGZ3Uvdi9OSztJszSWaab6iqquMEEB7rw=
github.com/sudo-bmitch/oci-digest v0.1.2/go.mod h1:SH6l5OIe0islKBZBedjiPOeET/0QwGL+/oYfQt51uQo=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
@@ -866,6 +876,8 @@ github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnH
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
@@ -963,8 +975,8 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1040,8 +1052,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1136,13 +1148,13 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1152,8 +1164,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+24
View File
@@ -9,8 +9,12 @@ import (
"github.com/containers/image/docker"
"github.com/containers/image/types"
"github.com/distribution/reference"
"github.com/regclient/regclient"
"github.com/regclient/regclient/types/ref"
)
var rc = regclient.New()
// GetRegistryTags retrieves all tags of an image from a container registry.
func GetRegistryTags(img reference.Named) ([]string, error) {
var tags []string
@@ -28,3 +32,23 @@ func GetRegistryTags(img reference.Named) ([]string, error) {
return tags, nil
}
// GetImageDigest resolves the content digest (sha256:...) for a specific
// image:tag reference by querying the registry manifest. This gives us a
// digest to pin to while keeping support for multi-arch deployments: when
// the tag points at a manifest list/OCI index, the digest returned is the
// digest of that top-level index, so the pin still resolves correctly
// regardless of the deploying host's architecture.
func GetImageDigest(img reference.Named, tag string) (string, error) {
r, err := ref.New(fmt.Sprintf("%s:%s", img.Name(), tag))
if err != nil {
return "", fmt.Errorf("parsing reference %s:%s: %w", img.Name(), tag, err)
}
m, err := rc.ManifestHead(context.Background(), r)
if err != nil {
return "", fmt.Errorf("fetching manifest for %s:%s: %w", img.Name(), tag, err)
}
return m.GetDescriptor().Digest.String(), nil
}
+2 -1
View File
@@ -34,7 +34,8 @@ func SmallSHA(hash string) string {
return hash[:8]
}
// RemoveSha remove image sha from a string that are added in some docker outputs
// RemoveSha remove image sha (digest suffix) from a string that are added in some docker outputs
// e.g., "image:tag@sha256:..." -> "image:tag"
func RemoveSha(str string) string {
return strings.Split(str, "@")[0]
}
+7
View File
@@ -6,6 +6,13 @@ import (
"github.com/stretchr/testify/assert"
)
func TestRemoveSha(t *testing.T) {
assert.Equal(t, "ubuntu:latest", RemoveSha("ubuntu:latest@sha256:1234567890abcdef"))
assert.Equal(t, "ubuntu:latest", RemoveSha("ubuntu:latest"))
assert.Equal(t, "my-repo/my-image:v1", RemoveSha("my-repo/my-image:v1@sha256:abcdef1234567890"))
assert.Equal(t, "my-repo/my-image", RemoveSha("my-repo/my-image@sha256:abcdef1234567890"))
}
func TestBoldDirtyDefault(t *testing.T) {
assert.Equal(t, "foo", BoldDirtyDefault("foo"))
}
+2
View File
@@ -52,6 +52,7 @@ func Clone(dir, url string) error {
URL: url,
Tags: git.AllTags,
ReferenceName: plumbing.ReferenceName("refs/heads/main"),
SingleBranch: true,
})
if err != nil && gitCloneIgnoreErr(err) {
@@ -70,6 +71,7 @@ func Clone(dir, url string) error {
URL: url,
Tags: git.AllTags,
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
SingleBranch: true,
})
if err != nil && gitCloneIgnoreErr(err) {
+36 -36
View File
@@ -7,7 +7,7 @@
msgid ""
msgstr "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-08-31 19:21+0000\n"
"POT-Creation-Date: 2026-06-14 17:56+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -189,7 +189,7 @@ msgstr ""
msgid "%d volumes removed successfully"
msgstr ""
#: ./pkg/recipe/git.go:198
#: ./pkg/recipe/git.go:197
#, c-format
msgid "%s (%s) has locally unstaged changes?"
msgstr ""
@@ -419,7 +419,7 @@ msgstr ""
msgid "%s sanitised as %s for new app"
msgstr ""
#: ./pkg/recipe/git.go:451
#: ./pkg/recipe/git.go:445
#, c-format
msgid "%s service is missing image tag?"
msgstr ""
@@ -902,7 +902,7 @@ msgstr ""
msgid "Generate the recipe catalogue"
msgstr ""
#: ./pkg/recipe/git.go:465
#: ./pkg/recipe/git.go:459
#, c-format
msgid "GetRecipeVersions encountered error for %s: %s (collected %d versions)"
msgstr ""
@@ -1815,7 +1815,7 @@ msgstr ""
msgid "are you sure?"
msgstr ""
#: ./pkg/recipe/git.go:163
#: ./pkg/recipe/git.go:162
#, c-format
msgid "attempting to checkout '%s' as chaos commit"
msgstr ""
@@ -1951,7 +1951,7 @@ msgstr ""
msgid "cancelled"
msgstr ""
#: ./pkg/catalogue/catalogue.go:59 ./pkg/recipe/git.go:252
#: ./pkg/catalogue/catalogue.go:59 ./pkg/recipe/git.go:251
#, c-format
msgid "cannot ensure %s is up-to-date, no git remotes configured"
msgstr ""
@@ -1966,7 +1966,7 @@ msgstr ""
msgid "cannot get label %s for %s"
msgstr ""
#: ./pkg/recipe/git.go:59
#: ./pkg/recipe/git.go:58
#, c-format
msgid "cannot redeploy previous chaos version (%s), did you mean to use \"--chaos\"?"
msgstr ""
@@ -2118,7 +2118,7 @@ msgstr ""
msgid "cmd"
msgstr ""
#: ./pkg/recipe/git.go:476
#: ./pkg/recipe/git.go:470
#, c-format
msgid "collected %s for %s"
msgstr ""
@@ -2456,7 +2456,7 @@ msgstr ""
msgid "destination directory does not exist"
msgstr ""
#: ./pkg/recipe/git.go:379
#: ./pkg/recipe/git.go:373
#, c-format
msgid "detected %s as tags for recipe %s"
msgstr ""
@@ -2653,7 +2653,7 @@ msgstr ""
msgid "ensure recipe: %s"
msgstr ""
#: ./pkg/recipe/git.go:57
#: ./pkg/recipe/git.go:56
#, c-format
msgid "ensuring env version %s"
msgstr ""
@@ -2751,12 +2751,12 @@ msgstr ""
msgid "failed to check git status of %s: %s"
msgstr ""
#: ./pkg/git/branch.go:95 ./pkg/recipe/git.go:232
#: ./pkg/git/branch.go:95 ./pkg/recipe/git.go:231
#, c-format
msgid "failed to check out %s in %s"
msgstr ""
#: ./pkg/recipe/git.go:418
#: ./pkg/recipe/git.go:412
#, c-format
msgid "failed to check out %s in %s: %s"
msgstr ""
@@ -2806,7 +2806,7 @@ msgstr ""
msgid "failed to generate random bytes: %w"
msgstr ""
#: ./pkg/recipe/git.go:427
#: ./pkg/recipe/git.go:421
#, c-format
msgid "failed to get compose config for %s: %s"
msgstr ""
@@ -2844,7 +2844,7 @@ msgstr ""
msgid "failed to parse image %s, saw: %s"
msgstr ""
#: ./pkg/recipe/git.go:437
#: ./pkg/recipe/git.go:431
#, c-format
msgid "failed to parse image for %s in %s: %s"
msgstr ""
@@ -2952,7 +2952,7 @@ msgstr ""
msgid "fetch all recipes"
msgstr ""
#: ./pkg/catalogue/catalogue.go:84 ./pkg/recipe/git.go:290
#: ./pkg/catalogue/catalogue.go:84 ./pkg/recipe/git.go:284
#, c-format
msgid "fetched latest git changes for %s"
msgstr ""
@@ -3070,12 +3070,12 @@ msgstr ""
msgid "git changes pushed"
msgstr ""
#: ./pkg/recipe/git.go:423
#: ./pkg/recipe/git.go:417
#, c-format
msgid "git checkout: %s in %s"
msgstr ""
#: ./pkg/git/clone.go:63 ./pkg/git/clone.go:100
#: ./pkg/git/clone.go:64 ./pkg/git/clone.go:102
#, c-format
msgid "git clone %s: cancelled due to interrupt"
msgstr ""
@@ -3085,17 +3085,17 @@ msgstr ""
msgid "git clone: %s"
msgstr ""
#: ./pkg/git/clone.go:87
#: ./pkg/git/clone.go:89
#, c-format
msgid "git clone: %s already exists"
msgstr ""
#: ./pkg/git/clone.go:58 ./pkg/git/clone.go:76 ./pkg/git/clone.go:85
#: ./pkg/git/clone.go:59 ./pkg/git/clone.go:78 ./pkg/git/clone.go:87
#, c-format
msgid "git clone: %s cloned successfully"
msgstr ""
#: ./pkg/git/clone.go:67
#: ./pkg/git/clone.go:68
msgid "git clone: main branch failed, attempting master branch"
msgstr ""
@@ -3150,7 +3150,7 @@ msgstr ""
msgid "git.coopcloud.tech repo exists"
msgstr ""
#: ./pkg/recipe/git.go:390
#: ./pkg/recipe/git.go:384
#, c-format
msgid "git: opening repository in %s"
msgstr ""
@@ -4061,7 +4061,7 @@ msgstr ""
msgid "proceed?"
msgstr ""
#: ./pkg/recipe/git.go:410
#: ./pkg/recipe/git.go:404
#, c-format
msgid "processing %s for %s"
msgstr ""
@@ -4129,7 +4129,7 @@ msgstr ""
msgid "re"
msgstr ""
#: ./pkg/recipe/git.go:158
#: ./pkg/recipe/git.go:157
#, c-format
msgid "read %s as tags for recipe %s"
msgstr ""
@@ -4865,17 +4865,17 @@ msgstr ""
msgid "skipping secret (because it already exists) on %s: %s"
msgstr ""
#: ./pkg/recipe/git.go:419
#: ./pkg/recipe/git.go:413
#, c-format
msgid "skipping tag %s: checkout failed: %s"
msgstr ""
#: ./pkg/recipe/git.go:428
#: ./pkg/recipe/git.go:422
#, c-format
msgid "skipping tag %s: invalid compose config: %s"
msgstr ""
#: ./pkg/recipe/git.go:438
#: ./pkg/recipe/git.go:432
#, c-format
msgid "skipping tag %s: invalid image reference in service %s: %s"
msgstr ""
@@ -4978,7 +4978,7 @@ msgstr ""
msgid "succeeded"
msgstr ""
#: ./pkg/recipe/git.go:185
#: ./pkg/recipe/git.go:184
#, c-format
msgid "successfully checked %s out to %s in %s"
msgstr ""
@@ -5138,17 +5138,17 @@ msgstr ""
msgid "un"
msgstr ""
#: ./pkg/recipe/git.go:194
#: ./pkg/recipe/git.go:193
#, c-format
msgid "unable to check git clean status in %s: %s"
msgstr ""
#: ./pkg/recipe/git.go:263
#: ./pkg/recipe/git.go:262
#, c-format
msgid "unable to check out default branch in %s: %s"
msgstr ""
#: ./pkg/git/clone.go:98
#: ./pkg/git/clone.go:100
#, c-format
msgid "unable to clean up git clone of %s: %s"
msgstr ""
@@ -5222,7 +5222,7 @@ msgstr ""
msgid "unable to discover SSH remote for %s"
msgstr ""
#: ./pkg/recipe/git.go:274
#: ./pkg/recipe/git.go:268
#, c-format
msgid "unable to fetch tags in %s: %s"
msgstr ""
@@ -5232,7 +5232,7 @@ msgstr ""
msgid "unable to get container matching %s: %s"
msgstr ""
#: ./pkg/recipe/git.go:286
#: ./pkg/recipe/git.go:280
#, c-format
msgid "unable to git pull in %s: %s"
msgstr ""
@@ -5261,12 +5261,12 @@ msgstr ""
msgid "unable to look up server context for %s: %s"
msgstr ""
#: ./cli/recipe/fetch.go:77 ./pkg/git/read.go:26 ./pkg/lint/recipe.go:491 ./pkg/recipe/git.go:243
#: ./cli/recipe/fetch.go:77 ./pkg/git/read.go:26 ./pkg/lint/recipe.go:491 ./pkg/recipe/git.go:242
#, c-format
msgid "unable to open %s: %s"
msgstr ""
#: ./pkg/recipe/git.go:258
#: ./pkg/recipe/git.go:257
#, c-format
msgid "unable to open git work tree in %s: %s"
msgstr ""
@@ -5326,7 +5326,7 @@ msgstr ""
msgid "unable to read new env %s: %s"
msgstr ""
#: ./pkg/recipe/git.go:248
#: ./pkg/recipe/git.go:247
#, c-format
msgid "unable to read remotes in %s: %s"
msgstr ""
@@ -5361,7 +5361,7 @@ msgstr ""
msgid "unable to reset commit after failed release attempt: %s"
msgstr ""
#: ./pkg/recipe/git.go:167
#: ./pkg/recipe/git.go:166
#, c-format
msgid "unable to resolve '%s': %s"
msgstr ""
Binary file not shown.
+53 -167
View File
@@ -1,17 +1,5 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-08-31 19:21+0000\n"
"PO-Revision-Date: 2026-09-01 22:35+0000\n"
"Last-Translator: chasqui <chasqui@cryptolab.net>\n"
"Language-Team: Spanish <https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.12.2\n"
msgstr "Project-Id-Version: \nReport-Msgid-Bugs-To: EMAIL\nPOT-Creation-Date: 2026-06-14 17:56+0200\nPO-Revision-Date: 2026-08-11 06:51+0000\nLast-Translator: chasqui <chasqui@cryptolab.net>\nLanguage-Team: Spanish <https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/>\nLanguage: es\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\nPlural-Forms: nplurals=2; plural=n != 1;\nX-Generator: Weblate 5.12.2\n"
#: cli/app/cp.go:38
msgid ""
@@ -112,26 +100,7 @@ msgid ""
"\n"
" # list apps of all servers which match a specific recipe\n"
" abra app ls -r gitea"
msgstr ""
" OPCIONES Y FLAGS\n"
" ────────────────\n"
" ejemplos de uso:\n"
"\n"
"┌─ Información detallada ─────────────────────────────────────┐\n"
"│ $ abra aplicacion listar --estado │\n"
"└─────────────────────────────────────────────────────────────┘\n"
"\n"
"┌─ Aplicaciones de una receta ────────────────────────────────┐\n"
"│ $ abra aplicacion listar --receta wordpress │\n"
"└─────────────────────────────────────────────────────────────┘\n"
"\n"
"┌─ Receta + información detallada ────────────────────────────┐\n"
"│ $ abra aplicacion listar --receta wordpress --estado │\n"
"└─────────────────────────────────────────────────────────────┘\n"
"\n"
"┌─ Forma abreviada ───────────────────────────────────────────┐\n"
"│ $ abra app ls -S │\n"
"└─────────────────────────────────────────────────────────────┘"
msgstr " # Listar todas las aplicaciones instaladas con mayor detalle\n abra aplicacion listar --estado\n\n # Listar todas las aplicaciones \"wordpress\" (puedes probar otras recetas)\n abra aplicacion listar --receta wordpress\n\n # Listar todas las aplicaciones \"wordpress\" con mayor detalle\n abra aplicacion listar --receta wordpress --estado\n\n # Listar todas las aplicaciones instaladas con mayor detalle usando los comandos abreviados\n abra app ls -S"
#: cli/app/move.go:59
msgid ""
@@ -244,21 +213,7 @@ msgstr " abra aplicacion entorno listar \"nombre_app.dominio\""
#: cli/app/remove.go:45
msgid " abra app remove 1312.net"
msgstr ""
" EJEMPLOS DE USO\n"
" ────────────────\n"
"\n"
"┌─ Borrar una aplicación ─────────────────────────────────────┐\n"
"│ $ abra aplicacion borrar \"nombre_app.dominio\" │\n"
"└─────────────────────────────────────────────────────────────┘\n"
"\n"
"┌─ Borrar sin confirmación ───────────────────────────────────┐\n"
"│ $ abra aplicacion borrar \"nombre_app.dominio\" --forzar │\n"
"└─────────────────────────────────────────────────────────────┘\n"
"\n"
"┌─ Borrar sin confirmación usando comando abreviado ──────────┐\n"
"│ $ abra app rm \"nombre_app.dominio\" -f │\n"
"└─────────────────────────────────────────────────────────────┘"
msgstr " abra aplicacion borrar \"nombre_app.dominio\""
#: cli/app/secret.go:346
msgid " abra app secret rm 1312.net oauth_key"
@@ -314,7 +269,7 @@ msgstr "%d volumen(es) eliminados con éxito"
msgid "%d volumes removed successfully"
msgstr "%d volúmenes eliminados correctamente"
#: pkg/recipe/git.go:198
#: pkg/recipe/git.go:197
#, c-format
msgid "%s (%s) has locally unstaged changes?"
msgstr "%s (%s) ¿tiene cambios locales?"
@@ -547,7 +502,7 @@ msgstr "%s requiere inserción de secretos antes del despliegue (#generate=false
msgid "%s sanitised as %s for new app"
msgstr "%s sanitisado como %s para nueva aplicación"
#: pkg/recipe/git.go:451
#: pkg/recipe/git.go:445
#, c-format
msgid "%s service is missing image tag?"
msgstr "¿falta la etiqueta de imagen del servicio %s?"
@@ -1129,26 +1084,7 @@ msgid ""
"Generate a report of all managed apps.\n"
"\n"
"Use \"--status/-S\" flag to query all servers for the live deployment status."
msgstr ""
"┌──────────────────────────────────────────────────────────────┐\n"
"│ MANUAL DE USO · COMANDO «LISTAR» │\n"
"└──────────────────────────────────────────────────────────────┘\n"
" DESCRIPCIÓN\n"
" ───────────\n"
" El comando:\n"
"\n"
" abra aplicacion listar\n"
"\n"
"Permite consultar rápidamente qué aplicaciones están siendo\n"
"gestionadas por Abra.\n"
"\n"
"Al ejecutarlo, Abra genera un informe con la lista de\n"
"aplicaciones disponibles y la información básica de cada una.\n"
"\n"
"En pocas palabras, puedes imaginar `listar` como una forma\n"
"de decirle a Abra:\n"
"\n"
" «muéstrame qué aplicaciones tengo»"
msgstr "BIENVENIDE AL MANUAL DE USO COMANDO \"LISTAR\"\n\nEl comando \"abra aplicacion listar\" permite consultar de forma rápida qué aplicaciones están siendo gestionadas. \nAl ejecutarlo, Abra genera un informe con la lista de aplicaciones disponibles y la información básica de cada una.\nPuedes imaginar el comando listar como una forma de decirle a Abra: «muéstrame qué aplicaciones tengo»\n\nLos comandos pueden aceptar opciones y flags que permiten modificar o ampliar la información que muestran, \nen este caso es bastante útil el uso de \"--estado\". Estas opciones se escriben después del comando siguiendo \nla sintaxis detallada más adelante."
#: cli/app/new.go:321
msgid "Generate app secrets?"
@@ -1174,7 +1110,7 @@ msgstr "Genera 🥷 secretos (contraseñas) automáticamente"
msgid "Generate the recipe catalogue"
msgstr "📋 Muestra el catálogo de 📜 recetas"
#: pkg/recipe/git.go:465
#: pkg/recipe/git.go:459
#, c-format
msgid "GetRecipeVersions encountered error for %s: %s (collected %d versions)"
msgstr "GetRecipeVersions encontró un error para %s: %s (recopiladas %d versiones)"
@@ -1516,48 +1452,24 @@ msgid ""
"To delete everything without prompt, use the \"--force/-f\" or the \"--no-input/n\"\n"
"flag."
msgstr ""
"┌──────────────────────────────────────────────────────────────┐\n"
"│ MANUAL DE USO · COMANDO «BORRAR» │\n"
"└──────────────────────────────────────────────────────────────┘\n"
" DESCRIPCIÓN\n"
" ───────────\n"
" El comando:\n"
"Elimina todo lo relacionado con una aplicación que ya está plegada.\n"
"\n"
" abra aplicacion borrar \"nombre_app.dominio\"\n"
"Por defecto, pedirá confirmación antes de continuar. Se borrarán \n"
"todos los secretos, volúmenes y el archivo .env local de la aplicación.\n"
"\n"
"elimina todo lo relacionado con una aplicación que ya está\n"
"desplegada.\n"
"Ejecuta este comando solo si estás seguro de querer quitar por \n"
"completo la aplicación y todos sus datos asociados. Esta acción \n"
"es destructiva. ¡Ten cuidado!\n"
"\n"
"Por defecto, Abra pedirá confirmación antes de continuar.\n"
"Si quieres borrar volúmenes o secretos específicos, \n"
"usa los subcomandos de eliminación bajo \"aplicación volumen\" \n"
"y \"aplicación secreto\" en su lugar.\n"
"\n"
"La eliminación incluye:\n"
"Ten en cuenta que si borras el archivo .env local sin eliminar primero \n"
"volúmenes y secretos, Abra *no* podrá ayudarte a eliminarlos después.\n"
"\n"
" ├── secretos\n"
" ├── volúmenes\n"
" └── archivo .env local\n"
"\n"
"\n"
" ¡ATENCIÓN!\n"
" ──────────\n"
"Esta es una acción destructiva.\n"
"\n"
"La aplicación y todos los datos asociados serán eliminados.\n"
"Ejecuta este comando únicamente si estás seguro de querer\n"
"quitar la aplicación por completo.\n"
"\n"
"\n"
"Si quieres eliminar volúmenes o secretos específicos, utiliza\n"
"los subcomandos correspondientes:\n"
"\n"
" abra aplicacion volumen\n"
" abra aplicacion secreto\n"
"\n"
"\n"
" IMPORTANTE\n"
" ──────────\n"
"Si eliminas el archivo .env local sin eliminar primero los\n"
"volúmenes y secretos, Abra no podrá ayudarte a eliminarlos\n"
"después."
"Para borrar todo sin confirmación, usa la opción \"--forzar/-f\" \n"
"o la opción \"--no-input/-n\"."
#. translators: Short description for `app volume remove` command
#: cli/app/volume.go:90
@@ -2086,33 +1998,7 @@ msgid ""
" {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}\n"
"\n"
"Use \"{{.CommandPath}} [command] --help\" for more information about a command.{{end}}\n"
msgstr ""
"COMANDO ABREVIADO\n"
"─────────────────\n"
" {{range .Aliases}}{{.}}{{end}}\n"
"\n"
"{{if .HasExample}}\n"
"{{.Example}}{{end}}{{if .HasAvailableSubCommands}}\n"
"\n"
"Comandos disponibles:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name \"help\"))}}\n"
" {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasHelpSubCommands}}\n"
"\n"
"Temas de ayuda adicionales:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}\n"
" {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}\n"
"\n"
" SINTAXIS\n"
" ────────{{if .Runnable}}\n"
" {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}\n"
" {{.CommandPath}} [command]{{end}}\n"
"\n"
"Opciones:{{if .HasAvailableLocalFlags}}\n"
"{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\n"
"\n"
"Opciones globales:{{if .HasAvailableInheritedFlags}}\n"
"{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\n"
"\n"
"{{if .HasAvailableSubCommands}}\n"
"Use \"{{.CommandPath}} [comando] --help\" para más información sobre un comando.{{end}}\n"
msgstr "Sintaxis del comando:{{if .Runnable}}\n {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}\n {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}\n\nComando y comando abreviado:\n {{.NameAndAliases}}{{end}}{{if .HasExample}}\n\nUSO DE\"OPCIONES\" y \"FLAGS\":\nImportante: 1312.net es únicamente un ejemplo utilizado para mostrar cómo debe configurarse este campo. No es necesario utilizar este nombre.\nAl ejecutar el comando, reemplaza 1312.net por el nombre o dominio real de tu aplicación.\n\n{{.Example}}{{end}}{{if .HasAvailableSubCommands}}\n\nComandos disponibles:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name \"help\"))}}\n {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\n\nOpciones:\n{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}\n\nOpciones globales:\n{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}\n\nTemas de ayuda adicionales:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}\n {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}\n\nUse \"{{.CommandPath}} [command] --help\" para más información sobre un comando.{{end}}\n"
#: cli/recipe/fetch.go:28
msgid "Using \"--force/-f\" Git syncs an existing recipe. It does not erase unstaged changes."
@@ -2410,7 +2296,7 @@ msgstr "¿A la aplicación le falta la etiqueta de imagen?"
msgid "are you sure?"
msgstr "¿Estás segurx?"
#: pkg/recipe/git.go:163
#: pkg/recipe/git.go:162
#, c-format
msgid "attempting to checkout '%s' as chaos commit"
msgstr "Intentando extraer '%s' como el commit de caos"
@@ -2553,7 +2439,7 @@ msgstr "no se pueden leer las recetas locales: %s"
msgid "cancelled"
msgstr "cancelado"
#: pkg/catalogue/catalogue.go:59 pkg/recipe/git.go:252
#: pkg/catalogue/catalogue.go:59 pkg/recipe/git.go:251
#, c-format
msgid "cannot ensure %s is up-to-date, no git remotes configured"
msgstr "no se puede asegurar que %s esté actualizado, no hay remotos de git configurados"
@@ -2568,7 +2454,7 @@ msgstr "no se puede encontrar la aplicación con el nombre %s"
msgid "cannot get label %s for %s"
msgstr "no se puede obtener la etiqueta %s para %s"
#: pkg/recipe/git.go:59
#: pkg/recipe/git.go:58
#, c-format
msgid "cannot redeploy previous chaos version (%s), did you mean to use \"--chaos\"?"
msgstr "no se puede redeplegar la versión anterior de caos (%s), ¿Era tu intención usar \"--caos\"?"
@@ -2728,7 +2614,7 @@ msgstr "tarea: Actualizar tags de una imagen"
msgid "cmd"
msgstr ""
#: pkg/recipe/git.go:476
#: pkg/recipe/git.go:470
#, c-format
msgid "collected %s for %s"
msgstr "colectado %s para %s"
@@ -3067,7 +2953,7 @@ msgstr "tiempo de espera agotado 🟠"
msgid "destination directory does not exist"
msgstr "el directorio de destino no existe"
#: pkg/recipe/git.go:379
#: pkg/recipe/git.go:373
#, c-format
msgid "detected %s as tags for recipe %s"
msgstr "detectado(s) %s como etiquetas para la receta %s"
@@ -3264,7 +3150,7 @@ msgstr "asegúrate que \"image: ...\" esté establecido en todos los servicios"
msgid "ensure recipe: %s"
msgstr "asegura receta: %s"
#: pkg/recipe/git.go:57
#: pkg/recipe/git.go:56
#, c-format
msgid "ensuring env version %s"
msgstr "asegurando la versión del entorno (env) %s"
@@ -3366,12 +3252,12 @@ msgstr "🛑 error al agregar notas de la versión: %s"
msgid "failed to check git status of %s: %s"
msgstr "🛑 error al comprobar el estado de git de %s: %s"
#: pkg/git/branch.go:95 pkg/recipe/git.go:232
#: pkg/git/branch.go:95 pkg/recipe/git.go:231
#, c-format
msgid "failed to check out %s in %s"
msgstr "🛑 error al cambiar a %s en %s"
#: pkg/recipe/git.go:418
#: pkg/recipe/git.go:412
#, c-format
msgid "failed to check out %s in %s: %s"
msgstr "🛑 error al cambiar a %s en %s: %s"
@@ -3421,7 +3307,7 @@ msgstr "🛑 error al crear el volumen %s en %s: %s"
msgid "failed to generate random bytes: %w"
msgstr "🛑 error al generar bytes aleatorios: %w"
#: pkg/recipe/git.go:427
#: pkg/recipe/git.go:421
#, c-format
msgid "failed to get compose config for %s: %s"
msgstr "🛑 error al obtener la configuración para %s: %s"
@@ -3459,7 +3345,7 @@ msgstr "🛑 error al migrar la configuración de la aplicación: %s"
msgid "failed to parse image %s, saw: %s"
msgstr "🛑 error al analizar la imagen %s; detectado: %s"
#: pkg/recipe/git.go:437
#: pkg/recipe/git.go:431
#, c-format
msgid "failed to parse image for %s in %s: %s"
msgstr "🛑 error al analizar la imagen %s; en: %s: %s"
@@ -3567,7 +3453,7 @@ msgstr "obtener [receta | --todos] [opciones]"
msgid "fetch all recipes"
msgstr "Obtener todas las recetas"
#: pkg/catalogue/catalogue.go:84 pkg/recipe/git.go:290
#: pkg/catalogue/catalogue.go:84 pkg/recipe/git.go:284
#, c-format
msgid "fetched latest git changes for %s"
msgstr "Se obtuvieron los últimos cambios de Git para %s"
@@ -3687,12 +3573,12 @@ msgstr "cambios de git confirmados"
msgid "git changes pushed"
msgstr "cambios de git enviados (push)"
#: pkg/recipe/git.go:423
#: pkg/recipe/git.go:417
#, c-format
msgid "git checkout: %s in %s"
msgstr ""
#: pkg/git/clone.go:63 pkg/git/clone.go:100
#: pkg/git/clone.go:64 pkg/git/clone.go:102
#, c-format
msgid "git clone %s: cancelled due to interrupt"
msgstr "git clone %s: cancelado por interrupción"
@@ -3702,17 +3588,17 @@ msgstr "git clone %s: cancelado por interrupción"
msgid "git clone: %s"
msgstr ""
#: pkg/git/clone.go:87
#: pkg/git/clone.go:89
#, c-format
msgid "git clone: %s already exists"
msgstr "git clone: %s ya existe"
#: pkg/git/clone.go:58 pkg/git/clone.go:76 pkg/git/clone.go:85
#: pkg/git/clone.go:59 pkg/git/clone.go:78 pkg/git/clone.go:87
#, c-format
msgid "git clone: %s cloned successfully"
msgstr "git clone: %s clonado correctamente"
#: pkg/git/clone.go:67
#: pkg/git/clone.go:68
msgid "git clone: main branch failed, attempting master branch"
msgstr "git clone: error en la rama principal; intentando la rama master"
@@ -3767,7 +3653,7 @@ msgstr "git-nombre"
msgid "git.coopcloud.tech repo exists"
msgstr "el repositorio git.coopcloud.tech existe"
#: pkg/recipe/git.go:390
#: pkg/recipe/git.go:384
#, c-format
msgid "git: opening repository in %s"
msgstr "git: abriendo repositorio en %s"
@@ -4705,7 +4591,7 @@ msgstr "mostrar salida legible por máquina"
msgid "proceed?"
msgstr "¿procedemos?"
#: pkg/recipe/git.go:410
#: pkg/recipe/git.go:404
#, c-format
msgid "processing %s for %s"
msgstr "procesando %s para %s"
@@ -4775,7 +4661,7 @@ msgstr ""
msgid "re"
msgstr ""
#: pkg/recipe/git.go:158
#: pkg/recipe/git.go:157
#, c-format
msgid "read %s as tags for recipe %s"
msgstr "leer %s como etiquetas para la receta %s"
@@ -5521,17 +5407,17 @@ msgstr "omitiendo generación de %s (generate=false)"
msgid "skipping secret (because it already exists) on %s: %s"
msgstr "omitiendo secreto (porque ya existe) en %s: %s"
#: pkg/recipe/git.go:419
#: pkg/recipe/git.go:413
#, c-format
msgid "skipping tag %s: checkout failed: %s"
msgstr "omitiendo la etiqueta %s: error en el checkout: %s"
#: pkg/recipe/git.go:428
#: pkg/recipe/git.go:422
#, c-format
msgid "skipping tag %s: invalid compose config: %s"
msgstr "omitiendo la etiqueta %s: configuración de compose inválida: %s"
#: pkg/recipe/git.go:438
#: pkg/recipe/git.go:432
#, c-format
msgid "skipping tag %s: invalid image reference in service %s: %s"
msgstr "omitiendo la etiqueta %s: referencia de imagen inválida en el servicio %s: %s"
@@ -5634,7 +5520,7 @@ msgstr "recortado %s a %s para el análisis"
msgid "succeeded"
msgstr "exitoso"
#: pkg/recipe/git.go:185
#: pkg/recipe/git.go:184
#, c-format
msgid "successfully checked %s out to %s in %s"
msgstr "se comprobó correctamente %s de %s en %s"
@@ -5796,17 +5682,17 @@ msgstr ""
msgid "un"
msgstr ""
#: pkg/recipe/git.go:194
#: pkg/recipe/git.go:193
#, c-format
msgid "unable to check git clean status in %s: %s"
msgstr "no se pudo comprobar el estado de git clean en %s: %s"
#: pkg/recipe/git.go:263
#: pkg/recipe/git.go:262
#, c-format
msgid "unable to check out default branch in %s: %s"
msgstr "no se pudo cambiar a la rama predeterminada en %s: %s"
#: pkg/git/clone.go:98
#: pkg/git/clone.go:100
#, c-format
msgid "unable to clean up git clone of %s: %s"
msgstr "no se pudo limpiar la clonación de Git de %s: %s"
@@ -5880,7 +5766,7 @@ msgstr "no se pudieron determinar las reglas de versionado de %s; listando todas
msgid "unable to discover SSH remote for %s"
msgstr "no se pudo detectar el remoto SSH para %s"
#: pkg/recipe/git.go:274
#: pkg/recipe/git.go:268
#, c-format
msgid "unable to fetch tags in %s: %s"
msgstr "no se pudieron obtener las etiquetas en %s: %s"
@@ -5890,7 +5776,7 @@ msgstr "no se pudieron obtener las etiquetas en %s: %s"
msgid "unable to get container matching %s: %s"
msgstr "no se pudo obtener el contenedor que coincide con %s: %s"
#: pkg/recipe/git.go:286
#: pkg/recipe/git.go:280
#, c-format
msgid "unable to git pull in %s: %s"
msgstr "no se pudo hacer git pull en %s: %s"
@@ -5920,12 +5806,12 @@ msgid "unable to look up server context for %s: %s"
msgstr "no se pudo obtener el contexto del servidor para %s: %s"
#: cli/recipe/fetch.go:77 pkg/git/read.go:26 pkg/lint/recipe.go:491
#: pkg/recipe/git.go:243
#: pkg/recipe/git.go:242
#, c-format
msgid "unable to open %s: %s"
msgstr "no se pudo abrir %s: %s"
#: pkg/recipe/git.go:258
#: pkg/recipe/git.go:257
#, c-format
msgid "unable to open git work tree in %s: %s"
msgstr "no se pudo abrir el git work tree en %s: %s"
@@ -5985,7 +5871,7 @@ msgstr "no se pudo consultar el estado de %s: %s"
msgid "unable to read new env %s: %s"
msgstr "no se pudo leer el nuevo env %s: %s"
#: pkg/recipe/git.go:248
#: pkg/recipe/git.go:247
#, c-format
msgid "unable to read remotes in %s: %s"
msgstr "no se pudieron leer los remotos en %s: %s"
@@ -6021,7 +5907,7 @@ msgstr "no se pudo renderizar a JSON: %s"
msgid "unable to reset commit after failed release attempt: %s"
msgstr "no se pudo establecer los streams de IO como terminal raw: %s"
#: pkg/recipe/git.go:167
#: pkg/recipe/git.go:166
#, c-format
msgid "unable to resolve '%s': %s"
msgstr "no se pudo resolver '%s': %s"
+67 -1
View File
@@ -122,6 +122,7 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
log.Debug(i18n.G("considering %s config(s) for tag update", strings.Join(composeFiles, ", ")))
updated := false
for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}}
@@ -166,6 +167,10 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
old := fmt.Sprintf("%s:%s", composeImage, composeTag)
new := fmt.Sprintf("%s:%s", composeImage, tag)
if old == new {
continue
}
replacedBytes := strings.Replace(string(bytes), old, new, -1)
log.Debug(i18n.G("updating %s to %s in %s", old, new, compose.Filename))
@@ -173,11 +178,72 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
return false, err
}
updated = true
}
}
}
return false, nil
return updated, nil
}
// UpdatePinnedTag updates an image reference to a pinned version (image:tag@sha256:...) in-place on local compose files.
func (r Recipe) UpdatePinnedTag(image, tag, digest string) (bool, error) {
glob := fmt.Sprintf("%s/compose**yml", r.Dir)
image = formatter.StripTagMeta(image)
composeFiles, err := filepath.Glob(glob)
if err != nil {
return false, err
}
updated := false
for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}}
sampleEnv, err := r.SampleEnv()
if err != nil {
return false, err
}
compose, err := loader.LoadComposefile(opts, sampleEnv)
if err != nil {
return false, err
}
for _, service := range compose.Services {
if service.Image == "" {
continue
}
img, _ := reference.ParseNormalizedNamed(formatter.StripDigest(service.Image))
composeImage := formatter.StripTagMeta(reference.Path(img))
if image == composeImage {
bytes, err := ioutil.ReadFile(composeFile)
if err != nil {
return false, err
}
old := service.Image
new := fmt.Sprintf("%s:%s@%s", composeImage, tag, digest)
if old == new {
continue
}
replacedBytes := strings.Replace(string(bytes), old, new, -1)
log.Debug(i18n.G("updating pinned reference %s to %s in %s", old, new, compose.Filename))
if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
return false, err
}
updated = true
}
}
}
return updated, nil
}
// UpdateLabel updates a label in-place on file system local compose files.
+1 -7
View File
@@ -16,7 +16,6 @@ import (
"coopcloud.tech/tagcmp"
"github.com/distribution/reference"
"github.com/go-git/go-git/v5"
gitCfg "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
)
@@ -263,12 +262,7 @@ func (r Recipe) EnsureUpToDate() error {
return errors.New(i18n.G("unable to check out default branch in %s: %s", r.Dir, err))
}
// the refspec is passed explicitly, because a repository cloned by an older abra has a
// single-branch refspec stored in its config and would otherwise never see other branches
fetchOpts := &git.FetchOptions{
Tags: git.AllTags,
RefSpecs: []gitCfg.RefSpec{"+refs/heads/*:refs/remotes/origin/*"},
}
fetchOpts := &git.FetchOptions{Tags: git.AllTags}
if err := repo.Fetch(fetchOpts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return errors.New(i18n.G("unable to fetch tags in %s: %s", r.Dir, err))
+1 -1
View File
@@ -3,7 +3,7 @@ version: "3.8"
services:
app:
image: nginx:1.31.6
image: nginx:1.31.4
secrets:
- test_pass_one
- test_pass_two
-33
View File
@@ -122,39 +122,6 @@ teardown(){
assert_failure
}
@test "create new app with commit from another branch" {
branchHash=$(_get_other_branch_hash)
if [[ -z "$branchHash" ]]; then
skip "$TEST_RECIPE has no branch besides main"
fi
# re-clone the recipe the way an older abra did, with a single-branch refspec. --no-local
# forces a real transfer, a local clone would hardlink the whole object database and leave
# the commit reachable
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE"
assert_success
run git clone -q --no-local --single-branch --branch main \
"$ABRA_DIR/origin-recipes/$TEST_RECIPE.git" "$ABRA_DIR/recipes/$TEST_RECIPE"
assert_success
# the commit has to be genuinely missing, otherwise this test passes for the wrong reason
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --verify "$branchHash^{commit}"
assert_failure
run $ABRA app new "$TEST_RECIPE" "$branchHash" \
--no-input \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN"
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
# the recipe names itself in TYPE, which differs per branch, so only the version is checked
run grep -q "TYPE=.*:${branchHash}$" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "does not overwrite existing env files" {
run $ABRA app new "$TEST_RECIPE" \
--no-input \
-7
View File
@@ -56,13 +56,6 @@ _get_tag_hash() {
echo $(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-list -n 1 "$1")
}
_get_other_branch_hash() {
# asked from the origin mirror, not from the recipe checkout, so that the result does not
# depend on what has been fetched. empty when the recipe only has a default branch
echo $(git ls-remote "$ABRA_DIR/origin-recipes/$TEST_RECIPE.git" \
| grep 'refs/heads/' | grep -v 'refs/heads/main$' | head -1 | cut -f1)
}
_get_head_hash() {
echo $(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" show -s --format="%H" HEAD)
}
+1 -1
View File
@@ -3,7 +3,7 @@ version: "3.8"
services:
app:
image: nginx:1.31.6
image: nginx:1.31.4
networks:
- proxy
deploy:
-4
View File
@@ -329,10 +329,6 @@ func (c *Config) unmarshalCore() {
if parsed := parseConfigBool(s.Options.Get(protectHFSKey)); parsed.IsSet() {
c.Core.ProtectHFS = parsed
}
if s.Options.Get(repositoryFormatVersionKey) == string(format.Version_1) {
c.Core.RepositoryFormatVersion = format.Version_1
}
}
func (c *Config) unmarshalUser() {
@@ -139,12 +139,8 @@ func (dw *deltaSelector) fixAndBreakChains(objectsToPack []*ObjectToPack) error
m[otp.Hash()] = otp
}
// visiting holds the objects on the current resolution path, so that a
// delta chain looping back on itself can be detected and broken.
visiting := make(map[plumbing.Hash]bool)
for _, otp := range objectsToPack {
if err := dw.fixAndBreakChainsOne(m, otp, visiting); err != nil {
if err := dw.fixAndBreakChainsOne(m, otp); err != nil {
return err
}
}
@@ -152,11 +148,7 @@ func (dw *deltaSelector) fixAndBreakChains(objectsToPack []*ObjectToPack) error
return nil
}
func (dw *deltaSelector) fixAndBreakChainsOne(
objectsToPack map[plumbing.Hash]*ObjectToPack,
otp *ObjectToPack,
visiting map[plumbing.Hash]bool,
) error {
func (dw *deltaSelector) fixAndBreakChainsOne(objectsToPack map[plumbing.Hash]*ObjectToPack, otp *ObjectToPack) error {
if !otp.Object.Type().IsDelta() {
return nil
}
@@ -182,21 +174,7 @@ func (dw *deltaSelector) fixAndBreakChainsOne(
return dw.undeltify(otp)
}
// Mark this object as being resolved before looking at its base, so that
// a delta based on itself is caught by the check below.
h := otp.Hash()
visiting[h] = true
defer delete(visiting, h)
// A delta chain that loops back onto an object we are already resolving
// cannot be written: every delta needs its base written first. Break the
// chain here instead of following the cycle, which would recurse until
// the goroutine stack is exhausted.
if visiting[do.BaseHash()] {
return dw.undeltify(otp)
}
if err := dw.fixAndBreakChainsOne(objectsToPack, base, visiting); err != nil {
if err := dw.fixAndBreakChainsOne(objectsToPack, base); err != nil {
return err
}
+2 -34
View File
@@ -474,14 +474,6 @@ type TreeWalker struct {
recursive bool
seen map[plumbing.Hash]bool
// skipPathValidation disables the pathutil.ValidTreePath check in Next.
// It is set by inspection-only callers (e.g. the revlist object walk)
// that never funnel entry names into the filesystem and must enumerate
// trees faithfully, including entries with names upstream Git accepts
// but that are unsafe to materialise (control characters, `.git`-shaped
// names).
skipPathValidation bool
s storer.EncodedObjectStorer
t *Tree
}
@@ -504,32 +496,10 @@ func NewTreeWalker(t *Tree, recursive bool, seen map[plumbing.Hash]bool) *TreeWa
}
}
// SkipPathValidation disables the pathutil.ValidTreePath check performed by
// Next, and must be called before the first Next.
//
// It is for inspection-only walks that never funnel an entry name into the
// filesystem — enumerating which objects exist, rather than materialising
// them — and that must therefore see the tree faithfully, including entries
// whose names upstream Git accepts but that are unsafe to check out. Callers
// that hand the returned name to filesystem or archive output must not use
// it; path safety for those is enforced at the materialisation boundaries
// (FindEntry, TreeEntryFile, archive, FileIter).
func (w *TreeWalker) SkipPathValidation() {
w.skipPathValidation = true
}
// Next returns the next object from the tree. Objects are returned in order
// and subtrees are included. After the last object has been returned further
// calls to Next() will return io.EOF.
//
// Each entry's name is validated against pathutil.ValidTreePath as it
// surfaces, so callers that funnel the returned name into filesystem
// or archive output can trust it is free of `.git`-shaped components,
// HFS+/NTFS variants, Windows reserved names, and traversal sequences.
// A malformed entry stops the walk with the validator's error;
// inspection-only callers that need to enumerate raw, unvalidated
// names can read Tree.Entries directly or call SkipPathValidation.
//
// In the current implementation any objects which cannot be found in the
// underlying repository will be skipped automatically. It is possible that this
// may change in future versions.
@@ -566,10 +536,8 @@ func (w *TreeWalker) Next() (name string, entry TreeEntry, err error) {
continue
}
if !w.skipPathValidation {
if err := pathutil.ValidTreePath(entry.Name); err != nil {
return name, entry, err
}
if err := pathutil.ValidTreePath(entry.Name); err != nil {
return name, entry, err
}
if entry.Mode == filemode.Dir {
-6
View File
@@ -106,12 +106,6 @@ func transformChildren(t *Tree) ([]noder.Noder, error) {
ret := make([]noder.Noder, 0, len(t.Entries))
walker := NewTreeWalker(t, false, nil) // don't recurse
// The diff walk is read-only and never materialises entry names into the
// filesystem, so it must enumerate the tree faithfully — including entries
// with names that are unsafe to check out but valid per upstream Git (e.g.
// control characters). Path safety is enforced at materialisation
// boundaries (FindEntry, TreeEntryFile, archive, FileIter), not here.
walker.SkipPathValidation()
// don't defer walker.Close() for efficiency reasons.
for {
_, e, err = walker.Next()
-7
View File
@@ -187,13 +187,6 @@ func iterateCommitTrees(
cb(tree.Hash)
treeWalker := object.NewTreeWalker(tree, true, seen)
// This walk only enumerates which objects are reachable, to decide what
// to send; it never materialises an entry name into the filesystem. It
// must therefore enumerate the tree faithfully, including entries with
// names that are unsafe to check out but valid per upstream Git (e.g.
// control characters). Path safety is enforced at materialisation
// boundaries (FindEntry, TreeEntryFile, archive, FileIter), not here.
treeWalker.SkipPathValidation()
for {
_, e, err := treeWalker.Next()
+22 -379
View File
@@ -6,12 +6,8 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"reflect"
"strconv"
@@ -77,7 +73,7 @@ func advertisedReferences(ctx context.Context, s *session, serviceName string) (
s.endpoint.String(), infoRefsPath, serviceName,
)
req, err := newRequest(http.MethodGet, url, nil)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
@@ -181,24 +177,6 @@ func NewClient(c *http.Client) transport.Transport {
// and other custom options specific to the client.
// If the net/http client is nil or empty, it will use a net/http client configured
// with http.DefaultTransport.
//
// Credentials this client adds where the transport cannot see them are not
// subject to the redirect stripping described on AuthMethod: a RoundTripper
// injects after the hop is decided, and Client.Jar is consulted after
// CheckRedirect, so a domain cookie still follows a redirect to a subdomain
// the transport counts as another origin. Apply them in an AuthMethod instead
// if that is not wanted. A CheckRedirect hook set on this client runs
// alongside the transport's own, but any header it adds when a redirect
// leaves the repository's origin is discarded the same way.
//
// A RoundTripper is therefore also how to authenticate to a new origin a
// redirect has moved the repository to: match on the request URL and inject
// the credential only for that origin, so it is not sent anywhere else. The
// transport keeps its own CheckRedirect on the copy it makes of this client,
// so the policy and the origin checks still apply.
//
// None of this applies to a RoundTripper that follows redirects itself:
// CheckRedirect is not consulted then, so no stripping happens at all.
func NewClientWithOptions(c *http.Client, opts *ClientOptions) transport.Transport {
if c == nil {
c = &http.Client{
@@ -392,20 +370,11 @@ func (s *session) ModifyEndpointIfRedirect(res *http.Response) error {
if !strings.HasSuffix(r.URL.Path, infoRefsPath) {
return fmt.Errorf("http redirect: target %q does not end with %s", r.URL.Path, infoRefsPath)
}
// A scheme is case-insensitive per RFC 3986, and checkRedirect folds case
// when it reads the same hop, so fold here too rather than reject a
// spelling that check let through. url.Parse and transport.NewEndpoint
// both lowercase what they parse, so only a hand-built URL or Endpoint
// arrives uppercased. The folded form is what gets stored below, so every
// later request built from the endpoint carries the canonical spelling.
scheme := strings.ToLower(r.URL.Scheme)
if scheme != "http" && scheme != "https" {
if r.URL.Scheme != "http" && r.URL.Scheme != "https" {
return fmt.Errorf("http redirect: unsupported scheme %q", r.URL.Scheme)
}
// schemeUpgrade rather than an inline comparison, so the one cross-scheme
// change go-git permits has a single definition shared with
// credentialsMayFollow.
if !strings.EqualFold(scheme, s.endpoint.Protocol) && !schemeUpgrade(s.endpoint.Protocol, scheme) {
if r.URL.Scheme != s.endpoint.Protocol &&
!(s.endpoint.Protocol == "http" && r.URL.Scheme == "https") {
return fmt.Errorf("http redirect: changes scheme from %q to %q", s.endpoint.Protocol, r.URL.Scheme)
}
@@ -415,20 +384,7 @@ func (s *session) ModifyEndpointIfRedirect(res *http.Response) error {
return err
}
// The session stores the endpoint and re-applies its credentials on every
// later request, so clear them once the redirect has left the origin they
// were issued for. This uses the same predicate as stripCredentials, so
// both halves share one definition of an origin.
//
// The two are deliberately asymmetric in one respect: stripCredentials is
// sticky over the whole chain, so an origin -> evil -> origin redirect
// leaves the discovery GET's later hops unauthenticated even though the
// chain returned home. This compares the endpoint only against the final
// URL, so the same round trip leaves the session authenticated. That is
// not a leak - the final URL's origin is the original one - but it means
// such a chain can make the discovery GET anonymous while the session's
// POSTs are authenticated, which can surface as a confusing 401.
if !credentialsMayFollow(endpointURL(s.endpoint), r.URL) {
if host != s.endpoint.Host || effectivePort(r.URL.Scheme, port) != effectivePort(s.endpoint.Protocol, s.endpoint.Port) {
s.endpoint.User = ""
s.endpoint.Password = ""
s.auth = nil
@@ -437,7 +393,7 @@ func (s *session) ModifyEndpointIfRedirect(res *http.Response) error {
s.endpoint.Host = host
s.endpoint.Port = port
s.endpoint.Protocol = scheme
s.endpoint.Protocol = r.URL.Scheme
s.endpoint.Path = r.URL.Path[:len(r.URL.Path)-len(infoRefsPath)]
return nil
}
@@ -463,132 +419,19 @@ func endpointPort(port string) (int, error) {
return parsed, nil
}
// schemeUpgrade reports whether the scheme transition from one URL to another
// is the one cross-scheme change go-git permits: a plain-http origin upgrading
// to https. It strictly improves confidentiality and is how servers steer
// clients off cleartext.
//
// Permitting it at all is a deliberate deviation: curl, git and the Fetch
// standard all count scheme as part of host identity and drop credentials on
// the upgrade. Auth is sent pre-emptively here, so an http origin has already
// spent its credential in cleartext on the first request and refusing the
// upgrade would break the clone without unspending it. The host is unchanged,
// where an on-path attacker needs a valid certificate to receive anything.
func schemeUpgrade(from, to string) bool {
return strings.EqualFold(from, "http") && strings.EqualFold(to, "https")
}
func effectivePort(scheme string, port int) int {
if port != 0 {
return port
}
// canonicalHost returns u's hostname in the form origins are compared in.
//
// An address literal is normalised by netip, so the many spellings of one
// address are one origin. Two literals are the same origin exactly when netip
// parses them to the same Addr, which is also how the WHATWG URL Standard
// compares hosts. An IPv4-mapped literal is deliberately not unmapped onto
// the IPv4 it dials: reaching the same endpoint is not the same authority,
// since net/http sends the literal as written in Host and a server may route
// the two spellings to different virtual hosts.
//
// netip also keeps a scope zone verbatim, which is what origin comparison
// needs: net resolves a zone to an interface by exact name, so folding %eth0
// onto %ETH0 would call two hosts the same origin that net dials down
// different interfaces.
//
// A registered name is ASCII-lowercased. That fold is the only liberty taken;
// every other difference in spelling is a different origin.
//
// A trailing root dot is one such difference and is kept, for the same reason
// as the IPv4-mapped literal: curl and the WHATWG URL Standard both hold
// "example.com." and "example.com" to be distinct hosts, and although
// crypto/tls and crypto/x509 fold the dot when they authenticate the peer,
// net/http sends the name as written in Host.
//
// The fold is deliberately ASCII-only. strings.ToLower and strings.EqualFold
// apply Unicode case mapping, which folds U+03C2 onto U+03C3 and so would
// call two hosts the same origin when they resolve to different servers. An
// ASCII-only fold cannot merge two names DNS keeps apart.
//
// No IDNA mapping is applied either, so a unicode hostname is a different
// origin from the punycode encoding of it, and from another Unicode case of
// itself, even though all three reach the same server. Mapping through
// golang.org/x/net/idna would join them, but it can only widen this equality,
// never narrow it, so leaving it out can cost a credential across such a
// redirect and cannot forward one. Against that cost, go-git pins x/net while
// net/http uses the copy vendored into the toolchain: the two are versioned
// separately, so a release that moves the Unicode tables under one and not
// the other would have this merge origins net/http still dials apart. That is
// the failure this comparison exists to prevent, and comparing bytes has no
// such mode.
func canonicalHost(u *url.URL) string {
host := u.Hostname()
if addr, err := netip.ParseAddr(host); err == nil {
return addr.String()
switch strings.ToLower(scheme) {
case "http":
return 80
case "https":
return 443
default:
return 0
}
b := []byte(host)
for i := range b {
if b[i] >= 'A' && b[i] <= 'Z' {
b[i] += 'a' - 'A'
}
}
return string(b)
}
// effectivePort returns u's port as the connection will use it: the scheme's
// well-known port when the URL does not spell one out, and without leading
// zeroes, so "https://x", "https://x:443" and "https://x:0443" all agree.
func effectivePort(u *url.URL) string {
port := u.Port()
if port == "" {
switch strings.ToLower(u.Scheme) {
case "http":
return "80"
case "https":
return "443"
default:
return ""
}
}
if trimmed := strings.TrimLeft(port, "0"); trimmed != "" {
return trimmed
}
return "0"
}
// credentialsMayFollow reports whether credentials issued for one URL may be
// sent to another.
//
// The relation is deliberately asymmetric: scheme, host and effective port
// must all match, except that a plain http origin may upgrade to https on the
// same host (see schemeUpgrade). That exception is confined to the two
// default ports: 80 to 443 is the upgrade servers actually steer clients
// through, whereas a non-default port carries no such convention, so
// http://host:8080 to https://host:8443 is a move to another origin like any
// other port change.
//
// Host matching is exact. Unlike Go's http.Client, which forwards credentials
// from a host to any subdomain of it, a subdomain is a different origin here —
// matching canonical git and libcurl.
func credentialsMayFollow(from, to *url.URL) bool {
if canonicalHost(from) != canonicalHost(to) {
return false
}
if strings.EqualFold(from.Scheme, to.Scheme) {
return effectivePort(from) == effectivePort(to)
}
return schemeUpgrade(from.Scheme, to.Scheme) &&
effectivePort(from) == "80" && effectivePort(to) == "443"
}
// endpointURL renders an Endpoint's origin as a URL, so that the session's
// credential clearing and the per-hop stripping share one definition of an
// origin and cannot drift apart.
func endpointURL(ep *transport.Endpoint) *url.URL {
host := strings.Trim(ep.Host, "[]")
if ep.Port != 0 {
host = net.JoinHostPort(host, strconv.Itoa(ep.Port))
} else if strings.Contains(host, ":") {
host = "[" + host + "]"
}
return &url.URL{Scheme: ep.Protocol, Host: host}
}
func (c *client) cloneHTTPClient(transport http.RoundTripper) *http.Client {
@@ -605,215 +448,26 @@ func wrapCheckRedirect(policy RedirectPolicy, next func(*http.Request, []*http.R
if err := checkRedirect(req, via, policy); err != nil {
return err
}
// Strip before the caller's hook so it observes what will actually
// be sent, and again afterwards so a hook of the common "preserve
// my headers across redirects" shape - which copies from via[0],
// the original unsanitized request - cannot reinstate them.
// Carrying credentials across an origin boundary is deliberately
// unsupported.
stripCredentials(req, via)
if next != nil {
if err := next(req, via); err != nil {
return err
}
return next(req, via)
}
stripCredentials(req, via)
return nil
}
}
// safeHeaders lists the headers go-git sets itself, none of which can carry a
// caller credential. stripCredentials keeps only these when a redirect leaves
// the credential's origin. Adding a name here makes it forwardable across an
// origin boundary - do not add anything a caller can put a secret in.
//
// This narrows rather than eliminates the exposure: an AuthMethod that writes
// a credential into one of these names directly - for example
// Header.Set("User-Agent", "token "+secret) - still survives a cross-origin
// redirect. Such a value is also sent to the origin and to any proxy in path,
// so it should not be placed there whether or not a redirect follows.
var safeHeaders = map[string]struct{}{
"User-Agent": {},
"Host": {},
"Accept": {},
"Content-Type": {},
"Content-Length": {},
}
func filterHeaders(h http.Header) http.Header {
filtered := make(http.Header)
for key, values := range h {
if _, ok := safeHeaders[http.CanonicalHeaderKey(key)]; ok {
filtered[key] = values
}
}
return filtered
}
// stripCredentials removes credentials from req once the redirect chain has
// left the origin of the original, credential-bearing request.
//
// CheckRedirect is the only hook that runs while a redirected request's
// headers are still mutable: http.Client.Do performs the entire chain
// internally, so anything the transport does after Do returns - including
// ModifyEndpointIfRedirect - is too late for the hops themselves.
//
// Two subtleties:
//
// - net/http rebuilds every redirect request from the original request's
// headers before calling this, so a header removed at one hop reappears
// at the next. The decision is therefore recomputed per hop.
// - The decision is sticky: once the chain has left the origin, credentials
// stay gone even if a later hop returns to it. Stickiness is derived from
// via rather than stored, because this closure is shared across a
// session's requests.
//
// Stripping keeps only the headers go-git sets itself (safeHeaders). An
// allowlist is used rather than a list of credential header names because
// caller credentials arrive under names that cannot be enumerated -
// PRIVATE-TOKEN, X-Api-Key, gateway headers - which is exactly what
// net/http's fixed list of sensitive header names gets wrong. It is also
// immune to header-name canonicalisation: an AuthMethod that writes a raw map
// key is still removed.
func stripCredentials(req *http.Request, via []*http.Request) {
if len(via) == 0 {
return
}
// net/http sets a URL on every request it builds, and req.URL is non-nil
// by construction: checkRedirect dereferences req.URL.Scheme on each path
// that returns nil, so it runs first or not at all. This nil check and
// the two in crossedOrigin are defensive, against a synthetic caller.
// Each treats a URL it cannot read as an origin crossing; removing one
// panics in canonicalHost rather than leaking.
if origin := via[0].URL; origin != nil && !crossedOrigin(origin, req, via) {
return
}
req.Header = filterHeaders(req.Header)
if req.URL != nil {
req.URL.User = nil
}
}
// crossedOrigin reports whether any hop so far, including the pending one, has
// left origin.
func crossedOrigin(origin *url.URL, req *http.Request, via []*http.Request) bool {
if req.URL == nil || !credentialsMayFollow(origin, req.URL) {
return true
}
for _, prev := range via[1:] {
if prev.URL == nil || !credentialsMayFollow(origin, prev.URL) {
return true
}
}
return false
}
// redactedURL returns the string form of u with the userinfo password
// replaced, for use in error messages. (*url.URL).String() renders the
// password verbatim, and request URLs are built from the endpoint, which
// carries whatever credentials the caller put in the clone URL.
func redactedURL(u *url.URL) string {
if u == nil {
return ""
}
if u.User == nil {
return u.String()
}
if _, hasPassword := u.User.Password(); !hasPassword {
return u.String()
}
redacted := *u
redacted.User = url.UserPassword(u.User.Username(), "REDACTED")
return redacted.String()
}
// redactedRawURL is redactedURL for a string that may not parse. Request URLs
// are assembled from Endpoint.String(), which re-emits Endpoint.Path raw, so a
// path holding a stray percent produces a string url.Parse rejects. url.Parse
// reports the input verbatim and applies no redaction of its own; only
// http.Client strips a password, and only from errors it raises itself.
func redactedRawURL(raw string) string {
i := strings.Index(raw, "://")
if i < 0 {
return raw
}
authority := raw[i+3:]
if end := strings.IndexByte(authority, '/'); end >= 0 {
authority = authority[:end]
}
at := strings.LastIndexByte(authority, '@')
if at < 0 {
return raw
}
colon := strings.IndexByte(authority[:at], ':')
if colon < 0 {
// Username only, left alone, as redactedURL leaves it.
return raw
}
return raw[:i+3+colon+1] + "REDACTED" + raw[i+3+at:]
}
// newRequest wraps http.NewRequest so that a URL it cannot parse does not
// reach the caller with the endpoint's credentials still in it.
func newRequest(method, rawURL string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequest(method, rawURL, body)
if err != nil {
var uerr *url.Error
if errors.As(err, &uerr) {
uerr.URL = redactedRawURL(uerr.URL)
}
return nil, err
}
return req, nil
}
func checkRedirect(req *http.Request, via []*http.Request, policy RedirectPolicy) error {
// CheckRedirect is the only hook that runs before the next hop leaves
// the client. ModifyEndpointIfRedirect inspects the chain after
// client.Do has followed all of it, so a hop rejected there has already
// carried the request headers to its server.
//
// The wording matches the message ModifyEndpointIfRedirect produces for
// the same hop, which this check reaches first.
if len(via) != 0 {
// A hop whose scheme cannot be read cannot be shown not to have been
// https, so it is assumed to have been, and a cleartext target is
// rejected. Skipping the comparison instead would let an
// undeterminable hop turn the check off, which is the wrong default
// for a credential control; crossedOrigin fails closed the same way.
// An empty scheme is as unreadable as a nil URL, so both take the
// assumed-https default.
//
// The comparisons fold case because a scheme is case-insensitive per
// RFC 3986. net/url lowercases what it parses, so only a hand-built
// URL reaches here uppercased - the same synthetic caller the nil
// checks guard against - and for that caller "HTTPS" to "http" is
// still a downgrade.
prevScheme := "https"
if prev := via[len(via)-1]; prev.URL != nil && prev.URL.Scheme != "" {
prevScheme = prev.URL.Scheme
}
if strings.EqualFold(prevScheme, "https") && strings.EqualFold(req.URL.Scheme, "http") {
return fmt.Errorf("http redirect: changes scheme from %q to %q: %s",
prevScheme, req.URL.Scheme, redactedURL(req.URL))
}
}
switch policy {
case FollowRedirects:
case NoFollowRedirects:
return fmt.Errorf("http redirect: redirects disabled to %s", redactedURL(req.URL))
return fmt.Errorf("http redirect: redirects disabled to %s", req.URL)
case "", FollowInitialRedirects:
if !isInitialRequest(req) {
return fmt.Errorf("http redirect: redirect on non-initial request to %s", redactedURL(req.URL))
return fmt.Errorf("http redirect: redirect on non-initial request to %s", req.URL)
}
default:
return fmt.Errorf("http redirect: invalid redirect policy %q", policy)
}
// Folded for the same reason as the guard above: a scheme is
// case-insensitive per RFC 3986, so a spelling the downgrade check read
// as https must not be rejected here as a scheme go-git cannot speak.
if !strings.EqualFold(req.URL.Scheme, "http") && !strings.EqualFold(req.URL.Scheme, "https") {
if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
return fmt.Errorf("http redirect: unsupported scheme %q", req.URL.Scheme)
}
if len(via) >= 10 {
@@ -827,17 +481,6 @@ func (*session) Close() error {
}
// AuthMethod is concrete implementation of common.AuthMethod for HTTP services
//
// Headers SetAuth adds are dropped when a redirect leaves the repository's
// origin: only the headers the transport sets itself survive that boundary.
// This applies to non-credential headers too, so an implementation that adds a
// trace or tenant header loses it on such a hop.
//
// That filter matches header names, not values. An implementation that writes
// a credential into a name the transport also uses - User-Agent, Host, Accept,
// Content-Type, Content-Length - has that value carried across the boundary
// with the name. Such a credential is also sent to the origin and to any proxy
// in path, so it should not be placed there whether or not a redirect follows.
type AuthMethod interface {
transport.AuthMethod
SetAuth(r *http.Request)
@@ -955,6 +598,6 @@ func (e *Err) StatusCode() int {
func (e *Err) Error() string {
return fmt.Sprintf("unexpected requesting %q status code: %d",
redactedURL(e.Response.Request.URL), e.Response.StatusCode,
e.Response.Request.URL, e.Response.StatusCode,
)
}
@@ -88,7 +88,7 @@ func (s *rpSession) doRequest(
body = content
}
req, err := newRequest(method, url, body)
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, plumbing.NewPermanentError(err)
}
+1 -1
View File
@@ -86,7 +86,7 @@ func (s *upSession) doRequest(
body = content
}
req, err := newRequest(method, url, body)
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, plumbing.NewPermanentError(err)
}
-1
View File
@@ -433,7 +433,6 @@ func dotGitCommonDirectory(fs billy.Filesystem) (commonDir billy.Filesystem, err
if err != nil {
return nil, err
}
defer ioutil.CheckClose(f, &err)
b, err := io.ReadAll(f)
if err != nil {
+1
View File
@@ -1,2 +1,3 @@
* -text
*.bin -text -diff
*.md text eol=lf
+700 -700
View File
File diff suppressed because it is too large Load Diff
+78 -78
View File
@@ -1,79 +1,79 @@
# Finite State Entropy
This package provides Finite State Entropy encoding and decoding.
Finite State Entropy (also referenced as [tANS](https://en.wikipedia.org/wiki/Asymmetric_numeral_systems#tANS))
encoding provides a fast near-optimal symbol encoding/decoding
for byte blocks as implemented in [zstandard](https://github.com/facebook/zstd).
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/fse)
## News
* Feb 2018: First implementation released. Consider this beta software for now.
# Usage
This package provides a low level interface that allows to compress single independent blocks.
Each block is separate, and there is no built in integrity checks.
This means that the caller should keep track of block sizes and also do checksums if needed.
Compressing a block is done via the [`Compress`](https://godoc.org/github.com/klauspost/compress/fse#Compress) function.
You must provide input and will receive the output and maybe an error.
These error values can be returned:
| Error | Description |
|---------------------|-----------------------------------------------------------------------------|
| `<nil>` | Everything ok, output is returned |
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
| `(error)` | An internal error occurred. |
As can be seen above there are errors that will be returned even under normal operation so it is important to handle these.
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/fse#Scratch) object
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
object can be used for both.
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
Decompressing is done by calling the [`Decompress`](https://godoc.org/github.com/klauspost/compress/fse#Decompress) function.
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
your input was likely corrupted.
It is important to note that a successful decoding does *not* mean your output matches your original input.
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
For more detailed usage, see examples in the [godoc documentation](https://godoc.org/github.com/klauspost/compress/fse#pkg-examples).
# Performance
A lot of factors are affecting speed. Block sizes and compressibility of the material are primary factors.
All compression functions are currently only running on the calling goroutine so only one core will be used per block.
The compressor is significantly faster if symbols are kept as small as possible. The highest byte value of the input
is used to reduce some of the processing, so if all your input is above byte value 64 for instance, it may be
beneficial to transpose all your input values down by 64.
With moderate block sizes around 64k speed are typically 200MB/s per core for compression and
around 300MB/s decompression speed.
The same hardware typically does Huffman (deflate) encoding at 125MB/s and decompression at 100MB/s.
# Plans
At one point, more internals will be exposed to facilitate more "expert" usage of the components.
A streaming interface is also likely to be implemented. Likely compatible with [FSE stream format](https://github.com/Cyan4973/FiniteStateEntropy/blob/dev/programs/fileio.c#L261).
# Contributing
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
# Finite State Entropy
This package provides Finite State Entropy encoding and decoding.
Finite State Entropy (also referenced as [tANS](https://en.wikipedia.org/wiki/Asymmetric_numeral_systems#tANS))
encoding provides a fast near-optimal symbol encoding/decoding
for byte blocks as implemented in [zstandard](https://github.com/facebook/zstd).
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/fse)
## News
* Feb 2018: First implementation released. Consider this beta software for now.
# Usage
This package provides a low level interface that allows to compress single independent blocks.
Each block is separate, and there is no built in integrity checks.
This means that the caller should keep track of block sizes and also do checksums if needed.
Compressing a block is done via the [`Compress`](https://godoc.org/github.com/klauspost/compress/fse#Compress) function.
You must provide input and will receive the output and maybe an error.
These error values can be returned:
| Error | Description |
|---------------------|-----------------------------------------------------------------------------|
| `<nil>` | Everything ok, output is returned |
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
| `(error)` | An internal error occurred. |
As can be seen above there are errors that will be returned even under normal operation so it is important to handle these.
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/fse#Scratch) object
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
object can be used for both.
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
Decompressing is done by calling the [`Decompress`](https://godoc.org/github.com/klauspost/compress/fse#Decompress) function.
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
your input was likely corrupted.
It is important to note that a successful decoding does *not* mean your output matches your original input.
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
For more detailed usage, see examples in the [godoc documentation](https://godoc.org/github.com/klauspost/compress/fse#pkg-examples).
# Performance
A lot of factors are affecting speed. Block sizes and compressibility of the material are primary factors.
All compression functions are currently only running on the calling goroutine so only one core will be used per block.
The compressor is significantly faster if symbols are kept as small as possible. The highest byte value of the input
is used to reduce some of the processing, so if all your input is above byte value 64 for instance, it may be
beneficial to transpose all your input values down by 64.
With moderate block sizes around 64k speed are typically 200MB/s per core for compression and
around 300MB/s decompression speed.
The same hardware typically does Huffman (deflate) encoding at 125MB/s and decompression at 100MB/s.
# Plans
At one point, more internals will be exposed to facilitate more "expert" usage of the components.
A streaming interface is also likely to be implemented. Likely compatible with [FSE stream format](https://github.com/Cyan4973/FiniteStateEntropy/blob/dev/programs/fileio.c#L261).
# Contributing
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
changes will likely not be accepted. If in doubt open an issue before writing the PR.
+89 -89
View File
@@ -1,89 +1,89 @@
# Huff0 entropy compression
This package provides Huff0 encoding and decoding as used in zstd.
[Huff0](https://github.com/Cyan4973/FiniteStateEntropy#new-generation-entropy-coders),
a Huffman codec designed for modern CPU, featuring OoO (Out of Order) operations on multiple ALU
(Arithmetic Logic Unit), achieving extremely fast compression and decompression speeds.
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/huff0)
## News
This is used as part of the [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression and decompression package.
This ensures that most functionality is well tested.
# Usage
This package provides a low level interface that allows to compress single independent blocks.
Each block is separate, and there is no built in integrity checks.
This means that the caller should keep track of block sizes and also do checksums if needed.
Compressing a block is done via the [`Compress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress1X) and
[`Compress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress4X) functions.
You must provide input and will receive the output and maybe an error.
These error values can be returned:
| Error | Description |
|---------------------|-----------------------------------------------------------------------------|
| `<nil>` | Everything ok, output is returned |
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
| `ErrTooBig` | Returned if the input block exceeds the maximum allowed size (128 Kib) |
| `(error)` | An internal error occurred. |
As can be seen above some of there are errors that will be returned even under normal operation so it is important to handle these.
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
object can be used for both.
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
The `Scratch` object will retain state that allows to re-use previous tables for encoding and decoding.
## Tables and re-use
Huff0 allows for reusing tables from the previous block to save space if that is expected to give better/faster results.
The Scratch object allows you to set a [`ReusePolicy`](https://godoc.org/github.com/klauspost/compress/huff0#ReusePolicy)
that controls this behaviour. See the documentation for details. This can be altered between each block.
Do however note that this information is *not* stored in the output block and it is up to the users of the package to
record whether [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable) should be called,
based on the boolean reported back from the CompressXX call.
If you want to store the table separate from the data, you can access them as `OutData` and `OutTable` on the
[`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object.
## Decompressing
The first part of decoding is to initialize the decoding table through [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable).
This will initialize the decoding tables.
You can supply the complete block to `ReadTable` and it will return the data part of the block
which can be given to the decompressor.
Decompressing is done by calling the [`Decompress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress1X)
or [`Decompress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress4X) function.
For concurrently decompressing content with a fixed table a stateless [`Decoder`](https://godoc.org/github.com/klauspost/compress/huff0#Decoder) can be requested which will remain correct as long as the scratch is unchanged. The capacity of the provided slice indicates the expected output size.
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
your input was likely corrupted.
It is important to note that a successful decoding does *not* mean your output matches your original input.
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
# Contributing
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
changes will likely not be accepted. If in doubt open an issue before writing the PR.
# Huff0 entropy compression
This package provides Huff0 encoding and decoding as used in zstd.
[Huff0](https://github.com/Cyan4973/FiniteStateEntropy#new-generation-entropy-coders),
a Huffman codec designed for modern CPU, featuring OoO (Out of Order) operations on multiple ALU
(Arithmetic Logic Unit), achieving extremely fast compression and decompression speeds.
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/huff0)
## News
This is used as part of the [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression and decompression package.
This ensures that most functionality is well tested.
# Usage
This package provides a low level interface that allows to compress single independent blocks.
Each block is separate, and there is no built in integrity checks.
This means that the caller should keep track of block sizes and also do checksums if needed.
Compressing a block is done via the [`Compress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress1X) and
[`Compress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress4X) functions.
You must provide input and will receive the output and maybe an error.
These error values can be returned:
| Error | Description |
|---------------------|-----------------------------------------------------------------------------|
| `<nil>` | Everything ok, output is returned |
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
| `ErrTooBig` | Returned if the input block exceeds the maximum allowed size (128 Kib) |
| `(error)` | An internal error occurred. |
As can be seen above some of there are errors that will be returned even under normal operation so it is important to handle these.
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
object can be used for both.
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
The `Scratch` object will retain state that allows to re-use previous tables for encoding and decoding.
## Tables and re-use
Huff0 allows for reusing tables from the previous block to save space if that is expected to give better/faster results.
The Scratch object allows you to set a [`ReusePolicy`](https://godoc.org/github.com/klauspost/compress/huff0#ReusePolicy)
that controls this behaviour. See the documentation for details. This can be altered between each block.
Do however note that this information is *not* stored in the output block and it is up to the users of the package to
record whether [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable) should be called,
based on the boolean reported back from the CompressXX call.
If you want to store the table separate from the data, you can access them as `OutData` and `OutTable` on the
[`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object.
## Decompressing
The first part of decoding is to initialize the decoding table through [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable).
This will initialize the decoding tables.
You can supply the complete block to `ReadTable` and it will return the data part of the block
which can be given to the decompressor.
Decompressing is done by calling the [`Decompress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress1X)
or [`Decompress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress4X) function.
For concurrently decompressing content with a fixed table a stateless [`Decoder`](https://godoc.org/github.com/klauspost/compress/huff0#Decoder) can be requested which will remain correct as long as the scratch is unchanged. The capacity of the provided slice indicates the expected output size.
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
your input was likely corrupted.
It is important to note that a successful decoding does *not* mean your output matches your original input.
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
# Contributing
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
changes will likely not be accepted. If in doubt open an issue before writing the PR.
+24
View File
@@ -0,0 +1,24 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
+57
View File
@@ -0,0 +1,57 @@
version: 2
builds:
-
id: "cpuid"
binary: cpuid
main: ./cmd/cpuid/main.go
env:
- CGO_ENABLED=0
flags:
- -ldflags=-s -w
goos:
- aix
- linux
- freebsd
- netbsd
- windows
- darwin
goarch:
- 386
- amd64
- arm64
goarm:
- 7
archives:
-
id: cpuid
name_template: "cpuid-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
format_overrides:
- goos: windows
format: zip
files:
- LICENSE
checksum:
name_template: 'checksums.txt'
changelog:
sort: asc
filters:
exclude:
- '^doc:'
- '^docs:'
- '^test:'
- '^tests:'
- '^Update\sREADME.md'
nfpms:
-
file_name_template: "cpuid_package_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
vendor: Klaus Post
homepage: https://github.com/klauspost/cpuid
maintainer: Klaus Post <klauspost@gmail.com>
description: CPUID Tool
license: BSD 3-Clause
formats:
- deb
- rpm
+35
View File
@@ -0,0 +1,35 @@
Developer Certificate of Origin
Version 1.1
Copyright (C) 2015- Klaus Post & Contributors.
Email: klauspost@gmail.com
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
+22
View File
@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Klaus Post
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+512
View File
@@ -0,0 +1,512 @@
# cpuid
Package cpuid provides information about the CPU running the current program.
CPU features are detected on startup, and kept for fast access through the life of the application.
Currently x86 / x64 (AMD64/i386) and ARM (ARM64) is supported, and no external C (cgo) code is used, which should make the library very easy to use.
You can access the CPU information by accessing the shared CPU variable of the cpuid library.
Package home: https://github.com/klauspost/cpuid
[![PkgGoDev](https://pkg.go.dev/badge/github.com/klauspost/cpuid)](https://pkg.go.dev/github.com/klauspost/cpuid/v2)
[![Go](https://github.com/klauspost/cpuid/actions/workflows/go.yml/badge.svg)](https://github.com/klauspost/cpuid/actions/workflows/go.yml)
## installing
`go get -u github.com/klauspost/cpuid/v2` using modules.
Drop `v2` for others.
Installing binary:
`go install github.com/klauspost/cpuid/v2/cmd/cpuid@latest`
Or download binaries from release page: https://github.com/klauspost/cpuid/releases
### Homebrew
For macOS/Linux users, you can install via [brew](https://brew.sh/)
```sh
$ brew install cpuid
```
## example
```Go
package main
import (
"fmt"
"strings"
. "github.com/klauspost/cpuid/v2"
)
func main() {
// Print basic CPU information:
fmt.Println("Name:", CPU.BrandName)
fmt.Println("PhysicalCores:", CPU.PhysicalCores)
fmt.Println("ThreadsPerCore:", CPU.ThreadsPerCore)
fmt.Println("LogicalCores:", CPU.LogicalCores)
fmt.Println("Family", CPU.Family, "Model:", CPU.Model, "Vendor ID:", CPU.VendorID)
fmt.Println("Features:", strings.Join(CPU.FeatureSet(), ","))
fmt.Println("Cacheline bytes:", CPU.CacheLine)
fmt.Println("L1 Data Cache:", CPU.Cache.L1D, "bytes")
fmt.Println("L1 Instruction Cache:", CPU.Cache.L1I, "bytes")
fmt.Println("L2 Cache:", CPU.Cache.L2, "bytes")
fmt.Println("L3 Cache:", CPU.Cache.L3, "bytes")
fmt.Println("Frequency", CPU.Hz, "hz")
// Test if we have these specific features:
if CPU.Supports(SSE, SSE2) {
fmt.Println("We have Streaming SIMD 2 Extensions")
}
}
```
Sample output:
```
>go run main.go
Name: AMD Ryzen 9 3950X 16-Core Processor
PhysicalCores: 16
ThreadsPerCore: 2
LogicalCores: 32
Family 23 Model: 113 Vendor ID: AMD
Features: ADX,AESNI,AVX,AVX2,BMI1,BMI2,CLMUL,CMOV,CX16,F16C,FMA3,HTT,HYPERVISOR,LZCNT,MMX,MMXEXT,NX,POPCNT,RDRAND,RDSEED,RDTSCP,SHA,SSE,SSE2,SSE3,SSE4,SSE42,SSE4A,SSSE3
Cacheline bytes: 64
L1 Data Cache: 32768 bytes
L1 Instruction Cache: 32768 bytes
L2 Cache: 524288 bytes
L3 Cache: 16777216 bytes
Frequency 0 hz
We have Streaming SIMD 2 Extensions
```
# usage
The `cpuid.CPU` provides access to CPU features. Use `cpuid.CPU.Supports()` to check for CPU features.
A faster `cpuid.CPU.Has()` is provided which will usually be inlined by the gc compiler.
To test a larger number of features, they can be combined using `f := CombineFeatures(CMOV, CMPXCHG8, X87, FXSR, MMX, SYSCALL, SSE, SSE2)`, etc.
This can be using with `cpuid.CPU.HasAll(f)` to quickly test if all features are supported.
Note that for some cpu/os combinations some features will not be detected.
`amd64` has rather good support and should work reliably on all platforms.
Note that hypervisors may not pass through all CPU features through to the guest OS,
so even if your host supports a feature it may not be visible on guests.
## arm64 feature detection
Not all operating systems provide ARM features directly
and there is no safe way to do so for the rest.
Currently `arm64/linux` and `arm64/freebsd` should be quite reliable.
`arm64/darwin` adds features expected from the M1 processor, but a lot remains undetected.
A `DetectARM()` can be used if you are able to control your deployment,
it will detect CPU features, but may crash if the OS doesn't intercept the calls.
A `-cpu.arm` flag for detecting unsafe ARM features can be added. See below.
Note that currently only features are detected on ARM,
no additional information is currently available.
## flags
It is possible to add flags that affects cpu detection.
For this the `Flags()` command is provided.
This must be called *before* `flag.Parse()` AND after the flags have been parsed `Detect()` must be called.
This means that any detection used in `init()` functions will not contain these flags.
Example:
```Go
package main
import (
"flag"
"fmt"
"strings"
"github.com/klauspost/cpuid/v2"
)
func main() {
cpuid.Flags()
flag.Parse()
cpuid.Detect()
// Test if we have these specific features:
if cpuid.CPU.Supports(cpuid.SSE, cpuid.SSE2) {
fmt.Println("We have Streaming SIMD 2 Extensions")
}
}
```
## commandline
Download as binary from: https://github.com/klauspost/cpuid/releases
Install from source:
`go install github.com/klauspost/cpuid/v2/cmd/cpuid@latest`
### Example
```
λ cpuid
Name: AMD Ryzen 9 3950X 16-Core Processor
Vendor String: AuthenticAMD
Vendor ID: AMD
PhysicalCores: 16
Threads Per Core: 2
Logical Cores: 32
CPU Family 23 Model: 113
Features: ADX,AESNI,AVX,AVX2,BMI1,BMI2,CLMUL,CLZERO,CMOV,CMPXCHG8,CPBOOST,CX16,F16C,FMA3,FXSR,FXSROPT,HTT,HYPERVISOR,LAHF,LZCNT,MCAOVERFLOW,MMX,MMXEXT,MOVBE,NX,OSXSAVE,POPCNT,RDRAND,RDSEED,RDTSCP,SCE,SHA,SSE,SSE2,SSE3,SSE4,SSE42,SSE4A,SSSE3,SUCCOR,X87,XSAVE
Microarchitecture level: 3
Cacheline bytes: 64
L1 Instruction Cache: 32768 bytes
L1 Data Cache: 32768 bytes
L2 Cache: 524288 bytes
L3 Cache: 16777216 bytes
```
### JSON Output:
```
λ cpuid --json
{
"BrandName": "AMD Ryzen 9 3950X 16-Core Processor",
"VendorID": 2,
"VendorString": "AuthenticAMD",
"PhysicalCores": 16,
"ThreadsPerCore": 2,
"LogicalCores": 32,
"Family": 23,
"Model": 113,
"CacheLine": 64,
"Hz": 0,
"BoostFreq": 0,
"Cache": {
"L1I": 32768,
"L1D": 32768,
"L2": 524288,
"L3": 16777216
},
"SGX": {
"Available": false,
"LaunchControl": false,
"SGX1Supported": false,
"SGX2Supported": false,
"MaxEnclaveSizeNot64": 0,
"MaxEnclaveSize64": 0,
"EPCSections": null
},
"Features": [
"ADX",
"AESNI",
"AVX",
"AVX2",
"BMI1",
"BMI2",
"CLMUL",
"CLZERO",
"CMOV",
"CMPXCHG8",
"CPBOOST",
"CX16",
"F16C",
"FMA3",
"FXSR",
"FXSROPT",
"HTT",
"HYPERVISOR",
"LAHF",
"LZCNT",
"MCAOVERFLOW",
"MMX",
"MMXEXT",
"MOVBE",
"NX",
"OSXSAVE",
"POPCNT",
"RDRAND",
"RDSEED",
"RDTSCP",
"SCE",
"SHA",
"SSE",
"SSE2",
"SSE3",
"SSE4",
"SSE42",
"SSE4A",
"SSSE3",
"SUCCOR",
"X87",
"XSAVE"
],
"X64Level": 3
}
```
### Check CPU microarch level
```
λ cpuid --check-level=3
2022/03/18 17:04:40 AMD Ryzen 9 3950X 16-Core Processor
2022/03/18 17:04:40 Microarchitecture level 3 is supported. Max level is 3.
Exit Code 0
λ cpuid --check-level=4
2022/03/18 17:06:18 AMD Ryzen 9 3950X 16-Core Processor
2022/03/18 17:06:18 Microarchitecture level 4 not supported. Max level is 3.
Exit Code 1
```
## Available flags
### x86 & amd64
| Feature Flag | Description |
|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ADX | Intel ADX (Multi-Precision Add-Carry Instruction Extensions) |
| AESNI | Advanced Encryption Standard New Instructions |
| AMD3DNOW | AMD 3DNOW |
| AMD3DNOWEXT | AMD 3DNowExt |
| AMXBF16 | Tile computational operations on BFLOAT16 numbers |
| AMXINT8 | Tile computational operations on 8-bit integers |
| AMXFP16 | Tile computational operations on FP16 numbers |
| AMXFP8 | Tile computational operations on FP8 numbers |
| AMXCOMPLEX | Tile computational operations on complex numbers |
| AMXTILE | Tile architecture |
| AMXTF32 | Matrix Multiplication of TF32 Tiles into Packed Single Precision Tile |
| AMXTRANSPOSE | Tile multiply where the first operand is transposed |
| APX_F | Intel APX |
| AVX | AVX functions |
| AVX10 | If set the Intel AVX10 Converged Vector ISA is supported |
| AVX10_128 | If set indicates that AVX10 128-bit vector support is present |
| AVX10_256 | If set indicates that AVX10 256-bit vector support is present |
| AVX10_512 | If set indicates that AVX10 512-bit vector support is present |
| AVX2 | AVX2 functions |
| AVX512BF16 | AVX-512 BFLOAT16 Instructions |
| AVX512BITALG | AVX-512 Bit Algorithms |
| AVX512BW | AVX-512 Byte and Word Instructions |
| AVX512CD | AVX-512 Conflict Detection Instructions |
| AVX512DQ | AVX-512 Doubleword and Quadword Instructions |
| AVX512ER | AVX-512 Exponential and Reciprocal Instructions |
| AVX512F | AVX-512 Foundation |
| AVX512FP16 | AVX-512 FP16 Instructions |
| AVX512IFMA | AVX-512 Integer Fused Multiply-Add Instructions |
| AVX512PF | AVX-512 Prefetch Instructions |
| AVX512VBMI | AVX-512 Vector Bit Manipulation Instructions |
| AVX512VBMI2 | AVX-512 Vector Bit Manipulation Instructions, Version 2 |
| AVX512VL | AVX-512 Vector Length Extensions |
| AVX512VNNI | AVX-512 Vector Neural Network Instructions |
| AVX512VP2INTERSECT | AVX-512 Intersect for D/Q |
| AVX512VPOPCNTDQ | AVX-512 Vector Population Count Doubleword and Quadword |
| AVXIFMA | AVX-IFMA instructions |
| AVXNECONVERT | AVX-NE-CONVERT instructions |
| AVXSLOW | Indicates the CPU performs 2 128 bit operations instead of one |
| AVXVNNI | AVX (VEX encoded) VNNI neural network instructions |
| AVXVNNIINT8 | AVX-VNNI-INT8 instructions |
| AVXVNNIINT16 | AVX-VNNI-INT16 instructions |
| BHI_CTRL | Branch History Injection and Intra-mode Branch Target Injection / CVE-2022-0001, CVE-2022-0002 / INTEL-SA-00598 |
| BMI1 | Bit Manipulation Instruction Set 1 |
| BMI2 | Bit Manipulation Instruction Set 2 |
| CETIBT | Intel CET Indirect Branch Tracking |
| CETSS | Intel CET Shadow Stack |
| CLDEMOTE | Cache Line Demote |
| CLMUL | Carry-less Multiplication |
| CLZERO | CLZERO instruction supported |
| CMOV | i686 CMOV |
| CMPCCXADD | CMPCCXADD instructions |
| CMPSB_SCADBS_SHORT | Fast short CMPSB and SCASB |
| CMPXCHG8 | CMPXCHG8 instruction |
| CPBOOST | Core Performance Boost |
| CPPC | AMD: Collaborative Processor Performance Control |
| CX16 | CMPXCHG16B Instruction |
| EFER_LMSLE_UNS | AMD: =Core::X86::Msr::EFER[LMSLE] is not supported, and MBZ |
| ENQCMD | Enqueue Command |
| ERMS | Enhanced REP MOVSB/STOSB |
| F16C | Half-precision floating-point conversion |
| FLUSH_L1D | Flush L1D cache |
| FMA3 | Intel FMA 3. Does not imply AVX. |
| FMA4 | Bulldozer FMA4 functions |
| FP128 | AMD: When set, the internal FP/SIMD execution datapath is 128-bits wide |
| FP256 | AMD: When set, the internal FP/SIMD execution datapath is 256-bits wide |
| FSRM | Fast Short Rep Mov |
| FXSR | FXSAVE, FXRESTOR instructions, CR4 bit 9 |
| FXSROPT | FXSAVE/FXRSTOR optimizations |
| GFNI | Galois Field New Instructions. May require other features (AVX, AVX512VL,AVX512F) based on usage. |
| HLE | Hardware Lock Elision |
| HRESET | If set CPU supports history reset and the IA32_HRESET_ENABLE MSR |
| HTT | Hyperthreading (enabled) |
| HWA | Hardware assert supported. Indicates support for MSRC001_10 |
| HYBRID_CPU | This part has CPUs of more than one type. |
| HYPERVISOR | This bit has been reserved by Intel & AMD for use by hypervisors |
| IA32_ARCH_CAP | IA32_ARCH_CAPABILITIES MSR (Intel) |
| IA32_CORE_CAP | IA32_CORE_CAPABILITIES MSR |
| IBPB | Indirect Branch Restricted Speculation (IBRS) and Indirect Branch Predictor Barrier (IBPB) |
| IBRS | AMD: Indirect Branch Restricted Speculation |
| IBRS_PREFERRED | AMD: IBRS is preferred over software solution |
| IBRS_PROVIDES_SMP | AMD: IBRS provides Same Mode Protection |
| IBS | Instruction Based Sampling (AMD) |
| IBSBRNTRGT | Instruction Based Sampling Feature (AMD) |
| IBSFETCHSAM | Instruction Based Sampling Feature (AMD) |
| IBSFFV | Instruction Based Sampling Feature (AMD) |
| IBSOPCNT | Instruction Based Sampling Feature (AMD) |
| IBSOPCNTEXT | Instruction Based Sampling Feature (AMD) |
| IBSOPSAM | Instruction Based Sampling Feature (AMD) |
| IBSRDWROPCNT | Instruction Based Sampling Feature (AMD) |
| IBSRIPINVALIDCHK | Instruction Based Sampling Feature (AMD) |
| IBS_FETCH_CTLX | AMD: IBS fetch control extended MSR supported |
| IBS_OPDATA4 | AMD: IBS op data 4 MSR supported |
| IBS_OPFUSE | AMD: Indicates support for IbsOpFuse |
| IBS_PREVENTHOST | Disallowing IBS use by the host supported |
| IBS_ZEN4 | Fetch and Op IBS support IBS extensions added with Zen4 |
| IDPRED_CTRL | IPRED_DIS |
| INT_WBINVD | WBINVD/WBNOINVD are interruptible. |
| INVLPGB | NVLPGB and TLBSYNC instruction supported |
| KEYLOCKER | Key locker |
| KEYLOCKERW | Key locker wide |
| LAHF | LAHF/SAHF in long mode |
| LAM | If set, CPU supports Linear Address Masking |
| LBRVIRT | LBR virtualization |
| LZCNT | LZCNT instruction |
| MCAOVERFLOW | MCA overflow recovery support. |
| MCDT_NO | Processor do not exhibit MXCSR Configuration Dependent Timing behavior and do not need to mitigate it. |
| MCOMMIT | MCOMMIT instruction supported |
| MD_CLEAR | VERW clears CPU buffers |
| MMX | standard MMX |
| MMXEXT | SSE integer functions or AMD MMX ext |
| MOVBE | MOVBE instruction (big-endian) |
| MOVDIR64B | Move 64 Bytes as Direct Store |
| MOVDIRI | Move Doubleword as Direct Store |
| MOVSB_ZL | Fast Zero-Length MOVSB |
| MPX | Intel MPX (Memory Protection Extensions) |
| MOVU | MOVU SSE instructions are more efficient and should be preferred to SSE MOVL/MOVH. MOVUPS is more efficient than MOVLPS/MOVHPS. MOVUPD is more efficient than MOVLPD/MOVHPD |
| MSRIRC | Instruction Retired Counter MSR available |
| MSRLIST | Read/Write List of Model Specific Registers |
| MSR_PAGEFLUSH | Page Flush MSR available |
| NRIPS | Indicates support for NRIP save on VMEXIT |
| NX | NX (No-Execute) bit |
| OSXSAVE | XSAVE enabled by OS |
| PCONFIG | PCONFIG for Intel Multi-Key Total Memory Encryption |
| POPCNT | POPCNT instruction |
| PPIN | AMD: Protected Processor Inventory Number support. Indicates that Protected Processor Inventory Number (PPIN) capability can be enabled |
| PREFETCHI | PREFETCHIT0/1 instructions |
| PSFD | Predictive Store Forward Disable |
| RDPRU | RDPRU instruction supported |
| RDRAND | RDRAND instruction is available |
| RDSEED | RDSEED instruction is available |
| RDTSCP | RDTSCP Instruction |
| RRSBA_CTRL | Restricted RSB Alternate |
| RTM | Restricted Transactional Memory |
| RTM_ALWAYS_ABORT | Indicates that the loaded microcode is forcing RTM abort. |
| SERIALIZE | Serialize Instruction Execution |
| SEV | AMD Secure Encrypted Virtualization supported |
| SEV_64BIT | AMD SEV guest execution only allowed from a 64-bit host |
| SEV_ALTERNATIVE | AMD SEV Alternate Injection supported |
| SEV_DEBUGSWAP | Full debug state swap supported for SEV-ES guests |
| SEV_ES | AMD SEV Encrypted State supported |
| SEV_RESTRICTED | AMD SEV Restricted Injection supported |
| SEV_SNP | AMD SEV Secure Nested Paging supported |
| SGX | Software Guard Extensions |
| SGXLC | Software Guard Extensions Launch Control |
| SGXPQC | Software Guard Extensions 256-bit Encryption |
| SHA | Intel SHA Extensions |
| SME | AMD Secure Memory Encryption supported |
| SME_COHERENT | AMD Hardware cache coherency across encryption domains enforced |
| SM3_X86 | SM3 instructions |
| SM4_X86 | SM4 instructions |
| SPEC_CTRL_SSBD | Speculative Store Bypass Disable |
| SRBDS_CTRL | SRBDS mitigation MSR available |
| SSE | SSE functions |
| SSE2 | P4 SSE functions |
| SSE3 | Prescott SSE3 functions |
| SSE4 | Penryn SSE4.1 functions |
| SSE42 | Nehalem SSE4.2 functions |
| SSE4A | AMD Barcelona microarchitecture SSE4a instructions |
| SSSE3 | Conroe SSSE3 functions |
| STIBP | Single Thread Indirect Branch Predictors |
| STIBP_ALWAYSON | AMD: Single Thread Indirect Branch Prediction Mode has Enhanced Performance and may be left Always On |
| STOSB_SHORT | Fast short STOSB |
| SUCCOR | Software uncorrectable error containment and recovery capability. |
| SVM | AMD Secure Virtual Machine |
| SVMDA | Indicates support for the SVM decode assists. |
| SVMFBASID | SVM, Indicates that TLB flush events, including CR3 writes and CR4.PGE toggles, flush only the current ASID's TLB entries. Also indicates support for the extended VMCBTLB_Control |
| SVML | AMD SVM lock. Indicates support for SVM-Lock. |
| SVMNP | AMD SVM nested paging |
| SVMPF | SVM pause intercept filter. Indicates support for the pause intercept filter |
| SVMPFT | SVM PAUSE filter threshold. Indicates support for the PAUSE filter cycle count threshold |
| SYSCALL | System-Call Extension (SCE): SYSCALL and SYSRET instructions. |
| SYSEE | SYSENTER and SYSEXIT instructions |
| TBM | AMD Trailing Bit Manipulation |
| TDX_GUEST | Intel Trust Domain Extensions Guest |
| TLB_FLUSH_NESTED | AMD: Flushing includes all the nested translations for guest translations |
| TME | Intel Total Memory Encryption. The following MSRs are supported: IA32_TME_CAPABILITY, IA32_TME_ACTIVATE, IA32_TME_EXCLUDE_MASK, and IA32_TME_EXCLUDE_BASE. |
| TOPEXT | TopologyExtensions: topology extensions support. Indicates support for CPUID Fn8000_001D_EAX_x[N:0]-CPUID Fn8000_001E_EDX. |
| TSA_L1_NO | AMD only: Not vulnerable to TSA-L1 |
| TSA_SQ_NO | AMD only: Not vulnerable to TSA-SQ |
| TSA_VERW_CLEAR | AMD: If set, the memory form of the VERW instruction may be used to help mitigate TSA |
| TSCRATEMSR | MSR based TSC rate control. Indicates support for MSR TSC ratio MSRC000_0104 |
| TSXLDTRK | Intel TSX Suspend Load Address Tracking |
| VAES | Vector AES. AVX(512) versions requires additional checks. |
| VMCBCLEAN | VMCB clean bits. Indicates support for VMCB clean bits. |
| VMPL | AMD VM Permission Levels supported |
| VMSA_REGPROT | AMD VMSA Register Protection supported |
| VMX | Virtual Machine Extensions |
| VPCLMULQDQ | Carry-Less Multiplication Quadword. Requires AVX for 3 register versions. |
| VTE | AMD Virtual Transparent Encryption supported |
| WAITPKG | TPAUSE, UMONITOR, UMWAIT |
| WBNOINVD | Write Back and Do Not Invalidate Cache |
| WRMSRNS | Non-Serializing Write to Model Specific Register |
| X87 | FPU |
| XGETBV1 | Supports XGETBV with ECX = 1 |
| XOP | Bulldozer XOP functions |
| XSAVE | XSAVE, XRESTOR, XSETBV, XGETBV |
| XSAVEC | Supports XSAVEC and the compacted form of XRSTOR. |
| XSAVEOPT | XSAVEOPT available |
| XSAVES | Supports XSAVES/XRSTORS and IA32_XSS |
# ARM features:
| Feature Flag | Description |
|--------------|------------------------------------------------------------------|
| AESARM | AES instructions |
| ARMCPUID | Some CPU ID registers readable at user-level |
| ASIMD | Advanced SIMD |
| ASIMDDP | SIMD Dot Product |
| ASIMDHP | Advanced SIMD half-precision floating point |
| ASIMDRDM | Rounding Double Multiply Accumulate/Subtract (SQRDMLAH/SQRDMLSH) |
| ATOMICS | Large System Extensions (LSE) |
| CRC32 | CRC32/CRC32C instructions |
| DCPOP | Data cache clean to Point of Persistence (DC CVAP) |
| EVTSTRM | Generic timer |
| FCMA | Floatin point complex number addition and multiplication |
| FHM | FMLAL and FMLSL instructions |
| FP | Single-precision and double-precision floating point |
| FPHP | Half-precision floating point |
| GPA | Generic Pointer Authentication |
| JSCVT | Javascript-style double->int convert (FJCVTZS) |
| LRCPC | Weaker release consistency (LDAPR, etc) |
| PMULL | Polynomial Multiply instructions (PMULL/PMULL2) |
| RNDR | Random Number instructions |
| TLB | Outer Shareable and TLB range maintenance instructions |
| TS | Flag manipulation instructions |
| SHA1 | SHA-1 instructions (SHA1C, etc) |
| SHA2 | SHA-2 instructions (SHA256H, etc) |
| SHA3 | SHA-3 instructions (EOR3, RAXI, XAR, BCAX) |
| SHA512 | SHA512 instructions |
| SM3 | SM3 instructions |
| SM4 | SM4 instructions |
| SVE | Scalable Vector Extension |
# license
This code is published under an MIT license. See LICENSE file for more information.
+1679
View File
File diff suppressed because it is too large Load Diff
+47
View File
@@ -0,0 +1,47 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//+build 386,!gccgo,!noasm,!appengine
// func asmCpuid(op uint32) (eax, ebx, ecx, edx uint32)
TEXT ·asmCpuid(SB), 7, $0
XORL CX, CX
MOVL op+0(FP), AX
CPUID
MOVL AX, eax+4(FP)
MOVL BX, ebx+8(FP)
MOVL CX, ecx+12(FP)
MOVL DX, edx+16(FP)
RET
// func asmCpuidex(op, op2 uint32) (eax, ebx, ecx, edx uint32)
TEXT ·asmCpuidex(SB), 7, $0
MOVL op+0(FP), AX
MOVL op2+4(FP), CX
CPUID
MOVL AX, eax+8(FP)
MOVL BX, ebx+12(FP)
MOVL CX, ecx+16(FP)
MOVL DX, edx+20(FP)
RET
// func xgetbv(index uint32) (eax, edx uint32)
TEXT ·asmXgetbv(SB), 7, $0
MOVL index+0(FP), CX
BYTE $0x0f; BYTE $0x01; BYTE $0xd0 // XGETBV
MOVL AX, eax+4(FP)
MOVL DX, edx+8(FP)
RET
// func asmRdtscpAsm() (eax, ebx, ecx, edx uint32)
TEXT ·asmRdtscpAsm(SB), 7, $0
BYTE $0x0F; BYTE $0x01; BYTE $0xF9 // RDTSCP
MOVL AX, eax+0(FP)
MOVL BX, ebx+4(FP)
MOVL CX, ecx+8(FP)
MOVL DX, edx+12(FP)
RET
// func asmDarwinHasAVX512() bool
TEXT ·asmDarwinHasAVX512(SB), 7, $0
MOVL $0, eax+0(FP)
RET
+72
View File
@@ -0,0 +1,72 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//+build amd64,!gccgo,!noasm,!appengine
// func asmCpuid(op uint32) (eax, ebx, ecx, edx uint32)
TEXT ·asmCpuid(SB), 7, $0
XORQ CX, CX
MOVL op+0(FP), AX
CPUID
MOVL AX, eax+8(FP)
MOVL BX, ebx+12(FP)
MOVL CX, ecx+16(FP)
MOVL DX, edx+20(FP)
RET
// func asmCpuidex(op, op2 uint32) (eax, ebx, ecx, edx uint32)
TEXT ·asmCpuidex(SB), 7, $0
MOVL op+0(FP), AX
MOVL op2+4(FP), CX
CPUID
MOVL AX, eax+8(FP)
MOVL BX, ebx+12(FP)
MOVL CX, ecx+16(FP)
MOVL DX, edx+20(FP)
RET
// func asmXgetbv(index uint32) (eax, edx uint32)
TEXT ·asmXgetbv(SB), 7, $0
MOVL index+0(FP), CX
BYTE $0x0f; BYTE $0x01; BYTE $0xd0 // XGETBV
MOVL AX, eax+8(FP)
MOVL DX, edx+12(FP)
RET
// func asmRdtscpAsm() (eax, ebx, ecx, edx uint32)
TEXT ·asmRdtscpAsm(SB), 7, $0
BYTE $0x0F; BYTE $0x01; BYTE $0xF9 // RDTSCP
MOVL AX, eax+0(FP)
MOVL BX, ebx+4(FP)
MOVL CX, ecx+8(FP)
MOVL DX, edx+12(FP)
RET
// From https://go-review.googlesource.com/c/sys/+/285572/
// func asmDarwinHasAVX512() bool
TEXT ·asmDarwinHasAVX512(SB), 7, $0-1
MOVB $0, ret+0(FP) // default to false
#ifdef GOOS_darwin // return if not darwin
#ifdef GOARCH_amd64 // return if not amd64
// These values from:
// https://github.com/apple/darwin-xnu/blob/xnu-4570.1.46/osfmk/i386/cpu_capabilities.h
#define commpage64_base_address 0x00007fffffe00000
#define commpage64_cpu_capabilities64 (commpage64_base_address+0x010)
#define commpage64_version (commpage64_base_address+0x01E)
#define hasAVX512F 0x0000004000000000
MOVQ $commpage64_version, BX
MOVW (BX), AX
CMPW AX, $13 // versions < 13 do not support AVX512
JL no_avx512
MOVQ $commpage64_cpu_capabilities64, BX
MOVQ (BX), AX
MOVQ $hasAVX512F, CX
ANDQ CX, AX
JZ no_avx512
MOVB $1, ret+0(FP)
no_avx512:
#endif
#endif
RET
+36
View File
@@ -0,0 +1,36 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//+build arm64,!gccgo,!noasm,!appengine
// See https://www.kernel.org/doc/Documentation/arm64/cpu-feature-registers.txt
// func getMidr
TEXT ·getMidr(SB), 7, $0
WORD $0xd5380000 // mrs x0, midr_el1 /* Main ID Register */
MOVD R0, midr+0(FP)
RET
// func getProcFeatures
TEXT ·getProcFeatures(SB), 7, $0
WORD $0xd5380400 // mrs x0, id_aa64pfr0_el1 /* Processor Feature Register 0 */
MOVD R0, procFeatures+0(FP)
RET
// func getInstAttributes
TEXT ·getInstAttributes(SB), 7, $0
WORD $0xd5380600 // mrs x0, id_aa64isar0_el1 /* Instruction Set Attribute Register 0 */
WORD $0xd5380621 // mrs x1, id_aa64isar1_el1 /* Instruction Set Attribute Register 1 */
MOVD R0, instAttrReg0+0(FP)
MOVD R1, instAttrReg1+8(FP)
RET
TEXT ·getVectorLength(SB), 7, $0
WORD $0xd2800002 // mov x2, #0
WORD $0x04225022 // addvl x2, x2, #1
WORD $0xd37df042 // lsl x2, x2, #3
WORD $0xd2800003 // mov x3, #0
WORD $0x04635023 // addpl x3, x3, #1
WORD $0xd37df063 // lsl x3, x3, #3
MOVD R2, vl+0(FP)
MOVD R3, pl+8(FP)
RET
+250
View File
@@ -0,0 +1,250 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//go:build arm64 && !gccgo && !noasm && !appengine
// +build arm64,!gccgo,!noasm,!appengine
package cpuid
import "runtime"
func getMidr() (midr uint64)
func getProcFeatures() (procFeatures uint64)
func getInstAttributes() (instAttrReg0, instAttrReg1 uint64)
func getVectorLength() (vl, pl uint64)
func initCPU() {
cpuid = func(uint32) (a, b, c, d uint32) { return 0, 0, 0, 0 }
cpuidex = func(x, y uint32) (a, b, c, d uint32) { return 0, 0, 0, 0 }
xgetbv = func(uint32) (a, b uint32) { return 0, 0 }
rdtscpAsm = func() (a, b, c, d uint32) { return 0, 0, 0, 0 }
}
func addInfo(c *CPUInfo, safe bool) {
// Seems to be safe to assume on ARM64
c.CacheLine = 64
detectOS(c)
// ARM64 disabled since it may crash if interrupt is not intercepted by OS.
if safe && !c.Has(ARMCPUID) && runtime.GOOS != "freebsd" {
return
}
midr := getMidr()
// MIDR_EL1 - Main ID Register
// https://developer.arm.com/docs/ddi0595/h/aarch64-system-registers/midr_el1
// x--------------------------------------------------x
// | Name | bits | visible |
// |--------------------------------------------------|
// | Implementer | [31-24] | y |
// |--------------------------------------------------|
// | Variant | [23-20] | y |
// |--------------------------------------------------|
// | Architecture | [19-16] | y |
// |--------------------------------------------------|
// | PartNum | [15-4] | y |
// |--------------------------------------------------|
// | Revision | [3-0] | y |
// x--------------------------------------------------x
switch (midr >> 24) & 0xff {
case 0xC0:
c.VendorString = "Ampere Computing"
c.VendorID = Ampere
case 0x41:
c.VendorString = "Arm Limited"
c.VendorID = ARM
case 0x42:
c.VendorString = "Broadcom Corporation"
c.VendorID = Broadcom
case 0x43:
c.VendorString = "Cavium Inc"
c.VendorID = Cavium
case 0x44:
c.VendorString = "Digital Equipment Corporation"
c.VendorID = DEC
case 0x46:
c.VendorString = "Fujitsu Ltd"
c.VendorID = Fujitsu
case 0x49:
c.VendorString = "Infineon Technologies AG"
c.VendorID = Infineon
case 0x4D:
c.VendorString = "Motorola or Freescale Semiconductor Inc"
c.VendorID = Motorola
case 0x4E:
c.VendorString = "NVIDIA Corporation"
c.VendorID = NVIDIA
case 0x50:
c.VendorString = "Applied Micro Circuits Corporation"
c.VendorID = AMCC
case 0x51:
c.VendorString = "Qualcomm Inc"
c.VendorID = Qualcomm
case 0x56:
c.VendorString = "Marvell International Ltd"
c.VendorID = Marvell
case 0x69:
c.VendorString = "Intel Corporation"
c.VendorID = Intel
}
// Lower 4 bits: Architecture
// Architecture Meaning
// 0b0001 Armv4.
// 0b0010 Armv4T.
// 0b0011 Armv5 (obsolete).
// 0b0100 Armv5T.
// 0b0101 Armv5TE.
// 0b0110 Armv5TEJ.
// 0b0111 Armv6.
// 0b1111 Architectural features are individually identified in the ID_* registers, see 'ID registers'.
// Upper 4 bit: Variant
// An IMPLEMENTATION DEFINED variant number.
// Typically, this field is used to distinguish between different product variants, or major revisions of a product.
c.Family = int(midr>>16) & 0xff
// PartNum, bits [15:4]
// An IMPLEMENTATION DEFINED primary part number for the device.
// On processors implemented by Arm, if the top four bits of the primary
// part number are 0x0 or 0x7, the variant and architecture are encoded differently.
// Revision, bits [3:0]
// An IMPLEMENTATION DEFINED revision number for the device.
c.Model = int(midr) & 0xffff
procFeatures := getProcFeatures()
// ID_AA64PFR0_EL1 - Processor Feature Register 0
// x--------------------------------------------------x
// | Name | bits | visible |
// |--------------------------------------------------|
// | DIT | [51-48] | y |
// |--------------------------------------------------|
// | SVE | [35-32] | y |
// |--------------------------------------------------|
// | GIC | [27-24] | n |
// |--------------------------------------------------|
// | AdvSIMD | [23-20] | y |
// |--------------------------------------------------|
// | FP | [19-16] | y |
// |--------------------------------------------------|
// | EL3 | [15-12] | n |
// |--------------------------------------------------|
// | EL2 | [11-8] | n |
// |--------------------------------------------------|
// | EL1 | [7-4] | n |
// |--------------------------------------------------|
// | EL0 | [3-0] | n |
// x--------------------------------------------------x
var f flagSet
// if procFeatures&(0xf<<48) != 0 {
// fmt.Println("DIT")
// }
f.setIf(procFeatures&(0xf<<32) != 0, SVE)
if procFeatures&(0xf<<20) != 15<<20 {
f.set(ASIMD)
// https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/id_aa64pfr0_el1
// 0b0001 --> As for 0b0000, and also includes support for half-precision floating-point arithmetic.
f.setIf(procFeatures&(0xf<<20) == 1<<20, FPHP, ASIMDHP)
}
f.setIf(procFeatures&(0xf<<16) != 0, FP)
instAttrReg0, instAttrReg1 := getInstAttributes()
// https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/id_aa64isar0_el1
//
// ID_AA64ISAR0_EL1 - Instruction Set Attribute Register 0
// x--------------------------------------------------x
// | Name | bits | visible |
// |--------------------------------------------------|
// | RNDR | [63-60] | y |
// |--------------------------------------------------|
// | TLB | [59-56] | y |
// |--------------------------------------------------|
// | TS | [55-52] | y |
// |--------------------------------------------------|
// | FHM | [51-48] | y |
// |--------------------------------------------------|
// | DP | [47-44] | y |
// |--------------------------------------------------|
// | SM4 | [43-40] | y |
// |--------------------------------------------------|
// | SM3 | [39-36] | y |
// |--------------------------------------------------|
// | SHA3 | [35-32] | y |
// |--------------------------------------------------|
// | RDM | [31-28] | y |
// |--------------------------------------------------|
// | ATOMICS | [23-20] | y |
// |--------------------------------------------------|
// | CRC32 | [19-16] | y |
// |--------------------------------------------------|
// | SHA2 | [15-12] | y |
// |--------------------------------------------------|
// | SHA1 | [11-8] | y |
// |--------------------------------------------------|
// | AES | [7-4] | y |
// x--------------------------------------------------x
f.setIf(instAttrReg0&(0xf<<60) != 0, RNDR)
f.setIf(instAttrReg0&(0xf<<56) != 0, TLB)
f.setIf(instAttrReg0&(0xf<<52) != 0, TS)
f.setIf(instAttrReg0&(0xf<<48) != 0, FHM)
f.setIf(instAttrReg0&(0xf<<44) != 0, ASIMDDP)
f.setIf(instAttrReg0&(0xf<<40) != 0, SM4)
f.setIf(instAttrReg0&(0xf<<36) != 0, SM3)
f.setIf(instAttrReg0&(0xf<<32) != 0, SHA3)
f.setIf(instAttrReg0&(0xf<<28) != 0, ASIMDRDM)
f.setIf(instAttrReg0&(0xf<<20) != 0, ATOMICS)
f.setIf(instAttrReg0&(0xf<<16) != 0, CRC32)
f.setIf(instAttrReg0&(0xf<<12) != 0, SHA2)
// https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/id_aa64isar0_el1
// 0b0010 --> As 0b0001, plus SHA512H, SHA512H2, SHA512SU0, and SHA512SU1 instructions implemented.
f.setIf(instAttrReg0&(0xf<<12) == 2<<12, SHA512)
f.setIf(instAttrReg0&(0xf<<8) != 0, SHA1)
f.setIf(instAttrReg0&(0xf<<4) != 0, AESARM)
// https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/id_aa64isar0_el1
// 0b0010 --> As for 0b0001, plus PMULL/PMULL2 instructions operating on 64-bit data quantities.
f.setIf(instAttrReg0&(0xf<<4) == 2<<4, PMULL)
// https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/id_aa64isar1_el1
//
// ID_AA64ISAR1_EL1 - Instruction set attribute register 1
// x--------------------------------------------------x
// | Name | bits | visible |
// |--------------------------------------------------|
// | GPI | [31-28] | y |
// |--------------------------------------------------|
// | GPA | [27-24] | y |
// |--------------------------------------------------|
// | LRCPC | [23-20] | y |
// |--------------------------------------------------|
// | FCMA | [19-16] | y |
// |--------------------------------------------------|
// | JSCVT | [15-12] | y |
// |--------------------------------------------------|
// | API | [11-8] | y |
// |--------------------------------------------------|
// | APA | [7-4] | y |
// |--------------------------------------------------|
// | DPB | [3-0] | y |
// x--------------------------------------------------x
// if instAttrReg1&(0xf<<28) != 0 {
// fmt.Println("GPI")
// }
f.setIf(instAttrReg1&(0xf<<28) != 24, GPA)
f.setIf(instAttrReg1&(0xf<<20) != 0, LRCPC)
f.setIf(instAttrReg1&(0xf<<16) != 0, FCMA)
f.setIf(instAttrReg1&(0xf<<12) != 0, JSCVT)
// if instAttrReg1&(0xf<<8) != 0 {
// fmt.Println("API")
// }
// if instAttrReg1&(0xf<<4) != 0 {
// fmt.Println("APA")
// }
f.setIf(instAttrReg1&(0xf<<0) != 0, DCPOP)
// Store
c.featureSet.or(f)
}
+17
View File
@@ -0,0 +1,17 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//go:build (!amd64 && !386 && !arm64) || gccgo || noasm || appengine
// +build !amd64,!386,!arm64 gccgo noasm appengine
package cpuid
func initCPU() {
cpuid = func(uint32) (a, b, c, d uint32) { return 0, 0, 0, 0 }
cpuidex = func(x, y uint32) (a, b, c, d uint32) { return 0, 0, 0, 0 }
xgetbv = func(uint32) (a, b uint32) { return 0, 0 }
rdtscpAsm = func() (a, b, c, d uint32) { return 0, 0, 0, 0 }
}
func addInfo(info *CPUInfo, safe bool) {}
func getVectorLength() (vl, pl uint64) { return 0, 0 }
+45
View File
@@ -0,0 +1,45 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//go:build (386 && !gccgo && !noasm && !appengine) || (amd64 && !gccgo && !noasm && !appengine)
// +build 386,!gccgo,!noasm,!appengine amd64,!gccgo,!noasm,!appengine
package cpuid
func asmCpuid(op uint32) (eax, ebx, ecx, edx uint32)
func asmCpuidex(op, op2 uint32) (eax, ebx, ecx, edx uint32)
func asmXgetbv(index uint32) (eax, edx uint32)
func asmRdtscpAsm() (eax, ebx, ecx, edx uint32)
func asmDarwinHasAVX512() bool
func initCPU() {
cpuid = asmCpuid
cpuidex = asmCpuidex
xgetbv = asmXgetbv
rdtscpAsm = asmRdtscpAsm
darwinHasAVX512 = asmDarwinHasAVX512
}
func addInfo(c *CPUInfo, safe bool) {
c.maxFunc = maxFunctionID()
c.maxExFunc = maxExtendedFunction()
c.BrandName = brandName()
c.CacheLine = cacheLine()
c.Family, c.Model, c.Stepping = familyModel()
c.featureSet = support()
c.SGX = hasSGX(c.featureSet.inSet(SGX), c.featureSet.inSet(SGXLC))
c.AMDMemEncryption = hasAMDMemEncryption(c.featureSet.inSet(SME) || c.featureSet.inSet(SEV))
c.ThreadsPerCore = threadsPerCore()
c.LogicalCores = logicalCores()
c.PhysicalCores = physicalCores()
c.VendorID, c.VendorString = vendorID()
c.HypervisorVendorID, c.HypervisorVendorString = hypervisorVendorID()
c.AVX10Level = c.supportAVX10()
c.cacheSize()
c.frequencies()
if c.maxFunc >= 0x0A {
eax, ebx, _, edx := cpuid(0x0A)
c.PMU = parseLeaf0AH(c, eax, ebx, edx)
}
}
func getVectorLength() (vl, pl uint64) { return 0, 0 }
+308
View File
@@ -0,0 +1,308 @@
// Code generated by "stringer -type=FeatureID,Vendor"; DO NOT EDIT.
package cpuid
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[ADX-1]
_ = x[AESNI-2]
_ = x[AMD3DNOW-3]
_ = x[AMD3DNOWEXT-4]
_ = x[AMXBF16-5]
_ = x[AMXFP16-6]
_ = x[AMXINT8-7]
_ = x[AMXFP8-8]
_ = x[AMXTILE-9]
_ = x[AMXTF32-10]
_ = x[AMXCOMPLEX-11]
_ = x[AMXTRANSPOSE-12]
_ = x[APX_F-13]
_ = x[AVX-14]
_ = x[AVX10-15]
_ = x[AVX10_128-16]
_ = x[AVX10_256-17]
_ = x[AVX10_512-18]
_ = x[AVX2-19]
_ = x[AVX512BF16-20]
_ = x[AVX512BITALG-21]
_ = x[AVX512BW-22]
_ = x[AVX512CD-23]
_ = x[AVX512DQ-24]
_ = x[AVX512ER-25]
_ = x[AVX512F-26]
_ = x[AVX512FP16-27]
_ = x[AVX512IFMA-28]
_ = x[AVX512PF-29]
_ = x[AVX512VBMI-30]
_ = x[AVX512VBMI2-31]
_ = x[AVX512VL-32]
_ = x[AVX512VNNI-33]
_ = x[AVX512VP2INTERSECT-34]
_ = x[AVX512VPOPCNTDQ-35]
_ = x[AVXIFMA-36]
_ = x[AVXNECONVERT-37]
_ = x[AVXSLOW-38]
_ = x[AVXVNNI-39]
_ = x[AVXVNNIINT8-40]
_ = x[AVXVNNIINT16-41]
_ = x[BHI_CTRL-42]
_ = x[BMI1-43]
_ = x[BMI2-44]
_ = x[CETIBT-45]
_ = x[CETSS-46]
_ = x[CLDEMOTE-47]
_ = x[CLMUL-48]
_ = x[CLZERO-49]
_ = x[CMOV-50]
_ = x[CMPCCXADD-51]
_ = x[CMPSB_SCADBS_SHORT-52]
_ = x[CMPXCHG8-53]
_ = x[CPBOOST-54]
_ = x[CPPC-55]
_ = x[CX16-56]
_ = x[EFER_LMSLE_UNS-57]
_ = x[ENQCMD-58]
_ = x[ERMS-59]
_ = x[F16C-60]
_ = x[FLUSH_L1D-61]
_ = x[FMA3-62]
_ = x[FMA4-63]
_ = x[FP128-64]
_ = x[FP256-65]
_ = x[FSRM-66]
_ = x[FXSR-67]
_ = x[FXSROPT-68]
_ = x[GFNI-69]
_ = x[HLE-70]
_ = x[HRESET-71]
_ = x[HTT-72]
_ = x[HWA-73]
_ = x[HYBRID_CPU-74]
_ = x[HYPERVISOR-75]
_ = x[IA32_ARCH_CAP-76]
_ = x[IA32_CORE_CAP-77]
_ = x[IBPB-78]
_ = x[IBPB_BRTYPE-79]
_ = x[IBRS-80]
_ = x[IBRS_PREFERRED-81]
_ = x[IBRS_PROVIDES_SMP-82]
_ = x[IBS-83]
_ = x[IBSBRNTRGT-84]
_ = x[IBSFETCHSAM-85]
_ = x[IBSFFV-86]
_ = x[IBSOPCNT-87]
_ = x[IBSOPCNTEXT-88]
_ = x[IBSOPSAM-89]
_ = x[IBSRDWROPCNT-90]
_ = x[IBSRIPINVALIDCHK-91]
_ = x[IBS_FETCH_CTLX-92]
_ = x[IBS_OPDATA4-93]
_ = x[IBS_OPFUSE-94]
_ = x[IBS_PREVENTHOST-95]
_ = x[IBS_ZEN4-96]
_ = x[IDPRED_CTRL-97]
_ = x[INT_WBINVD-98]
_ = x[INVLPGB-99]
_ = x[KEYLOCKER-100]
_ = x[KEYLOCKERW-101]
_ = x[LAHF-102]
_ = x[LAM-103]
_ = x[LBRVIRT-104]
_ = x[LZCNT-105]
_ = x[MCAOVERFLOW-106]
_ = x[MCDT_NO-107]
_ = x[MCOMMIT-108]
_ = x[MD_CLEAR-109]
_ = x[MMX-110]
_ = x[MMXEXT-111]
_ = x[MOVBE-112]
_ = x[MOVDIR64B-113]
_ = x[MOVDIRI-114]
_ = x[MOVSB_ZL-115]
_ = x[MOVU-116]
_ = x[MPX-117]
_ = x[MSRIRC-118]
_ = x[MSRLIST-119]
_ = x[MSR_PAGEFLUSH-120]
_ = x[NRIPS-121]
_ = x[NX-122]
_ = x[OSXSAVE-123]
_ = x[PCONFIG-124]
_ = x[POPCNT-125]
_ = x[PPIN-126]
_ = x[PREFETCHI-127]
_ = x[PSFD-128]
_ = x[RDPRU-129]
_ = x[RDRAND-130]
_ = x[RDSEED-131]
_ = x[RDTSCP-132]
_ = x[RRSBA_CTRL-133]
_ = x[RTM-134]
_ = x[RTM_ALWAYS_ABORT-135]
_ = x[SBPB-136]
_ = x[SERIALIZE-137]
_ = x[SEV-138]
_ = x[SEV_64BIT-139]
_ = x[SEV_ALTERNATIVE-140]
_ = x[SEV_DEBUGSWAP-141]
_ = x[SEV_ES-142]
_ = x[SEV_RESTRICTED-143]
_ = x[SEV_SNP-144]
_ = x[SGX-145]
_ = x[SGXLC-146]
_ = x[SGXPQC-147]
_ = x[SHA-148]
_ = x[SME-149]
_ = x[SME_COHERENT-150]
_ = x[SM3_X86-151]
_ = x[SM4_X86-152]
_ = x[SPEC_CTRL_SSBD-153]
_ = x[SRBDS_CTRL-154]
_ = x[SRSO_MSR_FIX-155]
_ = x[SRSO_NO-156]
_ = x[SRSO_USER_KERNEL_NO-157]
_ = x[SSE-158]
_ = x[SSE2-159]
_ = x[SSE3-160]
_ = x[SSE4-161]
_ = x[SSE42-162]
_ = x[SSE4A-163]
_ = x[SSSE3-164]
_ = x[STIBP-165]
_ = x[STIBP_ALWAYSON-166]
_ = x[STOSB_SHORT-167]
_ = x[SUCCOR-168]
_ = x[SVM-169]
_ = x[SVMDA-170]
_ = x[SVMFBASID-171]
_ = x[SVML-172]
_ = x[SVMNP-173]
_ = x[SVMPF-174]
_ = x[SVMPFT-175]
_ = x[SYSCALL-176]
_ = x[SYSEE-177]
_ = x[TBM-178]
_ = x[TDX_GUEST-179]
_ = x[TLB_FLUSH_NESTED-180]
_ = x[TME-181]
_ = x[TOPEXT-182]
_ = x[TSA_L1_NO-183]
_ = x[TSA_SQ_NO-184]
_ = x[TSA_VERW_CLEAR-185]
_ = x[TSCRATEMSR-186]
_ = x[TSXLDTRK-187]
_ = x[VAES-188]
_ = x[VMCBCLEAN-189]
_ = x[VMPL-190]
_ = x[VMSA_REGPROT-191]
_ = x[VMX-192]
_ = x[VPCLMULQDQ-193]
_ = x[VTE-194]
_ = x[WAITPKG-195]
_ = x[WBNOINVD-196]
_ = x[WRMSRNS-197]
_ = x[X87-198]
_ = x[XGETBV1-199]
_ = x[XOP-200]
_ = x[XSAVE-201]
_ = x[XSAVEC-202]
_ = x[XSAVEOPT-203]
_ = x[XSAVES-204]
_ = x[AESARM-205]
_ = x[ARMCPUID-206]
_ = x[ASIMD-207]
_ = x[ASIMDDP-208]
_ = x[ASIMDHP-209]
_ = x[ASIMDRDM-210]
_ = x[ATOMICS-211]
_ = x[CRC32-212]
_ = x[DCPOP-213]
_ = x[EVTSTRM-214]
_ = x[FCMA-215]
_ = x[FHM-216]
_ = x[FP-217]
_ = x[FPHP-218]
_ = x[GPA-219]
_ = x[JSCVT-220]
_ = x[LRCPC-221]
_ = x[PMULL-222]
_ = x[RNDR-223]
_ = x[TLB-224]
_ = x[TS-225]
_ = x[SHA1-226]
_ = x[SHA2-227]
_ = x[SHA3-228]
_ = x[SHA512-229]
_ = x[SM3-230]
_ = x[SM4-231]
_ = x[SVE-232]
_ = x[PMU_FIXEDCOUNTER_CYCLES-233]
_ = x[PMU_FIXEDCOUNTER_REFCYCLES-234]
_ = x[PMU_FIXEDCOUNTER_INSTRUCTIONS-235]
_ = x[PMU_FIXEDCOUNTER_TOPDOWN_SLOTS-236]
_ = x[lastID-237]
_ = x[firstID-0]
}
const _FeatureID_name = "firstIDADXAESNIAMD3DNOWAMD3DNOWEXTAMXBF16AMXFP16AMXINT8AMXFP8AMXTILEAMXTF32AMXCOMPLEXAMXTRANSPOSEAPX_FAVXAVX10AVX10_128AVX10_256AVX10_512AVX2AVX512BF16AVX512BITALGAVX512BWAVX512CDAVX512DQAVX512ERAVX512FAVX512FP16AVX512IFMAAVX512PFAVX512VBMIAVX512VBMI2AVX512VLAVX512VNNIAVX512VP2INTERSECTAVX512VPOPCNTDQAVXIFMAAVXNECONVERTAVXSLOWAVXVNNIAVXVNNIINT8AVXVNNIINT16BHI_CTRLBMI1BMI2CETIBTCETSSCLDEMOTECLMULCLZEROCMOVCMPCCXADDCMPSB_SCADBS_SHORTCMPXCHG8CPBOOSTCPPCCX16EFER_LMSLE_UNSENQCMDERMSF16CFLUSH_L1DFMA3FMA4FP128FP256FSRMFXSRFXSROPTGFNIHLEHRESETHTTHWAHYBRID_CPUHYPERVISORIA32_ARCH_CAPIA32_CORE_CAPIBPBIBPB_BRTYPEIBRSIBRS_PREFERREDIBRS_PROVIDES_SMPIBSIBSBRNTRGTIBSFETCHSAMIBSFFVIBSOPCNTIBSOPCNTEXTIBSOPSAMIBSRDWROPCNTIBSRIPINVALIDCHKIBS_FETCH_CTLXIBS_OPDATA4IBS_OPFUSEIBS_PREVENTHOSTIBS_ZEN4IDPRED_CTRLINT_WBINVDINVLPGBKEYLOCKERKEYLOCKERWLAHFLAMLBRVIRTLZCNTMCAOVERFLOWMCDT_NOMCOMMITMD_CLEARMMXMMXEXTMOVBEMOVDIR64BMOVDIRIMOVSB_ZLMOVUMPXMSRIRCMSRLISTMSR_PAGEFLUSHNRIPSNXOSXSAVEPCONFIGPOPCNTPPINPREFETCHIPSFDRDPRURDRANDRDSEEDRDTSCPRRSBA_CTRLRTMRTM_ALWAYS_ABORTSBPBSERIALIZESEVSEV_64BITSEV_ALTERNATIVESEV_DEBUGSWAPSEV_ESSEV_RESTRICTEDSEV_SNPSGXSGXLCSGXPQCSHASMESME_COHERENTSM3_X86SM4_X86SPEC_CTRL_SSBDSRBDS_CTRLSRSO_MSR_FIXSRSO_NOSRSO_USER_KERNEL_NOSSESSE2SSE3SSE4SSE42SSE4ASSSE3STIBPSTIBP_ALWAYSONSTOSB_SHORTSUCCORSVMSVMDASVMFBASIDSVMLSVMNPSVMPFSVMPFTSYSCALLSYSEETBMTDX_GUESTTLB_FLUSH_NESTEDTMETOPEXTTSA_L1_NOTSA_SQ_NOTSA_VERW_CLEARTSCRATEMSRTSXLDTRKVAESVMCBCLEANVMPLVMSA_REGPROTVMXVPCLMULQDQVTEWAITPKGWBNOINVDWRMSRNSX87XGETBV1XOPXSAVEXSAVECXSAVEOPTXSAVESAESARMARMCPUIDASIMDASIMDDPASIMDHPASIMDRDMATOMICSCRC32DCPOPEVTSTRMFCMAFHMFPFPHPGPAJSCVTLRCPCPMULLRNDRTLBTSSHA1SHA2SHA3SHA512SM3SM4SVEPMU_FIXEDCOUNTER_CYCLESPMU_FIXEDCOUNTER_REFCYCLESPMU_FIXEDCOUNTER_INSTRUCTIONSPMU_FIXEDCOUNTER_TOPDOWN_SLOTSlastID"
var _FeatureID_index = [...]uint16{0, 7, 10, 15, 23, 34, 41, 48, 55, 61, 68, 75, 85, 97, 102, 105, 110, 119, 128, 137, 141, 151, 163, 171, 179, 187, 195, 202, 212, 222, 230, 240, 251, 259, 269, 287, 302, 309, 321, 328, 335, 346, 358, 366, 370, 374, 380, 385, 393, 398, 404, 408, 417, 435, 443, 450, 454, 458, 472, 478, 482, 486, 495, 499, 503, 508, 513, 517, 521, 528, 532, 535, 541, 544, 547, 557, 567, 580, 593, 597, 608, 612, 626, 643, 646, 656, 667, 673, 681, 692, 700, 712, 728, 742, 753, 763, 778, 786, 797, 807, 814, 823, 833, 837, 840, 847, 852, 863, 870, 877, 885, 888, 894, 899, 908, 915, 923, 927, 930, 936, 943, 956, 961, 963, 970, 977, 983, 987, 996, 1000, 1005, 1011, 1017, 1023, 1033, 1036, 1052, 1056, 1065, 1068, 1077, 1092, 1105, 1111, 1125, 1132, 1135, 1140, 1146, 1149, 1152, 1164, 1171, 1178, 1192, 1202, 1214, 1221, 1240, 1243, 1247, 1251, 1255, 1260, 1265, 1270, 1275, 1289, 1300, 1306, 1309, 1314, 1323, 1327, 1332, 1337, 1343, 1350, 1355, 1358, 1367, 1383, 1386, 1392, 1401, 1410, 1424, 1434, 1442, 1446, 1455, 1459, 1471, 1474, 1484, 1487, 1494, 1502, 1509, 1512, 1519, 1522, 1527, 1533, 1541, 1547, 1553, 1561, 1566, 1573, 1580, 1588, 1595, 1600, 1605, 1612, 1616, 1619, 1621, 1625, 1628, 1633, 1638, 1643, 1647, 1650, 1652, 1656, 1660, 1664, 1670, 1673, 1676, 1679, 1702, 1728, 1757, 1787, 1793}
func (i FeatureID) String() string {
if i < 0 || i >= FeatureID(len(_FeatureID_index)-1) {
return "FeatureID(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _FeatureID_name[_FeatureID_index[i]:_FeatureID_index[i+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[VendorUnknown-0]
_ = x[Intel-1]
_ = x[AMD-2]
_ = x[VIA-3]
_ = x[Transmeta-4]
_ = x[NSC-5]
_ = x[KVM-6]
_ = x[MSVM-7]
_ = x[VMware-8]
_ = x[XenHVM-9]
_ = x[Bhyve-10]
_ = x[Hygon-11]
_ = x[SiS-12]
_ = x[RDC-13]
_ = x[Ampere-14]
_ = x[ARM-15]
_ = x[Broadcom-16]
_ = x[Cavium-17]
_ = x[DEC-18]
_ = x[Fujitsu-19]
_ = x[Infineon-20]
_ = x[Motorola-21]
_ = x[NVIDIA-22]
_ = x[AMCC-23]
_ = x[Qualcomm-24]
_ = x[Marvell-25]
_ = x[QEMU-26]
_ = x[QNX-27]
_ = x[ACRN-28]
_ = x[SRE-29]
_ = x[Apple-30]
_ = x[lastVendor-31]
}
const _Vendor_name = "VendorUnknownIntelAMDVIATransmetaNSCKVMMSVMVMwareXenHVMBhyveHygonSiSRDCAmpereARMBroadcomCaviumDECFujitsuInfineonMotorolaNVIDIAAMCCQualcommMarvellQEMUQNXACRNSREApplelastVendor"
var _Vendor_index = [...]uint8{0, 13, 18, 21, 24, 33, 36, 39, 43, 49, 55, 60, 65, 68, 71, 77, 80, 88, 94, 97, 104, 112, 120, 126, 130, 138, 145, 149, 152, 156, 159, 164, 174}
func (i Vendor) String() string {
if i < 0 || i >= Vendor(len(_Vendor_index)-1) {
return "Vendor(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Vendor_name[_Vendor_index[i]:_Vendor_index[i+1]]
}
+129
View File
@@ -0,0 +1,129 @@
// Copyright (c) 2020 Klaus Post, released under MIT License. See LICENSE file.
package cpuid
import (
"runtime"
"strings"
"golang.org/x/sys/unix"
)
func detectOS(c *CPUInfo) bool {
if runtime.GOOS != "ios" {
tryToFillCPUInfoFomSysctl(c)
}
// There are no hw.optional sysctl values for the below features on Mac OS 11.0
// to detect their supported state dynamically. Assume the CPU features that
// Apple Silicon M1 supports to be available as a minimal set of features
// to all Go programs running on darwin/arm64.
// TODO: Add more if we know them.
c.featureSet.setIf(runtime.GOOS != "ios", AESARM, PMULL, SHA1, SHA2)
return true
}
func sysctlGetBool(name string) bool {
value, err := unix.SysctlUint32(name)
if err != nil {
return false
}
return value != 0
}
func sysctlGetString(name string) string {
value, err := unix.Sysctl(name)
if err != nil {
return ""
}
return value
}
func sysctlGetInt(unknown int, names ...string) int {
for _, name := range names {
value, err := unix.SysctlUint32(name)
if err != nil {
continue
}
if value != 0 {
return int(value)
}
}
return unknown
}
func sysctlGetInt64(unknown int, names ...string) int {
for _, name := range names {
value64, err := unix.SysctlUint64(name)
if err != nil {
continue
}
if int(value64) != unknown {
return int(value64)
}
}
return unknown
}
func setFeature(c *CPUInfo, feature FeatureID, aliases ...string) {
for _, alias := range aliases {
set := sysctlGetBool(alias)
c.featureSet.setIf(set, feature)
if set {
break
}
}
}
func tryToFillCPUInfoFomSysctl(c *CPUInfo) {
c.BrandName = sysctlGetString("machdep.cpu.brand_string")
if len(c.BrandName) != 0 {
c.VendorString = strings.Fields(c.BrandName)[0]
}
c.PhysicalCores = sysctlGetInt(runtime.NumCPU(), "hw.physicalcpu")
c.ThreadsPerCore = sysctlGetInt(1, "machdep.cpu.thread_count", "kern.num_threads") /
sysctlGetInt(1, "hw.physicalcpu")
c.LogicalCores = sysctlGetInt(runtime.NumCPU(), "machdep.cpu.core_count")
c.Family = sysctlGetInt(0, "machdep.cpu.family", "hw.cpufamily")
c.Model = sysctlGetInt(0, "machdep.cpu.model")
c.CacheLine = sysctlGetInt64(0, "hw.cachelinesize")
c.Cache.L1I = sysctlGetInt64(-1, "hw.l1icachesize")
c.Cache.L1D = sysctlGetInt64(-1, "hw.l1dcachesize")
c.Cache.L2 = sysctlGetInt64(-1, "hw.l2cachesize")
c.Cache.L3 = sysctlGetInt64(-1, "hw.l3cachesize")
// ARM features:
//
// Note: On some Apple Silicon system, some feats have aliases. See:
// https://developer.apple.com/documentation/kernel/1387446-sysctlbyname/determining_instruction_set_characteristics
// When so, we look at all aliases and consider a feature available when at least one identifier matches.
setFeature(c, AESARM, "hw.optional.arm.FEAT_AES") // AES instructions
setFeature(c, ASIMD, "hw.optional.arm.AdvSIMD", "hw.optional.neon") // Advanced SIMD
setFeature(c, ASIMDDP, "hw.optional.arm.FEAT_DotProd") // SIMD Dot Product
setFeature(c, ASIMDHP, "hw.optional.arm.AdvSIMD_HPFPCvt", "hw.optional.neon_hpfp") // Advanced SIMD half-precision floating point
setFeature(c, ASIMDRDM, "hw.optional.arm.FEAT_RDM") // Rounding Double Multiply Accumulate/Subtract
setFeature(c, ATOMICS, "hw.optional.arm.FEAT_LSE", "hw.optional.armv8_1_atomics") // Large System Extensions (LSE)
setFeature(c, CRC32, "hw.optional.arm.FEAT_CRC32", "hw.optional.armv8_crc32") // CRC32/CRC32C instructions
setFeature(c, DCPOP, "hw.optional.arm.FEAT_DPB") // Data cache clean to Point of Persistence (DC CVAP)
setFeature(c, EVTSTRM, "hw.optional.arm.FEAT_ECV") // Generic timer
setFeature(c, FCMA, "hw.optional.arm.FEAT_FCMA", "hw.optional.armv8_3_compnum") // Floating point complex number addition and multiplication
setFeature(c, FHM, "hw.optional.armv8_2_fhm", "hw.optional.arm.FEAT_FHM") // FMLAL and FMLSL instructions
setFeature(c, FP, "hw.optional.floatingpoint") // Single-precision and double-precision floating point
setFeature(c, FPHP, "hw.optional.arm.FEAT_FP16", "hw.optional.neon_fp16") // Half-precision floating point
setFeature(c, GPA, "hw.optional.arm.FEAT_PAuth") // Generic Pointer Authentication
setFeature(c, JSCVT, "hw.optional.arm.FEAT_JSCVT") // Javascript-style double->int convert (FJCVTZS)
setFeature(c, LRCPC, "hw.optional.arm.FEAT_LRCPC") // Weaker release consistency (LDAPR, etc)
setFeature(c, PMULL, "hw.optional.arm.FEAT_PMULL") // Polynomial Multiply instructions (PMULL/PMULL2)
setFeature(c, RNDR, "hw.optional.arm.FEAT_RNG") // Random Number instructions
setFeature(c, TLB, "hw.optional.arm.FEAT_TLBIOS", "hw.optional.arm.FEAT_TLBIRANGE") // Outer Shareable and TLB range maintenance instructions
setFeature(c, TS, "hw.optional.arm.FEAT_FlagM", "hw.optional.arm.FEAT_FlagM2") // Flag manipulation instructions
setFeature(c, SHA1, "hw.optional.arm.FEAT_SHA1") // SHA-1 instructions (SHA1C, etc)
setFeature(c, SHA2, "hw.optional.arm.FEAT_SHA256") // SHA-2 instructions (SHA256H, etc)
setFeature(c, SHA3, "hw.optional.arm.FEAT_SHA3") // SHA-3 instructions (EOR3, RAXI, XAR, BCAX)
setFeature(c, SHA512, "hw.optional.arm.FEAT_SHA512") // SHA512 instructions
setFeature(c, SM3, "hw.optional.arm.FEAT_SM3") // SM3 instructions
setFeature(c, SM4, "hw.optional.arm.FEAT_SM4") // SM4 instructions
setFeature(c, SVE, "hw.optional.arm.FEAT_SVE") // Scalable Vector Extension
}
+208
View File
@@ -0,0 +1,208 @@
// Copyright (c) 2020 Klaus Post, released under MIT License. See LICENSE file.
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file located
// here https://github.com/golang/sys/blob/master/LICENSE
package cpuid
import (
"encoding/binary"
"io/ioutil"
"runtime"
)
// HWCAP bits.
const (
hwcap_FP = 1 << 0
hwcap_ASIMD = 1 << 1
hwcap_EVTSTRM = 1 << 2
hwcap_AES = 1 << 3
hwcap_PMULL = 1 << 4
hwcap_SHA1 = 1 << 5
hwcap_SHA2 = 1 << 6
hwcap_CRC32 = 1 << 7
hwcap_ATOMICS = 1 << 8
hwcap_FPHP = 1 << 9
hwcap_ASIMDHP = 1 << 10
hwcap_CPUID = 1 << 11
hwcap_ASIMDRDM = 1 << 12
hwcap_JSCVT = 1 << 13
hwcap_FCMA = 1 << 14
hwcap_LRCPC = 1 << 15
hwcap_DCPOP = 1 << 16
hwcap_SHA3 = 1 << 17
hwcap_SM3 = 1 << 18
hwcap_SM4 = 1 << 19
hwcap_ASIMDDP = 1 << 20
hwcap_SHA512 = 1 << 21
hwcap_SVE = 1 << 22
hwcap_ASIMDFHM = 1 << 23
hwcap_DIT = 1 << 24
hwcap_USCAT = 1 << 25
hwcap_ILRCPC = 1 << 26
hwcap_FLAGM = 1 << 27
hwcap_SSBS = 1 << 28
hwcap_SB = 1 << 29
hwcap_PACA = 1 << 30
hwcap_PACG = 1 << 31
hwcap_GCS = 1 << 32
hwcap2_DCPODP = 1 << 0
hwcap2_SVE2 = 1 << 1
hwcap2_SVEAES = 1 << 2
hwcap2_SVEPMULL = 1 << 3
hwcap2_SVEBITPERM = 1 << 4
hwcap2_SVESHA3 = 1 << 5
hwcap2_SVESM4 = 1 << 6
hwcap2_FLAGM2 = 1 << 7
hwcap2_FRINT = 1 << 8
hwcap2_SVEI8MM = 1 << 9
hwcap2_SVEF32MM = 1 << 10
hwcap2_SVEF64MM = 1 << 11
hwcap2_SVEBF16 = 1 << 12
hwcap2_I8MM = 1 << 13
hwcap2_BF16 = 1 << 14
hwcap2_DGH = 1 << 15
hwcap2_RNG = 1 << 16
hwcap2_BTI = 1 << 17
hwcap2_MTE = 1 << 18
hwcap2_ECV = 1 << 19
hwcap2_AFP = 1 << 20
hwcap2_RPRES = 1 << 21
hwcap2_MTE3 = 1 << 22
hwcap2_SME = 1 << 23
hwcap2_SME_I16I64 = 1 << 24
hwcap2_SME_F64F64 = 1 << 25
hwcap2_SME_I8I32 = 1 << 26
hwcap2_SME_F16F32 = 1 << 27
hwcap2_SME_B16F32 = 1 << 28
hwcap2_SME_F32F32 = 1 << 29
hwcap2_SME_FA64 = 1 << 30
hwcap2_WFXT = 1 << 31
hwcap2_EBF16 = 1 << 32
hwcap2_SVE_EBF16 = 1 << 33
hwcap2_CSSC = 1 << 34
hwcap2_RPRFM = 1 << 35
hwcap2_SVE2P1 = 1 << 36
hwcap2_SME2 = 1 << 37
hwcap2_SME2P1 = 1 << 38
hwcap2_SME_I16I32 = 1 << 39
hwcap2_SME_BI32I32 = 1 << 40
hwcap2_SME_B16B16 = 1 << 41
hwcap2_SME_F16F16 = 1 << 42
hwcap2_MOPS = 1 << 43
hwcap2_HBC = 1 << 44
hwcap2_SVE_B16B16 = 1 << 45
hwcap2_LRCPC3 = 1 << 46
hwcap2_LSE128 = 1 << 47
hwcap2_FPMR = 1 << 48
hwcap2_LUT = 1 << 49
hwcap2_FAMINMAX = 1 << 50
hwcap2_F8CVT = 1 << 51
hwcap2_F8FMA = 1 << 52
hwcap2_F8DP4 = 1 << 53
hwcap2_F8DP2 = 1 << 54
hwcap2_F8E4M3 = 1 << 55
hwcap2_F8E5M2 = 1 << 56
hwcap2_SME_LUTV2 = 1 << 57
hwcap2_SME_F8F16 = 1 << 58
hwcap2_SME_F8F32 = 1 << 59
hwcap2_SME_SF8FMA = 1 << 60
hwcap2_SME_SF8DP4 = 1 << 61
hwcap2_SME_SF8DP2 = 1 << 62
hwcap2_POE = 1 << 63
)
func detectOS(c *CPUInfo) bool {
// For now assuming no hyperthreading is reasonable.
c.LogicalCores = runtime.NumCPU()
c.PhysicalCores = c.LogicalCores
c.ThreadsPerCore = 1
if hwcap == 0 {
// We did not get values from the runtime.
// Try reading /proc/self/auxv
// From https://github.com/golang/sys
const (
_AT_HWCAP = 16
_AT_HWCAP2 = 26
uintSize = int(32 << (^uint(0) >> 63))
)
buf, err := ioutil.ReadFile("/proc/self/auxv")
if err != nil {
// e.g. on android /proc/self/auxv is not accessible, so silently
// ignore the error and leave Initialized = false. On some
// architectures (e.g. arm64) doinit() implements a fallback
// readout and will set Initialized = true again.
return false
}
bo := binary.LittleEndian
for len(buf) >= 2*(uintSize/8) {
var tag, val uint
switch uintSize {
case 32:
tag = uint(bo.Uint32(buf[0:]))
val = uint(bo.Uint32(buf[4:]))
buf = buf[8:]
case 64:
tag = uint(bo.Uint64(buf[0:]))
val = uint(bo.Uint64(buf[8:]))
buf = buf[16:]
}
switch tag {
case _AT_HWCAP:
hwcap = val
case _AT_HWCAP2:
// Not used
}
}
if hwcap == 0 {
return false
}
}
// HWCap was populated by the runtime from the auxiliary vector.
// Use HWCap information since reading aarch64 system registers
// is not supported in user space on older linux kernels.
c.featureSet.setIf(isSet(hwcap, hwcap_AES), AESARM)
c.featureSet.setIf(isSet(hwcap, hwcap_ASIMD), ASIMD)
c.featureSet.setIf(isSet(hwcap, hwcap_ASIMDDP), ASIMDDP)
c.featureSet.setIf(isSet(hwcap, hwcap_ASIMDHP), ASIMDHP)
c.featureSet.setIf(isSet(hwcap, hwcap_ASIMDRDM), ASIMDRDM)
c.featureSet.setIf(isSet(hwcap, hwcap_CPUID), ARMCPUID)
c.featureSet.setIf(isSet(hwcap, hwcap_CRC32), CRC32)
c.featureSet.setIf(isSet(hwcap, hwcap_DCPOP), DCPOP)
c.featureSet.setIf(isSet(hwcap, hwcap_EVTSTRM), EVTSTRM)
c.featureSet.setIf(isSet(hwcap, hwcap_FCMA), FCMA)
c.featureSet.setIf(isSet(hwcap, hwcap_ASIMDFHM), FHM)
c.featureSet.setIf(isSet(hwcap, hwcap_FP), FP)
c.featureSet.setIf(isSet(hwcap, hwcap_FPHP), FPHP)
c.featureSet.setIf(isSet(hwcap, hwcap_JSCVT), JSCVT)
c.featureSet.setIf(isSet(hwcap, hwcap_LRCPC), LRCPC)
c.featureSet.setIf(isSet(hwcap, hwcap_PMULL), PMULL)
c.featureSet.setIf(isSet(hwcap, hwcap2_RNG), RNDR)
// c.featureSet.setIf(isSet(hwcap, hwcap_), TLB)
// c.featureSet.setIf(isSet(hwcap, hwcap_), TS)
c.featureSet.setIf(isSet(hwcap, hwcap_SHA1), SHA1)
c.featureSet.setIf(isSet(hwcap, hwcap_SHA2), SHA2)
c.featureSet.setIf(isSet(hwcap, hwcap_SHA3), SHA3)
c.featureSet.setIf(isSet(hwcap, hwcap_SHA512), SHA512)
c.featureSet.setIf(isSet(hwcap, hwcap_SM3), SM3)
c.featureSet.setIf(isSet(hwcap, hwcap_SM4), SM4)
c.featureSet.setIf(isSet(hwcap, hwcap_SVE), SVE)
// The Samsung S9+ kernel reports support for atomics, but not all cores
// actually support them, resulting in SIGILL. See issue #28431.
// TODO(elias.naur): Only disable the optimization on bad chipsets on android.
c.featureSet.setIf(isSet(hwcap, hwcap_ATOMICS) && runtime.GOOS != "android", ATOMICS)
return true
}
func isSet(hwc uint, value uint) bool {
return hwc&value != 0
}
+16
View File
@@ -0,0 +1,16 @@
// Copyright (c) 2020 Klaus Post, released under MIT License. See LICENSE file.
//go:build arm64 && !linux && !darwin
// +build arm64,!linux,!darwin
package cpuid
import "runtime"
func detectOS(c *CPUInfo) bool {
c.PhysicalCores = runtime.NumCPU()
// For now assuming 1 thread per core...
c.ThreadsPerCore = 1
c.LogicalCores = c.PhysicalCores
return false
}
+8
View File
@@ -0,0 +1,8 @@
// Copyright (c) 2021 Klaus Post, released under MIT License. See LICENSE file.
//go:build nounsafe
// +build nounsafe
package cpuid
var hwcap uint
+11
View File
@@ -0,0 +1,11 @@
// Copyright (c) 2021 Klaus Post, released under MIT License. See LICENSE file.
//go:build !nounsafe
// +build !nounsafe
package cpuid
import _ "unsafe" // needed for go:linkname
//go:linkname hwcap internal/cpu.HWCap
var hwcap uint
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
set -e
go tool dist list | while IFS=/ read os arch; do
echo "Checking $os/$arch..."
echo " normal"
GOARCH=$arch GOOS=$os go build -o /dev/null .
echo " noasm"
GOARCH=$arch GOOS=$os go build -tags noasm -o /dev/null .
echo " appengine"
GOARCH=$arch GOOS=$os go build -tags appengine -o /dev/null .
echo " noasm,appengine"
GOARCH=$arch GOOS=$os go build -tags 'appengine noasm' -o /dev/null .
done
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.27@sha256:e432b43af23a9328d56a7c499be0476810aa344acbcf65fc7c455d4ff5a40602
FROM golang:1.27@sha256:eb37f58646a901dc7727cf448cae36daaefaba79de33b5058dab79aa4c04aefb
ENV GOOS=linux
ENV GOARCH=arm
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.27@sha256:e432b43af23a9328d56a7c499be0476810aa344acbcf65fc7c455d4ff5a40602
FROM golang:1.27@sha256:eb37f58646a901dc7727cf448cae36daaefaba79de33b5058dab79aa4c04aefb
ENV GOOS=linux
ENV GOARCH=arm64
+4 -23
View File
@@ -1,35 +1,18 @@
FUZZ_TIME ?= 1m
FUZZ_FLAGS ?=
# The fuzz targets of each package, along with the tags needed to build them.
FUZZ_TARGETS = ./test/:gofuzz .:sha1cd_asmtest
export CGO_ENABLED := 1
# The hashing tests run a second time with SHA-NI off, so that CPUs with it
# also cover the AVX2 fallback that CPUs without it use.
.PHONY: test
test:
go test -race -timeout 15s -tags sha1cd_asmtest ./...
SHA1CD_TEST_NOSHANI=1 go test -race -timeout 15s -tags sha1cd_asmtest ./test/
go test -race -timeout 15s ./...
.PHONY: bench
bench:
go test -benchmem -run=^$$ -bench ^Benchmark ./...
# go test only fuzzes a single target per invocation, so each one is run in
# turn for FUZZ_TIME.
.PHONY: fuzz
fuzz:
@set -e; for entry in $(FUZZ_TARGETS); do \
pkg="$${entry%%:*}"; tags="$${entry##*:}"; \
listed="$$(go test -tags "$$tags" -list '^Fuzz' "$$pkg")"; \
for target in $$(echo "$$listed" | grep '^Fuzz'); do \
echo "fuzzing $$target in $$pkg for $(FUZZ_TIME)"; \
go test $(FUZZ_FLAGS) -tags "$$tags" -run '^$$' -fuzz "^$$target"'$$' \
-fuzztime=$(FUZZ_TIME) "$$pkg"; \
done; \
done
go test -tags gofuzz -fuzz=. -fuzztime=$(FUZZ_TIME) ./test/
# Cross build project in arm/v7.
build-arm:
@@ -41,11 +24,9 @@ build-arm64:
docker build -t sha1cd-arm64 -f Dockerfile.arm64 .
docker run --rm sha1cd-arm64
# Build with cgo disabled. Vetting every package (not just ./cgo) is what
# catches the cgo and non-cgo builds drifting apart in their exported API.
# Build with cgo disabled.
build-nocgo:
CGO_ENABLED=0 go build ./...
CGO_ENABLED=0 go vet ./...
CGO_ENABLED=0 go build ./cgo
# Run cross-compilation to assure supported architectures.
cross-build: build-arm build-arm64 build-nocgo
-28
View File
@@ -1,28 +0,0 @@
// Package cpu detects the CPU features that sha1cd dispatches on.
//
// It covers only what the assembly implementations need, which keeps
// start up cheap and avoids an external dependency. Every flag is false
// where a feature cannot be detected safely, which selects the generic
// implementation.
package cpu
// X86 holds the features of the current amd64 CPU. All flags are false on
// other architectures.
var X86 struct {
// HasAVX and HasAVX2 are set only when the OS also preserves the YMM
// state, and HasAVX512F only when it preserves the ZMM and opmask state.
HasAVX bool
HasAVX2 bool
HasAVX512F bool
HasBMI1 bool
HasBMI2 bool
HasSHA bool
HasSSSE3 bool
HasSSE41 bool
}
// ARM64 holds the features of the current arm64 CPU. All flags are false on
// other architectures.
var ARM64 struct {
HasSHA1 bool
}
-63
View File
@@ -1,63 +0,0 @@
//go:build !noasm && gc && amd64
package cpu
// cpuid and xgetbv are implemented in cpu_amd64.s.
func cpuid(eaxArg, ecxArg uint32) (eax, ebx, ecx, edx uint32)
func xgetbv() (eax, edx uint32)
func init() {
const (
// CPUID EAX=1: ECX
ssse3 = 1 << 9
sse41 = 1 << 19
osxsave = 1 << 27
avx = 1 << 28
// CPUID EAX=7, ECX=0: EBX
bmi1 = 1 << 3
avx2 = 1 << 5
bmi2 = 1 << 8
avx512f = 1 << 16
sha = 1 << 29
// XCR0
xmmState = 1 << 1
ymmState = 1 << 2
opmaskState = 1 << 5
zmmHi256State = 1 << 6
hi16ZMMState = 1 << 7
zmmState = opmaskState | zmmHi256State | hi16ZMMState
)
maxID, _, _, _ := cpuid(0, 0)
if maxID < 1 {
return
}
_, _, ecx1, _ := cpuid(1, 0)
X86.HasSSSE3 = ecx1&ssse3 != 0
X86.HasSSE41 = ecx1&sse41 != 0
// VEX encoded instructions also need the OS to preserve the YMM state,
// which XGETBV reports once OSXSAVE says it is available.
// macOS enables the ZMM state lazily, so XCR0 may not report it and
// AVX-512 is then left unused, which is safe.
var osYMM, osZMM bool
if ecx1&(osxsave|avx) == osxsave|avx {
xcr0, _ := xgetbv()
osYMM = xcr0&(xmmState|ymmState) == xmmState|ymmState
osZMM = osYMM && xcr0&zmmState == zmmState
}
X86.HasAVX = osYMM
if maxID < 7 {
return
}
_, ebx7, _, _ := cpuid(7, 0)
X86.HasAVX2 = osYMM && ebx7&avx2 != 0
X86.HasAVX512F = osZMM && ebx7&avx512f != 0
X86.HasBMI1 = ebx7&bmi1 != 0
X86.HasBMI2 = ebx7&bmi2 != 0
X86.HasSHA = ebx7&sha != 0
}
-9
View File
@@ -1,9 +0,0 @@
//go:build !noasm && gc && arm64 && darwin
package cpu
// Every Apple arm64 chip implements the ARMv8 Cryptographic Extension. The Go
// runtime makes the same assumption for crypto/sha1 on darwin/arm64.
func init() {
ARM64.HasSHA1 = true
}
-12
View File
@@ -1,12 +0,0 @@
//go:build !noasm && gc && arm64 && freebsd
package cpu
// getisar0 is implemented in cpu_arm64_freebsd.s.
func getisar0() uint64
// FreeBSD emulates user space reads of ID_AA64ISAR0_EL1. Its SHA1 field, bits
// [11:8], is non zero when the SHA1 instructions are implemented.
func init() {
ARM64.HasSHA1 = (getisar0()>>8)&0xf != 0
}
-9
View File
@@ -1,9 +0,0 @@
//go:build !noasm && gc && arm64 && freebsd
#include "textflag.h"
// func getisar0() uint64
TEXT ·getisar0(SB), NOSPLIT, $0-8
MRS ID_AA64ISAR0_EL1, R0
MOVD R0, ret+0(FP)
RET
-29
View File
@@ -1,29 +0,0 @@
//go:build !noasm && gc && arm64 && linux
package cpu
import (
"os"
_ "unsafe" // for go:linkname
)
// runtime_getAuxv returns the auxiliary vector the kernel passed to the
// process. The runtime keeps it reachable for golang.org/x/sys/cpu and
// others, see go.dev/issue/57336 and go.dev/issue/67401.
//
//go:linkname runtime_getAuxv runtime.getAuxv
func runtime_getAuxv() []uintptr
// Linux, and so Android, reports the SHA1 instructions through HWCAP, as
// not every kernel lets user space read the ID registers.
func init() {
hwcap, ok := hwcapFromAuxv(runtime_getAuxv())
if !ok {
// The procfs copy may not be readable in restricted environments,
// in which case the generic implementation is used.
if buf, err := os.ReadFile("/proc/self/auxv"); err == nil {
hwcap, _ = hwcapFromProcAuxv(buf)
}
}
ARM64.HasSHA1 = hwcap&hwcapSHA1 != 0
}
-18
View File
@@ -1,18 +0,0 @@
//go:build !noasm && gc && arm64 && windows
package cpu
import "syscall"
// Windows reports the ARMv8 Cryptographic Extension, which includes the SHA1
// instructions, through IsProcessorFeaturePresent.
func init() {
const _PF_ARM_V8_CRYPTO_INSTRUCTIONS_AVAILABLE = 30
proc := syscall.NewLazyDLL("kernel32.dll").NewProc("IsProcessorFeaturePresent")
if proc.Find() != nil {
return
}
ret, _, _ := proc.Call(_PF_ARM_V8_CRYPTO_INSTRUCTIONS_AVAILABLE)
ARM64.HasSHA1 = ret != 0
}
-32
View File
@@ -1,32 +0,0 @@
package cpu
import "encoding/binary"
const (
_AT_HWCAP = 16
// hwcapSHA1 is HWCAP_SHA1 on linux/arm64.
hwcapSHA1 = 1 << 5
)
// hwcapFromAuxv returns AT_HWCAP from an auxiliary vector of tag and value
// pairs, and whether it was present.
func hwcapFromAuxv(auxv []uintptr) (uint64, bool) {
for i := 0; i+1 < len(auxv); i += 2 {
if auxv[i] == _AT_HWCAP {
return uint64(auxv[i+1]), true
}
}
return 0, false
}
// hwcapFromProcAuxv does the same for the contents of /proc/self/auxv on a
// 64-bit little endian system.
func hwcapFromProcAuxv(buf []byte) (uint64, bool) {
for ; len(buf) >= 16; buf = buf[16:] {
if binary.LittleEndian.Uint64(buf) == _AT_HWCAP {
return binary.LittleEndian.Uint64(buf[8:]), true
}
}
return 0, false
}
+18 -28
View File
@@ -4,39 +4,30 @@
package sha1cd
import (
"runtime"
"github.com/klauspost/cpuid/v2"
shared "github.com/pjbgf/sha1cd/internal"
"github.com/pjbgf/sha1cd/internal/cpu"
"github.com/pjbgf/sha1cd/ubc"
)
// hasSHANI reports whether blockAMD64 can run. It uses legacy SSE encodings
// only, so it does not need AVX and also runs on the Goldmont, Goldmont Plus
// and Tremont Atoms, which implement SHA-NI without AVX.
var hasSHANI = cpu.X86.HasSHA && cpu.X86.HasSSSE3 && cpu.X86.HasSSE41
var hasSHANI = (runtime.GOARCH == "amd64" &&
cpuid.CPU.Supports(cpuid.AVX) &&
cpuid.CPU.Supports(cpuid.SHA) &&
cpuid.CPU.Supports(cpuid.SSE3) &&
cpuid.CPU.Supports(cpuid.SSE4))
// blockAMD64 hashes a single chunk of p into the current state in h.
// p must hold at least one whole chunk. Anything beyond the first chunk is
// ignored, as the collision detection the caller runs afterwards inspects m1
// and cs for one chunk only.
// blockAMD64 hashes the message p into the current state in h.
// Both m1 and cs are used to store intermediate results which are used by the collision detection logic.
//
//go:noescape
func blockAMD64(h []uint32, p []byte, m1 []uint32, cs [][5]uint32)
func block(dig *digest, p []byte) {
switch {
case forceGeneric:
blockGeneric(dig, p)
case hasSHANI:
blockSHANI(dig, p)
case hasAVX2:
blockAVX2(dig, p)
default:
if forceGeneric || !hasSHANI {
blockGeneric(dig, p)
return
}
}
func blockSHANI(dig *digest, p []byte) {
m1 := [shared.Rounds]uint32{}
cs := [shared.PreStepState][shared.WordBuffers]uint32{}
@@ -46,15 +37,14 @@ func blockSHANI(dig *digest, p []byte) {
chunk := p[:shared.Chunk]
blockAMD64(dig.h[:], chunk, m1[:], cs[:])
// Assembly states need repair only when a disturbance vector survives.
if mask := ubc.CalculateDvMask(&m1); mask != 0 {
rectifyCompressionState(&m1, &cs)
if checkCollision(&m1, &cs, &dig.h, mask) {
dig.col = true
rectifyCompressionState(&m1, &cs)
blockAMD64(dig.h[:], chunk, m1[:], cs[:])
blockAMD64(dig.h[:], chunk, m1[:], cs[:])
}
col := checkCollision(&m1, &cs, &dig.h)
if col {
dig.col = true
blockAMD64(dig.h[:], chunk, m1[:], cs[:])
blockAMD64(dig.h[:], chunk, m1[:], cs[:])
}
p = p[shared.Chunk:]
+49 -44
View File
@@ -12,30 +12,27 @@
// - https://github.com/golang/go/blob/master/src/crypto/sha1/sha1block_amd64.s
// Reverse the dword order in abcd via PSHUFD then store the 16 bytes in one
// move, instead of issuing four PEXTRD's that each go through the store port.
// move, instead of issuing four VPEXTRD's that each go through the store port.
#define LOADCS(abcd, e, index, target) \
PSHUFD $0x1B, abcd, X8; \
MOVOU X8, ((index*20)+0)(target); \
VPSHUFD $0x1B, abcd, X8; \
VMOVDQU X8, ((index*20)+0)(target); \
MOVL e, ((index*20)+16)(target);
#define LOADM1(m1, index, target) \
PSHUFD $0x1B, m1, X8; \
MOVOU X8, ((index*16)+0)(target);
VPSHUFD $0x1B, m1, X8; \
VMOVDQU X8, ((index*16)+0)(target);
// func blockAMD64(h []uint32, p []byte, m1 []uint32, cs [][5]uint32)
// Requires: SHA, SSE2, SSE4.1, SSSE3
// Requires: AVX, SHA, SSE2, SSE4.1, SSSE3
TEXT ·blockAMD64(SB), NOSPLIT, $80-96
MOVQ h_base+0(FP), DI
MOVQ p_base+24(FP), SI
MOVQ p_len+32(FP), DX
MOVQ m1_base+48(FP), R13
MOVQ cs_base+72(FP), R15
// Truncate the length to whole chunks and skip the block if none is left.
// The caller compresses exactly one chunk per call, so that the collision
// detection it runs afterwards sees this chunk's m1 and cs.
ANDQ $-64, DX
JZ done
CMPQ DX, $0x00
JEQ done
ADDQ SI, DX
// Allocate space on the stack for saving ABCD and E0, and align it to 16 bytes
LEAQ 15(SP), AX
@@ -45,51 +42,52 @@ TEXT ·blockAMD64(SB), NOSPLIT, $80-96
// Load initial hash state
PINSRD $0x03, 16(DI), X5
MOVOU (DI), X0
VMOVDQU (DI), X0
PAND upper_mask<>+0(SB), X5
PSHUFD $0x1b, X0, X0
MOVO shuffle_mask<>+0(SB), X7
VMOVDQA shuffle_mask<>+0(SB), X7
loop:
// Save ABCD and E working values
MOVO X5, (AX)
MOVO X0, 16(AX)
VMOVDQA X5, (AX)
VMOVDQA X0, 16(AX)
// LOAD CS 0
PEXTRD $3, X5, R12
VPEXTRD $3, X5, R12
LOADCS(X0, R12, 0, R15)
// Rounds 0-3
MOVOU (SI), X1
VMOVDQU (SI), X1
PSHUFB X7, X1
PADDD X1, X5
MOVO X0, X6
VMOVDQA X0, X6
SHA1RNDS4 $0x00, X5, X0
LOADM1(X1, 0, R13)
// Rounds 4-7
MOVOU 16(SI), X2
VMOVDQU 16(SI), X2
PSHUFB X7, X2
SHA1NEXTE X2, X6
MOVO X0, X5
VMOVDQA X0, X5
SHA1RNDS4 $0x00, X6, X0
SHA1MSG1 X2, X1
LOADM1(X2, 1, R13)
// Rounds 8-11
MOVOU 32(SI), X3
VMOVDQU 32(SI), X3
PSHUFB X7, X3
SHA1NEXTE X3, X5
MOVO X0, X6
VMOVDQA X0, X6
SHA1RNDS4 $0x00, X5, X0
SHA1MSG1 X3, X2
PXOR X3, X1
LOADM1(X3, 2, R13)
// Rounds 12-15
MOVOU 48(SI), X4
VMOVDQU 48(SI), X4
PSHUFB X7, X4
SHA1NEXTE X4, X6
MOVO X0, X5
VMOVDQA X0, X5
SHA1MSG2 X4, X1
SHA1RNDS4 $0x00, X6, X0
SHA1MSG1 X4, X3
@@ -98,7 +96,7 @@ TEXT ·blockAMD64(SB), NOSPLIT, $80-96
// Rounds 16-19
SHA1NEXTE X1, X5
MOVO X0, X6
VMOVDQA X0, X6
SHA1MSG2 X1, X2
SHA1RNDS4 $0x00, X5, X0
SHA1MSG1 X1, X4
@@ -107,7 +105,7 @@ TEXT ·blockAMD64(SB), NOSPLIT, $80-96
// Rounds 20-23
SHA1NEXTE X2, X6
MOVO X0, X5
VMOVDQA X0, X5
SHA1MSG2 X2, X3
SHA1RNDS4 $0x01, X6, X0
SHA1MSG1 X2, X1
@@ -116,7 +114,7 @@ TEXT ·blockAMD64(SB), NOSPLIT, $80-96
// Rounds 24-27
SHA1NEXTE X3, X5
MOVO X0, X6
VMOVDQA X0, X6
SHA1MSG2 X3, X4
SHA1RNDS4 $0x01, X5, X0
SHA1MSG1 X3, X2
@@ -125,7 +123,7 @@ TEXT ·blockAMD64(SB), NOSPLIT, $80-96
// Rounds 28-31
SHA1NEXTE X4, X6
MOVO X0, X5
VMOVDQA X0, X5
SHA1MSG2 X4, X1
SHA1RNDS4 $0x01, X6, X0
SHA1MSG1 X4, X3
@@ -134,7 +132,7 @@ TEXT ·blockAMD64(SB), NOSPLIT, $80-96
// Rounds 32-35
SHA1NEXTE X1, X5
MOVO X0, X6
VMOVDQA X0, X6
SHA1MSG2 X1, X2
SHA1RNDS4 $0x01, X5, X0
SHA1MSG1 X1, X4
@@ -143,7 +141,7 @@ TEXT ·blockAMD64(SB), NOSPLIT, $80-96
// Rounds 36-39
SHA1NEXTE X2, X6
MOVO X0, X5
VMOVDQA X0, X5
SHA1MSG2 X2, X3
SHA1RNDS4 $0x01, X6, X0
SHA1MSG1 X2, X1
@@ -152,7 +150,7 @@ TEXT ·blockAMD64(SB), NOSPLIT, $80-96
// Rounds 40-43
SHA1NEXTE X3, X5
MOVO X0, X6
VMOVDQA X0, X6
SHA1MSG2 X3, X4
SHA1RNDS4 $0x02, X5, X0
SHA1MSG1 X3, X2
@@ -161,7 +159,7 @@ TEXT ·blockAMD64(SB), NOSPLIT, $80-96
// Rounds 44-47
SHA1NEXTE X4, X6
MOVO X0, X5
VMOVDQA X0, X5
SHA1MSG2 X4, X1
SHA1RNDS4 $0x02, X6, X0
SHA1MSG1 X4, X3
@@ -170,20 +168,21 @@ TEXT ·blockAMD64(SB), NOSPLIT, $80-96
// Rounds 48-51
SHA1NEXTE X1, X5
MOVO X0, X6
VMOVDQA X0, X6
SHA1MSG2 X1, X2
SHA1RNDS4 $0x02, X5, X0
VPEXTRD $0, X5, R12
SHA1MSG1 X1, X4
PXOR X1, X3
LOADM1(X1, 12, R13)
// derive pre-round 56's E out of round 51's A.
PEXTRD $3, X0, R12
VPEXTRD $3, X0, R12
ROLL $30, R12
// Rounds 52-55
SHA1NEXTE X2, X6
MOVO X0, X5
VMOVDQA X0, X5
SHA1MSG2 X2, X3
SHA1RNDS4 $0x02, X6, X0
SHA1MSG1 X2, X1
@@ -195,20 +194,21 @@ TEXT ·blockAMD64(SB), NOSPLIT, $80-96
// Rounds 56-59
SHA1NEXTE X3, X5
MOVO X0, X6
VMOVDQA X0, X6
SHA1MSG2 X3, X4
SHA1RNDS4 $0x02, X5, X0
VPEXTRD $0, X5, R12
SHA1MSG1 X3, X2
PXOR X3, X1
LOADM1(X3, 14, R13)
// derive pre-round 64's E out of round 59's A.
PEXTRD $3, X0, R12
VPEXTRD $3, X0, R12
ROLL $30, R12
// Rounds 60-63
SHA1NEXTE X4, X6
MOVO X0, X5
VMOVDQA X0, X5
SHA1MSG2 X4, X1
SHA1RNDS4 $0x03, X6, X0
SHA1MSG1 X4, X3
@@ -220,7 +220,7 @@ TEXT ·blockAMD64(SB), NOSPLIT, $80-96
// Rounds 64-67
SHA1NEXTE X1, X5
MOVO X0, X6
VMOVDQA X0, X6
SHA1MSG2 X1, X2
SHA1RNDS4 $0x03, X5, X0
SHA1MSG1 X1, X4
@@ -229,7 +229,7 @@ TEXT ·blockAMD64(SB), NOSPLIT, $80-96
// Rounds 68-71
SHA1NEXTE X2, X6
MOVO X0, X5
VMOVDQA X0, X5
SHA1MSG2 X2, X3
SHA1RNDS4 $0x03, X6, X0
PXOR X2, X4
@@ -237,14 +237,14 @@ TEXT ·blockAMD64(SB), NOSPLIT, $80-96
// Rounds 72-75
SHA1NEXTE X3, X5
MOVO X0, X6
VMOVDQA X0, X6
SHA1MSG2 X3, X4
SHA1RNDS4 $0x03, X5, X0
LOADM1(X3, 18, R13)
// Rounds 76-79
SHA1NEXTE X4, X6
MOVO X0, X5
VMOVDQA X0, X5
SHA1RNDS4 $0x03, X6, X0
LOADM1(X4, 19, R13)
@@ -252,9 +252,14 @@ TEXT ·blockAMD64(SB), NOSPLIT, $80-96
SHA1NEXTE (AX), X5
PADDD 16(AX), X0
// Check if we are done, if not return to the loop
ADDQ $0x40, SI
CMPQ SI, DX
JNE loop
// Write the hash state back to digest
PSHUFD $0x1b, X0, X0
MOVOU X0, (DI)
VMOVDQU X0, (DI)
PEXTRD $0x03, X5, 16(DI)
done:
+11 -15
View File
@@ -4,17 +4,15 @@
package sha1cd
import (
"runtime"
"github.com/klauspost/cpuid/v2"
shared "github.com/pjbgf/sha1cd/internal"
"github.com/pjbgf/sha1cd/internal/cpu"
"github.com/pjbgf/sha1cd/ubc"
)
var hasSHA1 = cpu.ARM64.HasSHA1
var hasSHA1 = (runtime.GOARCH == "arm64" && cpuid.CPU.Supports(cpuid.SHA1))
// blockARM64 hashes a single chunk of p into the current state in h.
// p must hold at least one whole chunk. Anything beyond the first chunk is
// ignored, as the collision detection the caller runs afterwards inspects m1
// and cs for one chunk only.
// blockARM64 hashes the message p into the current state in h.
// Both m1 and cs are used to store intermediate results which are used by the collision detection logic.
//
//go:noescape
@@ -36,15 +34,13 @@ func block(dig *digest, p []byte) {
blockARM64(dig.h[:], chunk, m1[:], cs[:])
// Assembly states need repair only when a disturbance vector survives.
if mask := ubc.CalculateDvMask(&m1); mask != 0 {
rectifyCompressionState(&m1, &cs)
if checkCollision(&m1, &cs, &dig.h, mask) {
dig.col = true
rectifyCompressionState(&m1, &cs)
col := checkCollision(&m1, &cs, &dig.h)
if col {
dig.col = true
blockARM64(dig.h[:], chunk, m1[:], cs[:])
blockARM64(dig.h[:], chunk, m1[:], cs[:])
}
blockARM64(dig.h[:], chunk, m1[:], cs[:])
blockARM64(dig.h[:], chunk, m1[:], cs[:])
}
p = p[shared.Chunk:]
+4 -2
View File
@@ -37,13 +37,15 @@ TEXT ·blockARM64(SB), NOSPLIT, $80-96
LSR $6, R2, R2
LSL $6, R2, R2
ADD R16, R2, R21
VLD1.P 16(R0), [V0.S4]
FMOVS (R0), F20
SUB $16, R0, R0
// The caller passes exactly one block; skip hashing only if p is shorter.
CBZ R2, end
loop:
CMP R16, R21
BLS end
// Load block (p) into 16-bytes vectors.
VLD1.P 16(R1), [V4.B16]
-100
View File
@@ -1,100 +0,0 @@
//go:build !noasm && gc && arm64 && !amd64 && sha1cd_asmtest
#include "textflag.h"
// callBlockARM64DirtyRegs calls blockARM64 with every general-purpose and
// vector register the caller does not own set to all ones. It is only built
// with the sha1cd_asmtest tag, to catch the assembly reading a register before
// writing it.
//
// func callBlockARM64DirtyRegs(h []uint32, p []byte, m1 []uint32, cs [][5]uint32)
TEXT ·callBlockARM64DirtyRegs(SB), NOSPLIT, $104-96
MOVD h_base+0(FP), R0
MOVD R0, 8(RSP)
MOVD h_len+8(FP), R0
MOVD R0, 16(RSP)
MOVD h_cap+16(FP), R0
MOVD R0, 24(RSP)
MOVD p_base+24(FP), R0
MOVD R0, 32(RSP)
MOVD p_len+32(FP), R0
MOVD R0, 40(RSP)
MOVD p_cap+40(FP), R0
MOVD R0, 48(RSP)
MOVD m1_base+48(FP), R0
MOVD R0, 56(RSP)
MOVD m1_len+56(FP), R0
MOVD R0, 64(RSP)
MOVD m1_cap+64(FP), R0
MOVD R0, 72(RSP)
MOVD cs_base+72(FP), R0
MOVD R0, 80(RSP)
MOVD cs_len+80(FP), R0
MOVD R0, 88(RSP)
MOVD cs_cap+88(FP), R0
MOVD R0, 96(RSP)
// R18 is reserved by the platform, R27 by the assembler, R28 holds g,
// R29 is the frame pointer and R30 the link register.
MOVD $-1, R0
MOVD R0, R1
MOVD R0, R2
MOVD R0, R3
MOVD R0, R4
MOVD R0, R5
MOVD R0, R6
MOVD R0, R7
MOVD R0, R8
MOVD R0, R9
MOVD R0, R10
MOVD R0, R11
MOVD R0, R12
MOVD R0, R13
MOVD R0, R14
MOVD R0, R15
MOVD R0, R16
MOVD R0, R17
MOVD R0, R19
MOVD R0, R20
MOVD R0, R21
MOVD R0, R22
MOVD R0, R23
MOVD R0, R24
MOVD R0, R25
MOVD R0, R26
VMOVI $0xff, V0.B16
VMOVI $0xff, V1.B16
VMOVI $0xff, V2.B16
VMOVI $0xff, V3.B16
VMOVI $0xff, V4.B16
VMOVI $0xff, V5.B16
VMOVI $0xff, V6.B16
VMOVI $0xff, V7.B16
VMOVI $0xff, V8.B16
VMOVI $0xff, V9.B16
VMOVI $0xff, V10.B16
VMOVI $0xff, V11.B16
VMOVI $0xff, V12.B16
VMOVI $0xff, V13.B16
VMOVI $0xff, V14.B16
VMOVI $0xff, V15.B16
VMOVI $0xff, V16.B16
VMOVI $0xff, V17.B16
VMOVI $0xff, V18.B16
VMOVI $0xff, V19.B16
VMOVI $0xff, V20.B16
VMOVI $0xff, V21.B16
VMOVI $0xff, V22.B16
VMOVI $0xff, V23.B16
VMOVI $0xff, V24.B16
VMOVI $0xff, V25.B16
VMOVI $0xff, V26.B16
VMOVI $0xff, V27.B16
VMOVI $0xff, V28.B16
VMOVI $0xff, V29.B16
VMOVI $0xff, V30.B16
VMOVI $0xff, V31.B16
BL ·blockARM64(SB)
RET
-55
View File
@@ -1,55 +0,0 @@
//go:build !noasm && gc && amd64
package sha1cd
import (
shared "github.com/pjbgf/sha1cd/internal"
"github.com/pjbgf/sha1cd/internal/cpu"
"github.com/pjbgf/sha1cd/ubc"
)
// hasAVX2 reports whether blockAVX2 can run. It is the fallback for CPUs
// without SHA-NI, such as Intel's big cores before Ice Lake.
var hasAVX2 = cpu.X86.HasAVX2 && cpu.X86.HasBMI1 && cpu.X86.HasBMI2
// scheduleAVX2 expands the message schedules of the blocks at pa and pb, which
// may be the same, into m1a and m1b.
//
//go:noescape
func scheduleAVX2(pa, pb *byte, m1a, m1b *[shared.Rounds]uint32)
// roundsBMI2 compresses the block whose schedule is in m1 into h, and stores
// the states before steps 0, 58 and 65 into cs.
//
//go:noescape
func roundsBMI2(h *[shared.WordBuffers]uint32, m1 *[shared.Rounds]uint32,
cs *[shared.PreStepState][shared.WordBuffers]uint32)
func blockAVX2(dig *digest, p []byte) {
var m1 [2][shared.Rounds]uint32
cs := [shared.PreStepState][shared.WordBuffers]uint32{}
for len(p) >= shared.Chunk {
// The schedules do not depend on the chaining state, so expand two
// blocks at once. The rounds must still run one block at a time, as a
// detected collision changes the state the next block starts from.
n, pb := 1, p
if len(p) >= 2*shared.Chunk {
n, pb = 2, p[shared.Chunk:]
}
scheduleAVX2(&p[0], &pb[0], &m1[0], &m1[1])
for j := 0; j < n; j++ {
w := &m1[j]
roundsBMI2(&dig.h, w, &cs)
if mask := ubc.CalculateDvMask(w); mask != 0 && checkCollision(w, &cs, &dig.h, mask) {
dig.col = true
roundsBMI2(&dig.h, w, &cs)
roundsBMI2(&dig.h, w, &cs)
}
}
p = p[n*shared.Chunk:]
}
}
-415
View File
@@ -1,415 +0,0 @@
//go:build !noasm && gc && amd64
#include "textflag.h"
// The fallback for CPUs without SHA-NI. The message schedule is expanded with
// AVX2 for two blocks at a time, one per 128-bit lane, as it does not depend
// on the chaining state. The rounds then run one block at a time with scalar
// BMI instructions, so that the collision detection can run between blocks
// and gets the exact states before steps 58 and 65, with no repair needed.
// func scheduleAVX2(pa, pb *byte, m1a, m1b *[80]uint32)
// Requires: AVX, AVX2
//
// Expands the blocks at pa and pb, which may be the same, into m1a and m1b.
// Y0-Y7 hold the last eight groups of four words, group g in Y(g%8).
TEXT ·scheduleAVX2(SB), NOSPLIT, $0-32
MOVQ pa+0(FP), AX
MOVQ pb+8(FP), BX
MOVQ m1a+16(FP), CX
MOVQ m1b+24(FP), DX
VBROADCASTI128 bswap_mask<>(SB), Y13
// W[0..3] loaded from the blocks
VMOVDQU 0(AX), X0
VINSERTI128 $1, 0(BX), Y0, Y0
VPSHUFB Y13, Y0, Y0
VMOVDQU X0, 0(CX)
VEXTRACTI128 $1, Y0, 0(DX)
// W[4..7] loaded from the blocks
VMOVDQU 16(AX), X1
VINSERTI128 $1, 16(BX), Y1, Y1
VPSHUFB Y13, Y1, Y1
VMOVDQU X1, 16(CX)
VEXTRACTI128 $1, Y1, 16(DX)
// W[8..11] loaded from the blocks
VMOVDQU 32(AX), X2
VINSERTI128 $1, 32(BX), Y2, Y2
VPSHUFB Y13, Y2, Y2
VMOVDQU X2, 32(CX)
VEXTRACTI128 $1, Y2, 32(DX)
// W[12..15] loaded from the blocks
VMOVDQU 48(AX), X3
VINSERTI128 $1, 48(BX), Y3, Y3
VPSHUFB Y13, Y3, Y3
VMOVDQU X3, 48(CX)
VEXTRACTI128 $1, Y3, 48(DX)
// W[16..19] = rol1(W[i-3] ^ W[i-8] ^ W[i-14] ^ W[i-16]). W[i+3] needs
// W[i] from this group, which is added in afterwards.
VPALIGNR $8, Y0, Y1, Y8 // W[i-14..i-11]
VPXOR Y0, Y8, Y8 // W[i-16..i-13]
VPXOR Y2, Y8, Y8 // W[i-8..i-5]
VPSRLDQ $4, Y3, Y9 // W[i-3..i-1], 0
VPXOR Y9, Y8, Y8
VPSRLD $31, Y8, Y9
VPSLLD $1, Y8, Y4
VPOR Y9, Y4, Y4
VPSLLDQ $12, Y8, Y10 // 0, 0, 0, the input to W[i]
VPSRLD $30, Y10, Y9
VPSLLD $2, Y10, Y10
VPOR Y9, Y10, Y10 // rol1(W[i]) in the lane of W[i+3]
VPXOR Y10, Y4, Y4
VMOVDQU X4, 64(CX)
VEXTRACTI128 $1, Y4, 64(DX)
// W[20..23] = rol1(W[i-3] ^ W[i-8] ^ W[i-14] ^ W[i-16]). W[i+3] needs
// W[i] from this group, which is added in afterwards.
VPALIGNR $8, Y1, Y2, Y8 // W[i-14..i-11]
VPXOR Y1, Y8, Y8 // W[i-16..i-13]
VPXOR Y3, Y8, Y8 // W[i-8..i-5]
VPSRLDQ $4, Y4, Y9 // W[i-3..i-1], 0
VPXOR Y9, Y8, Y8
VPSRLD $31, Y8, Y9
VPSLLD $1, Y8, Y5
VPOR Y9, Y5, Y5
VPSLLDQ $12, Y8, Y10 // 0, 0, 0, the input to W[i]
VPSRLD $30, Y10, Y9
VPSLLD $2, Y10, Y10
VPOR Y9, Y10, Y10 // rol1(W[i]) in the lane of W[i+3]
VPXOR Y10, Y5, Y5
VMOVDQU X5, 80(CX)
VEXTRACTI128 $1, Y5, 80(DX)
// W[24..27] = rol1(W[i-3] ^ W[i-8] ^ W[i-14] ^ W[i-16]). W[i+3] needs
// W[i] from this group, which is added in afterwards.
VPALIGNR $8, Y2, Y3, Y8 // W[i-14..i-11]
VPXOR Y2, Y8, Y8 // W[i-16..i-13]
VPXOR Y4, Y8, Y8 // W[i-8..i-5]
VPSRLDQ $4, Y5, Y9 // W[i-3..i-1], 0
VPXOR Y9, Y8, Y8
VPSRLD $31, Y8, Y9
VPSLLD $1, Y8, Y6
VPOR Y9, Y6, Y6
VPSLLDQ $12, Y8, Y10 // 0, 0, 0, the input to W[i]
VPSRLD $30, Y10, Y9
VPSLLD $2, Y10, Y10
VPOR Y9, Y10, Y10 // rol1(W[i]) in the lane of W[i+3]
VPXOR Y10, Y6, Y6
VMOVDQU X6, 96(CX)
VEXTRACTI128 $1, Y6, 96(DX)
// W[28..31] = rol1(W[i-3] ^ W[i-8] ^ W[i-14] ^ W[i-16]). W[i+3] needs
// W[i] from this group, which is added in afterwards.
VPALIGNR $8, Y3, Y4, Y8 // W[i-14..i-11]
VPXOR Y3, Y8, Y8 // W[i-16..i-13]
VPXOR Y5, Y8, Y8 // W[i-8..i-5]
VPSRLDQ $4, Y6, Y9 // W[i-3..i-1], 0
VPXOR Y9, Y8, Y8
VPSRLD $31, Y8, Y9
VPSLLD $1, Y8, Y7
VPOR Y9, Y7, Y7
VPSLLDQ $12, Y8, Y10 // 0, 0, 0, the input to W[i]
VPSRLD $30, Y10, Y9
VPSLLD $2, Y10, Y10
VPOR Y9, Y10, Y10 // rol1(W[i]) in the lane of W[i+3]
VPXOR Y10, Y7, Y7
VMOVDQU X7, 112(CX)
VEXTRACTI128 $1, Y7, 112(DX)
// W[32..35] = rol2(W[i-6] ^ W[i-16] ^ W[i-28] ^ W[i-32])
VPALIGNR $8, Y6, Y7, Y8 // W[i-6..i-3]
VPXOR Y4, Y8, Y8 // W[i-16..i-13]
VPXOR Y1, Y8, Y8 // W[i-28..i-25]
VPXOR Y0, Y8, Y8 // W[i-32..i-29]
VPSRLD $30, Y8, Y9
VPSLLD $2, Y8, Y0
VPOR Y9, Y0, Y0
VMOVDQU X0, 128(CX)
VEXTRACTI128 $1, Y0, 128(DX)
// W[36..39] = rol2(W[i-6] ^ W[i-16] ^ W[i-28] ^ W[i-32])
VPALIGNR $8, Y7, Y0, Y8 // W[i-6..i-3]
VPXOR Y5, Y8, Y8 // W[i-16..i-13]
VPXOR Y2, Y8, Y8 // W[i-28..i-25]
VPXOR Y1, Y8, Y8 // W[i-32..i-29]
VPSRLD $30, Y8, Y9
VPSLLD $2, Y8, Y1
VPOR Y9, Y1, Y1
VMOVDQU X1, 144(CX)
VEXTRACTI128 $1, Y1, 144(DX)
// W[40..43] = rol2(W[i-6] ^ W[i-16] ^ W[i-28] ^ W[i-32])
VPALIGNR $8, Y0, Y1, Y8 // W[i-6..i-3]
VPXOR Y6, Y8, Y8 // W[i-16..i-13]
VPXOR Y3, Y8, Y8 // W[i-28..i-25]
VPXOR Y2, Y8, Y8 // W[i-32..i-29]
VPSRLD $30, Y8, Y9
VPSLLD $2, Y8, Y2
VPOR Y9, Y2, Y2
VMOVDQU X2, 160(CX)
VEXTRACTI128 $1, Y2, 160(DX)
// W[44..47] = rol2(W[i-6] ^ W[i-16] ^ W[i-28] ^ W[i-32])
VPALIGNR $8, Y1, Y2, Y8 // W[i-6..i-3]
VPXOR Y7, Y8, Y8 // W[i-16..i-13]
VPXOR Y4, Y8, Y8 // W[i-28..i-25]
VPXOR Y3, Y8, Y8 // W[i-32..i-29]
VPSRLD $30, Y8, Y9
VPSLLD $2, Y8, Y3
VPOR Y9, Y3, Y3
VMOVDQU X3, 176(CX)
VEXTRACTI128 $1, Y3, 176(DX)
// W[48..51] = rol2(W[i-6] ^ W[i-16] ^ W[i-28] ^ W[i-32])
VPALIGNR $8, Y2, Y3, Y8 // W[i-6..i-3]
VPXOR Y0, Y8, Y8 // W[i-16..i-13]
VPXOR Y5, Y8, Y8 // W[i-28..i-25]
VPXOR Y4, Y8, Y8 // W[i-32..i-29]
VPSRLD $30, Y8, Y9
VPSLLD $2, Y8, Y4
VPOR Y9, Y4, Y4
VMOVDQU X4, 192(CX)
VEXTRACTI128 $1, Y4, 192(DX)
// W[52..55] = rol2(W[i-6] ^ W[i-16] ^ W[i-28] ^ W[i-32])
VPALIGNR $8, Y3, Y4, Y8 // W[i-6..i-3]
VPXOR Y1, Y8, Y8 // W[i-16..i-13]
VPXOR Y6, Y8, Y8 // W[i-28..i-25]
VPXOR Y5, Y8, Y8 // W[i-32..i-29]
VPSRLD $30, Y8, Y9
VPSLLD $2, Y8, Y5
VPOR Y9, Y5, Y5
VMOVDQU X5, 208(CX)
VEXTRACTI128 $1, Y5, 208(DX)
// W[56..59] = rol2(W[i-6] ^ W[i-16] ^ W[i-28] ^ W[i-32])
VPALIGNR $8, Y4, Y5, Y8 // W[i-6..i-3]
VPXOR Y2, Y8, Y8 // W[i-16..i-13]
VPXOR Y7, Y8, Y8 // W[i-28..i-25]
VPXOR Y6, Y8, Y8 // W[i-32..i-29]
VPSRLD $30, Y8, Y9
VPSLLD $2, Y8, Y6
VPOR Y9, Y6, Y6
VMOVDQU X6, 224(CX)
VEXTRACTI128 $1, Y6, 224(DX)
// W[60..63] = rol2(W[i-6] ^ W[i-16] ^ W[i-28] ^ W[i-32])
VPALIGNR $8, Y5, Y6, Y8 // W[i-6..i-3]
VPXOR Y3, Y8, Y8 // W[i-16..i-13]
VPXOR Y0, Y8, Y8 // W[i-28..i-25]
VPXOR Y7, Y8, Y8 // W[i-32..i-29]
VPSRLD $30, Y8, Y9
VPSLLD $2, Y8, Y7
VPOR Y9, Y7, Y7
VMOVDQU X7, 240(CX)
VEXTRACTI128 $1, Y7, 240(DX)
// W[64..67] = rol2(W[i-6] ^ W[i-16] ^ W[i-28] ^ W[i-32])
VPALIGNR $8, Y6, Y7, Y8 // W[i-6..i-3]
VPXOR Y4, Y8, Y8 // W[i-16..i-13]
VPXOR Y1, Y8, Y8 // W[i-28..i-25]
VPXOR Y0, Y8, Y8 // W[i-32..i-29]
VPSRLD $30, Y8, Y9
VPSLLD $2, Y8, Y0
VPOR Y9, Y0, Y0
VMOVDQU X0, 256(CX)
VEXTRACTI128 $1, Y0, 256(DX)
// W[68..71] = rol2(W[i-6] ^ W[i-16] ^ W[i-28] ^ W[i-32])
VPALIGNR $8, Y7, Y0, Y8 // W[i-6..i-3]
VPXOR Y5, Y8, Y8 // W[i-16..i-13]
VPXOR Y2, Y8, Y8 // W[i-28..i-25]
VPXOR Y1, Y8, Y8 // W[i-32..i-29]
VPSRLD $30, Y8, Y9
VPSLLD $2, Y8, Y1
VPOR Y9, Y1, Y1
VMOVDQU X1, 272(CX)
VEXTRACTI128 $1, Y1, 272(DX)
// W[72..75] = rol2(W[i-6] ^ W[i-16] ^ W[i-28] ^ W[i-32])
VPALIGNR $8, Y0, Y1, Y8 // W[i-6..i-3]
VPXOR Y6, Y8, Y8 // W[i-16..i-13]
VPXOR Y3, Y8, Y8 // W[i-28..i-25]
VPXOR Y2, Y8, Y8 // W[i-32..i-29]
VPSRLD $30, Y8, Y9
VPSLLD $2, Y8, Y2
VPOR Y9, Y2, Y2
VMOVDQU X2, 288(CX)
VEXTRACTI128 $1, Y2, 288(DX)
// W[76..79] = rol2(W[i-6] ^ W[i-16] ^ W[i-28] ^ W[i-32])
VPALIGNR $8, Y1, Y2, Y8 // W[i-6..i-3]
VPXOR Y7, Y8, Y8 // W[i-16..i-13]
VPXOR Y4, Y8, Y8 // W[i-28..i-25]
VPXOR Y3, Y8, Y8 // W[i-32..i-29]
VPSRLD $30, Y8, Y9
VPSLLD $2, Y8, Y3
VPOR Y9, Y3, Y3
VMOVDQU X3, 304(CX)
VEXTRACTI128 $1, Y3, 304(DX)
VZEROUPPER
RET
// Each round adds W[i], K, f(b, c, d) and rol5(a) into e and rotates b by 30.
// The callers rotate the register names instead of moving values around, and
// rol5(a) goes in last, as a is the only input the previous round produced.
#define ROUND_CH(a, b, c, d, e, i) \
ADDL ((i)*4)(SI), e; \
ADDL $0x5a827999, e; \
ANDNL d, b, AX; \
MOVL c, BX; \
ANDL b, BX; \
ADDL AX, e; \
ADDL BX, e; \
RORXL $27, a, CX; \
RORXL $2, b, b; \
ADDL CX, e
#define ROUND_PARITY(a, b, c, d, e, i, k) \
ADDL ((i)*4)(SI), e; \
ADDL k, e; \
MOVL b, AX; \
XORL c, AX; \
XORL d, AX; \
ADDL AX, e; \
RORXL $27, a, CX; \
RORXL $2, b, b; \
ADDL CX, e
// maj(b, c, d) = (b & c) + ((b ^ c) & d), as the two never share a bit.
#define ROUND_MAJ(a, b, c, d, e, i) \
ADDL ((i)*4)(SI), e; \
ADDL $0x8f1bbcdc, e; \
MOVL b, AX; \
XORL c, AX; \
ANDL d, AX; \
MOVL b, BX; \
ANDL c, BX; \
ADDL AX, e; \
ADDL BX, e; \
RORXL $27, a, CX; \
RORXL $2, b, b; \
ADDL CX, e
#define SAVECS(a, b, c, d, e, index) \
MOVL a, ((index)*20+0)(DX); \
MOVL b, ((index)*20+4)(DX); \
MOVL c, ((index)*20+8)(DX); \
MOVL d, ((index)*20+12)(DX); \
MOVL e, ((index)*20+16)(DX)
// func roundsBMI2(h *[5]uint32, m1 *[80]uint32, cs *[3][5]uint32)
// Requires: BMI1, BMI2
//
// Compresses the block whose schedule is in m1 into h, storing the states
// before steps 0, 58 and 65 into cs.
TEXT ·roundsBMI2(SB), NOSPLIT, $0-24
MOVQ h+0(FP), DI
MOVQ m1+8(FP), SI
MOVQ cs+16(FP), DX
MOVL 0(DI), R8
MOVL 4(DI), R9
MOVL 8(DI), R10
MOVL 12(DI), R11
MOVL 16(DI), R12
SAVECS(R8, R9, R10, R11, R12, 0)
ROUND_CH(R8, R9, R10, R11, R12, 0)
ROUND_CH(R12, R8, R9, R10, R11, 1)
ROUND_CH(R11, R12, R8, R9, R10, 2)
ROUND_CH(R10, R11, R12, R8, R9, 3)
ROUND_CH(R9, R10, R11, R12, R8, 4)
ROUND_CH(R8, R9, R10, R11, R12, 5)
ROUND_CH(R12, R8, R9, R10, R11, 6)
ROUND_CH(R11, R12, R8, R9, R10, 7)
ROUND_CH(R10, R11, R12, R8, R9, 8)
ROUND_CH(R9, R10, R11, R12, R8, 9)
ROUND_CH(R8, R9, R10, R11, R12, 10)
ROUND_CH(R12, R8, R9, R10, R11, 11)
ROUND_CH(R11, R12, R8, R9, R10, 12)
ROUND_CH(R10, R11, R12, R8, R9, 13)
ROUND_CH(R9, R10, R11, R12, R8, 14)
ROUND_CH(R8, R9, R10, R11, R12, 15)
ROUND_CH(R12, R8, R9, R10, R11, 16)
ROUND_CH(R11, R12, R8, R9, R10, 17)
ROUND_CH(R10, R11, R12, R8, R9, 18)
ROUND_CH(R9, R10, R11, R12, R8, 19)
ROUND_PARITY(R8, R9, R10, R11, R12, 20, $0x6ed9eba1)
ROUND_PARITY(R12, R8, R9, R10, R11, 21, $0x6ed9eba1)
ROUND_PARITY(R11, R12, R8, R9, R10, 22, $0x6ed9eba1)
ROUND_PARITY(R10, R11, R12, R8, R9, 23, $0x6ed9eba1)
ROUND_PARITY(R9, R10, R11, R12, R8, 24, $0x6ed9eba1)
ROUND_PARITY(R8, R9, R10, R11, R12, 25, $0x6ed9eba1)
ROUND_PARITY(R12, R8, R9, R10, R11, 26, $0x6ed9eba1)
ROUND_PARITY(R11, R12, R8, R9, R10, 27, $0x6ed9eba1)
ROUND_PARITY(R10, R11, R12, R8, R9, 28, $0x6ed9eba1)
ROUND_PARITY(R9, R10, R11, R12, R8, 29, $0x6ed9eba1)
ROUND_PARITY(R8, R9, R10, R11, R12, 30, $0x6ed9eba1)
ROUND_PARITY(R12, R8, R9, R10, R11, 31, $0x6ed9eba1)
ROUND_PARITY(R11, R12, R8, R9, R10, 32, $0x6ed9eba1)
ROUND_PARITY(R10, R11, R12, R8, R9, 33, $0x6ed9eba1)
ROUND_PARITY(R9, R10, R11, R12, R8, 34, $0x6ed9eba1)
ROUND_PARITY(R8, R9, R10, R11, R12, 35, $0x6ed9eba1)
ROUND_PARITY(R12, R8, R9, R10, R11, 36, $0x6ed9eba1)
ROUND_PARITY(R11, R12, R8, R9, R10, 37, $0x6ed9eba1)
ROUND_PARITY(R10, R11, R12, R8, R9, 38, $0x6ed9eba1)
ROUND_PARITY(R9, R10, R11, R12, R8, 39, $0x6ed9eba1)
ROUND_MAJ(R8, R9, R10, R11, R12, 40)
ROUND_MAJ(R12, R8, R9, R10, R11, 41)
ROUND_MAJ(R11, R12, R8, R9, R10, 42)
ROUND_MAJ(R10, R11, R12, R8, R9, 43)
ROUND_MAJ(R9, R10, R11, R12, R8, 44)
ROUND_MAJ(R8, R9, R10, R11, R12, 45)
ROUND_MAJ(R12, R8, R9, R10, R11, 46)
ROUND_MAJ(R11, R12, R8, R9, R10, 47)
ROUND_MAJ(R10, R11, R12, R8, R9, 48)
ROUND_MAJ(R9, R10, R11, R12, R8, 49)
ROUND_MAJ(R8, R9, R10, R11, R12, 50)
ROUND_MAJ(R12, R8, R9, R10, R11, 51)
ROUND_MAJ(R11, R12, R8, R9, R10, 52)
ROUND_MAJ(R10, R11, R12, R8, R9, 53)
ROUND_MAJ(R9, R10, R11, R12, R8, 54)
ROUND_MAJ(R8, R9, R10, R11, R12, 55)
ROUND_MAJ(R12, R8, R9, R10, R11, 56)
ROUND_MAJ(R11, R12, R8, R9, R10, 57)
SAVECS(R10, R11, R12, R8, R9, 1)
ROUND_MAJ(R10, R11, R12, R8, R9, 58)
ROUND_MAJ(R9, R10, R11, R12, R8, 59)
ROUND_PARITY(R8, R9, R10, R11, R12, 60, $0xca62c1d6)
ROUND_PARITY(R12, R8, R9, R10, R11, 61, $0xca62c1d6)
ROUND_PARITY(R11, R12, R8, R9, R10, 62, $0xca62c1d6)
ROUND_PARITY(R10, R11, R12, R8, R9, 63, $0xca62c1d6)
ROUND_PARITY(R9, R10, R11, R12, R8, 64, $0xca62c1d6)
SAVECS(R8, R9, R10, R11, R12, 2)
ROUND_PARITY(R8, R9, R10, R11, R12, 65, $0xca62c1d6)
ROUND_PARITY(R12, R8, R9, R10, R11, 66, $0xca62c1d6)
ROUND_PARITY(R11, R12, R8, R9, R10, 67, $0xca62c1d6)
ROUND_PARITY(R10, R11, R12, R8, R9, 68, $0xca62c1d6)
ROUND_PARITY(R9, R10, R11, R12, R8, 69, $0xca62c1d6)
ROUND_PARITY(R8, R9, R10, R11, R12, 70, $0xca62c1d6)
ROUND_PARITY(R12, R8, R9, R10, R11, 71, $0xca62c1d6)
ROUND_PARITY(R11, R12, R8, R9, R10, 72, $0xca62c1d6)
ROUND_PARITY(R10, R11, R12, R8, R9, 73, $0xca62c1d6)
ROUND_PARITY(R9, R10, R11, R12, R8, 74, $0xca62c1d6)
ROUND_PARITY(R8, R9, R10, R11, R12, 75, $0xca62c1d6)
ROUND_PARITY(R12, R8, R9, R10, R11, 76, $0xca62c1d6)
ROUND_PARITY(R11, R12, R8, R9, R10, 77, $0xca62c1d6)
ROUND_PARITY(R10, R11, R12, R8, R9, 78, $0xca62c1d6)
ROUND_PARITY(R9, R10, R11, R12, R8, 79, $0xca62c1d6)
ADDL R8, 0(DI)
ADDL R9, 4(DI)
ADDL R10, 8(DI)
ADDL R11, 12(DI)
ADDL R12, 16(DI)
RET
// Swaps the bytes of each word, as SHA-1 reads the block big endian.
DATA bswap_mask<>+0(SB)/8, $0x0405060700010203
DATA bswap_mask<>+8(SB)/8, $0x0c0d0e0f08090a0b
GLOBL bswap_mask<>(SB), RODATA|NOPTR, $16
+46 -32
View File
@@ -20,9 +20,7 @@ var forceGeneric bool
// blockGeneric is a portable, pure Go version of the SHA-1 block step.
// It's used by sha1block_generic.go and tests.
func blockGeneric(dig *digest, p []byte) {
// Expand directly into the schedule retained for collision detection.
// Every word is overwritten for each block, including rehashes.
var m1 [shared.Rounds]uint32
var w [16]uint32
// cs stores the pre-step compression state for only the steps required for the
// collision detection, which are 0, 58 and 65.
@@ -31,6 +29,7 @@ func blockGeneric(dig *digest, p []byte) {
h0, h1, h2, h3, h4 := dig.h[0], dig.h[1], dig.h[2], dig.h[3], dig.h[4]
for len(p) >= shared.Chunk {
m1 := [shared.Rounds]uint32{}
hi := 1
// Collision attacks are thwarted by hashing a detected near-collision block 3 times.
@@ -52,27 +51,36 @@ func blockGeneric(dig *digest, p []byte) {
for ; i < 16; i++ {
// load step
j := i * 4
m1[i] = uint32(p[j])<<24 | uint32(p[j+1])<<16 | uint32(p[j+2])<<8 | uint32(p[j+3])
w[i] = uint32(p[j])<<24 | uint32(p[j+1])<<16 | uint32(p[j+2])<<8 | uint32(p[j+3])
f := b&c | (^b)&d
t := bits.RotateLeft32(a, 5) + f + e + m1[i] + shared.K0
t := bits.RotateLeft32(a, 5) + f + e + w[i&0xf] + shared.K0
a, b, c, d, e = t, a, bits.RotateLeft32(b, 30), c, d
// Store compression state for the collision detection.
m1[i] = w[i&0xf]
}
for ; i < 20; i++ {
tmp := m1[i-3] ^ m1[i-8] ^ m1[i-14] ^ m1[i-16]
m1[i] = tmp<<1 | tmp>>(32-1)
tmp := w[(i-3)&0xf] ^ w[(i-8)&0xf] ^ w[(i-14)&0xf] ^ w[(i)&0xf]
w[i&0xf] = tmp<<1 | tmp>>(32-1)
f := b&c | (^b)&d
t := bits.RotateLeft32(a, 5) + f + e + m1[i] + shared.K0
t := bits.RotateLeft32(a, 5) + f + e + w[i&0xf] + shared.K0
a, b, c, d, e = t, a, bits.RotateLeft32(b, 30), c, d
// Store compression state for the collision detection.
m1[i] = w[i&0xf]
}
for ; i < 40; i++ {
tmp := m1[i-3] ^ m1[i-8] ^ m1[i-14] ^ m1[i-16]
m1[i] = tmp<<1 | tmp>>(32-1)
tmp := w[(i-3)&0xf] ^ w[(i-8)&0xf] ^ w[(i-14)&0xf] ^ w[(i)&0xf]
w[i&0xf] = tmp<<1 | tmp>>(32-1)
f := b ^ c ^ d
t := bits.RotateLeft32(a, 5) + f + e + m1[i] + shared.K1
t := bits.RotateLeft32(a, 5) + f + e + w[i&0xf] + shared.K1
a, b, c, d, e = t, a, bits.RotateLeft32(b, 30), c, d
// Store compression state for the collision detection.
m1[i] = w[i&0xf]
}
for ; i < 60; i++ {
if i == 58 {
@@ -80,12 +88,15 @@ func blockGeneric(dig *digest, p []byte) {
cs[1] = [shared.WordBuffers]uint32{a, b, c, d, e}
}
tmp := m1[i-3] ^ m1[i-8] ^ m1[i-14] ^ m1[i-16]
m1[i] = tmp<<1 | tmp>>(32-1)
tmp := w[(i-3)&0xf] ^ w[(i-8)&0xf] ^ w[(i-14)&0xf] ^ w[(i)&0xf]
w[i&0xf] = tmp<<1 | tmp>>(32-1)
f := ((b | c) & d) | (b & c)
t := bits.RotateLeft32(a, 5) + f + e + m1[i] + shared.K2
t := bits.RotateLeft32(a, 5) + f + e + w[i&0xf] + shared.K2
a, b, c, d, e = t, a, bits.RotateLeft32(b, 30), c, d
// Store compression state for the collision detection.
m1[i] = w[i&0xf]
}
for ; i < 80; i++ {
if i == 65 {
@@ -93,12 +104,15 @@ func blockGeneric(dig *digest, p []byte) {
cs[2] = [shared.WordBuffers]uint32{a, b, c, d, e}
}
tmp := m1[i-3] ^ m1[i-8] ^ m1[i-14] ^ m1[i-16]
m1[i] = tmp<<1 | tmp>>(32-1)
tmp := w[(i-3)&0xf] ^ w[(i-8)&0xf] ^ w[(i-14)&0xf] ^ w[(i)&0xf]
w[i&0xf] = tmp<<1 | tmp>>(32-1)
f := b ^ c ^ d
t := bits.RotateLeft32(a, 5) + f + e + m1[i] + shared.K3
t := bits.RotateLeft32(a, 5) + f + e + w[i&0xf] + shared.K3
a, b, c, d, e = t, a, bits.RotateLeft32(b, 30), c, d
// Store compression state for the collision detection.
m1[i] = w[i&0xf]
}
h0 += a
@@ -114,7 +128,7 @@ func blockGeneric(dig *digest, p []byte) {
if hi == 1 {
h := [shared.WordBuffers]uint32{h0, h1, h2, h3, h4}
col := checkCollision(&m1, &cs, &h, ubc.CalculateDvMask(&m1))
col := checkCollision(&m1, &cs, &h)
if col {
dig.col = true
hi++
@@ -133,9 +147,8 @@ func checkCollision(
m1 *[shared.Rounds]uint32,
cs *[shared.PreStepState][shared.WordBuffers]uint32,
h *[shared.WordBuffers]uint32,
mask uint32,
) bool {
if mask != 0 {
if mask := ubc.CalculateDvMask(m1); mask != 0 {
dvs := ubc.SHA1_dvs()
for i := 0; dvs[i].DvType != 0; i++ {
@@ -277,22 +290,23 @@ func rectifyCompressionState(
return
}
// The words are loaded and stored one at a time. Passing [5]uint32 values
// around made the compiler reload them with wider moves than they were
// stored with, which stalled on store forwarding each time.
func3 := func(state [shared.WordBuffers]uint32, i int) [shared.WordBuffers]uint32 {
a, b, c, d, e := state[0], state[1], state[2], state[3], state[4]
// Advance cs[1] from the state before step 56 to the one before step 58.
a, b, c, d, e := cs[1][0], cs[1][1], cs[1][2], cs[1][3], cs[1][4]
for i := 56; i < 58; i++ {
f := ((b | c) & d) | (b & c)
t := bits.RotateLeft32(a, 5) + f + e + m1[i] + shared.K2
a, b, c, d, e = t, a, bits.RotateLeft32(b, 30), c, d
return [shared.WordBuffers]uint32{a, b, c, d, e}
}
func4 := func(state [shared.WordBuffers]uint32, i int) [shared.WordBuffers]uint32 {
a, b, c, d, e := state[0], state[1], state[2], state[3], state[4]
f := b ^ c ^ d
t := bits.RotateLeft32(a, 5) + f + e + m1[i] + shared.K3
a, b, c, d, e = t, a, bits.RotateLeft32(b, 30), c, d
return [shared.WordBuffers]uint32{a, b, c, d, e}
}
cs[1][0], cs[1][1], cs[1][2], cs[1][3], cs[1][4] = a, b, c, d, e
// Advance cs[2] from the state before step 64 to the one before step 65.
a, b, c, d, e = cs[2][0], cs[2][1], cs[2][2], cs[2][3], cs[2][4]
f := b ^ c ^ d
t := bits.RotateLeft32(a, 5) + f + e + m1[64] + shared.K3
cs[2][0], cs[2][1], cs[2][2], cs[2][3], cs[2][4] = t, a, bits.RotateLeft32(b, 30), c, d
cs57 := func3(cs[1], 56)
cs[1] = func3(cs57, 57)
cs[2] = func4(cs[2], 64)
}
-51
View File
@@ -1,51 +0,0 @@
//go:build !noasm && gc && amd64
package ubc
import (
"unsafe"
"github.com/pjbgf/sha1cd/internal/cpu"
)
// useAVX512 reports whether calculateDvMaskAVX512 can run. Its only
// instructions outside AVX-512F are VMOVD and VZEROUPPER, which need AVX.
// HasAVX512F is set only when the CPU reports AVX as well.
var useAVX512 = cpu.X86.HasAVX512F
//go:noescape
func calculateDvMaskAVX512(W *[80]uint32, terms *uint32, groups int) uint32
// avx512Lanes is the number of terms a group of avx512Terms describes, and
// avx512Fields the vectors it holds for them.
const (
avx512Lanes = 16
avx512Fields = 6
)
// avx512Groups is how many passes of the kernel loop the table needs.
const avx512Groups = len(avx512Terms) / (avx512Fields * avx512Lanes)
// alignedTerms is a copy of avx512Terms that starts on a cache line. The
// linker aligns data to 32 bytes at most, and a 64 byte load that straddles
// two cache lines costs about twice as much.
var alignedTerms = func() *uint32 {
buf := make([]uint32, len(avx512Terms)+16)
off := (64 - uintptr(unsafe.Pointer(&buf[0]))%64) % 64 / 4
copy(buf[off:], avx512Terms[:])
return &buf[off]
}()
// CalculateDvMask takes as input an expanded message block and
// verifies the unavoidable bitconditions for all listed DVs. It returns
// a dvmask where each bit belonging to a DV is set if all unavoidable
// bitconditions for that DV have been met.
func CalculateDvMask(W *[80]uint32) uint32 {
if W == nil {
return 0
}
if useAVX512 {
return calculateDvMaskAVX512(W, alignedTerms, avx512Groups)
}
return calculateDvMaskGeneric(W)
}
-52
View File
@@ -1,52 +0,0 @@
//go:build !noasm && gc && amd64
#include "textflag.h"
// func calculateDvMaskAVX512(W *[80]uint32, terms *uint32, groups int) uint32
// Requires: AVX, AVX512F
//
// Each group of avx512Terms describes 16 of the bit tests of CalculateDvMask,
// as vectors of word indices, shift counts, expected parities and the masks
// that clear the DVs of a failing test. One pass of the loop runs a group.
//
// terms only has to be 64 byte aligned for speed, as otherwise every load of
// it straddles two cache lines. The word indices are biased by 34, matching
// the two vectors of W loaded below.
TEXT ·calculateDvMaskAVX512(SB), NOSPLIT, $0-28
MOVQ W+0(FP), AX
MOVQ terms+8(FP), BX
MOVQ groups+16(FP), CX
VMOVDQU32 136(AX), Z0 // W[34..49]
VMOVDQU32 200(AX), Z1 // W[50..65]
VPTERNLOGD $0xff, Z2, Z2, Z2 // mask = all ones
MOVL $1, DX
VPBROADCASTD DX, Z3
loop:
VMOVDQU32 0(BX), Z4
VMOVDQU32 64(BX), Z5
VPERMI2D Z1, Z0, Z4 // W[a]
VPERMI2D Z1, Z0, Z5 // W[b]
VPSRLVD 128(BX), Z4, Z4 // >> ka
VPSRLVD 192(BX), Z5, Z5 // >> kb
VPTERNLOGD $0x96, 256(BX), Z5, Z4 // ^ e
VPTESTMD Z3, Z4, K1 // the terms whose bit test fails
VPANDD 320(BX), Z2, K1, Z2 // clear their DVs
ADDQ $384, BX
DECQ CX
JNZ loop
// AND the 16 lanes together, first the 128 bit lanes and then the
// words within them.
VSHUFI64X2 $0x4e, Z2, Z2, Z4
VPANDD Z4, Z2, Z2
VSHUFI64X2 $0xb1, Z2, Z2, Z4
VPANDD Z4, Z2, Z2
VPSHUFD $0x4e, Z2, Z4
VPANDD Z4, Z2, Z2
VPSHUFD $0xb1, Z2, Z4
VPANDD Z4, Z2, Z2
VMOVD X2, AX
VZEROUPPER
MOVL AX, ret+24(FP)
RET
-111
View File
@@ -1,111 +0,0 @@
//go:build !noasm && gc && amd64
package ubc
// avx512Terms holds the bit tests of CalculateDvMask in the layout the
// AVX-512 kernel consumes, 16 terms per group. Each term says that a set of
// DVs survives only if bit ka of W[a] XOR bit kb of W[b] equals e, and names
// the mask that clears those DVs when it does not.
//
// The six vectors of a group are, in order: a-34, b-34, ka, kb, e, and the
// clear mask. Word indices are biased by 34 because the kernel keeps W[34..65]
// in two registers. A group is 384 bytes, and the kernel runs one per pass.
//
// The terms are those of calculateDvMaskGeneric, which is a port of
// ubc_check.c and has not changed since the collision detection was published
// in 2017. TestAVX512MatchesGeneric holds the two implementations to the same
// results over inputs that reach every DV, so a mistake here is a test
// failure rather than a missed collision. The comment above each group names
// its terms as a.ka^b.kb=e.
var avx512Terms = [960]uint32{
// terms 0-15
// 44.29^45.29=0 49.29^50.29=0 48.29^49.29=0 47.4^50.29=0 47.29^48.29=0 46.4^49.29=0 46.29^47.29=0 45.4^48.29=0
// 45.29^46.29=0 44.4^47.29=0 43.4^46.29=0 43.29^44.29=0 42.4^45.29=0 41.4^44.29=0 40.29^41.29=0 54.29^55.29=0
0x0000000a, 0x0000000f, 0x0000000e, 0x0000000d, 0x0000000d, 0x0000000c, 0x0000000c, 0x0000000b, 0x0000000b, 0x0000000a, 0x00000009, 0x00000009, 0x00000008, 0x00000007, 0x00000006, 0x00000014,
0x0000000b, 0x00000010, 0x0000000f, 0x00000010, 0x0000000e, 0x0000000f, 0x0000000d, 0x0000000e, 0x0000000c, 0x0000000d, 0x0000000c, 0x0000000a, 0x0000000b, 0x0000000a, 0x00000007, 0x00000015,
0x0000001d, 0x0000001d, 0x0000001d, 0x00000004, 0x0000001d, 0x00000004, 0x0000001d, 0x00000004, 0x0000001d, 0x00000004, 0x00000004, 0x0000001d, 0x00000004, 0x00000004, 0x0000001d, 0x0000001d,
0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d,
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
0xfd7c5f7f, 0x3d7efff7, 0x9f5f7ffb, 0x7dfedddf, 0xcfcfdffd, 0xbf7f7777, 0xe7e7f7fe, 0xdfdfdddb, 0xf5f57dff, 0xefeff775, 0xf7f7fdda, 0xff5ed7df, 0xfdfd7f75, 0xff7edfda, 0x7ff5ff5d, 0x3f77dfff,
// terms 16-31
// 53.29^54.29=0 52.29^53.29=0 50.4^53.29=0 50.29^51.29=0 49.4^52.29=0 48.4^51.29=0 42.29^43.29=0 41.29^42.29=0
// 40.4^43.29=0 39.4^42.29=0 38.4^41.29=0 37.4^40.29=0 55.29^56.29=0 52.4^55.29=0 51.4^54.29=0 51.29^52.29=0
0x00000013, 0x00000012, 0x00000010, 0x00000010, 0x0000000f, 0x0000000e, 0x00000008, 0x00000007, 0x00000006, 0x00000005, 0x00000004, 0x00000003, 0x00000015, 0x00000012, 0x00000011, 0x00000011,
0x00000014, 0x00000013, 0x00000013, 0x00000011, 0x00000012, 0x00000011, 0x00000009, 0x00000008, 0x00000009, 0x00000008, 0x00000007, 0x00000006, 0x00000016, 0x00000015, 0x00000014, 0x00000012,
0x0000001d, 0x0000001d, 0x00000004, 0x0000001d, 0x00000004, 0x00000004, 0x0000001d, 0x0000001d, 0x00000004, 0x00000004, 0x00000004, 0x00000004, 0x0000001d, 0x00000004, 0x00000004, 0x0000001d,
0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d,
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x9fddf7ff, 0xcfeefdff, 0xdfed77ff, 0x75fdffdf, 0xeff6ddff, 0xf7fd777f, 0xffcff5f7, 0xffe7fd7b, 0x7fdff7f5, 0xbfeffdfa, 0x5ff7ff7d, 0xaffdffde, 0x7def7fff, 0x7f6f7fff, 0xbfd7dfff, 0xe7f7ff7f,
// terms 32-47
// 36.4^40.29=0 53.29^56.29=1 51.29^54.29=1 50.29^52.29=1 49.29^51.29=1 48.29^50.29=1 47.29^49.29=1 46.29^48.29=1
// 45.6^47.6=0 45.29^47.29=1 44.6^46.6=0 44.29^46.29=1 41.1^42.6=1 40.1^41.6=1 40.4^42.4=1 39.1^40.6=1
0x00000002, 0x00000013, 0x00000011, 0x00000010, 0x0000000f, 0x0000000e, 0x0000000d, 0x0000000c, 0x0000000b, 0x0000000b, 0x0000000a, 0x0000000a, 0x00000007, 0x00000006, 0x00000006, 0x00000005,
0x00000006, 0x00000016, 0x00000014, 0x00000012, 0x00000011, 0x00000010, 0x0000000f, 0x0000000e, 0x0000000d, 0x0000000d, 0x0000000c, 0x0000000c, 0x00000008, 0x00000007, 0x00000008, 0x00000006,
0x00000004, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x00000006, 0x0000001d, 0x00000006, 0x0000001d, 0x00000001, 0x00000001, 0x00000004, 0x00000001,
0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x00000006, 0x0000001d, 0x00000006, 0x0000001d, 0x00000006, 0x00000006, 0x00000004, 0x00000006,
0x00000000, 0x00000001, 0x00000001, 0x00000001, 0x00000001, 0x00000001, 0x00000001, 0x00000001, 0x00000000, 0x00000001, 0x00000000, 0x00000001, 0x00000001, 0x00000001, 0x00000001, 0x00000001,
0xffeefdf7, 0xffcf7fff, 0xfff5f7ff, 0xfffeddff, 0xffff777f, 0xffffdddf, 0xfffff777, 0xfffffddb, 0xffffbbbf, 0xffffff75, 0xffffeeef, 0xffffffda, 0xfbfbfeff, 0xfeffbfbf, 0x7ffffff5, 0xffbfefef,
// terms 48-63
// 39.4^41.4=1 38.4^40.4=1 37.4^39.4=1 36.1^37.6=1 35.4^39.29=0 63.0^64.5=1 63.1^64.6=1 62.0^63.5=1
// 61.0^62.5=1 61.2^62.7=1 60.0^61.5=1 58.29^59.29=0 57.29^58.29=0 56.4^59.29=0 56.29^59.29=1 56.29^57.29=0
0x00000005, 0x00000004, 0x00000003, 0x00000002, 0x00000001, 0x0000001d, 0x0000001d, 0x0000001c, 0x0000001b, 0x0000001b, 0x0000001a, 0x00000018, 0x00000017, 0x00000016, 0x00000016, 0x00000016,
0x00000007, 0x00000006, 0x00000005, 0x00000003, 0x00000005, 0x0000001e, 0x0000001e, 0x0000001d, 0x0000001c, 0x0000001c, 0x0000001b, 0x00000019, 0x00000018, 0x00000019, 0x00000019, 0x00000017,
0x00000004, 0x00000004, 0x00000004, 0x00000001, 0x00000004, 0x00000000, 0x00000001, 0x00000000, 0x00000000, 0x00000002, 0x00000000, 0x0000001d, 0x0000001d, 0x00000004, 0x0000001d, 0x0000001d,
0x00000004, 0x00000004, 0x00000004, 0x00000006, 0x0000001d, 0x00000005, 0x00000006, 0x00000005, 0x00000005, 0x00000007, 0x00000005, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001d,
0x00000001, 0x00000001, 0x00000001, 0x00000001, 0x00000000, 0x00000001, 0x00000001, 0x00000001, 0x00000001, 0x00000001, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000,
0xbffffffa, 0x5ffffffd, 0xaffffffe, 0xfffbefbf, 0xfff7ff7b, 0xffefff7f, 0xfffefffb, 0xfff7ffdf, 0xfffdfff7, 0xfffbffef, 0xfffefffb, 0xddffffff, 0xef7fffff, 0xd7ffffff, 0xf5ffffff, 0xf7dfffff,
// terms 64-79
// 55.4^58.29=0 54.4^57.29=0 53.4^56.29=0 50.6^51.1=0 48.6^50.6=0 48.29^55.29=1 47.6^49.6=0 47.6^48.1=0
// 46.6^48.6=0 46.6^47.1=0 44.1^45.6=1 43.6^45.6=0 42.6^44.6=0 42.6^43.1=0 41.6^42.1=0 40.6^41.1=0
0x00000015, 0x00000014, 0x00000013, 0x00000010, 0x0000000e, 0x0000000e, 0x0000000d, 0x0000000d, 0x0000000c, 0x0000000c, 0x0000000a, 0x00000009, 0x00000008, 0x00000008, 0x00000007, 0x00000006,
0x00000018, 0x00000017, 0x00000016, 0x00000011, 0x00000010, 0x00000015, 0x0000000f, 0x0000000e, 0x0000000e, 0x0000000d, 0x0000000b, 0x0000000b, 0x0000000a, 0x00000009, 0x00000008, 0x00000007,
0x00000004, 0x00000004, 0x00000004, 0x00000006, 0x00000006, 0x0000001d, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000001, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006,
0x0000001d, 0x0000001d, 0x0000001d, 0x00000001, 0x00000006, 0x0000001d, 0x00000006, 0x00000001, 0x00000006, 0x00000001, 0x00000006, 0x00000006, 0x00000006, 0x00000001, 0x00000001, 0x00000001,
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
0xedffffff, 0xf77fffff, 0xfddfffff, 0xfffbefff, 0xfffbefff, 0xffff5fff, 0xffffbbff, 0xfbffffbf, 0xffffeeff, 0xfeffffef, 0xffbfbfff, 0xfffffbbf, 0xfffffeef, 0xfbfbffff, 0xfeffbfff, 0xffbfefff,
// terms 80-95
// 39.4^43.29=0 38.4^42.29=0 37.1^38.6=1 37.4^41.29=0 36.4^38.4=1 35.1^36.6=1 35.3^39.28=0 61.1^62.6=1
// 59.5^63.30=0 58.0^63.30=1 62.1^63.6=1 60.5^64.30=0 59.0^64.30=1 40.6^42.6=0 62.2^63.7=1 41.6^43.6=0
0x00000005, 0x00000004, 0x00000003, 0x00000003, 0x00000002, 0x00000001, 0x00000001, 0x0000001b, 0x00000019, 0x00000018, 0x0000001c, 0x0000001a, 0x00000019, 0x00000006, 0x0000001c, 0x00000007,
0x00000009, 0x00000008, 0x00000004, 0x00000007, 0x00000004, 0x00000002, 0x00000005, 0x0000001c, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001e, 0x0000001e, 0x00000008, 0x0000001d, 0x00000009,
0x00000004, 0x00000004, 0x00000001, 0x00000004, 0x00000004, 0x00000001, 0x00000003, 0x00000001, 0x00000005, 0x00000000, 0x00000001, 0x00000005, 0x00000000, 0x00000006, 0x00000002, 0x00000006,
0x0000001d, 0x0000001d, 0x00000006, 0x0000001d, 0x00000004, 0x00000006, 0x0000001c, 0x00000006, 0x0000001e, 0x0000001e, 0x00000006, 0x0000001e, 0x0000001e, 0x00000006, 0x00000007, 0x00000006,
0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x00000001, 0x00000001, 0x00000000, 0x00000001, 0x00000000, 0x00000001, 0x00000001, 0x00000000, 0x00000001, 0x00000000, 0x00000001, 0x00000000,
0xfdff7fff, 0xff7fdfff, 0xffffbeff, 0xffdff7ff, 0xd7ffffff, 0xfffffbef, 0xfff7dfff, 0xfffffffe, 0xfffffffe, 0xfffffffe, 0xfffffffd, 0xfffffffd, 0xfffffffd, 0xffffffef, 0xffffffbf, 0xffffffbf,
// terms 96-111
// 63.2^64.7=1 48.6^49.1=0 49.6^50.1=0 42.1^50.1=1 39.6^40.1=0 38.1^40.1=1 36.4^37.4=1 43.1^51.1=1
// 37.4^38.4=1 51.6^52.1=0 49.6^51.6=0 37.1^37.6=0 35.5^39.30=0 38.4^39.4=1 47.1^51.1=1 36.3^40.28=0
0x0000001d, 0x0000000e, 0x0000000f, 0x00000008, 0x00000005, 0x00000004, 0x00000002, 0x00000009, 0x00000003, 0x00000011, 0x0000000f, 0x00000003, 0x00000001, 0x00000004, 0x0000000d, 0x00000002,
0x0000001e, 0x0000000f, 0x00000010, 0x00000010, 0x00000006, 0x00000006, 0x00000003, 0x00000011, 0x00000004, 0x00000012, 0x00000011, 0x00000003, 0x00000005, 0x00000005, 0x00000011, 0x00000006,
0x00000002, 0x00000006, 0x00000006, 0x00000001, 0x00000006, 0x00000001, 0x00000004, 0x00000001, 0x00000004, 0x00000006, 0x00000006, 0x00000001, 0x00000005, 0x00000004, 0x00000001, 0x00000003,
0x00000007, 0x00000001, 0x00000001, 0x00000001, 0x00000001, 0x00000001, 0x00000004, 0x00000001, 0x00000004, 0x00000001, 0x00000006, 0x00000006, 0x0000001e, 0x00000004, 0x00000001, 0x0000001c,
0x00000001, 0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x00000001, 0x00000001, 0x00000001, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000001, 0x00000000,
0xfffffeff, 0xfffffeff, 0xfffffbff, 0xfffffbff, 0xfffffbff, 0xfffffbff, 0xfffff7ff, 0xffffefff, 0xffffdfff, 0xffffbfff, 0xffffbfff, 0xffffbfff, 0xffffbfff, 0xffff7fff, 0xfffbffff, 0xffefffff,
// terms 112-127
// 35.30^40.28=1 37.3^41.28=0 36.30^41.28=1 53.6^54.1=0 51.6^53.6=0 50.1^54.1=1 45.6^46.1=0 37.5^41.30=0
// 36.0^41.30=1 55.29^58.29=1 38.3^42.28=0 37.30^42.28=1 54.6^55.1=0 52.6^54.6=0 51.1^55.1=1 45.1^47.1=1
0x00000001, 0x00000003, 0x00000002, 0x00000013, 0x00000011, 0x00000010, 0x0000000b, 0x00000003, 0x00000002, 0x00000015, 0x00000004, 0x00000003, 0x00000014, 0x00000012, 0x00000011, 0x0000000b,
0x00000006, 0x00000007, 0x00000007, 0x00000014, 0x00000013, 0x00000014, 0x0000000c, 0x00000007, 0x00000007, 0x00000018, 0x00000008, 0x00000008, 0x00000015, 0x00000014, 0x00000015, 0x0000000d,
0x0000001e, 0x00000003, 0x0000001e, 0x00000006, 0x00000006, 0x00000001, 0x00000006, 0x00000005, 0x00000000, 0x0000001d, 0x00000003, 0x0000001e, 0x00000006, 0x00000006, 0x00000001, 0x00000001,
0x0000001c, 0x0000001c, 0x0000001c, 0x00000001, 0x00000006, 0x00000001, 0x00000001, 0x0000001e, 0x0000001e, 0x0000001d, 0x0000001c, 0x0000001c, 0x00000001, 0x00000006, 0x00000001, 0x00000001,
0x00000001, 0x00000000, 0x00000001, 0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x00000000, 0x00000001, 0x00000001, 0x00000000, 0x00000001, 0x00000000, 0x00000000, 0x00000001, 0x00000001,
0xffefffff, 0xffdfffff, 0xffdfffff, 0xffbfffff, 0xffbfffff, 0xffbfffff, 0xffbfffff, 0xffbfffff, 0xffbfffff, 0xff7fffff, 0xff7fffff, 0xff7fffff, 0xfeffffff, 0xfeffffff, 0xfeffffff, 0xfeffffff,
// terms 128-143
// 38.5^42.30=0 37.0^42.30=1 39.3^43.28=0 38.30^43.28=1 55.6^56.1=0 53.6^55.6=0 52.1^56.1=1 46.1^48.1=1
// 39.5^43.30=0 38.0^43.30=1 59.29^60.29=0 40.3^44.28=0 40.4^44.29=0 39.30^44.28=1 58.29^61.29=1 57.4^61.29=0
0x00000004, 0x00000003, 0x00000005, 0x00000004, 0x00000015, 0x00000013, 0x00000012, 0x0000000c, 0x00000005, 0x00000004, 0x00000019, 0x00000006, 0x00000006, 0x00000005, 0x00000018, 0x00000017,
0x00000008, 0x00000008, 0x00000009, 0x00000009, 0x00000016, 0x00000015, 0x00000016, 0x0000000e, 0x00000009, 0x00000009, 0x0000001a, 0x0000000a, 0x0000000a, 0x0000000a, 0x0000001b, 0x0000001b,
0x00000005, 0x00000000, 0x00000003, 0x0000001e, 0x00000006, 0x00000006, 0x00000001, 0x00000001, 0x00000005, 0x00000000, 0x0000001d, 0x00000003, 0x00000004, 0x0000001e, 0x0000001d, 0x00000004,
0x0000001e, 0x0000001e, 0x0000001c, 0x0000001c, 0x00000001, 0x00000006, 0x00000001, 0x00000001, 0x0000001e, 0x0000001e, 0x0000001d, 0x0000001c, 0x0000001d, 0x0000001c, 0x0000001d, 0x0000001d,
0x00000000, 0x00000001, 0x00000000, 0x00000001, 0x00000000, 0x00000000, 0x00000001, 0x00000001, 0x00000000, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000001, 0x00000000,
0xfeffffff, 0xfeffffff, 0xfdffffff, 0xfdffffff, 0xfbffffff, 0xfbffffff, 0xfbffffff, 0xfbffffff, 0xfbffffff, 0xfbffffff, 0xf7ffffff, 0xf7ffffff, 0xf7ffffff, 0xf7ffffff, 0xefffffff, 0xefffffff,
// terms 144-159
// 41.3^45.28=0 41.4^45.29=0 58.4^62.29=0 42.3^46.28=0 42.4^46.29=0 59.4^63.29=0 57.4^59.29=0 43.3^47.28=0
// 43.4^47.29=0 60.4^64.29=0 44.3^48.28=0 44.4^48.29=0 35.0^35.0=0 35.0^35.0=0 35.0^35.0=0 35.0^35.0=0
0x00000007, 0x00000007, 0x00000018, 0x00000008, 0x00000008, 0x00000019, 0x00000017, 0x00000009, 0x00000009, 0x0000001a, 0x0000000a, 0x0000000a, 0x00000001, 0x00000001, 0x00000001, 0x00000001,
0x0000000b, 0x0000000b, 0x0000001c, 0x0000000c, 0x0000000c, 0x0000001d, 0x00000019, 0x0000000d, 0x0000000d, 0x0000001e, 0x0000000e, 0x0000000e, 0x00000001, 0x00000001, 0x00000001, 0x00000001,
0x00000003, 0x00000004, 0x00000004, 0x00000003, 0x00000004, 0x00000004, 0x00000004, 0x00000003, 0x00000004, 0x00000004, 0x00000003, 0x00000004, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x0000001c, 0x0000001d, 0x0000001d, 0x0000001c, 0x0000001d, 0x0000001d, 0x0000001d, 0x0000001c, 0x0000001d, 0x0000001d, 0x0000001c, 0x0000001d, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
0xefffffff, 0xefffffff, 0xdfffffff, 0xdfffffff, 0xdfffffff, 0xbfffffff, 0xbfffffff, 0xbfffffff, 0xbfffffff, 0x7fffffff, 0x7fffffff, 0x7fffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
}
-14
View File
@@ -1,14 +0,0 @@
//go:build noasm || !gc || !amd64
package ubc
// CalculateDvMask takes as input an expanded message block and
// verifies the unavoidable bitconditions for all listed DVs. It returns
// a dvmask where each bit belonging to a DV is set if all unavoidable
// bitconditions for that DV have been met.
func CalculateDvMask(W *[80]uint32) uint32 {
if W == nil {
return 0
}
return calculateDvMaskGeneric(W)
}
+8 -4
View File
@@ -23,12 +23,16 @@ type DvInfo struct {
Dm [80]uint32
}
// calculateDvMaskGeneric is the portable implementation of CalculateDvMask.
// The AVX-512 kernel runs the same bit tests from avx512Terms, and
// TestAVX512MatchesGeneric holds the two to the same results.
// CalculateDvMask takes as input an expanded message block and
// verifies the unavoidable bitconditions for all listed DVs. It returns
// a dvmask where each bit belonging to a DV is set if all unavoidable
// bitconditions for that DV have been met.
//
//go:nosplit
func calculateDvMaskGeneric(W *[80]uint32) uint32 {
func CalculateDvMask(W *[80]uint32) uint32 {
if W == nil {
return 0
}
mask := uint32(0xFFFFFFFF)
mask &= (((((W[44] ^ W[45]) >> 29) & 1) - 1) | ^(DV_I_48_0_bit | DV_I_51_0_bit | DV_I_52_0_bit | DV_II_45_0_bit | DV_II_46_0_bit | DV_II_50_0_bit | DV_II_51_0_bit))
mask &= (((((W[49] ^ W[50]) >> 29) & 1) - 1) | ^(DV_I_46_0_bit | DV_II_45_0_bit | DV_II_50_0_bit | DV_II_51_0_bit | DV_II_55_0_bit | DV_II_56_0_bit))
+15
View File
@@ -0,0 +1,15 @@
*
!.git/
!build/root.tgz
!cmd/
!config/
!internal/
!mod/
!pkg/
!regclient/
!scheme/
!types/
!vendor/
!go.*
!*.go
!Makefile
+5
View File
@@ -0,0 +1,5 @@
artifacts/
bin/
output/
vendor/
.regctl_conf_ci.json
+19
View File
@@ -0,0 +1,19 @@
# all lists use a `-`
MD004:
style: dash
# allow tabs in code blocks (for Go)
MD010:
code_blocks: false
# disable line length, prefer one sentence per line for PRs
MD013: false
# emphasis with underscore (`_emphasis_`)
MD049:
style: "underscore"
# bold with asterisk (`**bold**`)
MD050:
style: "asterisk"
+1
View File
@@ -0,0 +1 @@
GoVersionOverride = "1.26.3"
+53
View File
@@ -0,0 +1,53 @@
{"name":"docker-arg-alpine-digest","key":"docker.io/library/alpine:3.23.4","version":"sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11"}
{"name":"docker-arg-alpine-tag","key":"docker.io/library/alpine","version":"3.23.4"}
{"name":"docker-arg-ecr","key":"https://github.com/awslabs/amazon-ecr-credential-helper.git","version":"v0.12.0"}
{"name":"docker-arg-gcr","key":"https://github.com/GoogleCloudPlatform/docker-credential-gcr.git","version":"v2.1.32"}
{"name":"docker-arg-go-digest","key":"docker.io/library/golang:1.26.3-alpine","version":"sha256:91eda9776261207ea25fd06b5b7fed8d397dd2c0a283e77f2ab6e91bfa71079d"}
{"name":"docker-arg-go-tag","key":"docker.io/library/golang","version":"1.26.3"}
{"name":"docker-arg-lunajson","key":"https://github.com/grafi-tt/lunajson.git:master","version":"e3a9666eb1275741e887e29926b144f8daee3bef"}
{"name":"docker-arg-semver","key":"https://github.com/kikito/semver.lua.git:master","version":"a4b708ba243208d46e575da870af969dca46a94d"}
{"name":"gha-alpine-digest","key":"docker.io/library/alpine:3.23.4","version":"sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11"}
{"name":"gha-alpine-tag-base","key":"docker.io/library/alpine","version":"3"}
{"name":"gha-alpine-tag-comment","key":"docker.io/library/alpine","version":"3.23.4"}
{"name":"gha-cosign-version","key":"https://github.com/sigstore/cosign.git","version":"v3.0.6"}
{"name":"gha-golang-matrix","key":"golang-matrix","version":"[\"1.25\", \"1.26\"]"}
{"name":"gha-golang-release","key":"golang-latest","version":"1.26"}
{"name":"gha-syft-version","key":"docker.io/anchore/syft","version":"v1.44.0"}
{"name":"gha-uses-commit","key":"https://github.com/actions/checkout.git:v6.0.2","version":"de0fac2e4500dabe0009e67214ff5f5447ce83dd"}
{"name":"gha-uses-commit","key":"https://github.com/actions/setup-go.git:v6.4.0","version":"4a3601121dd01d1626a1e23e37211e3254c1c06c"}
{"name":"gha-uses-commit","key":"https://github.com/actions/stale.git:v10.3.0","version":"eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899"}
{"name":"gha-uses-commit","key":"https://github.com/actions/upload-artifact.git:v7.0.1","version":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a"}
{"name":"gha-uses-commit","key":"https://github.com/anchore/sbom-action.git:v0.24.0","version":"e22c389904149dbc22b58101806040fa8d37a610"}
{"name":"gha-uses-commit","key":"https://github.com/docker/build-push-action.git:v7.2.0","version":"f9f3042f7e2789586610d6e8b85c8f03e5195baf"}
{"name":"gha-uses-commit","key":"https://github.com/docker/login-action.git:v4.2.0","version":"650006c6eb7dba73a995cc03b0b2d7f5ca915bee"}
{"name":"gha-uses-commit","key":"https://github.com/docker/setup-buildx-action.git:v4.1.0","version":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5"}
{"name":"gha-uses-commit","key":"https://github.com/regclient/actions.git:main","version":"c70ad64367908075211b10dcd2ab9fad4bfa1816"}
{"name":"gha-uses-commit","key":"https://github.com/sigstore/cosign-installer.git:v4.1.2","version":"6f9f17788090df1f26f669e9d70d6ae9567deba6"}
{"name":"gha-uses-commit","key":"https://github.com/softprops/action-gh-release.git:v3.0.0","version":"b4309332981a82ec1c5618f44dd2e27cc8bfbfda"}
{"name":"gha-uses-semver","key":"https://github.com/actions/checkout.git","version":"v6.0.2"}
{"name":"gha-uses-semver","key":"https://github.com/actions/setup-go.git","version":"v6.4.0"}
{"name":"gha-uses-semver","key":"https://github.com/actions/stale.git","version":"v10.3.0"}
{"name":"gha-uses-semver","key":"https://github.com/actions/upload-artifact.git","version":"v7.0.1"}
{"name":"gha-uses-semver","key":"https://github.com/anchore/sbom-action.git","version":"v0.24.0"}
{"name":"gha-uses-semver","key":"https://github.com/docker/build-push-action.git","version":"v7.2.0"}
{"name":"gha-uses-semver","key":"https://github.com/docker/login-action.git","version":"v4.2.0"}
{"name":"gha-uses-semver","key":"https://github.com/docker/setup-buildx-action.git","version":"v4.1.0"}
{"name":"gha-uses-semver","key":"https://github.com/sigstore/cosign-installer.git","version":"v4.1.2"}
{"name":"gha-uses-semver","key":"https://github.com/softprops/action-gh-release.git","version":"v3.0.0"}
{"name":"go-mod-golang-release","key":"golang-oldest","version":"1.25.0"}
{"name":"makefile-ci-distribution","key":"docker.io/library/registry","version":"3.1.1"}
{"name":"makefile-ci-zot","key":"ghcr.io/project-zot/zot-linux-amd64","version":"v2.1.17"}
{"name":"makefile-go-vulncheck","key":"https://go.googlesource.com/vuln.git","version":"v1.3.0"}
{"name":"makefile-gofumpt","key":"https://github.com/mvdan/gofumpt.git","version":"v0.10.0"}
{"name":"makefile-gomajor","key":"https://github.com/icholy/gomajor.git","version":"v0.15.0"}
{"name":"makefile-gosec","key":"https://github.com/securego/gosec.git","version":"v2.26.1"}
{"name":"makefile-markdown-lint","key":"docker.io/davidanson/markdownlint-cli2","version":"v0.22.1"}
{"name":"makefile-osv-scanner","key":"https://github.com/google/osv-scanner.git","version":"v2.3.8"}
{"name":"makefile-staticcheck","key":"https://github.com/dominikh/go-tools.git","version":"v0.7.0"}
{"name":"makefile-syft-container-digest","key":"anchore/syft:v1.44.0","version":"sha256:86fde6445b483d902fe011dd9f68c4987dd94e07da1e9edc004e3c2422650de6"}
{"name":"makefile-syft-container-tag","key":"anchore/syft","version":"v1.44.0"}
{"name":"makefile-syft-version","key":"docker.io/anchore/syft","version":"v1.44.0"}
{"name":"osv-golang-release","key":"docker.io/library/golang","version":"1.26.3"}
{"name":"shell-alpine-digest","key":"docker.io/library/alpine:3.23.4","version":"sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11"}
{"name":"shell-alpine-tag-base","key":"docker.io/library/alpine","version":"3"}
{"name":"shell-alpine-tag-comment","key":"docker.io/library/alpine","version":"3.23.4"}
+346
View File
@@ -0,0 +1,346 @@
files:
"build/Dockerfile*":
processors:
- docker-arg-alpine-tag
- docker-arg-alpine-digest
- docker-arg-go-tag
- docker-arg-go-digest
- docker-arg-ecr
- docker-arg-gcr
- docker-arg-lunajson
- docker-arg-semver
"build/oci-image.sh":
processors:
- shell-alpine-tag-base
- shell-alpine-tag-comment
- shell-alpine-digest
".github/workflows/*.yml":
processors:
- gha-golang-matrix
- gha-golang-release
- gha-uses-vx
- gha-uses-semver
- gha-uses-commit
- gha-syft-version
- gha-cosign-version
- gha-alpine-tag-base
- gha-alpine-tag-comment
- gha-alpine-digest
"Makefile":
processors:
- makefile-gofumpt
- makefile-gomajor
- makefile-go-vulncheck
- makefile-markdown-lint
- makefile-gosec
- makefile-osv-scanner
- makefile-staticcheck
- makefile-syft-version
- makefile-syft-container-tag
- makefile-syft-container-digest
- makefile-ci-distribution
- makefile-ci-zot
"go.mod":
processors:
- go-mod-golang-release
".osv-scanner.toml":
processors:
- osv-golang-release
x-processor-tmpl:
git-commit: &git-commit
key: "{{ .SourceArgs.url }}:{{ .SourceArgs.ref }}"
scan: "regexp"
source: "git-commit"
filter:
expr: "^{{ .SourceArgs.ref }}$"
git-tag-semver: &git-tag-semver
key: "{{ .SourceArgs.url }}"
scan: "regexp"
source: "git-tag"
filter:
expr: '^v?\d+\.\d+\.\d+$'
sort:
method: "semver"
registry-digest: &registry-digest
key: "{{ .SourceArgs.image }}"
scan: "regexp"
source: "registry-digest"
registry-tag-semver: &registry-tag-semver
key: "{{ .SourceArgs.repo }}"
scan: "regexp"
source: "registry-tag"
filter:
expr: '^v?\d+\.\d+\.\d+$'
sort:
method: "semver"
processors:
docker-arg-alpine-tag:
<<: *registry-tag-semver
scanArgs:
regexp: '^ARG ALPINE_VER=(?P<Version>v?\d+\.\d+\.\d+)@(?P<SHA>sha256:[0-9a-f]+)\s*$'
sourceArgs:
repo: "docker.io/library/alpine"
docker-arg-alpine-digest:
<<: *registry-digest
scanArgs:
regexp: '^ARG ALPINE_VER=(?P<Tag>v?\d+\.\d+\.\d+)@(?P<Version>sha256:[0-9a-f]+)\s*$'
sourceArgs:
image: "docker.io/library/alpine:{{.ScanMatch.Tag}}"
docker-arg-go-tag:
<<: *registry-tag-semver
scanArgs:
regexp: '^ARG GO_VER=(?P<Version>[a-z0-9\-\.]+)-alpine@(?P<SHA>sha256:[0-9a-f]+)\s*$'
sourceArgs:
repo: "docker.io/library/golang"
docker-arg-go-digest:
<<: *registry-digest
scanArgs:
regexp: '^ARG GO_VER=(?P<Tag>[a-z0-9\-\.]+)@(?P<Version>sha256:[0-9a-f]+)\s*$'
sourceArgs:
image: "docker.io/library/golang:{{.ScanMatch.Tag}}"
docker-arg-ecr:
<<: *git-tag-semver
scanArgs:
regexp: '^ARG ECR_HELPER_VER=(?P<Version>v?\d+\.\d+\.\d+)\s*$'
sourceArgs:
url: "https://github.com/awslabs/amazon-ecr-credential-helper.git"
# get the version for the ecr-login nested package in the repo
filter:
expr: '^ecr-login/v?\d+\.\d+\.\d+$'
# sort and output only the version number without the package name prefix
sort:
method: "semver"
template: '{{ index (split . "/") 1 }}'
template: '{{ index (split .Version "/") 1 }}'
docker-arg-gcr:
<<: *git-tag-semver
scanArgs:
regexp: '^ARG GCR_HELPER_VER=(?P<Version>v?\d+\.\d+\.\d+)\s*$'
sourceArgs:
url: "https://github.com/GoogleCloudPlatform/docker-credential-gcr.git"
docker-arg-lunajson:
<<: *git-commit
scanArgs:
regexp: '^ARG LUNAJSON_COMMIT=(?P<Version>[0-9a-f]+)\s*$'
sourceArgs:
url: "https://github.com/grafi-tt/lunajson.git"
ref: master
docker-arg-semver:
<<: *git-commit
scanArgs:
regexp: '^ARG SEMVER_COMMIT=(?P<Version>[0-9a-f]+)\s*$'
sourceArgs:
url: "https://github.com/kikito/semver.lua.git"
ref: master
gha-alpine-digest:
<<: *registry-digest
scanArgs:
regexp: '^\s*ALPINE_DIGEST: "(?P<Version>sha256:[0-9a-f]+)"\s*#\s*(?P<Tag>\d+\.\d+\.\d+)\s*$'
sourceArgs:
image: "docker.io/library/alpine:{{ .ScanMatch.Tag }}"
gha-alpine-tag-base:
<<: *registry-tag-semver
scanArgs:
regexp: '^\s*ALPINE_NAME: "alpine:(?P<Version>v?\d+)"\s*$'
sourceArgs:
repo: "docker.io/library/alpine"
# only return the major version number in the tag to support detecting a change in the base image
template: '{{ index ( split .Version "." ) 0 }}'
gha-alpine-tag-comment:
<<: *registry-tag-semver
scanArgs:
regexp: '^\s*ALPINE_DIGEST: "(?P<Digest>sha256:[0-9a-f]+)"\s*#\s*(?P<Version>v?\d+\.\d+\.\d+)\s*$'
sourceArgs:
repo: "docker.io/library/alpine"
gha-cosign-version:
<<: *git-tag-semver
scanArgs:
regexp: '^\s*cosign-release: "(?P<Version>v?[0-9\.]+)"\s*$'
sourceArgs:
url: "https://github.com/sigstore/cosign.git"
filter:
expr: '^v?3\.\d+\.\d+$' # pin to v3, v4 will remove support for older clients
gha-golang-matrix:
<<: *registry-tag-semver
key: "golang-matrix"
scanArgs:
regexp: '^\s*gover: (?P<Version>\[["0-9, \.]+\])\s*$'
sourceArgs:
repo: "docker.io/library/golang"
filter:
expr: '^v?\d+\.\d+$'
template: '["{{ index .VerMap ( index .VerList 1 ) }}", "{{ index .VerMap ( index .VerList 0 ) }}"]'
gha-golang-release:
<<: *registry-tag-semver
key: "golang-latest"
scanArgs:
regexp: '^\s*RELEASE_GO_VER: "(?P<Version>v?[0-9\.]+)"\s*$'
sourceArgs:
repo: "docker.io/library/golang"
filter:
expr: '^v?\d+\.\d+$'
gha-syft-version:
<<: *registry-tag-semver
scanArgs:
regexp: '^\s*syft-version: "(?P<Version>v?[0-9\.]+)"\s*$'
sourceArgs:
repo: "docker.io/anchore/syft"
gha-uses-vx:
<<: *git-tag-semver
scanArgs:
regexp: '^\s+-?\s+uses: (?P<Repo>[^@/]+/[^@/]+)[^@]*@(?P<Commit>[0-9a-f]+)\s+#\s+(?P<Version>v?\d+)\s*$'
sourceArgs:
url: "https://github.com/{{ .ScanMatch.Repo }}.git"
filter:
expr: '^v?\d+$'
gha-uses-semver:
<<: *git-tag-semver
scanArgs:
regexp: '^\s+-?\s+uses: (?P<Repo>[^@/]+/[^@/]+)[^@]*@(?P<Commit>[0-9a-f]+)\s+#\s+(?P<Version>v?\d+\.\d+\.\d+)\s*$'
sourceArgs:
url: "https://github.com/{{ .ScanMatch.Repo }}.git"
gha-uses-commit:
<<: *git-commit
scanArgs:
regexp: '^\s+-?\s+uses: (?P<Repo>[^@/]+/[^@/]+)[^@]*@(?P<Version>[0-9a-f]+)\s+#\s+(?P<Ref>[\w\d\.]+)\s*$'
sourceArgs:
url: "https://github.com/{{ .ScanMatch.Repo }}.git"
ref: "{{ .ScanMatch.Ref }}"
go-mod-golang-release:
<<: *registry-tag-semver
key: "golang-oldest"
scanArgs:
regexp: '^go (?P<Version>[0-9\.]+)\s*$'
sourceArgs:
repo: "docker.io/library/golang"
filter:
expr: '^\d+\.\d+$'
template: '{{ index .VerMap ( index .VerList 1 ) }}.0'
makefile-ci-distribution:
<<: *registry-tag-semver
scanArgs:
regexp: '^CI_DISTRIBUTION_VER\?=(?P<Version>v?[0-9\.]+)\s*$'
sourceArgs:
repo: "docker.io/library/registry"
makefile-ci-zot:
<<: *registry-tag-semver
scanArgs:
regexp: '^CI_ZOT_VER\?=(?P<Version>v?[0-9\.]+)\s*$'
sourceArgs:
repo: "ghcr.io/project-zot/zot-linux-amd64"
makefile-gofumpt:
<<: *git-tag-semver
scanArgs:
regexp: '^GOFUMPT_VER\?=(?P<Version>v?[0-9\.]+)\s*$'
sourceArgs:
url: "https://github.com/mvdan/gofumpt.git"
makefile-gomajor:
<<: *git-tag-semver
scanArgs:
regexp: '^GOMAJOR_VER\?=(?P<Version>v?[0-9\.]+)\s*$'
sourceArgs:
url: "https://github.com/icholy/gomajor.git"
makefile-gosec:
<<: *git-tag-semver
scanArgs:
regexp: '^GOSEC_VER\?=(?P<Version>v?[0-9\.]+)\s*$'
sourceArgs:
url: "https://github.com/securego/gosec.git"
makefile-go-vulncheck:
<<: *git-tag-semver
scanArgs:
regexp: '^GO_VULNCHECK_VER\?=(?P<Version>v?[0-9\.]+)\s*$'
sourceArgs:
url: "https://go.googlesource.com/vuln.git"
makefile-markdown-lint:
<<: *registry-tag-semver
scanArgs:
regexp: '^MARKDOWN_LINT_VER\?=(?P<Version>v?[0-9\.]+)\s*$'
sourceArgs:
repo: "docker.io/davidanson/markdownlint-cli2"
makefile-osv-scanner:
<<: *git-tag-semver
scanArgs:
regexp: '^OSV_SCANNER_VER\?=(?P<Version>v?[0-9\.]+)\s*$'
sourceArgs:
url: "https://github.com/google/osv-scanner.git"
makefile-staticcheck:
<<: *git-tag-semver
scanArgs:
regexp: '^STATICCHECK_VER\?=(?P<Version>v?[0-9\.]+)\s*$'
sourceArgs:
url: "https://github.com/dominikh/go-tools.git"
filter:
# repo also has dated tags, ignore versions without a preceding "v"
expr: '^v\d+\.\d+\.\d+$'
makefile-syft-container-tag:
<<: *registry-tag-semver
scanArgs:
regexp: '^SYFT_CONTAINER\?=(?P<Repo>[^:]*):(?P<Version>v?[0-9\.]+)@(?P<Digest>sha256:[0-9a-f]+)\s*$'
sourceArgs:
repo: "{{ .ScanMatch.Repo }}"
makefile-syft-container-digest:
<<: *registry-digest
scanArgs:
regexp: '^SYFT_CONTAINER\?=(?P<Image>[^:]*):(?P<Tag>v?[0-9\.]+)@(?P<Version>sha256:[0-9a-f]+)\s*$'
sourceArgs:
image: "{{ .ScanMatch.Image }}:{{.ScanMatch.Tag}}"
makefile-syft-version:
<<: *registry-tag-semver
scanArgs:
regexp: '^SYFT_VERSION\?=(?P<Version>v[0-9\.]+)\s*$'
sourceArgs:
repo: "docker.io/anchore/syft"
osv-golang-release:
<<: *registry-tag-semver
scanArgs:
regexp: '^GoVersionOverride = "(?P<Version>v?[0-9\.]+)"\s*$'
sourceArgs:
repo: "docker.io/library/golang"
shell-alpine-tag-base:
<<: *registry-tag-semver
scanArgs:
regexp: '^\s*ALPINE_NAME="alpine:(?P<Version>v?\d+)"\s*$'
sourceArgs:
repo: "docker.io/library/alpine"
# only return the major version number in the tag to support detecting a change in the base image
template: '{{ index ( split .Version "." ) 0 }}'
shell-alpine-tag-comment:
<<: *registry-tag-semver
scanArgs:
regexp: '^\s*ALPINE_DIGEST="(?P<Digest>sha256:[0-9a-f]+)"\s*#\s*(?P<Version>v?\d+\.\d+\.\d+)\s*$'
sourceArgs:
repo: "docker.io/library/alpine"
shell-alpine-digest:
<<: *registry-digest
scanArgs:
regexp: '^\s*ALPINE_DIGEST="(?P<Version>sha256:[0-9a-f]+)"\s*#\s*(?P<Tag>\d+\.\d+\.\d+)\s*$'
sourceArgs:
image: "docker.io/library/alpine:{{ .ScanMatch.Tag }}"
scans:
regexp:
type: "regexp"
sources:
git-commit:
type: "git"
args:
type: "commit"
git-tag:
type: "git"
args:
type: "tag"
registry-digest:
type: "registry"
registry-tag:
type: "registry"
args:
type: "tag"
+134
View File
@@ -0,0 +1,134 @@
# Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of
any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address,
without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
<git@bmitch.net> or slack (I'm found on the CNCF, Docker, OCI, and OpenSSF
slacks).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
+80
View File
@@ -0,0 +1,80 @@
# Contributing
## Reporting security issues
Please see [SECURITY.md](security.md) for the process to report security issues.
## Reporting other issues
Please search for similar issues and if none are seen, report an issue at [github.com/regclient/regclient/issues](https://github.com/regclient/regclient/issues).
## Code style
This project attempts to follow these principles:
- Code is canonical Go, following styles and patterns commonly used by the Go community.
- Dependencies outside of the Go standard library should be minimized.
- Dependencies should be pinned to a specific digest and tracked by Go or version-check.
- Unit tests are strongly encouraged with a focus on test coverage of the successful path and common errors.
- Linters and other style formatting tools are used, please run `make all` before committing any changes.
## LLM Policy
This project expects all contributions to be developed by a human or created with a reproducible tool.
Developers using an AI/LLM tool to generate their contribution are expected to fully understand the entire contribution and the logic behind its design.
Contributions that appear to have been generated by an AI/LLM without a human review may result in a ban from future contributions to the project.
## Pull requests
PRs are welcome following the below guides:
- For anything beyond a minor fix, opening an issue is suggested to discuss possible solutions.
- Changes should be rebased on the main branch.
- Changes should be squashed to a single commit per logical change.
All changes must be signed (`git commit -s`) to indicate you agree to the [Developer Certificate or Origin](https://developercertificate.org/):
```text
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
The sign-off will include the following message in your commit:
```text
Signed-off-by: Your Name <your-email@example.org>
```
This needs to be your real name, no aliases please.
+191
View File
@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2020 The regclient Authors.
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.
+285
View File
@@ -0,0 +1,285 @@
COMMANDS?=regctl regsync regbot
BINARIES?=$(addprefix bin/,$(COMMANDS))
IMAGES?=$(addprefix docker-,$(COMMANDS))
ARTIFACT_PLATFORMS?=linux-amd64 linux-arm64 linux-ppc64le linux-s390x linux-riscv64 darwin-amd64 darwin-arm64 windows-amd64.exe freebsd-amd64
ARTIFACTS?=$(foreach cmd,$(addprefix artifacts/,$(COMMANDS)),$(addprefix $(cmd)-,$(ARTIFACT_PLATFORMS)))
IMAGE_PLATFORMS?=linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/riscv64
VCS_REPO?="https://github.com/regclient/regclient.git"
VCS_REF?=$(shell git rev-list -1 HEAD)
ifneq ($(shell git status --porcelain 2>/dev/null),)
VCS_REF := $(VCS_REF)-dirty
endif
VCS_VERSION?=$(shell vcs_describe="$$(git describe --all)"; \
vcs_version="(devel)"; \
if [ "$${vcs_describe}" != "$${vcs_describe#tags/}" ]; then \
vcs_version="$${vcs_describe#tags/}"; \
elif [ "$${vcs_describe}" != "$${vcs_describe#heads/}" ]; then \
vcs_version="$${vcs_describe#heads/}"; \
if [ "main" = "$${vcs_version}" ]; then vcs_version=edge; fi; \
fi; \
echo "$${vcs_version}" | sed -r 's#/+#-#g')
VCS_TAG?=$(shell git describe --tags --abbrev=0 2>/dev/null || true)
VCS_SEC?=$(shell git log -1 --format=%ct)
VCS_DATE?=$(shell date -d "@$(VCS_SEC)" +%Y-%m-%dT%H:%M:%SZ --utc)
LD_FLAGS?=-s -w -extldflags -static -buildid= -X \"github.com/regclient/regclient/internal/version.vcsTag=$(VCS_TAG)\"
GO_BUILD_FLAGS?=-trimpath -ldflags "$(LD_FLAGS)"
DOCKERFILE_EXT?=$(shell if docker build --help 2>/dev/null | grep -q -- '--progress'; then echo ".buildkit"; fi)
DOCKER_ARGS?=--build-arg "VCS_REF=$(VCS_REF)" --build-arg "VCS_VERSION=$(VCS_VERSION)" --build-arg "SOURCE_DATE_EPOCH=$(VCS_SEC)" --build-arg "BUILD_DATE=$(VCS_DATE)"
GOPATH?=$(shell go env GOPATH)
PWD:=$(shell pwd)
VER_BUMP?=$(shell command -v version-bump 2>/dev/null)
VER_BUMP_CONTAINER?=sudobmitch/version-bump:edge
ifeq "$(strip $(VER_BUMP))" ''
VER_BUMP=docker run --rm \
-v "$(shell pwd)/:$(shell pwd)/" -w "$(shell pwd)" \
-u "$(shell id -u):$(shell id -g)" \
$(VER_BUMP_CONTAINER)
endif
MARKDOWN_LINT_VER?=v0.22.1
GOFUMPT_VER?=v0.10.0
GOMAJOR_VER?=v0.15.0
GOSEC_VER?=v2.26.1
GO_VULNCHECK_VER?=v1.3.0
OSV_SCANNER_VER?=v2.3.8
SYFT?=$(shell command -v syft 2>/dev/null)
SYFT_CMD_VER:=$(shell [ -x "$(SYFT)" ] && echo "v$$($(SYFT) version | awk '/^Version: / {print $$2}')" || echo "0")
SYFT_VERSION?=v1.44.0
SYFT_CONTAINER?=anchore/syft:v1.44.0@sha256:86fde6445b483d902fe011dd9f68c4987dd94e07da1e9edc004e3c2422650de6
ifneq "$(SYFT_CMD_VER)" "$(SYFT_VERSION)"
SYFT=docker run --rm \
-v "$(shell pwd)/:$(shell pwd)/" -w "$(shell pwd)" \
-u "$(shell id -u):$(shell id -g)" \
$(SYFT_CONTAINER)
endif
STATICCHECK_VER?=v0.7.0
CI_DISTRIBUTION_VER?=3.1.1
CI_ZOT_VER?=v2.1.17
.PHONY: .FORCE
.FORCE:
.PHONY: all
all: fmt gofumpt gofix goimports vet test lint binaries ## Full build of Go binaries (including fmt, vet, test, and lint)
.PHONY: fmt
fmt: ## go fmt
go fmt ./...
.PHONY: gofumpt
gofumpt: $(GOPATH)/bin/gofumpt ## gofumpt is a stricter alternative to go fmt
gofumpt -l -w .
.PHONY: gofix
gofix: ## go fix
go fix ./...
goimports: $(GOPATH)/bin/goimports
$(GOPATH)/bin/goimports -w -format-only -local github.com/regclient .
.PHONY: vet
vet: ## go vet
go vet ./...
.PHONY: test
test: ## go test
go test -cover -race ./...
.PHONY: lint
lint: lint-go lint-goimports lint-md lint-gosec ## Run all linting
.PHONY: lint-go
lint-go: $(GOPATH)/bin/gofumpt $(GOPATH)/bin/staticcheck .FORCE ## Run linting for Go
$(GOPATH)/bin/staticcheck -checks all ./...
$(GOPATH)/bin/gofumpt -l -d .
errors=$$(go fix -diff ./...); if [ "$${errors}" != "" ]; then echo "$${errors}"; exit 1; fi
lint-goimports: $(GOPATH)/bin/goimports
@if [ -n "$$($(GOPATH)/bin/goimports -l -format-only -local github.com/regclient .)" ]; then \
echo $(GOPATH)/bin/goimports -d -format-only -local github.com/regclient .; \
$(GOPATH)/bin/goimports -d -format-only -local github.com/regclient .; \
exit 1; \
fi
# excluding types/platform pending resultion to https://github.com/securego/gosec/issues/1116
.PHONY: lint-gosec
lint-gosec: $(GOPATH)/bin/gosec .FORCE ## Run gosec
$(GOPATH)/bin/gosec -terse -exclude-dir types/platform ./...
.PHONY: lint-md
lint-md: .FORCE ## Run linting for markdown
docker run --rm -v "$(PWD):/workdir:ro" davidanson/markdownlint-cli2:$(MARKDOWN_LINT_VER) \
"**/*.md" "#vendor"
.PHONY: vulnerability-scan
vulnerability-scan: osv-scanner vulncheck-go ## Run all vulnerability scanners
.PHONY: osv-scanner
osv-scanner: $(GOPATH)/bin/osv-scanner .FORCE ## Run OSV Scanner
$(GOPATH)/bin/osv-scanner scan --config .osv-scanner.toml -r --licenses="Apache-2.0,BSD-3-Clause,MIT,CC-BY-SA-4.0,UNKNOWN" .
.PHONY: vulncheck-go
vulncheck-go: $(GOPATH)/bin/govulncheck .FORCE ## Run govulncheck
$(GOPATH)/bin/govulncheck ./...
.PHONY: vendor
vendor: ## Vendor Go modules
go mod vendor
.PHONY: binaries
binaries: $(BINARIES) ## Build Go binaries
bin/%: .FORCE
CGO_ENABLED=0 go build ${GO_BUILD_FLAGS} -o bin/$* ./cmd/$*
.PHONY: docker
docker: $(IMAGES) ## Build Docker images
docker-%: .FORCE
docker build -t regclient/$* -f build/Dockerfile.$*$(DOCKERFILE_EXT) $(DOCKER_ARGS) .
docker build -t regclient/$*:alpine -f build/Dockerfile.$*$(DOCKERFILE_EXT) --target release-alpine $(DOCKER_ARGS) .
.PHONY: oci-image
oci-image: $(addprefix oci-image-,$(COMMANDS)) ## Build reproducible images to an OCI Layout
oci-image-%: bin/regctl .FORCE
PATH="$(PWD)/bin:$(PATH)" build/oci-image.sh -r scratch -i "$*" -p "$(IMAGE_PLATFORMS)"
PATH="$(PWD)/bin:$(PATH)" build/oci-image.sh -r alpine -i "$*" -p "$(IMAGE_PLATFORMS)" -b "alpine:3"
.PHONY: test-docker
test-docker: $(addprefix test-docker-,$(COMMANDS)) ## Build multi-platform docker images (but do not tag)
test-docker-%:
docker buildx build --platform="$(IMAGE_PLATFORMS)" -f build/Dockerfile.$*.buildkit .
docker buildx build --platform="$(IMAGE_PLATFORMS)" -f build/Dockerfile.$*.buildkit --target release-alpine .
.PHONY: ci
ci: ci-distribution ci-zot ## Run CI tests against self hosted registries
.PHONY: ci-distribution
ci-distribution:
docker run --rm -d -p 5000 \
--label regclient-ci=true --name regclient-ci-distribution \
-e "REGISTRY_STORAGE_DELETE_ENABLED=true" \
docker.io/library/registry:$(CI_DISTRIBUTION_VER)
./build/ci-test.sh -t localhost:$$(docker port regclient-ci-distribution 5000 | head -1 | cut -f2 -d:)/test-ci
docker stop regclient-ci-distribution
.PHONY: ci-zot
ci-zot:
docker run --rm -d -p 5000 \
--label regclient-ci=true --name regclient-ci-zot \
-v "$$(pwd)/build/zot-config.json:/etc/zot/config.json:ro" \
ghcr.io/project-zot/zot-linux-amd64:$(CI_ZOT_VER)
./build/ci-test.sh -t localhost:$$(docker port regclient-ci-zot 5000 | head -1 | cut -f2 -d:)/test-ci
docker stop regclient-ci-zot
.PHONY: artifacts
artifacts: $(ARTIFACTS) ## Generate artifacts
.PHONY: artifact-pre
artifact-pre:
mkdir -p artifacts
artifacts/%: artifact-pre .FORCE
@set -e; \
target="$*"; \
command="$${target%%-*}"; \
platform_ext="$${target#*-}"; \
platform="$${platform_ext%.*}"; \
export GOOS="$${platform%%-*}"; \
export GOARCH="$${platform#*-}"; \
echo export GOOS=$${GOOS}; \
echo export GOARCH=$${GOARCH}; \
echo go build ${GO_BUILD_FLAGS} -o "$@" ./cmd/$${command}/; \
CGO_ENABLED=0 go build ${GO_BUILD_FLAGS} -o "$@" ./cmd/$${command}/; \
$(SYFT) scan -q "file:$@" --source-name "$${command}" -o cyclonedx-json >"artifacts/$${command}-$${platform}.cyclonedx.json"; \
$(SYFT) scan -q "file:$@" --source-name "$${command}" -o spdx-json >"artifacts/$${command}-$${platform}.spdx.json"
.PHONY: clean
clean: ## delete generated content
[ ! -d artifacts ] || rm -r artifacts
[ ! -d bin ] || rm -r bin
[ ! -d output ] || rm -r output
[ ! -d vendor ] || rm -r vendor
.PHONY: plugin-user
plugin-user:
mkdir -p ${HOME}/.docker/cli-plugins/
cp docker-plugin/docker-regclient ${HOME}/.docker/cli-plugins/docker-regctl
.PHONY: plugin-host
plugin-host:
sudo cp docker-plugin/docker-regclient /usr/libexec/docker/cli-plugins/docker-regctl
.PHONY: util-golang-major
util-golang-major: $(GOPATH)/bin/gomajor ## check for major dependency updates
$(GOPATH)/bin/gomajor list
.PHONY: util-golang-update
util-golang-update: ## update go module versions
go get -u -t ./...
go mod tidy
[ ! -d vendor ] || go mod vendor
.PHONY: util-release-preview
util-release-preview: $(GOPATH)/bin/gorelease ## preview changes for next release
git checkout main
./.github/release.sh -d
gorelease
.PHONY: util-release-run
util-release-run: ## generate a new release
git checkout main
./.github/release.sh
.PHONY: util-version-check
util-version-check: ## check all dependencies for updates
$(VER_BUMP) check
.PHONY: util-version-update
util-version-update: ## update versions on all dependencies
$(VER_BUMP) update
$(GOPATH)/bin/gofumpt: .FORCE
@[ -f "$(GOPATH)/bin/gofumpt" ] \
&& [ "$$($(GOPATH)/bin/gofumpt -version | cut -f 1 -d ' ')" = "$(GOFUMPT_VER)" ] \
|| go install mvdan.cc/gofumpt@$(GOFUMPT_VER)
$(GOPATH)/bin/gomajor: .FORCE
@[ -f "$(GOPATH)/bin/gomajor" ] \
&& [ "$$($(GOPATH)/bin/gomajor version | grep '^version' | cut -f 2 -d ' ')" = "$(GOMAJOR_VER)" ] \
|| go install github.com/icholy/gomajor@$(GOMAJOR_VER)
$(GOPATH)/bin/goimports: .FORCE
@[ -f "$(GOPATH)/bin/goimports" ] && [ "$$(go version | cut -f3 -d' ')" = "$$(go version $(GOPATH)/bin/goimports | cut -f2 -d' ')" ] \
|| go install golang.org/x/tools/cmd/goimports@latest
$(GOPATH)/bin/gorelease: .FORCE
@[ -f "$(GOPATH)/bin/gorelease" ] && [ "$$(go version | cut -f3 -d' ')" = "$$(go version $(GOPATH)/bin/gorelease | cut -f2 -d' ')" ] \
|| go install golang.org/x/exp/cmd/gorelease@latest
$(GOPATH)/bin/gosec: .FORCE
@[ -f $(GOPATH)/bin/gosec ] \
&& [ "$$($(GOPATH)/bin/gosec -version | grep '^Version' | cut -f 2 -d ' ')" = "$(GOSEC_VER)" ] \
|| go install -ldflags '-X main.Version=$(GOSEC_VER) -X main.GitTag=$(GOSEC_VER)' \
github.com/securego/gosec/v2/cmd/gosec@$(GOSEC_VER)
$(GOPATH)/bin/staticcheck: .FORCE
@[ -f $(GOPATH)/bin/staticcheck ] \
&& [ "$$($(GOPATH)/bin/staticcheck -version | cut -f 3 -d ' ' | tr -d '()')" = "$(STATICCHECK_VER)" ] \
|| go install "honnef.co/go/tools/cmd/staticcheck@$(STATICCHECK_VER)"
$(GOPATH)/bin/govulncheck: .FORCE
@[ -f $(GOPATH)/bin/govulncheck ] \
&& [ $$(go version -m $(GOPATH)/bin/govulncheck | \
awk -F ' ' '{ if ($$1 == "mod" && $$2 == "golang.org/x/vuln") { printf "%s\n", $$3 } }') = "$(GO_VULNCHECK_VER)" ] \
|| CGO_ENABLED=0 go install "golang.org/x/vuln/cmd/govulncheck@$(GO_VULNCHECK_VER)"
$(GOPATH)/bin/osv-scanner: .FORCE
@[ -f $(GOPATH)/bin/osv-scanner ] \
&& [ "$$(osv-scanner --version | awk -F ': ' '{ if ($$1 == "osv-scanner version") { printf "%s\n", $$2 } }')" = "$(OSV_SCANNER_VER)" ] \
|| CGO_ENABLED=0 go install "github.com/google/osv-scanner/v2/cmd/osv-scanner@$(OSV_SCANNER_VER)"
.PHONY: help
help: # Display help
@awk -F ':|##' '/^[^\t].+?:.*?##/ { printf "\033[36m%-30s\033[0m %s\n", $$1, $$NF }' $(MAKEFILE_LIST)
+116
View File
@@ -0,0 +1,116 @@
# regclient
[![Go Workflow Status](https://img.shields.io/github/actions/workflow/status/regclient/regclient/go.yml?branch=main&label=Go%20build)](https://github.com/regclient/regclient/actions/workflows/go.yml)
[![Docker Workflow Status](https://img.shields.io/github/actions/workflow/status/regclient/regclient/docker.yml?branch=main&label=Docker%20build)](https://github.com/regclient/regclient/actions/workflows/docker.yml)
[![Dependency Workflow Status](https://img.shields.io/github/actions/workflow/status/regclient/regclient/version-check.yml?branch=main&label=Dependency%20check)](https://github.com/regclient/regclient/actions/workflows/version-check.yml)
[![Vulnerability Workflow Status](https://img.shields.io/github/actions/workflow/status/regclient/regclient/vulnscans.yml?branch=main&label=Vulnerability%20check)](https://github.com/regclient/regclient/actions/workflows/vulnscans.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/regclient/regclient.svg)](https://pkg.go.dev/github.com/regclient/regclient)
![License](https://img.shields.io/github/license/regclient/regclient)
[![Go Report Card](https://goreportcard.com/badge/github.com/regclient/regclient)](https://goreportcard.com/report/github.com/regclient/regclient)
[![GitHub Downloads](https://img.shields.io/github/downloads/regclient/regclient/total?label=GitHub%20downloads)](https://github.com/regclient/regclient/releases)
regclient is a client interface to OCI conformant registries and content shipped with the OCI Image Layout.
It includes a Go library and several CLI commands.
## regclient Go Library Features
- Runs without a container runtime and without privileged access to the local host.
- Querying for a tag listing, repository listing, and remotely inspecting the contents of images.
- Efficiently copying and retagging images, only pulling layers when required, and without changing the image digest.
- Support for multi-platform images.
- Support for querying, creating, and copying OCI Artifacts, allowing arbitrary data to be stored in an OCI registry.
- Support for packaging OCI Artifacts with an Index of multiple artifacts, which can be used for platform specific artifacts.
- Support for querying OCI referrers, copying referrers, and pushing content with an OCI subject field, associating artifacts with other content on the registry.
- Support for the “digest tags” used by projects like sigstore/cosign, allowing the content to be included when copying images.
- Efficiently query for an image digest.
- Efficiently query for pull rate limits used by Docker Hub.
- Import and export content into OCI Layouts and Docker formatted tar files.
- Support OCI Layouts in all commands as a local disk equivalent of a repository.
- Support for deleting tags, manifests, and blobs.
- Ability to mutate existing images, including:
- Settings annotations or labels
- Deleting content from layers
- Changing timestamps for reproducibility
- Converting between Docker and OCI media types
- Replacing the base image layers
- Add or remove volumes and exposed ports
- Change digest algorithms
- Support for registry warning headers, which may be used to notify users of issues with the server or content they are using.
- Automatically import logins from the docker CLI, and registry certificates from the docker engine.
- Automatic retry, and fallback to a chunked blob push, when network issues are encountered.
The full Go references is available on [pkg.go.dev](https://pkg.go.dev/github.com/regclient/regclient).
## regctl Features
`regctl` is a CLI interface to the `regclient` library.
In addition to the features listed for `regclient`, `regctl` adds the following abilities:
- Generating multi-platform manifests from multiple images that may have been separately built.
- Repackage a multi-platform image with only the requested platforms.
- Push and pull arbitrary OCI artifacts.
- Recursively list all content associated with an image.
- Extract files from a layer or image.
- Compare images, showing the differences between manifests, the config, and layers.
- Formatted output using Go templates.
The project website includes [usage instructions](https://regclient.org/usage/regctl/) and a [CLI reference](https://regclient.org/cli/regctl/).
## regsync features
`regsync` is an image mirroring tool.
It will copy images between two locations with the following additional features:
- Ability to run on a cron schedule, one time synchronization, or only report stale images.
- Uses a yaml configuration.
- Each source may be an entire registry (not recommended), a repository, or a single image, with the ability to filter repositories and tags.
- Support for multi-platform images, OCI referrers, “digest tags”, and copying to or from an OCI Layout (for maintaining a mirror over an air-gap).
- Ability to mirror multiple images concurrently.
- Support for copying a single platform from multi-platform images.
- Ability to backup an existing image before overwriting the tag.
- Ability to postpone mirror step when rate limit (used by Docker Hub) is below a threshold.
- Can use user’s docker configuration for user credentials and registry certificates.
The project website includes [usage instructions](https://regclient.org/usage/regsync/) and a [CLI reference](https://regclient.org/cli/regsync/).
## regbot features
`regbot` is a scripting tool on top of the `regclient` API with the following features:
- Ability to run on a cron schedule, one time execution, or test with a dry-run mode.
- Uses a yaml configuration.
- Scripts are written in Lua and executed directly in Go.
- Built-in functions include:
- Repository list
- Tag list
- Image manifest (either head or get, and optional resolving multi-platform reference)
- Image config (this includes the creation time, labels, and other details shown in a docker image inspect)
- Image rate limit and a wait function to delay the script when rate limit remaining is below a threshold
- Image copy
- Manifest delete
- Tag delete
The project website includes [usage instructions](https://regclient.org/usage/regbot/) and a [CLI reference](https://regclient.org/cli/regbot/).
## Development Status
This project is using v0 version numbers due to Go's backwards compatibility requirements of a v1 release.
The library and commands are stable for external use.
Minor version updates may contain breaking changes, however effort is made to first deprecate and provide warnings to give users time to move off of older APIs and commands.
## Installing
See the [installation instructions](https://regclient.org/install/) on the project website for the various ways to download or build CLI binaries.
## Usage
See the [project documentation](https://regclient.org/usage/).
## Contributors
<a href="https://github.com/regclient/regclient/graphs/contributors">
<img src="https://contrib.rocks/image?repo=regclient/regclient" alt="contributor list"/>
</a>
<!-- markdownlint-disable-file MD033 -->
+5
View File
@@ -0,0 +1,5 @@
# Reporting security issues
Please report security issues directly in GitHub at <https://github.com/regclient/regclient/security/advisories/new> or alternatively email <git@bmitch.net>.
We will typically respond within 7 working days of your report. If the issue is confirmed as a vulnerability, we will open a Security Advisory and acknowledge your contributions as part of it. This project follows a 90 day disclosure timeline.
+283
View File
@@ -0,0 +1,283 @@
package regclient
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"log/slog"
"time"
"github.com/regclient/regclient/internal/pqueue"
"github.com/regclient/regclient/internal/reqmeta"
"github.com/regclient/regclient/scheme"
"github.com/regclient/regclient/types"
"github.com/regclient/regclient/types/blob"
"github.com/regclient/regclient/types/descriptor"
"github.com/regclient/regclient/types/errs"
"github.com/regclient/regclient/types/ref"
"github.com/regclient/regclient/types/warning"
)
const blobCBFreq = time.Millisecond * 100
type blobOpt struct {
callback func(kind types.CallbackKind, instance string, state types.CallbackState, cur, total int64)
readerHook func(*blob.BReader) (*blob.BReader, error)
}
// BlobOpts define options for the Image* commands.
type BlobOpts func(*blobOpt)
// BlobWithCallback provides progress data to a callback function.
func BlobWithCallback(callback func(kind types.CallbackKind, instance string, state types.CallbackState, cur, total int64)) BlobOpts {
return func(opts *blobOpt) {
opts.callback = callback
}
}
// BlobWithReaderHook is called in [RegClient.BlobCopy] with the blob source.
// The returned [blob.BReader] is pushed to the target.
// If the hook returns an error, the copy will fail.
func BlobWithReaderHook(hook func(*blob.BReader) (*blob.BReader, error)) BlobOpts {
return func(opts *blobOpt) {
opts.readerHook = hook
}
}
// BlobCopy copies a blob between two locations.
// If the blob already exists in the target, the copy is skipped.
// A server side cross repository blob mount is attempted.
func (rc *RegClient) BlobCopy(ctx context.Context, refSrc ref.Ref, refTgt ref.Ref, d descriptor.Descriptor, opts ...BlobOpts) error {
if !refSrc.IsSetRepo() {
return fmt.Errorf("refSrc is not set: %s%.0w", refSrc.CommonName(), errs.ErrInvalidReference)
}
if !refTgt.IsSetRepo() {
return fmt.Errorf("refTgt is not set: %s%.0w", refTgt.CommonName(), errs.ErrInvalidReference)
}
var opt blobOpt
for _, optFn := range opts {
optFn(&opt)
}
// dedup warnings
if w := warning.FromContext(ctx); w == nil {
ctx = warning.NewContext(ctx, &warning.Warning{Hook: warning.DefaultHook()})
}
tDesc := d
tDesc.URLs = []string{} // ignore URLs when pushing to target
if opt.callback != nil {
opt.callback(types.CallbackBlob, d.Digest.String(), types.CallbackStarted, 0, d.Size)
}
// for the same repository, there's nothing to copy
if ref.EqualRepository(refSrc, refTgt) {
if opt.callback != nil {
opt.callback(types.CallbackBlob, d.Digest.String(), types.CallbackSkipped, 0, d.Size)
}
rc.slog.Debug("Blob copy skipped, same repo",
slog.String("src", refSrc.Reference),
slog.String("tgt", refTgt.Reference),
slog.String("digest", string(d.Digest)))
return nil
}
// check if layer already exists
if _, err := rc.BlobHead(ctx, refTgt, tDesc); err == nil {
if opt.callback != nil {
opt.callback(types.CallbackBlob, d.Digest.String(), types.CallbackSkipped, 0, d.Size)
}
rc.slog.Debug("Blob copy skipped, already exists",
slog.String("src", refSrc.Reference),
slog.String("tgt", refTgt.Reference),
slog.String("digest", string(d.Digest)))
return nil
}
// acquire throttle for both src and tgt to avoid deadlocks
tList := []*pqueue.Queue[reqmeta.Data]{}
schemeSrcAPI, err := rc.schemeGet(refSrc.Scheme)
if err != nil {
return err
}
schemeTgtAPI, err := rc.schemeGet(refTgt.Scheme)
if err != nil {
return err
}
if tSrc, ok := schemeSrcAPI.(scheme.Throttler); ok {
tList = append(tList, tSrc.Throttle(refSrc, false)...)
}
if tTgt, ok := schemeTgtAPI.(scheme.Throttler); ok {
tList = append(tList, tTgt.Throttle(refTgt, true)...)
}
if len(tList) > 0 {
ctxMulti, done, err := pqueue.AcquireMulti[reqmeta.Data](ctx, reqmeta.Data{Kind: reqmeta.Blob, Size: d.Size}, tList...)
if err != nil {
return err
}
if done != nil {
defer done()
}
ctx = ctxMulti
}
// try mounting blob from the source repo is the registry is the same
if ref.EqualRegistry(refSrc, refTgt) {
err := rc.BlobMount(ctx, refSrc, refTgt, d)
if err == nil {
if opt.callback != nil {
opt.callback(types.CallbackBlob, d.Digest.String(), types.CallbackSkipped, 0, d.Size)
}
rc.slog.Debug("Blob copy performed server side with registry mount",
slog.String("src", refSrc.Reference),
slog.String("tgt", refTgt.Reference),
slog.String("digest", string(d.Digest)))
return nil
}
rc.slog.Warn("Failed to mount blob",
slog.String("src", refSrc.Reference),
slog.String("tgt", refTgt.Reference),
slog.String("err", err.Error()))
}
// fast options failed, download layer from source and push to target
blobIO, err := rc.BlobGet(ctx, refSrc, d)
if err != nil {
if !errors.Is(err, context.Canceled) {
rc.slog.Warn("Failed to retrieve blob",
slog.String("src", refSrc.Reference),
slog.String("digest", string(d.Digest)),
slog.String("err", err.Error()))
}
return err
}
if opt.callback != nil {
opt.callback(types.CallbackBlob, d.Digest.String(), types.CallbackStarted, 0, d.Size)
ticker := time.NewTicker(blobCBFreq)
done := make(chan bool)
defer func() {
close(done)
ticker.Stop()
if ctx.Err() == nil {
opt.callback(types.CallbackBlob, d.Digest.String(), types.CallbackFinished, d.Size, d.Size)
}
}()
go func() {
for {
select {
case <-done:
return
case <-ticker.C:
offset, err := blobIO.Seek(0, io.SeekCurrent)
if err == nil && offset > 0 {
opt.callback(types.CallbackBlob, d.Digest.String(), types.CallbackActive, offset, d.Size)
}
}
}
}()
}
if opt.readerHook != nil {
blobIO, err = opt.readerHook(blobIO)
if err != nil {
rc.slog.Warn("Failed to apply reader hook to blob",
slog.String("src", refSrc.Reference),
slog.String("err", err.Error()))
return err
}
}
defer blobIO.Close()
if _, err := rc.BlobPut(ctx, refTgt, blobIO.GetDescriptor(), blobIO); err != nil {
if !errors.Is(err, context.Canceled) {
rc.slog.Warn("Failed to push blob",
slog.String("src", refSrc.Reference),
slog.String("tgt", refTgt.Reference),
slog.String("err", err.Error()))
}
return err
}
return nil
}
// BlobDelete removes a blob from the registry.
// This method should only be used to repair a damaged registry.
// Typically a server side garbage collection should be used to purge unused blobs.
func (rc *RegClient) BlobDelete(ctx context.Context, r ref.Ref, d descriptor.Descriptor) error {
if !r.IsSetRepo() {
return fmt.Errorf("ref is not set: %s%.0w", r.CommonName(), errs.ErrInvalidReference)
}
schemeAPI, err := rc.schemeGet(r.Scheme)
if err != nil {
return err
}
return schemeAPI.BlobDelete(ctx, r, d)
}
// BlobGet retrieves a blob, returning a reader.
// This reader must be closed to free up resources that limit concurrent pulls.
func (rc *RegClient) BlobGet(ctx context.Context, r ref.Ref, d descriptor.Descriptor) (blob.Reader, error) {
data, err := d.GetData()
if err == nil {
return blob.NewReader(blob.WithDesc(d), blob.WithRef(r), blob.WithReader(bytes.NewReader(data))), nil
}
if !r.IsSetRepo() {
return nil, fmt.Errorf("ref is not set: %s%.0w", r.CommonName(), errs.ErrInvalidReference)
}
schemeAPI, err := rc.schemeGet(r.Scheme)
if err != nil {
return nil, err
}
return schemeAPI.BlobGet(ctx, r, d)
}
// BlobGetOCIConfig retrieves an OCI config from a blob, automatically extracting the JSON.
func (rc *RegClient) BlobGetOCIConfig(ctx context.Context, r ref.Ref, d descriptor.Descriptor) (blob.OCIConfig, error) {
if !r.IsSetRepo() {
return nil, fmt.Errorf("ref is not set: %s%.0w", r.CommonName(), errs.ErrInvalidReference)
}
b, err := rc.BlobGet(ctx, r, d)
if err != nil {
return nil, err
}
return b.ToOCIConfig()
}
// BlobHead is used to verify if a blob exists and is accessible.
func (rc *RegClient) BlobHead(ctx context.Context, r ref.Ref, d descriptor.Descriptor) (blob.Reader, error) {
if !r.IsSetRepo() {
return nil, fmt.Errorf("ref is not set: %s%.0w", r.CommonName(), errs.ErrInvalidReference)
}
schemeAPI, err := rc.schemeGet(r.Scheme)
if err != nil {
return nil, err
}
return schemeAPI.BlobHead(ctx, r, d)
}
// BlobMount attempts to perform a server side copy/mount of the blob between repositories.
func (rc *RegClient) BlobMount(ctx context.Context, refSrc ref.Ref, refTgt ref.Ref, d descriptor.Descriptor) error {
if !refSrc.IsSetRepo() {
return fmt.Errorf("ref is not set: %s%.0w", refSrc.CommonName(), errs.ErrInvalidReference)
}
if !refTgt.IsSetRepo() {
return fmt.Errorf("ref is not set: %s%.0w", refTgt.CommonName(), errs.ErrInvalidReference)
}
schemeAPI, err := rc.schemeGet(refSrc.Scheme)
if err != nil {
return err
}
return schemeAPI.BlobMount(ctx, refSrc, refTgt, d)
}
// BlobPut uploads a blob to a repository.
// Descriptor is optional, leave size and digest to zero value if unknown.
// Reader must also be an [io.Seeker] to support chunked upload fallback.
//
// This will attempt an anonymous blob mount first which some registries may support.
// It will then try doing a full put of the blob without chunking (most widely supported).
// If the full put fails, it will fall back to a chunked upload (useful for flaky networks).
func (rc *RegClient) BlobPut(ctx context.Context, r ref.Ref, d descriptor.Descriptor, rdr io.Reader) (descriptor.Descriptor, error) {
if !r.IsSetRepo() {
return descriptor.Descriptor{}, fmt.Errorf("ref is not set: %s%.0w", r.CommonName(), errs.ErrInvalidReference)
}
schemeAPI, err := rc.schemeGet(r.Scheme)
if err != nil {
return descriptor.Descriptor{}, err
}
return schemeAPI.BlobPut(ctx, r, d, rdr)
}
+100
View File
@@ -0,0 +1,100 @@
package config
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"strings"
)
// credHelper wraps a command that manages user credentials.
type credHelper struct {
prog string
env map[string]string
}
func newCredHelper(prog string, env map[string]string) *credHelper {
return &credHelper{prog: prog, env: env}
}
func (ch *credHelper) run(arg string, input io.Reader) ([]byte, error) {
//#nosec G204 only untrusted arg is a hostname which the executed command should not trust
cmd := exec.Command(ch.prog, arg)
cmd.Env = os.Environ()
if ch.env != nil {
for k, v := range ch.env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
}
}
cmd.Stderr = os.Stderr
cmd.Stdin = input
return cmd.Output()
}
type credStore struct {
ServerURL string `json:"ServerURL"`
Username string `json:"Username"`
Secret string `json:"Secret"` //#nosec G117 exported struct intentionally holds secrets
}
// get requests a credential from the helper for a given host.
func (ch *credHelper) get(host *Host) error {
hostname := host.Hostname
if host.CredHost != "" {
hostname = host.CredHost
}
hostIn := strings.NewReader(hostname)
credOut := credStore{
Username: host.User,
Secret: host.Pass,
}
outB, err := ch.run("get", hostIn)
if err != nil {
outS := strings.TrimSpace(string(outB))
return fmt.Errorf("error getting credentials, output: %s, error: %w", outS, err)
}
err = json.NewDecoder(bytes.NewReader(outB)).Decode(&credOut)
if err != nil {
return fmt.Errorf("error reading credentials: %w", err)
}
if credOut.Username == tokenUser {
host.User = ""
host.Pass = ""
host.Token = credOut.Secret
} else {
host.User = credOut.Username
host.Pass = credOut.Secret
host.Token = ""
}
return nil
}
// list returns a list of hosts supported by the credential helper.
func (ch *credHelper) list() ([]Host, error) {
credList := map[string]string{}
outB, err := ch.run("list", bytes.NewReader([]byte{}))
if err != nil {
outS := strings.TrimSpace(string(outB))
return nil, fmt.Errorf("error getting credential list, output: %s, error: %w", outS, err)
}
err = json.NewDecoder(bytes.NewReader(outB)).Decode(&credList)
if err != nil {
return nil, fmt.Errorf("error reading credential list: %w", err)
}
hostList := []Host{}
for host, user := range credList {
if !HostValidate(host) {
continue
}
h := HostNewName(host)
h.User = user
h.CredHelper = ch.prog
hostList = append(hostList, *h)
}
return hostList, nil
}
// TODO: store method not implemented
+210
View File
@@ -0,0 +1,210 @@
package config
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"os"
"strings"
"github.com/regclient/regclient/internal/conffile"
"github.com/regclient/regclient/types/errs"
)
const (
// dockerEnv is the environment variable used to look for Docker's config.json.
dockerEnv = "DOCKER_CONFIG"
// dockerEnvConfig is used to inject the config as an environment variable.
dockerEnvConfig = "DOCKER_AUTH_CONFIG"
// dockerDir is the directory name for Docker's config (inside the users home directory).
dockerDir = ".docker"
// dockerConfFile is the name of Docker's config file.
dockerConfFile = "config.json"
// dockerHelperPre is the prefix of docker credential helpers.
dockerHelperPre = "docker-credential-"
)
// dockerConfig is used to parse the ~/.docker/config.json
type dockerConfig struct {
AuthConfigs map[string]dockerAuthConfig `json:"auths"`
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
DetachKeys string `json:"detachKeys,omitempty"`
CredentialsStore string `json:"credsStore,omitempty"`
CredentialHelpers map[string]string `json:"credHelpers,omitempty"`
Proxies map[string]dockerProxyConfig `json:"proxies,omitempty"`
}
// dockerProxyConfig contains proxy configuration settings
type dockerProxyConfig struct {
HTTPProxy string `json:"httpProxy,omitempty"`
HTTPSProxy string `json:"httpsProxy,omitempty"`
NoProxy string `json:"noProxy,omitempty"`
FTPProxy string `json:"ftpProxy,omitempty"`
AllProxy string `json:"allProxy,omitempty"`
}
// dockerAuthConfig contains the auths
type dockerAuthConfig struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"` //#nosec G117 exported struct intentionally holds secrets
Auth string `json:"auth,omitempty"`
ServerAddress string `json:"serveraddress,omitempty"`
// IdentityToken is used to authenticate the user and get
// an access token for the registry.
IdentityToken string `json:"identitytoken,omitempty"`
// RegistryToken is a bearer token to be sent to a registry
RegistryToken string `json:"registrytoken,omitempty"`
}
// DockerLoad returns a slice of hosts from the users docker config.
// This will search for the config.json in either the DOCKER_CONFIG identified directory or the default .docker directory.
// It also includes hosts extracted from the DOCKER_AUTH_CONFIG variable.
// If the config file is missing and no value is injected using an environment variable, an empty list is returned.
func DockerLoad() ([]Host, error) {
hosts := []Host{}
errList := []error{}
// load from a file
cf := conffile.New(
conffile.WithHomeDir(dockerDir, dockerConfFile, true),
conffile.WithEnvDir(dockerEnv, dockerConfFile),
)
rdr, err := cf.Open()
if err != nil && !errors.Is(err, fs.ErrNotExist) {
errList = append(errList, err)
} else if err == nil {
defer rdr.Close()
hostsFile, err := dockerParse(rdr)
if err != nil {
errList = append(errList, err)
} else {
hosts = append(hosts, hostsFile...)
}
}
// load from an env var
hostsEnv, err := DockerLoadEnv(dockerEnvConfig)
if err != nil && !errors.Is(err, errs.ErrNotFound) {
errList = append(errList, err)
} else if err == nil {
hosts = append(hosts, hostsEnv...)
}
// return the concatenated result, only wrapping an error list if necessary
if len(errList) == 1 {
return hosts, errList[0]
} else {
return hosts, errors.Join(errList...)
}
}
// DockerLoadFile returns a slice of hosts from a named docker config file.
func DockerLoadFile(fname string) ([]Host, error) {
//#nosec G304 scoping file operations to a directory is not yet a feature of regclient.
rdr, err := os.Open(fname)
if err != nil && errors.Is(err, fs.ErrNotExist) {
return []Host{}, nil
} else if err != nil {
return nil, err
}
defer rdr.Close()
return dockerParse(rdr)
}
// DockerLoadEnv returns a slice of hosts extracted from the config injected in an environment variable.
func DockerLoadEnv(envName string) ([]Host, error) {
envVal := os.Getenv(envName)
if envVal == "" {
return []Host{}, errs.ErrNotFound
}
return dockerParse(strings.NewReader(envVal))
}
// dockerParse parses a docker config into a slice of Hosts.
func dockerParse(rdr io.Reader) ([]Host, error) {
dc := dockerConfig{}
if err := json.NewDecoder(rdr).Decode(&dc); err != nil && !errors.Is(err, io.EOF) {
return nil, err
}
hosts := []Host{}
for name, auth := range dc.AuthConfigs {
if !HostValidate(name) {
continue
}
h, err := dockerAuthToHost(name, dc, auth)
if err != nil {
continue
}
hosts = append(hosts, h)
}
// also include default entries for credential helpers
for name, helper := range dc.CredentialHelpers {
if !HostValidate(name) {
continue
}
h := HostNewName(name)
h.CredHelper = dockerHelperPre + helper
if _, ok := dc.AuthConfigs[name]; ok {
continue // skip fields with auth config
}
hosts = append(hosts, *h)
}
// add credStore entries
if dc.CredentialsStore != "" {
ch := newCredHelper(dockerHelperPre+dc.CredentialsStore, map[string]string{})
csHosts, err := ch.list()
if err == nil {
hosts = append(hosts, csHosts...)
}
}
return hosts, nil
}
// dockerAuthToHost parses an auth entry from a docker config into a Host.
func dockerAuthToHost(name string, conf dockerConfig, auth dockerAuthConfig) (Host, error) {
helper := ""
if conf.CredentialHelpers != nil && conf.CredentialHelpers[name] != "" {
helper = dockerHelperPre + conf.CredentialHelpers[name]
}
// parse base64 auth into user/pass
if auth.Auth != "" {
var err error
auth.Username, auth.Password, err = decodeAuth(auth.Auth)
if err != nil {
return Host{}, err
}
}
if (auth.Username == "" || auth.Password == "") && auth.IdentityToken == "" && helper == "" {
return Host{}, fmt.Errorf("no credentials found for %s", name)
}
h := HostNewName(name)
// ignore unknown names
if h.Name != DockerRegistry && !strings.HasSuffix(strings.TrimSuffix(name, "/"), h.Name) {
return Host{}, fmt.Errorf("rejecting entry with repository: %s", name)
}
h.User = auth.Username
h.Pass = auth.Password
h.Token = auth.IdentityToken
h.CredHelper = helper
return *h, nil
}
// decodeAuth extracts a base64 encoded user:pass into the username and password.
func decodeAuth(authStr string) (string, string, error) {
if authStr == "" {
return "", "", nil
}
decoded, err := base64.StdEncoding.DecodeString(authStr)
if err != nil {
return "", "", err
}
userPass := strings.SplitN(string(decoded), ":", 2)
if len(userPass) != 2 {
return "", "", fmt.Errorf("invalid auth configuration file")
}
return userPass[0], strings.Trim(userPass[1], "\x00"), nil
}
+521
View File
@@ -0,0 +1,521 @@
// Package config is used for all regclient configuration settings.
package config
import (
"encoding/json"
"fmt"
"io"
"log/slog"
"maps"
"slices"
"strings"
"time"
"github.com/regclient/regclient/internal/timejson"
)
// TLSConf specifies whether TLS is enabled and verified for a host.
type TLSConf int
const (
// TLSUndefined indicates TLS is not passed, defaults to Enabled.
TLSUndefined TLSConf = iota
// TLSEnabled uses TLS (https) for the connection.
TLSEnabled
// TLSInsecure uses TLS but does not verify CA.
TLSInsecure
// TLSDisabled does not use TLS (http).
TLSDisabled
)
const (
// DockerRegistry is the name resolved in docker images on Hub.
DockerRegistry = "docker.io"
// DockerRegistryAuth is the name provided in docker's config for Hub.
DockerRegistryAuth = "https://index.docker.io/v1/"
// DockerRegistryDNS is the host to connect to for Hub.
DockerRegistryDNS = "registry-1.docker.io"
// defaultExpire is the default time to expire a credential and force re-authentication.
defaultExpire = time.Hour * 1
// defaultCredHelperRetry is the time to refresh a credential from a failed credential helper command.
defaultCredHelperRetry = time.Second * 5
// defaultConcurrent is the default number of concurrent registry connections.
defaultConcurrent = 3
// defaultReqPerSec is the default maximum frequency to send requests to a registry.
defaultReqPerSec = 0
// tokenUser is the username returned by credential helpers that indicates the password is an identity token.
tokenUser = "<token>"
)
// MarshalJSON converts TLSConf to a json string using MarshalText.
func (t TLSConf) MarshalJSON() ([]byte, error) {
s, err := t.MarshalText()
if err != nil {
return []byte(""), err
}
return json.Marshal(string(s))
}
// MarshalText converts TLSConf to a string.
func (t TLSConf) MarshalText() ([]byte, error) {
var s string
switch t {
default:
s = ""
case TLSEnabled:
s = "enabled"
case TLSInsecure:
s = "insecure"
case TLSDisabled:
s = "disabled"
}
return []byte(s), nil
}
// UnmarshalJSON converts TLSConf from a json string.
func (t *TLSConf) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
return t.UnmarshalText([]byte(s))
}
// UnmarshalText converts TLSConf from a string.
func (t *TLSConf) UnmarshalText(b []byte) error {
switch strings.ToLower(string(b)) {
default:
return fmt.Errorf("unknown TLS value \"%s\"", b)
case "":
*t = TLSUndefined
case "enabled":
*t = TLSEnabled
case "insecure":
*t = TLSInsecure
case "disabled":
*t = TLSDisabled
}
return nil
}
// Host defines settings for connecting to a registry.
type Host struct {
Name string `json:"-" yaml:"registry,omitempty"` // Name of the registry (required) (yaml configs pass this as a field, json provides this from the object key)
TLS TLSConf `json:"tls,omitempty" yaml:"tls"` // TLS setting: enabled (default), disabled, insecure
RegCert string `json:"regcert,omitempty" yaml:"regcert"` // public pem cert of registry
ClientCert string `json:"clientCert,omitempty" yaml:"clientCert"` // public pem cert for client (mTLS)
ClientKey string `json:"clientKey,omitempty" yaml:"clientKey"` //#nosec G117 private pem cert for client (mTLS)
Hostname string `json:"hostname,omitempty" yaml:"hostname"` // hostname of registry, default is the registry name
User string `json:"user,omitempty" yaml:"user"` // username, not used with credHelper
Pass string `json:"pass,omitempty" yaml:"pass"` //#nosec G117 password, not used with credHelper
Token string `json:"token,omitempty" yaml:"token"` // token, experimental for specific APIs
CredHelper string `json:"credHelper,omitempty" yaml:"credHelper"` // credential helper command for requesting logins
CredExpire timejson.Duration `json:"credExpire,omitempty" yaml:"credExpire"` // time until credential expires
CredHost string `json:"credHost,omitempty" yaml:"credHost"` // used when a helper hostname doesn't match Hostname
PathPrefix string `json:"pathPrefix,omitempty" yaml:"pathPrefix"` // used for mirrors defined within a repository namespace
Mirrors []string `json:"mirrors,omitempty" yaml:"mirrors"` // list of other Host Names to use as mirrors
Priority uint `json:"priority,omitempty" yaml:"priority"` // priority when sorting mirrors, higher priority attempted first
RepoAuth bool `json:"repoAuth,omitempty" yaml:"repoAuth"` // tracks a separate auth per repo
API string `json:"api,omitempty" yaml:"api"` // Deprecated: registry API to use
APIOpts map[string]string `json:"apiOpts,omitempty" yaml:"apiOpts"` // options for APIs
BlobChunk int64 `json:"blobChunk,omitempty" yaml:"blobChunk"` // size of each blob chunk
BlobMax int64 `json:"blobMax,omitempty" yaml:"blobMax"` // threshold to switch to chunked upload, -1 to disable, 0 for regclient.blobMaxPut
ReqPerSec float64 `json:"reqPerSec,omitempty" yaml:"reqPerSec"` // requests per second
ReqConcurrent int64 `json:"reqConcurrent,omitempty" yaml:"reqConcurrent"` // concurrent requests, default is defaultConcurrent(3)
Scheme string `json:"scheme,omitempty" yaml:"scheme"` // Deprecated: use TLS instead
credRefresh time.Time `json:"-" yaml:"-"` // internal use, when to refresh credentials
}
// Cred defines a user credential for accessing a registry.
type Cred struct {
User, Password, Token string //#nosec G117 exported struct intentionally holds secrets
}
// HostNew creates a default Host entry.
func HostNew() *Host {
h := Host{
TLS: TLSEnabled,
APIOpts: map[string]string{},
ReqConcurrent: int64(defaultConcurrent),
ReqPerSec: float64(defaultReqPerSec),
}
return &h
}
// HostNewName creates a default Host with a hostname.
func HostNewName(name string) *Host {
return HostNewDefName(nil, name)
}
// HostNewDefName creates a host using provided defaults and hostname.
func HostNewDefName(def *Host, name string) *Host {
var h Host
if def == nil {
h = *HostNew()
} else {
h = *def
// configure required defaults
if h.TLS == TLSUndefined {
h.TLS = TLSEnabled
}
if h.APIOpts == nil {
h.APIOpts = map[string]string{}
}
if h.ReqConcurrent == 0 {
h.ReqConcurrent = int64(defaultConcurrent)
}
if h.ReqPerSec == 0 {
h.ReqPerSec = float64(defaultReqPerSec)
}
// copy any fields that are not passed by value
if len(h.APIOpts) > 0 {
orig := h.APIOpts
h.APIOpts = map[string]string{}
maps.Copy(h.APIOpts, orig)
}
if h.Mirrors != nil {
orig := h.Mirrors
h.Mirrors = make([]string, len(orig))
copy(h.Mirrors, orig)
}
}
// configure host
scheme, registry, _ := parseName(name)
if scheme == "http" {
h.TLS = TLSDisabled
}
// Docker Hub is a special case
if registry == DockerRegistry {
h.Name = DockerRegistry
h.Hostname = DockerRegistryDNS
h.CredHost = DockerRegistryAuth
return &h
}
h.Name = registry
h.Hostname = registry
if name != registry {
h.CredHost = name
}
return &h
}
// HostValidate returns true if the scheme is missing or a known value, and the path is not set.
func HostValidate(name string) bool {
scheme, _, path := parseName(name)
return path == "" && (scheme == "https" || scheme == "http")
}
// GetCred returns the credential, fetching from a credential helper if needed.
func (host *Host) GetCred() Cred {
// refresh from credHelper if needed
if host.CredHelper != "" && (host.credRefresh.IsZero() || time.Now().After(host.credRefresh)) {
host.refreshHelper()
}
return Cred{User: host.User, Password: host.Pass, Token: host.Token}
}
func (host *Host) refreshHelper() {
if host.CredHelper == "" {
return
}
if host.CredExpire <= 0 {
host.CredExpire = timejson.Duration(defaultExpire)
}
// run a cred helper, calling get method
ch := newCredHelper(host.CredHelper, map[string]string{})
err := ch.get(host)
if err != nil {
host.credRefresh = time.Now().Add(defaultCredHelperRetry)
} else {
host.credRefresh = time.Now().Add(time.Duration(host.CredExpire))
}
}
// IsZero returns true if the struct is set to the zero value or the result of [HostNew].
func (host Host) IsZero() bool {
if (host.TLS != TLSUndefined && host.TLS != TLSEnabled) ||
host.RegCert != "" ||
host.ClientCert != "" ||
host.ClientKey != "" ||
(host.Hostname != "" && host.Hostname != host.Name) ||
host.User != "" ||
host.Pass != "" ||
host.Token != "" ||
host.CredHelper != "" ||
host.CredExpire != 0 ||
host.CredHost != "" ||
host.PathPrefix != "" ||
len(host.Mirrors) != 0 ||
host.Priority != 0 ||
host.RepoAuth ||
len(host.APIOpts) != 0 ||
host.BlobChunk != 0 ||
host.BlobMax != 0 ||
(host.ReqPerSec != 0 && host.ReqPerSec != float64(defaultReqPerSec)) ||
(host.ReqConcurrent != 0 && host.ReqConcurrent != int64(defaultConcurrent)) ||
!host.credRefresh.IsZero() {
return false
}
return true
}
// Merge adds fields from a new config host entry.
func (host *Host) Merge(newHost Host, log *slog.Logger) error {
name := newHost.Name
if name == "" {
name = host.Name
}
if log == nil {
log = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
}
// merge the existing and new config host
if host.Name == "" {
// only set the name if it's not initialized, this shouldn't normally change
host.Name = newHost.Name
}
if newHost.CredHelper == "" && (newHost.Pass != "" || host.Token != "") {
// unset existing cred helper for user/pass or token
host.CredHelper = ""
host.CredExpire = 0
}
if newHost.CredHelper != "" && newHost.User == "" && newHost.Pass == "" && newHost.Token == "" {
// unset existing user/pass/token for cred helper
host.User = ""
host.Pass = ""
host.Token = ""
}
if newHost.User != "" {
if host.User != "" && host.User != newHost.User {
log.Warn("Changing login user for registry",
slog.String("orig", host.User),
slog.String("new", newHost.User),
slog.String("host", name))
}
host.User = newHost.User
}
if newHost.Pass != "" {
if host.Pass != "" && host.Pass != newHost.Pass {
log.Warn("Changing login password for registry",
slog.String("host", name))
}
host.Pass = newHost.Pass
}
if newHost.Token != "" {
if host.Token != "" && host.Token != newHost.Token {
log.Warn("Changing login token for registry",
slog.String("host", name))
}
host.Token = newHost.Token
}
if newHost.CredHelper != "" {
if host.CredHelper != "" && host.CredHelper != newHost.CredHelper {
log.Warn("Changing credential helper for registry",
slog.String("host", name),
slog.String("orig", host.CredHelper),
slog.String("new", newHost.CredHelper))
}
host.CredHelper = newHost.CredHelper
}
if newHost.CredExpire != 0 {
if host.CredExpire != 0 && host.CredExpire != newHost.CredExpire {
log.Warn("Changing credential expire for registry",
slog.String("host", name),
slog.Any("orig", host.CredExpire),
slog.Any("new", newHost.CredExpire))
}
host.CredExpire = newHost.CredExpire
}
if newHost.CredHost != "" {
if host.CredHost != "" && host.CredHost != newHost.CredHost {
log.Warn("Changing credential host for registry",
slog.String("host", name),
slog.String("orig", host.CredHost),
slog.String("new", newHost.CredHost))
}
host.CredHost = newHost.CredHost
}
if newHost.TLS != TLSUndefined {
if host.TLS != TLSUndefined && host.TLS != newHost.TLS {
tlsOrig, _ := host.TLS.MarshalText()
tlsNew, _ := newHost.TLS.MarshalText()
log.Warn("Changing TLS settings for registry",
slog.String("orig", string(tlsOrig)),
slog.String("new", string(tlsNew)),
slog.String("host", name))
}
host.TLS = newHost.TLS
}
if newHost.RegCert != "" {
if host.RegCert != "" && host.RegCert != newHost.RegCert {
log.Warn("Changing certificate settings for registry",
slog.String("orig", host.RegCert),
slog.String("new", newHost.RegCert),
slog.String("host", name))
}
host.RegCert = newHost.RegCert
}
if newHost.ClientCert != "" {
if host.ClientCert != "" && host.ClientCert != newHost.ClientCert {
log.Warn("Changing client certificate settings for registry",
slog.String("orig", host.ClientCert),
slog.String("new", newHost.ClientCert),
slog.String("host", name))
}
host.ClientCert = newHost.ClientCert
}
if newHost.ClientKey != "" {
if host.ClientKey != "" && host.ClientKey != newHost.ClientKey {
log.Warn("Changing client certificate key settings for registry",
slog.String("host", name))
}
host.ClientKey = newHost.ClientKey
}
if newHost.Hostname != "" {
if host.Hostname != "" && host.Hostname != newHost.Hostname {
log.Warn("Changing hostname settings for registry",
slog.String("orig", host.Hostname),
slog.String("new", newHost.Hostname),
slog.String("host", name))
}
host.Hostname = newHost.Hostname
}
if newHost.PathPrefix != "" {
newHost.PathPrefix = strings.Trim(newHost.PathPrefix, "/") // leading and trailing / are not needed
if host.PathPrefix != "" && host.PathPrefix != newHost.PathPrefix {
log.Warn("Changing path prefix settings for registry",
slog.String("orig", host.PathPrefix),
slog.String("new", newHost.PathPrefix),
slog.String("host", name))
}
host.PathPrefix = newHost.PathPrefix
}
if len(newHost.Mirrors) > 0 {
if len(host.Mirrors) > 0 && !slices.Equal(host.Mirrors, newHost.Mirrors) {
log.Warn("Changing mirror settings for registry",
slog.Any("orig", host.Mirrors),
slog.Any("new", newHost.Mirrors),
slog.String("host", name))
}
host.Mirrors = newHost.Mirrors
}
if newHost.Priority != 0 {
if host.Priority != 0 && host.Priority != newHost.Priority {
log.Warn("Changing priority settings for registry",
slog.Uint64("orig", uint64(host.Priority)),
slog.Uint64("new", uint64(newHost.Priority)),
slog.String("host", name))
}
host.Priority = newHost.Priority
}
if newHost.RepoAuth {
host.RepoAuth = newHost.RepoAuth
}
// TODO: eventually delete
if newHost.API != "" {
log.Warn("API field has been deprecated",
slog.String("api", newHost.API),
slog.String("host", name))
}
if len(newHost.APIOpts) > 0 {
if len(host.APIOpts) > 0 {
merged := maps.Clone(host.APIOpts)
for k, v := range newHost.APIOpts {
if host.APIOpts[k] != "" && host.APIOpts[k] != v {
log.Warn("Changing APIOpts setting for registry",
slog.String("orig", host.APIOpts[k]),
slog.String("new", newHost.APIOpts[k]),
slog.String("opt", k),
slog.String("host", name))
}
merged[k] = v
}
host.APIOpts = merged
} else {
host.APIOpts = newHost.APIOpts
}
}
if newHost.BlobChunk > 0 {
if host.BlobChunk != 0 && host.BlobChunk != newHost.BlobChunk {
log.Warn("Changing blobChunk settings for registry",
slog.Int64("orig", host.BlobChunk),
slog.Int64("new", newHost.BlobChunk),
slog.String("host", name))
}
host.BlobChunk = newHost.BlobChunk
}
if newHost.BlobMax != 0 {
if host.BlobMax != 0 && host.BlobMax != newHost.BlobMax {
log.Warn("Changing blobMax settings for registry",
slog.Int64("orig", host.BlobMax),
slog.Int64("new", newHost.BlobMax),
slog.String("host", name))
}
host.BlobMax = newHost.BlobMax
}
if newHost.ReqPerSec != 0 {
if host.ReqPerSec != 0 && host.ReqPerSec != newHost.ReqPerSec {
log.Warn("Changing reqPerSec settings for registry",
slog.Float64("orig", host.ReqPerSec),
slog.Float64("new", newHost.ReqPerSec),
slog.String("host", name))
}
host.ReqPerSec = newHost.ReqPerSec
}
if newHost.ReqConcurrent > 0 {
if host.ReqConcurrent != 0 && host.ReqConcurrent != newHost.ReqConcurrent {
log.Warn("Changing reqPerSec settings for registry",
slog.Int64("orig", host.ReqConcurrent),
slog.Int64("new", newHost.ReqConcurrent),
slog.String("host", name))
}
host.ReqConcurrent = newHost.ReqConcurrent
}
return nil
}
// parseName splits a registry into the scheme, hostname, and repository/path.
func parseName(name string) (string, string, string) {
scheme := "https"
path := ""
// Docker Hub is a special case
if name == DockerRegistryAuth || name == DockerRegistryDNS || name == DockerRegistry {
return scheme, DockerRegistry, ""
}
// handle http/https prefix
i := strings.Index(name, "://")
if i > 0 {
scheme = name[:i]
name = name[i+3:]
}
// trim any repository path
i = strings.Index(name, "/")
if i > 0 {
path = name[i+1:]
name = name[:i]
}
return scheme, name, path
}
+1905
View File
File diff suppressed because it is too large Load Diff
+923
View File
@@ -0,0 +1,923 @@
// Package auth is used for HTTP authentication
package auth
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"slices"
"strings"
"sync"
"time"
"github.com/regclient/regclient/internal/regnet"
"github.com/regclient/regclient/types/errs"
)
type charLU byte
var charLUs [256]charLU
var defaultClientID = "regclient"
// minTokenLife tokens are required to last at least 60 seconds to support older docker clients
var minTokenLife = 60
// tokenBuffer is used to renew a token before it expires to account for time to process requests on the server
var tokenBuffer = time.Second * 5
const (
isSpace charLU = 1 << iota
isToken
)
func init() {
for c := range 256 {
charLUs[c] = 0
if strings.ContainsRune(" \t\r\n", rune(c)) {
charLUs[c] |= isSpace
}
if (rune('a') <= rune(c) && rune(c) <= rune('z')) || (rune('A') <= rune(c) && rune(c) <= rune('Z') || (rune('0') <= rune(c) && rune(c) <= rune('9')) || strings.ContainsRune("-._~+/", rune(c))) {
charLUs[c] |= isToken
}
}
}
// CredsFn is passed to lookup credentials for a given hostname, response is a username and password or empty strings
type CredsFn func(host string) Cred
// Cred is returned by the CredsFn.
// If Token is provided and auth method is bearer, it will attempt to use it as a refresh token.
// Else if user and password are provided, they are attempted with all auth methods.
// Else if neither are provided and auth method is bearer, an anonymous login is attempted.
type Cred struct {
//#nosec G117 exported struct intentionally holds secrets
User, Password string // clear text username and password
Token string // refresh token only used for bearer auth
}
// challenge is the extracted contents of the WWW-Authenticate header.
type challenge struct {
authType string
params map[string]string
}
// handler handles a challenge for a host to return an auth header
type handler interface {
AddScope(scope string) error
ProcessChallenge(challenge) error
UpdateRequest(*http.Request) error
}
// handlerBuild is used to make a new handler for a specific authType and URL
type handlerBuild func(client *http.Client, clientID, host string, credFn CredsFn, slog *slog.Logger) handler
// Opts configures options for NewAuth
type Opts func(*Auth)
// Auth is used to handle authentication requests.
type Auth struct {
httpClient *http.Client
clientID string
credsFn CredsFn
hbs map[string]handlerBuild // handler builders based on authType
hs map[string]map[string]handler // handlers based on url and authType
authTypes []string
slog *slog.Logger
mu sync.Mutex
}
// NewAuth creates a new Auth
func NewAuth(opts ...Opts) *Auth {
a := &Auth{
httpClient: &http.Client{},
clientID: defaultClientID,
credsFn: DefaultCredsFn,
hbs: map[string]handlerBuild{},
hs: map[string]map[string]handler{},
authTypes: []string{},
slog: slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})),
}
for _, opt := range opts {
opt(a)
}
if len(a.authTypes) == 0 {
a.addDefaultHandlers()
}
return a
}
// WithCreds provides a user/pass lookup for a url
func WithCreds(f CredsFn) Opts {
return func(a *Auth) {
if f != nil {
a.credsFn = f
}
}
}
// WithHTTPClient uses a specific http client with requests
func WithHTTPClient(h *http.Client) Opts {
return func(a *Auth) {
if h != nil {
a.httpClient = h
}
}
}
// WithClientID uses a client ID with request headers
func WithClientID(clientID string) Opts {
return func(a *Auth) {
a.clientID = clientID
}
}
// WithHandler includes a handler for a specific auth type
func WithHandler(authType string, hb handlerBuild) Opts {
return func(a *Auth) {
lcat := strings.ToLower(authType)
a.hbs[lcat] = hb
a.authTypes = append(a.authTypes, lcat)
}
}
// WithDefaultHandlers includes a Basic and Bearer handler, this is automatically added with "WithHandler" is not called
func WithDefaultHandlers() Opts {
return func(a *Auth) {
a.addDefaultHandlers()
}
}
// WithLog injects a Logger
func WithLog(slog *slog.Logger) Opts {
return func(a *Auth) {
a.slog = slog
}
}
// AddScope extends an existing auth with additional scopes.
// This is used to pre-populate scopes with the Docker convention rather than
// depend on the registry to respond with the correct http status and headers.
func (a *Auth) AddScope(host, scope string) error {
a.mu.Lock()
defer a.mu.Unlock()
success := false
if a.hs[host] == nil {
return errs.ErrNoNewChallenge
}
for _, at := range a.authTypes {
if a.hs[host][at] != nil {
err := a.hs[host][at].AddScope(scope)
if err == nil {
success = true
} else if err != errs.ErrNoNewChallenge {
return err
}
}
}
if !success {
return errs.ErrNoNewChallenge
}
a.slog.Debug("Auth scope added",
slog.String("host", host),
slog.String("scope", scope))
return nil
}
// HandleResponse parses the 401 response, extracting the WWW-Authenticate
// header and verifying the requirement is different from what was included in
// the last request
func (a *Auth) HandleResponse(resp *http.Response) error {
a.mu.Lock()
defer a.mu.Unlock()
// verify response is an access denied
if resp.StatusCode != http.StatusUnauthorized {
return errs.ErrUnsupported
}
// extract host and auth header
host := resp.Request.URL.Host
cl, err := ParseAuthHeaders(resp.Header.Values("WWW-Authenticate"))
if err != nil {
return err
}
a.slog.Debug("Auth request parsed",
slog.Any("challenge", cl))
if len(cl) < 1 {
return errs.ErrEmptyChallenge
}
goodChallenge := false
// loop over the received challenge(s)
for _, c := range cl {
if _, ok := a.hbs[c.authType]; !ok {
a.slog.Warn("Unsupported auth type",
slog.String("authtype", c.authType))
continue
}
// setup a handler for the host and auth type
if _, ok := a.hs[host]; !ok {
a.hs[host] = map[string]handler{}
}
if _, ok := a.hs[host][c.authType]; !ok {
h := a.hbs[c.authType](a.httpClient, a.clientID, host, a.credsFn, a.slog)
if h == nil {
continue
}
a.hs[host][c.authType] = h
}
// process the challenge with that handler
err := a.hs[host][c.authType].ProcessChallenge(c)
if err == nil {
goodChallenge = true
} else if err == errs.ErrNoNewChallenge {
// handle race condition when another request updates the challenge
// detect that by seeing the current auth header is different
prevAH := resp.Request.Header.Get("Authorization")
err := a.hs[host][c.authType].UpdateRequest(resp.Request)
if err == nil && prevAH != resp.Request.Header.Get("Authorization") {
goodChallenge = true
}
} else {
return err
}
}
if !goodChallenge {
return errs.ErrHTTPUnauthorized
}
return nil
}
// UpdateRequest adds Authorization headers to a request
func (a *Auth) UpdateRequest(req *http.Request) error {
a.mu.Lock()
defer a.mu.Unlock()
host := req.URL.Host
if a.hs[host] == nil {
return nil
}
var err error
for _, at := range a.authTypes {
if a.hs[host][at] != nil {
err = a.hs[host][at].UpdateRequest(req)
if err != nil {
a.slog.Debug("Failed to generate auth",
slog.String("err", err.Error()),
slog.String("host", host),
slog.String("authtype", at))
continue
}
break
}
}
if err != nil {
return err
}
return nil
}
func (a *Auth) addDefaultHandlers() {
if _, ok := a.hbs["basic"]; !ok {
a.hbs["basic"] = NewBasicHandler
a.authTypes = append(a.authTypes, "basic")
}
if _, ok := a.hbs["bearer"]; !ok {
a.hbs["bearer"] = NewBearerHandler
a.authTypes = append(a.authTypes, "bearer")
}
}
// DefaultCredsFn is used to return no credentials when auth is not configured with a CredsFn
// This avoids the need to check for nil pointers
func DefaultCredsFn(h string) Cred {
return Cred{}
}
// ParseAuthHeaders extracts the scheme and realm from WWW-Authenticate headers
func ParseAuthHeaders(ahl []string) ([]challenge, error) {
var cl []challenge
for _, ah := range ahl {
c, err := parseAuthHeader(ah)
if err != nil {
return nil, fmt.Errorf("failed to parse challenge header: %s, %w", ah, err)
}
cl = append(cl, c...)
}
return cl, nil
}
// parseAuthHeader parses a single header line for WWW-Authenticate
// Example values:
// Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:samalba/my-app:pull,push"
// Basic realm="GitHub Package Registry"
func parseAuthHeader(ah string) ([]challenge, error) {
var cl []challenge
var c *challenge
curElement := []byte{}
curKey := ""
stateElement := "string"
stateSyntax := "start"
for _, b := range []byte(ah) {
switch stateElement {
case "string":
// string: ignore leading space, enter quote only if first character, handle escapes, handle valid tokens, else end
if charLUs[b]&isToken != 0 {
// add valid tokens to element
curElement = append(curElement, b)
} else if charLUs[b]&isSpace != 0 && len(curElement) == 0 {
// ignore leading spaces
} else if b == '"' && len(curElement) == 0 {
stateElement = "quote"
} else if b == '\\' {
stateElement = "escape_string"
} else {
stateElement = "end"
}
case "quote":
// quote: handle escapes, handle closing quote (quote_end to read next character), all other tokens are valid
if b == '\\' {
stateElement = "escape_quote"
} else if b == '"' {
stateElement = "quote_end"
} else {
curElement = append(curElement, b)
}
case "quote_end":
// any character after the close quote is the end of the element
stateElement = "end"
case "escape_string":
// escape_string: handle any character and return to string state
curElement = append(curElement, b)
stateElement = "string"
case "escape_quote":
// escape_quote: handle any character and return to quote state
curElement = append(curElement, b)
stateElement = "quote"
case "end":
// finished parsing element, continue to processing element according to the current state
default:
return nil, fmt.Errorf("unhandled element case: %w", errs.ErrParsingFailed)
}
if stateElement != "end" {
// continue parsing the element until it ends
continue
}
// syntax looks at each string within the overall challenge syntax
switch stateSyntax {
case "start":
// start: (start of auth_type) read auth_type and space (end_auth_type) or auth_type and comma (start)
if charLUs[b]&isSpace != 0 && len(curElement) > 0 {
stateSyntax = "end_auth_type"
} else if b == ',' && len(curElement) > 0 {
// state remains at start
} else {
return nil, fmt.Errorf("start element did not end with a space or comma: %w", errs.ErrParsingFailed)
}
c = &challenge{authType: strings.ToLower(string(curElement)), params: map[string]string{}}
cl = append(cl, *c)
case "start_or_param":
// start_or_param: (after param_value) read auth_type and space (end_auth_type) or param_key and equals (param_value)
if charLUs[b]&isSpace != 0 && len(curElement) > 0 {
c = &challenge{authType: strings.ToLower(string(curElement)), params: map[string]string{}}
cl = append(cl, *c)
stateSyntax = "end_auth_type"
} else if b == '=' && len(curElement) > 0 {
curKey = strings.ToLower(string(curElement))
stateSyntax = "param_value"
} else {
return nil, fmt.Errorf("expected auth type or param: %w", errs.ErrParsingFailed)
}
case "end_auth_type":
// end_auth_type: (after reading auth_type) read param_key and equals (param_value) or just a comma (start)
if b == '=' && len(curElement) > 0 {
curKey = strings.ToLower(string(curElement))
stateSyntax = "param_value"
} else if b == ',' && len(curElement) == 0 {
// ignore white space between end of auth_type and comma
stateSyntax = "start"
} else {
return nil, fmt.Errorf("expected param or comma: %w", errs.ErrParsingFailed)
}
case "param_value":
// param_value: (after param_key) read param_value and comma (start_or_param)
if b == ',' {
c.params[curKey] = string(curElement)
stateSyntax = "start_or_param"
curKey = ""
} else {
return nil, fmt.Errorf("expected param value: %w", errs.ErrParsingFailed)
}
default:
return nil, fmt.Errorf("unhandled syntax case: %w", errs.ErrParsingFailed)
}
// reset element state
stateElement = "string"
curElement = []byte{}
}
// at end of parsing, if the element is not empty, process according to syntax state:
if len(curElement) > 0 {
// ensure this is not within an unclosed quote or partial escape
if stateElement != "string" && stateElement != "quote_end" {
return nil, fmt.Errorf("eol element in state %s: %w", stateElement, errs.ErrParsingFailed)
}
switch stateSyntax {
case "start", "start_or_param":
// add a new auth type if a string is seen at the start, before any equals
c = &challenge{authType: strings.ToLower(string(curElement)), params: map[string]string{}}
cl = append(cl, *c)
case "param_value":
// add the last param key=val
c.params[curKey] = string(curElement)
case "end_auth_type":
// missing equals for param
return nil, fmt.Errorf("eol at param without value: %w", errs.ErrParsingFailed)
}
}
return cl, nil
}
// basicHandler supports Basic auth type requests
type basicHandler struct {
realm string
host string
credsFn CredsFn
}
// NewBasicHandler creates a new BasicHandler
func NewBasicHandler(client *http.Client, clientID, host string, credsFn CredsFn, slog *slog.Logger) handler {
return &basicHandler{
realm: "",
host: host,
credsFn: credsFn,
}
}
// AddScope is not valid for BasicHandler
func (b *basicHandler) AddScope(scope string) error {
return errs.ErrNoNewChallenge
}
// ProcessChallenge for BasicHandler is a noop
func (b *basicHandler) ProcessChallenge(c challenge) error {
if _, ok := c.params["realm"]; !ok {
return errs.ErrInvalidChallenge
}
if b.realm != c.params["realm"] {
b.realm = c.params["realm"]
return nil
}
return errs.ErrNoNewChallenge
}
// UpdateRequest for BasicHandler generates base64 encoded user/pass for a host
func (b *basicHandler) UpdateRequest(req *http.Request) error {
cred := b.credsFn(b.host)
if cred.User == "" || cred.Password == "" {
return fmt.Errorf("no credentials available: %w", errs.ErrHTTPUnauthorized)
}
req.Header.Set("Authorization", fmt.Sprintf("Basic %s",
base64.StdEncoding.EncodeToString([]byte(cred.User+":"+cred.Password))))
return nil
}
// bearerHandler supports Bearer auth type requests
type bearerHandler struct {
client *http.Client
clientID string
realm, service string
host string
credsFn CredsFn
scopes []string
tokenURL *url.URL
token bearerToken
slog *slog.Logger
}
// bearerToken is the json response to the Bearer request
type bearerToken struct {
Token string `json:"token"`
AccessToken string `json:"access_token"` //#nosec G117 exported struct intentionally holds secrets
ExpiresIn int `json:"expires_in"`
IssuedAt time.Time `json:"issued_at"`
RefreshToken string `json:"refresh_token"` //#nosec G117 exported struct intentionally holds secrets
Scope string `json:"scope"`
}
// NewBearerHandler creates a new BearerHandler
func NewBearerHandler(client *http.Client, clientID, host string, credsFn CredsFn, slog *slog.Logger) handler {
return &bearerHandler{
client: client,
clientID: clientID,
host: host,
credsFn: credsFn,
realm: "",
service: "",
scopes: []string{},
slog: slog,
}
}
// AddScope appends a new scope if it doesn't already exist
func (b *bearerHandler) AddScope(scope string) error {
if b.scopeExists(scope) {
if b.token.Token == "" || !b.isExpired() {
return errs.ErrNoNewChallenge
}
return nil
}
b.addScope(scope)
return nil
}
func (b *bearerHandler) addScope(scope string) {
if !b.tryExtendExistingScope(scope) {
b.scopes = append(b.scopes, scope)
}
// delete old token
b.token.Token = ""
}
var knownActions = []string{"pull", "push", "delete"}
// tryExtendExistingScope extends an existing scope if both the new scope and the current scope contain only knownActions.
// It returns true if actions are added or are already present. Otherwise, it returns false,
// indicating that the new scope should be appended to b.scopes instead.
func (b *bearerHandler) tryExtendExistingScope(scope string) bool {
repo, actions, ok := parseScope(scope)
if !ok {
return false
}
scopePrefix := "repository:" + repo + ":"
for i, cur := range b.scopes {
if !strings.HasPrefix(cur, scopePrefix) {
continue
}
_, curActions, curOk := parseScope(cur)
if !curOk {
continue
}
for _, a := range actions {
if !slices.Contains(curActions, a) {
curActions = append(curActions, a)
}
}
b.scopes[i] = scopePrefix + strings.Join(curActions, ",")
return true
}
return false
}
// parseScope splits a scope into the repo and slice of actions.
// Unknown actions in the scope will set bool to false.
func parseScope(scope string) (string, []string, bool) {
scopeSplit := strings.SplitN(scope, ":", 3)
if scopeSplit[0] != "repository" || len(scopeSplit) < 3 {
return "", nil, false
}
actionSplit := strings.Split(scopeSplit[2], ",")
for _, a := range actionSplit {
if !slices.Contains(knownActions, a) {
return "", nil, false
}
}
return scopeSplit[1], actionSplit, true
}
// ProcessChallenge handles WWW-Authenticate header for bearer tokens
// Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:samalba/my-app:pull,push"
func (b *bearerHandler) ProcessChallenge(c challenge) error {
if _, ok := c.params["realm"]; !ok {
return errs.ErrInvalidChallenge
}
if _, ok := c.params["service"]; !ok {
c.params["service"] = ""
}
if _, ok := c.params["scope"]; !ok {
c.params["scope"] = ""
}
existingScope := b.scopeExists(c.params["scope"])
if b.realm == c.params["realm"] && b.service == c.params["service"] && existingScope && (b.token.Token == "" || !b.isExpired()) {
return errs.ErrNoNewChallenge
}
if b.realm == "" {
b.realm = c.params["realm"]
} else if b.realm != c.params["realm"] {
return errs.ErrInvalidChallenge
}
if b.service == "" {
b.service = c.params["service"]
} else if b.service != c.params["service"] {
return errs.ErrInvalidChallenge
}
if !existingScope {
b.addScope(c.params["scope"])
}
return nil
}
// UpdateRequest for BearerHandler adds a bearer token to the request.
func (b *bearerHandler) UpdateRequest(req *http.Request) error {
// handle relative realm values
if b.tokenURL == nil {
u, err := req.URL.Parse(b.realm)
if err != nil {
return err
}
b.tokenURL = u
}
// verify tokenURL is allowed for request URL
if err := regnet.AllowRedirect(*req.URL, *b.tokenURL); err != nil {
return err
}
// if unexpired token already exists, return it
if b.token.Token != "" && !b.isExpired() {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", b.token.Token))
return nil
}
// attempt to post if a refresh token is available or token auth is being used
cred := b.credsFn(b.host)
if b.token.RefreshToken != "" || cred.Token != "" {
if err := b.tryPost(cred); err == nil {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", b.token.Token))
return nil
} else if err != errs.ErrHTTPUnauthorized {
return fmt.Errorf("failed to request auth token (post): %w%.0w", err, errs.ErrHTTPUnauthorized)
}
}
// attempt a get (with basic auth if user/pass available)
if err := b.tryGet(cred); err == nil {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", b.token.Token))
return nil
} else if err != errs.ErrHTTPUnauthorized {
return fmt.Errorf("failed to request auth token (get): %w%.0w", err, errs.ErrHTTPUnauthorized)
}
return errs.ErrHTTPUnauthorized
}
// isExpired returns true when token issue date is either 0, token has expired,
// or will expire within buffer time
func (b *bearerHandler) isExpired() bool {
if b.token.IssuedAt.IsZero() {
return true
}
expireSec := b.token.IssuedAt.Add(time.Duration(b.token.ExpiresIn) * time.Second)
expireSec = expireSec.Add(tokenBuffer * -1)
return time.Now().After(expireSec)
}
// tryGet requests a new token with a GET request
func (b *bearerHandler) tryGet(cred Cred) error {
//#nosec G704 inputs follow specification
req, err := http.NewRequest("GET", b.tokenURL.String(), nil)
if err != nil {
return err
}
reqParams := req.URL.Query()
reqParams.Add("client_id", b.clientID)
// Note, an offline_token should not be requested by default due to broken OAuth2 implementations returning an invalid token
if b.service != "" {
reqParams.Add("service", b.service)
}
for _, s := range b.scopes {
reqParams.Add("scope", s)
}
if cred.User != "" && cred.Password != "" {
reqParams.Add("account", cred.User)
req.SetBasicAuth(cred.User, cred.Password)
}
req.Header.Add("User-Agent", b.clientID)
req.URL.RawQuery = reqParams.Encode()
//#nosec G704 inputs follow specification
resp, err := b.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return b.validateResponse(resp)
}
// tryPost requests a new token via a POST request
func (b *bearerHandler) tryPost(cred Cred) error {
form := url.Values{}
if len(b.scopes) > 0 {
form.Set("scope", strings.Join(b.scopes, " "))
}
if b.service != "" {
form.Set("service", b.service)
}
form.Set("client_id", b.clientID)
if b.token.RefreshToken != "" {
form.Set("grant_type", "refresh_token")
form.Set("refresh_token", b.token.RefreshToken)
} else if cred.Token != "" {
form.Set("grant_type", "refresh_token")
form.Set("refresh_token", cred.Token)
} else if cred.User != "" && cred.Password != "" {
form.Set("grant_type", "password")
form.Set("username", cred.User)
form.Set("password", cred.Password)
}
//#nosec G704 inputs are user controlled or follow specification
req, err := http.NewRequest("POST", b.tokenURL.String(), strings.NewReader(form.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
req.Header.Add("User-Agent", b.clientID)
//#nosec G704 inputs are user controlled or follow specification
resp, err := b.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return b.validateResponse(resp)
}
// scopeExists check if the scope already exists within the list of scopes
func (b *bearerHandler) scopeExists(search string) bool {
if search == "" {
return true
}
searchRepo, searchActions, searchOk := parseScope(search)
if !searchOk {
return slices.Contains(b.scopes, search)
}
scopePrefix := "repository:" + searchRepo + ":"
for _, scope := range b.scopes {
if scope == search {
return true
}
if !strings.HasPrefix(scope, scopePrefix) {
continue
}
_, actions, ok := parseScope(scope)
if !ok {
continue
}
for _, sa := range searchActions {
if !slices.Contains(actions, sa) {
return false
}
}
return true
}
return false
}
// validateResponse extracts the returned token
func (b *bearerHandler) validateResponse(resp *http.Response) error {
if resp.StatusCode != 200 {
return errs.ErrHTTPUnauthorized
}
// decode response and if successful, update token
decoder := json.NewDecoder(resp.Body)
decoded := bearerToken{}
if err := decoder.Decode(&decoded); err != nil {
return err
}
b.token = decoded
if b.token.ExpiresIn < minTokenLife {
b.token.ExpiresIn = minTokenLife
}
// If token is already expired, it was sent with a zero value or
// there may be a clock skew between the client and auth server.
// Also handle cases of remote time in the future.
// But if remote time is slightly in the past, leave as is so token
// expires here before the server.
if b.isExpired() || b.token.IssuedAt.After(time.Now()) {
b.token.IssuedAt = time.Now().UTC()
}
// AccessToken and Token should be the same and we use Token elsewhere
if b.token.AccessToken != "" {
b.token.Token = b.token.AccessToken
}
return nil
}
// jwtHubHandler supports JWT auth type requests.
type jwtHubHandler struct {
client *http.Client
clientID string
realm string
host string
credsFn CredsFn
jwt string
}
type jwtHubPost struct {
User string `json:"username"`
Pass string `json:"password"` //#nosec G117 exported struct intentionally holds secrets
}
type jwtHubResp struct {
Detail string `json:"detail"`
Token string `json:"token"`
RefreshToken string `json:"refresh_token"` //#nosec G117 exported struct intentionally holds secrets
}
// NewJWTHubHandler creates a new JWTHandler for Docker Hub.
func NewJWTHubHandler(client *http.Client, clientID, host string, credsFn CredsFn, slog *slog.Logger) handler {
// JWT handler is only tested against Hub, and the API is Hub specific
if host == "hub.docker.com" {
return &jwtHubHandler{
client: client,
clientID: clientID,
host: host,
credsFn: credsFn,
realm: "https://hub.docker.com/v2/users/login",
}
}
return nil
}
// AddScope is not valid for JWTHubHandler
func (j *jwtHubHandler) AddScope(scope string) error {
return errs.ErrNoNewChallenge
}
// ProcessChallenge handles WWW-Authenticate header for JWT auth on Docker Hub
func (j *jwtHubHandler) ProcessChallenge(c challenge) error {
cred := j.credsFn(j.host)
// use token if provided
if cred.Token != "" {
j.jwt = cred.Token
return nil
}
// send a login request to hub
bodyBytes, err := json.Marshal(jwtHubPost{
User: cred.User,
Pass: cred.Password, //#nosec G117 field name follows spec
})
if err != nil {
return err
}
req, err := http.NewRequest("POST", j.realm, bytes.NewReader(bodyBytes))
if err != nil {
return err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
req.Header.Add("User-Agent", j.clientID)
//#nosec G704 inputs are user controlled or follow specification requirements
resp, err := j.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 || resp.StatusCode >= 300 {
return errs.ErrHTTPUnauthorized
}
var bodyParsed jwtHubResp
err = json.Unmarshal(body, &bodyParsed)
if err != nil {
return err
}
j.jwt = bodyParsed.Token
return nil
}
// UpdateRequest for JWTHubHandler adds JWT header
func (j *jwtHubHandler) UpdateRequest(req *http.Request) error {
if len(j.jwt) > 0 {
req.Header.Set("Authorization", fmt.Sprintf("JWT %s", j.jwt))
return nil
}
return errs.ErrHTTPUnauthorized
}
+48
View File
@@ -0,0 +1,48 @@
package auth
import (
"github.com/regclient/regclient/types/errs"
)
var (
// ErrEmptyChallenge indicates an issue with the received challenge in the WWW-Authenticate header
//
// Deprecated: replace with [errs.ErrEmptyChallenge].
//go:fix inline
ErrEmptyChallenge = errs.ErrEmptyChallenge
// ErrInvalidChallenge indicates an issue with the received challenge in the WWW-Authenticate header
//
// Deprecated: replace with [errs.ErrInvalidChallenge].
//go:fix inline
ErrInvalidChallenge = errs.ErrInvalidChallenge
// ErrNoNewChallenge indicates a challenge update did not result in any change
//
// Deprecated: replace with [errs.ErrNoNewChallenge].
//go:fix inline
ErrNoNewChallenge = errs.ErrNoNewChallenge
// ErrNotFound indicates no credentials found for basic auth
//
// Deprecated: replace with [errs.ErrNotFound].
//go:fix inline
ErrNotFound = errs.ErrNotFound
// ErrNotImplemented returned when method has not been implemented yet
//
// Deprecated: replace with [errs.ErrNotImplemented].
//go:fix inline
ErrNotImplemented = errs.ErrNotImplemented
// ErrParseFailure indicates the WWW-Authenticate header could not be parsed
//
// Deprecated: replace with [errs.ErrParseFailure].
//go:fix inline
ErrParseFailure = errs.ErrParsingFailed
// ErrUnauthorized request was not authorized
//
// Deprecated: replace with [errs.ErrUnauthorized].
//go:fix inline
ErrUnauthorized = errs.ErrHTTPUnauthorized
// ErrUnsupported indicates the request was unsupported
//
// Deprecated: replace with [errs.ErrUnsupported].
//go:fix inline
ErrUnsupported = errs.ErrUnsupported
)
+181
View File
@@ -0,0 +1,181 @@
//go:build go1.18
// Package cache is used to store values with limits.
// Items are automatically pruned when too many entries are stored, or values become stale.
package cache
import (
"sort"
"sync"
"time"
"github.com/regclient/regclient/types/errs"
)
type Cache[k comparable, v any] struct {
mu sync.Mutex
minAge time.Duration
maxAge time.Duration
minCount int
maxCount int
timer *time.Timer
entries map[k]*Entry[v]
}
type Entry[v any] struct {
used time.Time
value v
}
type sortKeys[k comparable] struct {
keys []k
lessFn func(a, b k) bool
}
type conf struct {
minAge time.Duration
maxCount int
}
type cacheOpts func(*conf)
func WithAge(age time.Duration) cacheOpts {
return func(c *conf) {
c.minAge = age
}
}
func WithCount(count int) cacheOpts {
return func(c *conf) {
c.maxCount = count
}
}
func New[k comparable, v any](opts ...cacheOpts) Cache[k, v] {
c := conf{}
for _, opt := range opts {
opt(&c)
}
maxAge := c.minAge + (c.minAge / 10)
minCount := 0
if c.maxCount > 0 {
minCount = int(float64(c.maxCount) * 0.9)
}
return Cache[k, v]{
minAge: c.minAge,
maxAge: maxAge,
minCount: minCount,
maxCount: c.maxCount,
entries: map[k]*Entry[v]{},
}
}
func (c *Cache[k, v]) Delete(key k) {
if c == nil {
return
}
c.mu.Lock()
defer c.mu.Unlock()
delete(c.entries, key)
if len(c.entries) == 0 && c.timer != nil {
c.timer.Stop()
c.timer = nil
}
}
func (c *Cache[k, v]) Set(key k, val v) {
if c == nil {
return
}
c.mu.Lock()
defer c.mu.Unlock()
c.entries[key] = &Entry[v]{
used: time.Now(),
value: val,
}
if len(c.entries) > c.maxCount {
c.pruneLocked()
} else if c.timer == nil {
// prune resets the timer, so this is only needed if the prune wasn't triggered
c.timer = time.AfterFunc(c.maxAge, c.prune)
}
}
func (c *Cache[k, v]) Get(key k) (v, error) {
if c == nil {
var val v
return val, errs.ErrNotFound
}
c.mu.Lock()
defer c.mu.Unlock()
if e, ok := c.entries[key]; ok {
if e.used.Add(c.minAge).Before(time.Now()) {
// entry expired
go c.prune()
} else {
c.entries[key].used = time.Now()
return e.value, nil
}
}
var val v
return val, errs.ErrNotFound
}
func (c *Cache[k, v]) prune() {
c.mu.Lock()
defer c.mu.Unlock()
c.pruneLocked()
}
func (c *Cache[k, v]) pruneLocked() {
// sort key list by last used date
keyList := make([]k, 0, len(c.entries))
for key := range c.entries {
keyList = append(keyList, key)
}
sk := sortKeys[k]{
keys: keyList,
lessFn: func(a, b k) bool {
return c.entries[a].used.Before(c.entries[b].used)
},
}
sort.Sort(&sk)
// prune entries
now := time.Now()
cutoff := now.Add(c.minAge * -1)
nextTime := now
delCount := len(keyList) - c.minCount
for i, key := range keyList {
if i < delCount || c.entries[key].used.Before(cutoff) {
delete(c.entries, key)
} else {
nextTime = c.entries[key].used
break
}
}
// set next timer
if len(c.entries) > 0 {
dur := nextTime.Sub(now) + c.maxAge
if c.timer == nil {
// this shouldn't be possible
c.timer = time.AfterFunc(dur, c.prune)
} else {
c.timer.Reset(dur)
}
} else if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
}
func (sk *sortKeys[k]) Len() int {
return len(sk.keys)
}
func (sk *sortKeys[k]) Less(i, j int) bool {
return sk.lessFn(sk.keys[i], sk.keys[j])
}
func (sk *sortKeys[k]) Swap(i, j int) {
sk.keys[i], sk.keys[j] = sk.keys[j], sk.keys[i]
}
+188
View File
@@ -0,0 +1,188 @@
// Package conffile wraps the read and write of configuration files
package conffile
import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/user"
"path/filepath"
)
type File struct {
perms int
fullname string
}
type Opt func(*File)
// New returns a new File.
// The last successful option determines the filename.
func New(opts ...Opt) *File {
f := File{perms: 0o600}
for _, fn := range opts {
fn(&f)
}
if f.fullname == "" {
return nil
}
return &f
}
// WithAppDir determines the filename from the XDG or Windows specification.
// By default, this is based in $HOME/.config on Linux and %APPDATA% on Windows.
// If the file does not exist, this will set the filename only if "force" is true.
func WithAppDir(unixDir, winDir, name string, force bool) Opt {
var dir string
if winDir == "" {
dir = unixDir
} else {
dir = osString(unixDir, winDir)
}
return func(f *File) {
fullname := filepath.Join(appDir(), dir, name)
if force || exists(fullname) {
f.fullname = fullname
}
}
}
// WithDirName determines the filename from a subdirectory in the user's HOME.
//
// Deprecated: Replace with [WithHomeDir]
//
//go:fix inline
func WithDirName(dir, name string) Opt {
return WithHomeDir(dir, name, true)
}
// WithEnvFile sets the fullname to the environment value if defined.
func WithEnvFile(envVar string) Opt {
return func(f *File) {
val := os.Getenv(envVar)
if val != "" {
f.fullname = val
}
}
}
// WithEnvDir sets the fullname to the environment value + filename if the environment variable is defined.
func WithEnvDir(envVar, name string) Opt {
return func(f *File) {
val := os.Getenv(envVar)
if val != "" {
f.fullname = filepath.Join(val, name)
}
}
}
// WithFullname specifies the filename.
// This will always set the filename even if the file does not exist.
func WithFullname(fullname string) Opt {
return func(f *File) {
f.fullname = fullname
}
}
// WithHomeDir determines the filename from a subdirectory in the user's HOME
// e.g. dir=".app", name="config.json", sets the fullname to "$HOME/.app/config.json".
// If the file does not exist, this will set the filename only if "force" is true.
func WithHomeDir(dir, name string, force bool) Opt {
return func(f *File) {
filename := filepath.Join(homeDir(), dir, name)
if force || exists(filename) {
f.fullname = filename
}
}
}
// WithPerms specifies the permissions to create a file with (default 0600).
func WithPerms(perms int) Opt {
return func(f *File) {
f.perms = perms
}
}
func (f *File) Name() string {
return f.fullname
}
func (f *File) Open() (io.ReadCloser, error) {
return os.Open(f.fullname)
}
func (f *File) Write(rdr io.Reader) error {
// create temp file/open
dir := filepath.Dir(f.fullname)
if err := os.MkdirAll(dir, 0o700); err != nil {
return err
}
tmp, err := os.CreateTemp(dir, filepath.Base(f.fullname))
if err != nil {
return err
}
tmpStat, err := tmp.Stat()
if err != nil {
return err
}
tmpName := tmpStat.Name()
tmpFullname := filepath.Join(dir, tmpName)
defer os.Remove(tmpFullname)
// copy from rdr to temp file
_, err = io.Copy(tmp, rdr)
errC := tmp.Close()
if err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
if errC != nil {
return fmt.Errorf("failed to close config: %w", errC)
}
// adjust file ownership/permissions
mode := os.FileMode(0o600)
uid := os.Getuid()
gid := os.Getgid()
// adjust defaults based on existing file if available
stat, err := os.Stat(f.fullname)
if err == nil {
// adjust mode to existing file
if stat.Mode().IsRegular() {
mode = stat.Mode()
}
uid, gid, _ = getFileOwner(stat)
} else if !errors.Is(err, fs.ErrNotExist) {
return err
}
// update mode and owner of temp file
//#nosec G703 tempfile location is user controlled
if err := os.Chmod(tmpFullname, mode); err != nil {
return err
}
if uid > 0 && gid > 0 {
//#nosec G703 tempfile location is user controlled
_ = os.Chown(tmpFullname, uid, gid)
}
// move temp file to target filename
//#nosec G703 tempfile location is user controlled
return os.Rename(tmpFullname, f.fullname)
}
func exists(name string) bool {
_, err := os.Stat(name)
return err == nil
}
func homeDir() string {
home := os.Getenv(homeEnv)
if home == "" {
u, err := user.Current()
if err == nil {
home = u.HomeDir
}
}
return home
}
@@ -0,0 +1,37 @@
//go:build !windows
package conffile
import (
"io/fs"
"os"
"path/filepath"
"syscall"
)
const (
appDirEnv = "XDG_CONFIG_HOME"
homeEnv = "HOME"
)
func appDir() string {
appDir := os.Getenv(appDirEnv)
if appDir == "" {
home := homeDir()
appDir = filepath.Join(home, ".config")
}
return appDir
}
func getFileOwner(stat fs.FileInfo) (int, int, error) {
var uid, gid int
if sysstat, ok := stat.Sys().(*syscall.Stat_t); ok {
uid = int(sysstat.Uid)
gid = int(sysstat.Gid)
}
return uid, gid, nil
}
func osString(unix, _ string) string {
return unix
}
@@ -0,0 +1,31 @@
//go:build windows
package conffile
import (
"io/fs"
"os"
"path/filepath"
)
const (
appDirEnv = "APPDATA"
homeEnv = "USERPROFILE"
)
func appDir() string {
appDir := os.Getenv(appDirEnv)
if appDir == "" {
home := homeDir()
appDir = filepath.Join(home, "AppData")
}
return appDir
}
func getFileOwner(_ fs.FileInfo) (int, int, error) {
return 0, 0, nil
}
func osString(_, win string) string {
return win
}
+198
View File
@@ -0,0 +1,198 @@
// Package httplink parses the Link header from HTTP responses according to RFC5988
package httplink
import (
"fmt"
"strings"
"github.com/regclient/regclient/types/errs"
)
type (
Links []Link
Link struct {
URI string
Param map[string]string
}
)
type charLU byte
var charLUs [256]charLU
const (
isSpace charLU = 1 << iota
isToken
isAlphaNum
)
func init() {
for c := range 256 {
charLUs[c] = 0
if strings.ContainsRune(" \t\r\n", rune(c)) {
charLUs[c] |= isSpace
}
if (rune('a') <= rune(c) && rune(c) <= rune('z')) || (rune('A') <= rune(c) && rune(c) <= rune('Z') || (rune('0') <= rune(c) && rune(c) <= rune('9'))) {
charLUs[c] |= isAlphaNum | isToken
}
if strings.ContainsRune("!#$%&'()*+-./:<=>?@[]^_`{|}~", rune(c)) {
charLUs[c] |= isToken
}
}
}
// Parse reads "Link" http headers into an array of Link structs.
// Header array should be the output of resp.Header.Values("link").
func Parse(headers []string) (Links, error) {
links := []Link{}
for _, h := range headers {
state := "init"
var ub, pnb, pvb []byte
parms := map[string]string{}
endLink := func() {
links = append(links, Link{
URI: string(ub),
Param: parms,
})
// reset state
ub, pnb, pvb = []byte{}, []byte{}, []byte{}
parms = map[string]string{}
}
endParm := func() {
if _, ok := parms[string(pnb)]; !ok {
parms[string(pnb)] = string(pvb)
}
// reset parm
pnb, pvb = []byte{}, []byte{}
}
for i, b := range []byte(h) {
switch state {
case "init":
if b == '<' {
state = "uriQuoted"
} else if charLUs[b]&isToken != 0 {
state = "uri"
ub = append(ub, b)
} else if charLUs[b]&isSpace != 0 || b == ',' {
// noop
} else {
// unknown character
return nil, fmt.Errorf("unknown character in position %d of %s: %w", i, h, errs.ErrParsingFailed)
}
case "uri":
// parse tokens until space or comma
if charLUs[b]&isToken != 0 {
ub = append(ub, b)
} else if charLUs[b]&isSpace != 0 {
state = "fieldSep"
} else if b == ';' {
state = "parmName"
} else if b == ',' {
state = "init"
endLink()
} else {
// unknown character
return nil, fmt.Errorf("unknown character in position %d of %s: %w", i, h, errs.ErrParsingFailed)
}
case "uriQuoted":
// parse tokens until quote
if b == '>' {
state = "fieldSep"
} else {
ub = append(ub, b)
}
case "fieldSep":
if b == ';' {
state = "parmName"
} else if b == ',' {
state = "init"
endLink()
} else if charLUs[b]&isSpace != 0 {
// noop
} else {
// unknown character
return nil, fmt.Errorf("unknown character in position %d of %s: %w", i, h, errs.ErrParsingFailed)
}
case "parmName":
if len(pnb) > 0 && b == '=' {
state = "parmValue"
} else if len(pnb) > 0 && b == '*' {
state = "parmNameStar"
} else if charLUs[b]&isAlphaNum != 0 {
pnb = append(pnb, b)
} else if len(pnb) == 0 && charLUs[b]&isSpace != 0 {
// noop
} else {
// unknown character
return nil, fmt.Errorf("unknown character in position %d of %s: %w", i, h, errs.ErrParsingFailed)
}
case "parmNameStar":
if b == '=' {
state = "parmValue"
} else {
// unknown character
return nil, fmt.Errorf("unknown character in position %d of %s: %w", i, h, errs.ErrParsingFailed)
}
case "parmValue":
if len(pvb) == 0 {
if charLUs[b]&isToken != 0 {
pvb = append(pvb, b)
} else if b == '"' {
state = "parmValueQuoted"
} else {
// unknown character
return nil, fmt.Errorf("unknown character in position %d of %s: %w", i, h, errs.ErrParsingFailed)
}
} else {
if charLUs[b]&isToken != 0 {
pvb = append(pvb, b)
} else if charLUs[b]&isSpace != 0 {
state = "fieldSep"
endParm()
} else if b == ';' {
state = "parmName"
endParm()
} else if b == ',' {
state = "init"
endParm()
endLink()
} else {
// unknown character
return nil, fmt.Errorf("unknown character in position %d of %s: %w", i, h, errs.ErrParsingFailed)
}
}
case "parmValueQuoted":
if b == '"' {
state = "fieldSep"
endParm()
} else {
pvb = append(pvb, b)
}
}
}
// check for valid state at end of header
switch state {
case "parmValue":
endParm()
endLink()
case "uri", "fieldSep":
endLink()
case "init":
// noop
default:
return nil, fmt.Errorf("unexpected end state %s for header %s: %w", state, h, errs.ErrParsingFailed)
}
}
return links, nil
}
// Get returns a link with a specific parm value, e.g. rel="next"
func (links Links) Get(parm, val string) (Link, error) {
for _, link := range links {
if link.Param != nil && link.Param[parm] == val {
return link, nil
}
}
return Link{}, errs.ErrNotFound
}

Some files were not shown because too many files have changed in this diff Show More