Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f03f1920c0 | ||
|
|
e30f77caca | ||
|
|
33086eaebc | ||
|
|
73801daae8 | ||
|
|
5e8162bc66 | ||
|
|
6d9896fc46 | ||
|
|
540842b76a | ||
|
|
eeb4ea8b9c | ||
|
|
1bffd1831d | ||
|
|
eb62135e3b | ||
|
|
e8a494d9f4 | ||
|
|
35c29580bb | ||
|
|
e458c531f7 | ||
|
|
d0d156dc30 | ||
|
|
14f05407f3 | ||
|
|
e611a3be38 | ||
|
|
b5f7d26c9d | ||
|
|
5b42b3d306 | ||
|
|
d99e86fb0e | ||
|
|
ade646abbc | ||
|
|
80b3b90905 | ||
|
|
fab3f0b97b | ||
|
|
3819ee38f5 | ||
|
|
74261a7a01 |
@@ -7,3 +7,4 @@
|
||||
/bin
|
||||
dist/
|
||||
tests/integration/.bats
|
||||
.devbox
|
||||
|
||||
+7
-1
@@ -222,7 +222,13 @@ synchronize your local app environment values with what is deployed live.`),
|
||||
mergedEnv[k] = v
|
||||
}
|
||||
|
||||
if !strings.Contains(recipeEnvVar, ":") {
|
||||
// The recipe version is always carried by TYPE (RECIPE, when present,
|
||||
// is a bare source pointer). Stamp the deployed version onto TYPE and
|
||||
// leave RECIPE untouched so the two never drift apart.
|
||||
if t, ok := mergedEnv["TYPE"]; ok {
|
||||
name, _, _ := strings.Cut(t, ":")
|
||||
mergedEnv["TYPE"] = fmt.Sprintf("%s:%s", name, version)
|
||||
} else {
|
||||
mergedEnv[recipeKey] = fmt.Sprintf("%s:%s", mergedEnv[recipeKey], version)
|
||||
}
|
||||
|
||||
|
||||
+17
-1
@@ -32,6 +32,18 @@ deploy <domain>" to do so.
|
||||
You can see what recipes are available (i.e. values for the [recipe] argument)
|
||||
by running "abra recipe ls".
|
||||
|
||||
In addition to short catalogue names, [recipe] also accepts arbitrary git
|
||||
URLs to use a recipe from outside the catalogue (e.g. a fork or work in
|
||||
progress). Any of these forms is accepted:
|
||||
|
||||
abra app new git.example.com/user/recipe
|
||||
abra app new https://git.example.com/user/recipe
|
||||
abra app new git@git.example.com:user/recipe
|
||||
|
||||
In that case a RECIPE=<canonical name> line is written to the app's .env
|
||||
file so a subsequent "abra app deploy" (on this or another machine) will
|
||||
re-fetch the recipe from the same git source.
|
||||
|
||||
Recipe commit hashes are supported values for "[version]".
|
||||
|
||||
Passing the "--secrets/-S" flag will automatically generate secrets for your
|
||||
@@ -295,7 +307,7 @@ func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
|
||||
if appDomain == "" && !internal.NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: i18n.G("Specify app domain"),
|
||||
Default: fmt.Sprintf("%s.%s", recipe.Name, server),
|
||||
Default: fmt.Sprintf("%s.%s", recipe.ShortName(), server),
|
||||
}
|
||||
if err := survey.AskOne(prompt, &appDomain); err != nil {
|
||||
return err
|
||||
@@ -306,6 +318,10 @@ func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
|
||||
return errors.New(i18n.G("no domain provided"))
|
||||
}
|
||||
|
||||
if strings.ContainsAny(appDomain, "/\\") {
|
||||
return errors.New(i18n.G("invalid domain '%s': must not contain '/' or '\\'", appDomain))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -188,6 +188,12 @@ beforehand. See "abra app backup" for more.`),
|
||||
appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
|
||||
}
|
||||
|
||||
// NOTE: stamp the deployed version label ourselves rather than relying
|
||||
// on the value baked into the recipe's compose.yml. Git-URL/WIP recipes
|
||||
// often carry a stale or missing label, which would otherwise leave the
|
||||
// live deployment reporting the wrong version after a rollback.
|
||||
appPkg.SetVersionLabel(compose, stackName, chosenDowngrade)
|
||||
|
||||
// Gather secrets
|
||||
secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged)
|
||||
if err != nil {
|
||||
|
||||
+24
-4
@@ -200,6 +200,12 @@ beforehand. See "abra app backup" for more.`),
|
||||
appPkg.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
|
||||
}
|
||||
|
||||
// NOTE: stamp the deployed version label ourselves rather than relying
|
||||
// on the value baked into the recipe's compose.yml. Git-URL/WIP recipes
|
||||
// often carry a stale or missing label, which would otherwise leave the
|
||||
// live deployment reporting the wrong version after an upgrade.
|
||||
appPkg.SetVersionLabel(compose, stackName, chosenUpgrade)
|
||||
|
||||
envVars, err := appPkg.CheckEnv(app)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -346,9 +352,17 @@ func getReleaseNotes(
|
||||
return errors.New(i18n.G("parsing chosen upgrade version failed: %s", err))
|
||||
}
|
||||
|
||||
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
|
||||
if err != nil {
|
||||
return errors.New(i18n.G("parsing deployment version failed: %s", err))
|
||||
// The deployed version can be "unknown" (e.g. deployed without a version
|
||||
// label, as is common for git-URL/work-in-progress recipes). In that case
|
||||
// there is no lower bound to filter by, so we gather notes for every
|
||||
// version below the chosen one instead of failing.
|
||||
var parsedDeployedVersion tagcmp.Tag
|
||||
haveDeployedVersion := deployMeta.Version != config.UNKNOWN_DEFAULT
|
||||
if haveDeployedVersion {
|
||||
parsedDeployedVersion, err = tagcmp.Parse(deployMeta.Version)
|
||||
if err != nil {
|
||||
return errors.New(i18n.G("parsing deployment version failed: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
for _, version := range internal.SortVersionsDesc(versions) {
|
||||
@@ -357,7 +371,7 @@ func getReleaseNotes(
|
||||
return errors.New(i18n.G("parsing recipe version failed: %s", err))
|
||||
}
|
||||
|
||||
if parsedVersion.IsGreaterThan(parsedDeployedVersion) &&
|
||||
if (!haveDeployedVersion || parsedVersion.IsGreaterThan(parsedDeployedVersion)) &&
|
||||
parsedVersion.IsLessThan(parsedChosenUpgrade) {
|
||||
note, err := app.Recipe.GetReleaseNotes(version, app.Domain)
|
||||
if err != nil {
|
||||
@@ -420,6 +434,12 @@ func validateUpgradeVersionArg(
|
||||
return errors.New(i18n.G("'%s' is not a known version for %s", specificVersion, app.Recipe.Name))
|
||||
}
|
||||
|
||||
// Without a known deployed version we cannot tell whether the requested
|
||||
// version is an upgrade; trust the explicit choice rather than failing.
|
||||
if deployMeta.Version == config.UNKNOWN_DEFAULT {
|
||||
return nil
|
||||
}
|
||||
|
||||
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
|
||||
if err != nil {
|
||||
return errors.New(i18n.G("'%s' is not a known version", deployMeta.Version))
|
||||
|
||||
@@ -59,9 +59,15 @@ func ValidateRecipe(args []string, cmdName string) recipe.Recipe {
|
||||
log.Fatal(i18n.G("no recipe name provided"))
|
||||
}
|
||||
|
||||
if _, ok := knownRecipes[recipeName]; !ok {
|
||||
if !strings.Contains(recipeName, "/") {
|
||||
log.Fatal(i18n.G("no recipe '%s' exists?", recipeName))
|
||||
recipeName = recipe.NormalizeRecipeName(recipeName)
|
||||
|
||||
lookupName := recipeName
|
||||
if i := strings.LastIndex(lookupName, ":"); i >= 0 {
|
||||
lookupName = lookupName[:i]
|
||||
}
|
||||
if _, ok := knownRecipes[lookupName]; !ok {
|
||||
if !strings.Contains(lookupName, "/") {
|
||||
log.Fatal(i18n.G("no recipe '%s' exists? pass a git URL (e.g. https://git.example.com/user/recipe) to use a recipe outside the catalogue", lookupName))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@ package recipe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
@@ -41,6 +43,7 @@ var RecipeListCommand = &cobra.Command{
|
||||
|
||||
headers := []string{
|
||||
i18n.G("name"),
|
||||
i18n.G("source"),
|
||||
i18n.G("category"),
|
||||
i18n.G("status"),
|
||||
i18n.G("healthcheck"),
|
||||
@@ -56,6 +59,7 @@ var RecipeListCommand = &cobra.Command{
|
||||
for _, recipe := range recipes {
|
||||
row := []string{
|
||||
recipe.Name,
|
||||
i18n.G("catalogue"),
|
||||
recipe.Category,
|
||||
strconv.Itoa(recipe.Features.Status),
|
||||
recipe.Features.Healthcheck,
|
||||
@@ -76,6 +80,23 @@ var RecipeListCommand = &cobra.Command{
|
||||
}
|
||||
}
|
||||
|
||||
externals := externalRecipes(catl)
|
||||
sort.Strings(externals)
|
||||
for _, name := range externals {
|
||||
row := []string{
|
||||
name,
|
||||
i18n.G("external"),
|
||||
"-", "-", "-", "-", "-", "-", "-",
|
||||
}
|
||||
if pattern != "" {
|
||||
if !strings.Contains(name, pattern) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
table.Row(row...)
|
||||
rows = append(rows, row)
|
||||
}
|
||||
|
||||
if len(rows) > 0 {
|
||||
if internal.MachineReadable {
|
||||
out, err := formatter.ToJSON(headers, rows)
|
||||
@@ -93,6 +114,30 @@ var RecipeListCommand = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
// externalRecipes returns canonical names of locally-cloned recipes that
|
||||
// were sourced from an arbitrary git URL (i.e. they carry a .abra-source
|
||||
// sidecar) and are not already listed in the catalogue.
|
||||
func externalRecipes(catl recipe.RecipeCatalogue) []string {
|
||||
dirs, err := recipe.GetRecipesLocal()
|
||||
if err != nil {
|
||||
log.Debug(i18n.G("can't read local recipes: %s", err))
|
||||
return nil
|
||||
}
|
||||
|
||||
var names []string
|
||||
for _, dir := range dirs {
|
||||
canonical := recipe.ReadRecipeSource(path.Join(config.RECIPES_DIR, dir))
|
||||
if canonical == "" {
|
||||
continue
|
||||
}
|
||||
if _, inCatalogue := catl[canonical]; inCatalogue {
|
||||
continue
|
||||
}
|
||||
names = append(names, canonical)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
var (
|
||||
pattern string
|
||||
)
|
||||
|
||||
@@ -129,6 +129,9 @@ interface.`),
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// digest pinned images shared between services are only checked once
|
||||
digestChecked := make(map[string]bool)
|
||||
|
||||
for _, service := range config.Services {
|
||||
img, err := reference.ParseNormalizedNamed(service.Image)
|
||||
if err != nil {
|
||||
@@ -178,6 +181,13 @@ interface.`),
|
||||
|
||||
sort.Sort(tagcmp.ByTagDesc(compatible))
|
||||
|
||||
if canonical, ok := img.(reference.Canonical); ok && !digestChecked[service.Image] {
|
||||
digestChecked[service.Image] = true
|
||||
if err := refreshDigest(recipe, img, image, canonical.Digest().String()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(compatible) == 0 && !allTags {
|
||||
log.Info(i18n.G("no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag))
|
||||
continue // skip on to the next tag and don't update any compose files
|
||||
@@ -364,6 +374,47 @@ interface.`),
|
||||
},
|
||||
}
|
||||
|
||||
// refreshDigest checks whether the current tag of a digest pinned image was
|
||||
// re-published upstream with a new digest and offers to update the pin.
|
||||
func refreshDigest(recipe recipePkg.Recipe, img reference.Named, image, digest string) error {
|
||||
tag := img.(reference.NamedTagged).Tag()
|
||||
|
||||
registryDigest, err := client.GetRegistryDigest(img, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if registryDigest == digest {
|
||||
log.Debug(i18n.G("digest of %s:%s is up to date", image, tag))
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Warn(i18n.G("%s:%s was re-published upstream (digest %s → %s)", image, tag, digest, registryDigest))
|
||||
|
||||
update := false
|
||||
if !internal.NoInput {
|
||||
prompt := &survey.Confirm{
|
||||
Message: i18n.G("update digest of %s:%s?", image, tag),
|
||||
Default: true,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &update); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !update {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := recipe.UpdateTag(image, tag); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info(i18n.G("digest updated for %s:%s", image, tag))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
allTags bool
|
||||
createCommit bool
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ func main() {
|
||||
Version = "dev"
|
||||
}
|
||||
if Commit == "" {
|
||||
Commit = " "
|
||||
Commit = "unknown-commit"
|
||||
}
|
||||
|
||||
cli.Run(Version, Commit)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module coopcloud.tech/abra
|
||||
|
||||
go 1.26.0
|
||||
go 1.27
|
||||
|
||||
require (
|
||||
coopcloud.tech/tagcmp v0.0.0-20260515102403-c26951b55977
|
||||
@@ -9,18 +9,25 @@ require (
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/charmbracelet/log v1.0.0
|
||||
github.com/containers/image v3.0.2+incompatible
|
||||
github.com/decentral1se/passgen v1.0.1
|
||||
github.com/distribution/reference v0.6.0
|
||||
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.2
|
||||
github.com/go-git/go-git/v5 v5.19.3
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/leonelquinteros/gotext v1.7.2
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8
|
||||
github.com/leonelquinteros/gotext v1.8.0
|
||||
github.com/moby/sys/signal v0.7.1
|
||||
github.com/moby/term v0.5.2
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/schollz/progressbar/v3 v3.19.1
|
||||
golang.org/x/term v0.46.0
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/stretchr/testify v1.12.1
|
||||
golang.org/x/sys v0.49.0
|
||||
golang.org/x/term v0.47.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gotest.tools/v3 v3.5.2
|
||||
)
|
||||
@@ -48,24 +55,29 @@ require (
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/containers/storage v1.38.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.5 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fvbommel/sortorder v1.1.0 // 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.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.9.2 // 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
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
@@ -73,7 +85,6 @@ require (
|
||||
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/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
|
||||
@@ -84,7 +95,9 @@ require (
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.1.0 // indirect
|
||||
github.com/moby/patternmatcher v0.6.1 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/morikuni/aec v1.1.0 // indirect
|
||||
@@ -95,16 +108,20 @@ 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.6.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.7.0 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // 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
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
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/theupdateframework/notary v0.7.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
@@ -122,10 +139,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.53.0 // indirect
|
||||
golang.org/x/crypto v0.56.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/net v0.56.0 // indirect
|
||||
golang.org/x/text v0.39.0 // indirect
|
||||
golang.org/x/net v0.57.0 // indirect
|
||||
golang.org/x/text v0.41.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
|
||||
@@ -135,27 +152,6 @@ require (
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/containers/image v3.0.2+incompatible
|
||||
github.com/containers/storage v1.38.2 // indirect
|
||||
github.com/decentral1se/passgen v1.0.1
|
||||
github.com/docker/docker-credential-helpers v0.9.5 // indirect
|
||||
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8
|
||||
github.com/moby/patternmatcher v0.6.1 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
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/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.48.0
|
||||
)
|
||||
|
||||
replace github.com/docker/cli v28.4.0+incompatible => git.coopcloud.tech/toolshed/docker-cli v28.5.3-0.20260202112816-30df2d0b3a00+incompatible
|
||||
|
||||
replace github.com/spf13/cobra => github.com/decentral1se/cobra v1.10.2
|
||||
|
||||
@@ -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.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-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-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.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
|
||||
github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
|
||||
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-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=
|
||||
@@ -585,8 +585,6 @@ github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdY
|
||||
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/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=
|
||||
@@ -604,8 +602,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc=
|
||||
github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8=
|
||||
github.com/leonelquinteros/gotext v1.8.0 h1:wwpxINWmWq3JN3WFI9BoSupBFrvAqxZTtf3qppijYkU=
|
||||
github.com/leonelquinteros/gotext v1.8.0/go.mod h1:Wqke380+8unZq2F4oTmmqmbUY9zWQMzwImdC34XOAo8=
|
||||
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||
@@ -752,8 +750,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.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
||||
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
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/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=
|
||||
@@ -965,8 +963,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.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
|
||||
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
|
||||
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=
|
||||
@@ -1042,8 +1040,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.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
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/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=
|
||||
@@ -1138,13 +1136,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.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
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/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.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
|
||||
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
|
||||
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=
|
||||
@@ -1154,8 +1152,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.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
|
||||
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
|
||||
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/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=
|
||||
|
||||
+111
-51
@@ -260,12 +260,30 @@ func ReadAppEnvFile(appFile AppFile, name AppName) (App, error) {
|
||||
func NewApp(env envfile.AppEnv, name string, appFile AppFile) (App, error) {
|
||||
domain := env["DOMAIN"]
|
||||
|
||||
recipeName, exists := env["RECIPE"]
|
||||
if !exists {
|
||||
recipeName, exists = env["TYPE"]
|
||||
if !exists {
|
||||
return App{}, errors.New(i18n.G("%s is missing the TYPE env var?", name))
|
||||
}
|
||||
typeVar, hasType := env["TYPE"]
|
||||
recipeVar, hasRecipe := env["RECIPE"]
|
||||
if !hasType && !hasRecipe {
|
||||
return App{}, errors.New(i18n.G("%s is missing the TYPE env var?", name))
|
||||
}
|
||||
|
||||
// The recipe identity is taken from RECIPE when present (it records the
|
||||
// canonical git source for externally-sourced recipes), otherwise from
|
||||
// TYPE. The deployed version is always carried by TYPE when a TYPE line
|
||||
// exists; only legacy apps that set RECIPE alone carry it there. RECIPE
|
||||
// itself must stay a bare source pointer, never a versioned value.
|
||||
identity := typeVar
|
||||
if hasRecipe {
|
||||
identity = recipeVar
|
||||
}
|
||||
|
||||
versionSource := identity
|
||||
if hasType {
|
||||
versionSource = typeVar
|
||||
}
|
||||
|
||||
recipeName, _, _ := strings.Cut(identity, ":")
|
||||
if _, version, found := strings.Cut(versionSource, ":"); found && version != "" {
|
||||
recipeName = fmt.Sprintf("%s:%s", recipeName, version)
|
||||
}
|
||||
|
||||
return App{
|
||||
@@ -392,11 +410,15 @@ func TemplateAppEnvSample(r recipe.Recipe, appName, server, domain string) error
|
||||
|
||||
newContents := strings.Replace(
|
||||
string(read),
|
||||
fmt.Sprintf("%s.example.com", r.Name),
|
||||
fmt.Sprintf("%s.example.com", r.ShortName()),
|
||||
domain,
|
||||
-1,
|
||||
)
|
||||
|
||||
if strings.Contains(r.Name, "/") {
|
||||
newContents = injectRecipeLine(newContents, r.Name)
|
||||
}
|
||||
|
||||
err = os.WriteFile(appEnvPath, []byte(newContents), 0)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -407,6 +429,37 @@ func TemplateAppEnvSample(r recipe.Recipe, appName, server, domain string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// injectRecipeLine ensures the env file carries a RECIPE=<canonical name>
|
||||
// line so a downstream `abra app deploy` (potentially on another machine)
|
||||
// can re-fetch the recipe from its original git source. If a RECIPE= line
|
||||
// already exists in the copied .env.sample it is replaced; otherwise a new
|
||||
// line is inserted immediately after the TYPE= line, or appended at the
|
||||
// end if no TYPE= line is present.
|
||||
func injectRecipeLine(contents, canonicalName string) string {
|
||||
lines := strings.Split(contents, "\n")
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimLeft(line, " \t")
|
||||
if strings.HasPrefix(trimmed, "RECIPE=") {
|
||||
lines[i] = "RECIPE=" + canonicalName
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
}
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimLeft(line, " \t")
|
||||
if strings.HasPrefix(trimmed, "TYPE=") {
|
||||
out := make([]string, 0, len(lines)+1)
|
||||
out = append(out, lines[:i+1]...)
|
||||
out = append(out, "RECIPE="+canonicalName)
|
||||
out = append(out, lines[i+1:]...)
|
||||
return strings.Join(out, "\n")
|
||||
}
|
||||
}
|
||||
if contents != "" && !strings.HasSuffix(contents, "\n") {
|
||||
contents += "\n"
|
||||
}
|
||||
return contents + "RECIPE=" + canonicalName + "\n"
|
||||
}
|
||||
|
||||
// SanitiseAppName makes a app name usable with Docker by replacing illegal
|
||||
// characters.
|
||||
func SanitiseAppName(name string) string {
|
||||
@@ -514,10 +567,22 @@ func ExposeAllEnv(
|
||||
_, exists := service.Environment[k]
|
||||
if !exists {
|
||||
value := v
|
||||
if k == "TYPE" || k == "RECIPE" {
|
||||
switch k {
|
||||
case "TYPE":
|
||||
// NOTE(d1): don't use the wrong version from the app env
|
||||
// since we are deploying a new version
|
||||
value = toDeployVersion
|
||||
// since we are deploying a new version. Keep the
|
||||
// recipe name already recorded in TYPE and only
|
||||
// swap in the version being deployed.
|
||||
name, _, _ := strings.Cut(v, ":")
|
||||
if _, version, found := strings.Cut(toDeployVersion, ":"); found {
|
||||
value = fmt.Sprintf("%s:%s", name, version)
|
||||
} else {
|
||||
value = name
|
||||
}
|
||||
case "RECIPE":
|
||||
// RECIPE records the canonical git source only; the
|
||||
// version is carried by TYPE, so keep it bare here.
|
||||
value, _, _ = strings.Cut(toDeployVersion, ":")
|
||||
}
|
||||
service.Environment[k] = &value
|
||||
log.Debug(i18n.G("%s: %s: %s", stackName, k, value))
|
||||
@@ -642,60 +707,55 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var (
|
||||
dirtyVersion string
|
||||
skipped bool
|
||||
lines []string
|
||||
scanner = bufio.NewScanner(file)
|
||||
lines []string
|
||||
hasType bool
|
||||
scanner = bufio.NewScanner(file)
|
||||
)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !strings.HasPrefix(line, "RECIPE=") && !strings.HasPrefix(line, "TYPE=") {
|
||||
lines = append(lines, line)
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "#") {
|
||||
lines = append(lines, line)
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(line, version) && !a.Recipe.Dirty && !strings.HasSuffix(line, config.DIRTY_DEFAULT) {
|
||||
skipped = true
|
||||
lines = append(lines, line)
|
||||
continue
|
||||
}
|
||||
|
||||
splitted := strings.Split(line, ":")
|
||||
|
||||
line = fmt.Sprintf("%s:%s", splitted[0], version)
|
||||
lines = append(lines, line)
|
||||
if strings.HasPrefix(line, "TYPE=") {
|
||||
hasType = true
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
file.Close()
|
||||
log.Fatal(err)
|
||||
}
|
||||
file.Close()
|
||||
|
||||
// The recipe version is carried by a single field: TYPE when present (the
|
||||
// standard recipe key), otherwise the legacy RECIPE key. When both exist
|
||||
// (git-URL recipes) RECIPE stays a bare source pointer and only TYPE is
|
||||
// versioned, so the two can never drift apart.
|
||||
targetPrefix := "RECIPE="
|
||||
if hasType {
|
||||
targetPrefix = "TYPE="
|
||||
}
|
||||
|
||||
for i, line := range lines {
|
||||
if !strings.HasPrefix(line, targetPrefix) || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
name, _, _ := strings.Cut(line, ":")
|
||||
lines[i] = fmt.Sprintf("%s:%s", name, version)
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
log.Debug(i18n.G("skipping writing version %s because dry run", version))
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if a.Recipe.Dirty && dirtyVersion != "" {
|
||||
version = dirtyVersion
|
||||
}
|
||||
|
||||
if !dryRun {
|
||||
if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
log.Debug(i18n.G("skipping writing version %s because dry run", version))
|
||||
}
|
||||
|
||||
if !skipped {
|
||||
log.Debug(i18n.G("version %s saved to %s.env", version, a.Domain))
|
||||
} else {
|
||||
log.Debug(i18n.G("skipping version %s write as already exists in %s.env", version, a.Domain))
|
||||
}
|
||||
log.Debug(i18n.G("version %s saved to %s.env", version, a.Domain))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package app_test
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
@@ -271,6 +273,60 @@ func TestWriteRecipeVersionOverwrite(t *testing.T) {
|
||||
assert.Equal(t, "foo", app.Recipe.EnvVersion)
|
||||
}
|
||||
|
||||
// TestWriteRecipeVersionSingleField ensures the version is written to exactly
|
||||
// one field: TYPE when present (even alongside a RECIPE source pointer, as with
|
||||
// git-URL recipes), otherwise the legacy RECIPE field. RECIPE must stay bare
|
||||
// when a TYPE line exists so the two can never drift apart.
|
||||
func TestWriteRecipeVersionSingleField(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "git recipe: TYPE and RECIPE both present",
|
||||
input: "TYPE=myrecipe\nRECIPE=git.example.com/user/myrecipe\nDOMAIN=x\n",
|
||||
want: "TYPE=myrecipe:1.2.3+4.5.6\nRECIPE=git.example.com/user/myrecipe\nDOMAIN=x",
|
||||
},
|
||||
{
|
||||
name: "git recipe: re-versioning replaces only TYPE",
|
||||
input: "TYPE=myrecipe:0.1.0+1.0.0\nRECIPE=git.example.com/user/myrecipe\nDOMAIN=x\n",
|
||||
want: "TYPE=myrecipe:1.2.3+4.5.6\nRECIPE=git.example.com/user/myrecipe\nDOMAIN=x",
|
||||
},
|
||||
{
|
||||
name: "legacy recipe: only RECIPE present",
|
||||
input: "RECIPE=test_recipe\nDOMAIN=x\n",
|
||||
want: "RECIPE=test_recipe:1.2.3+4.5.6\nDOMAIN=x",
|
||||
},
|
||||
{
|
||||
name: "catalogue recipe: only TYPE present",
|
||||
input: "TYPE=nextcloud\nDOMAIN=x\n",
|
||||
want: "TYPE=nextcloud:1.2.3+4.5.6\nDOMAIN=x",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p := filepath.Join(t.TempDir(), "app.env")
|
||||
if err := os.WriteFile(p, []byte(tc.input), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
app := appPkg.App{Path: p, Domain: "x"}
|
||||
if err := app.WriteRecipeVersion("1.2.3+4.5.6", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, tc.want, string(got))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteRecipeVersionUnknown(t *testing.T) {
|
||||
test.Setup()
|
||||
t.Cleanup(func() { test.Teardown() })
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package app
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestInjectRecipeLineAfterType(t *testing.T) {
|
||||
in := "DOMAIN=example.com\nTYPE=foo\nVERSION=1.0\n"
|
||||
want := "DOMAIN=example.com\nTYPE=foo\nRECIPE=org/foo\nVERSION=1.0\n"
|
||||
got := injectRecipeLine(in, "org/foo")
|
||||
if got != want {
|
||||
t.Errorf("injectRecipeLine inserted RECIPE in wrong position\nwant:\n%q\ngot:\n%q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectRecipeLineReplacesExisting(t *testing.T) {
|
||||
in := "TYPE=foo\nRECIPE=old\nDOMAIN=example.com\n"
|
||||
want := "TYPE=foo\nRECIPE=org/foo\nDOMAIN=example.com\n"
|
||||
got := injectRecipeLine(in, "org/foo")
|
||||
if got != want {
|
||||
t.Errorf("injectRecipeLine should replace existing RECIPE line\nwant:\n%q\ngot:\n%q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectRecipeLineNoTypeAppends(t *testing.T) {
|
||||
in := "DOMAIN=example.com\n"
|
||||
want := "DOMAIN=example.com\nRECIPE=org/foo\n"
|
||||
got := injectRecipeLine(in, "org/foo")
|
||||
if got != want {
|
||||
t.Errorf("injectRecipeLine should append when TYPE is missing\nwant:\n%q\ngot:\n%q", want, got)
|
||||
}
|
||||
}
|
||||
+52
-1
@@ -4,18 +4,35 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"github.com/containers/image/docker"
|
||||
"github.com/containers/image/manifest"
|
||||
"github.com/containers/image/types"
|
||||
"github.com/distribution/reference"
|
||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// NOTE: containers/image does not request OCI image indexes by default.
|
||||
// Many multi-arch images (e.g. on Docker Hub) are published as OCI indexes
|
||||
// and the registry refuses to serve them without the matching Accept header.
|
||||
if !slices.Contains(manifest.DefaultRequestedManifestMIMETypes, imgspecv1.MediaTypeImageIndex) {
|
||||
manifest.DefaultRequestedManifestMIMETypes = append(
|
||||
manifest.DefaultRequestedManifestMIMETypes,
|
||||
imgspecv1.MediaTypeImageIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// GetRegistryTags retrieves all tags of an image from a container registry.
|
||||
func GetRegistryTags(img reference.Named) ([]string, error) {
|
||||
var tags []string
|
||||
|
||||
ref, err := docker.ParseReference(fmt.Sprintf("//%s", img))
|
||||
// NOTE: strip tag and digest, containers/image refuses references
|
||||
// which have both (e.g. nextcloud:34.0.5-fpm@sha256:...)
|
||||
ref, err := docker.ParseReference(fmt.Sprintf("//%s", reference.TrimNamed(img)))
|
||||
if err != nil {
|
||||
return tags, errors.New(i18n.G("failed to parse image %s, saw: %s", img, err.Error()))
|
||||
}
|
||||
@@ -28,3 +45,37 @@ func GetRegistryTags(img reference.Named) ([]string, error) {
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// GetRegistryDigest retrieves the manifest digest of an image tag from a
|
||||
// container registry. For multi-arch images, this is the digest of the image
|
||||
// index, which is what Docker resolves "image:tag" to.
|
||||
func GetRegistryDigest(img reference.Named, tag string) (string, error) {
|
||||
tagged, err := reference.WithTag(reference.TrimNamed(img), tag)
|
||||
if err != nil {
|
||||
return "", errors.New(i18n.G("failed to parse image %s, saw: %s", img, err.Error()))
|
||||
}
|
||||
|
||||
ref, err := docker.ParseReference(fmt.Sprintf("//%s", tagged))
|
||||
if err != nil {
|
||||
return "", errors.New(i18n.G("failed to parse image %s, saw: %s", tagged, err.Error()))
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
src, err := ref.NewImageSource(ctx, &types.SystemContext{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
manifestBytes, _, err := src.GetManifest(ctx, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
digest, err := manifest.Digest(manifestBytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return digest.String(), nil
|
||||
}
|
||||
|
||||
@@ -27,10 +27,16 @@ var BoldUnderlineStyle = lipgloss.NewStyle().
|
||||
Underline(true)
|
||||
|
||||
func ShortenID(str string) string {
|
||||
if len(str) < 12 {
|
||||
return str
|
||||
}
|
||||
return str[:12]
|
||||
}
|
||||
|
||||
func SmallSHA(hash string) string {
|
||||
if len(hash) < 8 {
|
||||
return hash
|
||||
}
|
||||
return hash[:8]
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ func IsClean(repoPath string) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Ignore the abra-managed sidecar file that records the canonical
|
||||
// source URL for externally-cloned recipes; it lives alongside the
|
||||
// recipe but is not part of the upstream tree.
|
||||
patterns = append(patterns, gitignore.ParsePattern(".abra-source", nil))
|
||||
|
||||
if len(patterns) > 0 {
|
||||
worktree.Excludes = append(patterns, worktree.Excludes...)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package git
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
@@ -13,3 +15,37 @@ func TestIsClean(t *testing.T) {
|
||||
assert.Equal(t, isClean, false)
|
||||
assert.True(t, errors.Is(err, git.ErrRepositoryNotExists))
|
||||
}
|
||||
|
||||
// TestIsCleanIgnoresAbraSource confirms that the .abra-source sidecar
|
||||
// file written by abra next to externally-cloned recipes does not cause
|
||||
// IsClean to report the worktree as dirty.
|
||||
func TestIsCleanIgnoresAbraSource(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
if _, err := git.PlainInit(dir, false); err != nil {
|
||||
t.Fatalf("git init failed: %s", err)
|
||||
}
|
||||
|
||||
sidecar := filepath.Join(dir, ".abra-source")
|
||||
if err := os.WriteFile(sidecar, []byte("git.example.com/u/recipe\n"), 0o644); err != nil {
|
||||
t.Fatalf("writing sidecar failed: %s", err)
|
||||
}
|
||||
|
||||
isClean, err := IsClean(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("IsClean returned error: %s", err)
|
||||
}
|
||||
assert.True(t, isClean, "expected worktree with only .abra-source to be reported clean")
|
||||
|
||||
// Sanity check: an unrelated untracked file should still mark it dirty.
|
||||
other := filepath.Join(dir, "random.txt")
|
||||
if err := os.WriteFile(other, []byte("hello"), 0o644); err != nil {
|
||||
t.Fatalf("writing extra file failed: %s", err)
|
||||
}
|
||||
|
||||
isClean, err = IsClean(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("IsClean returned error: %s", err)
|
||||
}
|
||||
assert.False(t, isClean, "expected worktree with unrelated untracked file to be reported dirty")
|
||||
}
|
||||
|
||||
@@ -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-10-14 10:32-0400\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"
|
||||
@@ -4475,7 +4475,7 @@ msgstr ""
|
||||
msgid "retrieving docker auth token: failed create docker cli: %s"
|
||||
msgstr ""
|
||||
|
||||
#: ./pkg/recipe/recipe.go:659
|
||||
#: ./pkg/recipe/recipe.go:656
|
||||
msgid "retrieving recipes"
|
||||
msgstr ""
|
||||
|
||||
|
||||
Binary file not shown.
@@ -2,7 +2,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-10-14 10:32-0400\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"
|
||||
@@ -5125,7 +5125,7 @@ msgstr "versiones recuperadas del repositorio local de recetas"
|
||||
msgid "retrieving docker auth token: failed create docker cli: %s"
|
||||
msgstr "recuperando token de autenticación de Docker: error al crear docker cli: %s"
|
||||
|
||||
#: pkg/recipe/recipe.go:659
|
||||
#: pkg/recipe/recipe.go:656
|
||||
msgid "retrieving recipes"
|
||||
msgstr "obteniendo 📜 recetas"
|
||||
|
||||
|
||||
+49
-5
@@ -6,8 +6,10 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
@@ -110,7 +112,14 @@ func (r Recipe) GetVersionLabelLocal() (string, error) {
|
||||
return label, nil
|
||||
}
|
||||
|
||||
// getRegistryDigest is a variable so that it can be stubbed in tests.
|
||||
var getRegistryDigest = client.GetRegistryDigest
|
||||
|
||||
// UpdateTag updates an image tag in-place on file system local compose files.
|
||||
// Images which are pinned to a digest (e.g. "nextcloud:34.0.5-fpm@sha256:...")
|
||||
// have their digest updated to the one of the new tag, as retrieved from the
|
||||
// container registry. Passing the current tag of a digest pinned image
|
||||
// refreshes the digest, e.g. when upstream re-published the same tag.
|
||||
func (r Recipe) UpdateTag(image, tag string) (bool, error) {
|
||||
fullPattern := fmt.Sprintf("%s/compose**yml", r.Dir)
|
||||
image = formatter.StripTagMeta(image)
|
||||
@@ -122,6 +131,11 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
|
||||
|
||||
log.Debug(i18n.G("considering %s config(s) for tag update", strings.Join(composeFiles, ", ")))
|
||||
|
||||
var (
|
||||
updated bool
|
||||
newDigest string
|
||||
)
|
||||
|
||||
for _, composeFile := range composeFiles {
|
||||
opts := stack.Deploy{Composefiles: []string{composeFile}}
|
||||
|
||||
@@ -140,7 +154,7 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
|
||||
continue // may be a compose.$optional.yml file
|
||||
}
|
||||
|
||||
img, _ := reference.ParseNormalizedNamed(service.Image)
|
||||
img, err := reference.ParseNormalizedNamed(service.Image)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -154,30 +168,60 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
var composeDigest string
|
||||
if canonical, ok := img.(reference.Canonical); ok {
|
||||
composeDigest = canonical.Digest().String()
|
||||
}
|
||||
|
||||
composeImage := formatter.StripTagMeta(reference.Path(img))
|
||||
|
||||
log.Debug(i18n.G("parsed %s from %s", composeTag, service.Image))
|
||||
|
||||
if image == composeImage {
|
||||
bytes, err := ioutil.ReadFile(composeFile)
|
||||
bytes, err := os.ReadFile(composeFile)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
old := fmt.Sprintf("%s:%s", composeImage, composeTag)
|
||||
new := fmt.Sprintf("%s:%s", composeImage, tag)
|
||||
replacedBytes := strings.Replace(string(bytes), old, new, -1)
|
||||
|
||||
if composeDigest != "" {
|
||||
if newDigest == "" {
|
||||
newDigest, err = getRegistryDigest(img, tag)
|
||||
if err != nil {
|
||||
return false, errors.New(i18n.G("unable to retrieve digest for %s: %s", new, err))
|
||||
}
|
||||
}
|
||||
|
||||
old = fmt.Sprintf("%s@%s", old, composeDigest)
|
||||
new = fmt.Sprintf("%s@%s", new, newDigest)
|
||||
}
|
||||
|
||||
if old == new {
|
||||
continue
|
||||
}
|
||||
|
||||
// NOTE: only match the full image reference, so that "foo:1.2" does
|
||||
// not match "foo:1.2.3" or the "foo:1.2" part of "foo:1.2@sha256:..."
|
||||
pattern := regexp.MustCompile(regexp.QuoteMeta(old) + `([^@\w.\-]|$)`)
|
||||
replaced := pattern.ReplaceAllString(string(bytes), new+"${1}")
|
||||
if replaced == string(bytes) {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debug(i18n.G("updating %s to %s in %s", old, new, compose.Filename))
|
||||
|
||||
if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
|
||||
if err := os.WriteFile(compose.Filename, []byte(replaced), 0o764); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
// UpdateLabel updates a label in-place on file system local compose files.
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
oldDigest = "sha256:1111111111111111111111111111111111111111111111111111111111111111"
|
||||
newDigest = "sha256:2222222222222222222222222222222222222222222222222222222222222222"
|
||||
)
|
||||
|
||||
func setupUpdateTagRecipe(t *testing.T, compose string) (Recipe, string) {
|
||||
t.Helper()
|
||||
|
||||
dir := t.TempDir()
|
||||
composePath := path.Join(dir, "compose.yml")
|
||||
envPath := path.Join(dir, ".env.sample")
|
||||
|
||||
assert.NoError(t, os.WriteFile(composePath, []byte(compose), 0o644))
|
||||
assert.NoError(t, os.WriteFile(envPath, []byte("TYPE=foo\n"), 0o644))
|
||||
|
||||
return Recipe{
|
||||
Name: "foo",
|
||||
Dir: dir,
|
||||
ComposePath: composePath,
|
||||
SampleEnvPath: envPath,
|
||||
}, composePath
|
||||
}
|
||||
|
||||
func stubRegistryDigest(t *testing.T, digest string) *[]string {
|
||||
t.Helper()
|
||||
|
||||
var calls []string
|
||||
orig := getRegistryDigest
|
||||
getRegistryDigest = func(img reference.Named, tag string) (string, error) {
|
||||
calls = append(calls, reference.Path(img)+":"+tag)
|
||||
return digest, nil
|
||||
}
|
||||
t.Cleanup(func() { getRegistryDigest = orig })
|
||||
|
||||
return &calls
|
||||
}
|
||||
|
||||
func TestUpdateTag(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
compose string
|
||||
image string
|
||||
tag string
|
||||
expected string
|
||||
updated bool
|
||||
lookups int
|
||||
}{
|
||||
{
|
||||
name: "plain tag",
|
||||
compose: "version: \"3.8\"\nservices:\n app:\n image: nginx:1.31.7\n",
|
||||
image: "nginx",
|
||||
tag: "1.31.8",
|
||||
expected: "version: \"3.8\"\nservices:\n app:\n image: nginx:1.31.8\n",
|
||||
updated: true,
|
||||
},
|
||||
{
|
||||
name: "shorter tag does not mangle longer tag",
|
||||
compose: "version: \"3.8\"\nservices:\n app:\n image: nginx:1.31\n web:\n image: nginx:1.31.7\n",
|
||||
image: "nginx",
|
||||
tag: "1.32",
|
||||
expected: "version: \"3.8\"\nservices:\n app:\n image: nginx:1.32\n web:\n image: nginx:1.32\n",
|
||||
updated: true,
|
||||
},
|
||||
{
|
||||
name: "digest is updated with tag",
|
||||
compose: "version: \"3.8\"\nservices:\n app:\n image: nextcloud:34.0.5-fpm@" + oldDigest + "\n",
|
||||
image: "nextcloud",
|
||||
tag: "34.0.6-fpm",
|
||||
expected: "version: \"3.8\"\nservices:\n app:\n image: nextcloud:34.0.6-fpm@" + newDigest + "\n",
|
||||
updated: true,
|
||||
lookups: 1,
|
||||
},
|
||||
{
|
||||
name: "digest is refreshed for same tag",
|
||||
compose: "version: \"3.8\"\nservices:\n app:\n image: \"nextcloud:34.0.5-fpm@" + oldDigest + "\"\n",
|
||||
image: "nextcloud",
|
||||
tag: "34.0.5-fpm",
|
||||
expected: "version: \"3.8\"\nservices:\n app:\n image: \"nextcloud:34.0.5-fpm@" + newDigest + "\"\n",
|
||||
updated: true,
|
||||
lookups: 1,
|
||||
},
|
||||
{
|
||||
name: "same tag without digest is a no-op",
|
||||
compose: "version: \"3.8\"\nservices:\n app:\n image: nextcloud:34.0.5-fpm\n",
|
||||
image: "nextcloud",
|
||||
tag: "34.0.5-fpm",
|
||||
expected: "version: \"3.8\"\nservices:\n app:\n image: nextcloud:34.0.5-fpm\n",
|
||||
updated: false,
|
||||
},
|
||||
{
|
||||
name: "pinned and unpinned services sharing an image",
|
||||
compose: "version: \"3.8\"\nservices:\n app:\n image: nextcloud:34.0.5-fpm@" + oldDigest + "\n cron:\n image: nextcloud:34.0.5-fpm\n",
|
||||
image: "nextcloud",
|
||||
tag: "34.0.6-fpm",
|
||||
expected: "version: \"3.8\"\nservices:\n app:\n image: nextcloud:34.0.6-fpm@" + newDigest + "\n cron:\n image: nextcloud:34.0.6-fpm\n",
|
||||
updated: true,
|
||||
lookups: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r, composePath := setupUpdateTagRecipe(t, tc.compose)
|
||||
calls := stubRegistryDigest(t, newDigest)
|
||||
|
||||
updated, err := r.UpdateTag(tc.image, tc.tag)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.updated, updated)
|
||||
assert.Len(t, *calls, tc.lookups)
|
||||
|
||||
content, err := os.ReadFile(composePath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.expected, string(content))
|
||||
})
|
||||
}
|
||||
}
|
||||
+102
-10
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -20,6 +21,11 @@ import (
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// SourceFile is the sidecar file written next to an externally-cloned
|
||||
// recipe so the canonical "host/path" name can be recovered later (the
|
||||
// on-disk directory name escapes "/" and "." to "_", which is lossy).
|
||||
const SourceFile = ".abra-source"
|
||||
|
||||
type EnsureContext struct {
|
||||
Chaos bool
|
||||
Offline bool
|
||||
@@ -59,7 +65,7 @@ func (r Recipe) Ensure(ctx EnsureContext) error {
|
||||
return errors.New(i18n.G(`cannot redeploy previous chaos version (%s), did you mean to use "--chaos"?`))
|
||||
}
|
||||
|
||||
if _, err := r.EnsureVersion(r.EnvVersion); err != nil {
|
||||
if _, err := r.EnsureVersionCtx(ctx, r.EnvVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -79,6 +85,12 @@ func (r Recipe) EnsureExists() error {
|
||||
if err := gitPkg.Clone(r.Dir, r.GitURL); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.Contains(r.Name, "/") {
|
||||
sidecar := path.Join(r.Dir, SourceFile)
|
||||
if err := os.WriteFile(sidecar, []byte(r.Name+"\n"), 0o644); err != nil {
|
||||
log.Debug(i18n.G("failed to write recipe source sidecar %s: %s", sidecar, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := gitPkg.EnsureGitRepo(r.Dir); err != nil {
|
||||
@@ -88,6 +100,18 @@ func (r Recipe) EnsureExists() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadRecipeSource returns the canonical name recorded in the .abra-source
|
||||
// sidecar inside the given recipe directory, or the empty string if no
|
||||
// sidecar exists. This lets callers recover the unescaped "host/path"
|
||||
// form for externally-cloned recipes.
|
||||
func ReadRecipeSource(recipeDir string) string {
|
||||
data, err := os.ReadFile(path.Join(recipeDir, SourceFile))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
// IsChaosCommit determines if a version sttring is a chaos commit or not.
|
||||
func (r Recipe) IsChaosCommit(version string) (bool, error) {
|
||||
isChaosCommit := false
|
||||
@@ -125,6 +149,13 @@ func (r Recipe) IsChaosCommit(version string) (bool, error) {
|
||||
|
||||
// EnsureVersion checks whether a specific version exists for a recipe.
|
||||
func (r Recipe) EnsureVersion(version string) (bool, error) {
|
||||
return r.EnsureVersionCtx(EnsureContext{}, version)
|
||||
}
|
||||
|
||||
// EnsureVersionCtx is like EnsureVersion but is aware of the surrounding
|
||||
// EnsureContext, so that e.g. offline mode can skip network fetches when
|
||||
// resolving a branch name that isn't already available locally.
|
||||
func (r Recipe) EnsureVersionCtx(ctx EnsureContext, version string) (bool, error) {
|
||||
isChaosCommit := false
|
||||
|
||||
if err := gitPkg.EnsureGitRepo(r.Dir); err != nil {
|
||||
@@ -159,30 +190,46 @@ func (r Recipe) EnsureVersion(version string) (bool, error) {
|
||||
}
|
||||
|
||||
var opts *git.CheckoutOptions
|
||||
var checkedOutRef string
|
||||
if tagRef.String() == "" {
|
||||
log.Debug(i18n.G("attempting to checkout '%s' as chaos commit", version))
|
||||
|
||||
hash, err := repo.ResolveRevision(plumbing.Revision(version))
|
||||
if err != nil {
|
||||
log.Fatal(i18n.G("unable to resolve '%s': %s", version, err))
|
||||
}
|
||||
hash, hashErr := repo.ResolveRevision(plumbing.Revision(version))
|
||||
if hashErr != nil {
|
||||
branchHash, found, branchErr := resolveBranch(repo, version, ctx.Offline)
|
||||
if errors.Is(branchErr, errOfflineBranchUnresolved) {
|
||||
return isChaosCommit, errors.New(i18n.G("'%s' is not available locally and cannot be fetched in offline mode; retry without --offline", version))
|
||||
}
|
||||
if branchErr != nil {
|
||||
return isChaosCommit, branchErr
|
||||
}
|
||||
if !found {
|
||||
return isChaosCommit, errors.New(i18n.G("unable to resolve '%s': %s", version, hashErr))
|
||||
}
|
||||
|
||||
opts = &git.CheckoutOptions{Hash: *hash, Create: false, Force: true}
|
||||
isChaosCommit = true
|
||||
opts = &git.CheckoutOptions{Hash: branchHash, Create: false, Force: true}
|
||||
isChaosCommit = true
|
||||
checkedOutRef = version
|
||||
} else {
|
||||
opts = &git.CheckoutOptions{Hash: *hash, Create: false, Force: true}
|
||||
isChaosCommit = true
|
||||
checkedOutRef = hash.String()
|
||||
}
|
||||
} else {
|
||||
opts = &git.CheckoutOptions{Branch: tagRef, Create: false, Force: true}
|
||||
checkedOutRef = tagRef.Short()
|
||||
}
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return isChaosCommit, nil
|
||||
return isChaosCommit, err
|
||||
}
|
||||
|
||||
if err := worktree.Checkout(opts); err != nil {
|
||||
return isChaosCommit, nil
|
||||
return isChaosCommit, err
|
||||
}
|
||||
|
||||
log.Debug(i18n.G("successfully checked %s out to %s in %s", r.Name, tagRef.Short(), r.Dir))
|
||||
log.Debug(i18n.G("successfully checked %s out to %s in %s", r.Name, checkedOutRef, r.Dir))
|
||||
|
||||
return isChaosCommit, nil
|
||||
}
|
||||
@@ -485,6 +532,51 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
||||
return versions, uniqueWarnings, nil
|
||||
}
|
||||
|
||||
// resolveBranch resolves name as a branch on the "origin" remote, fetching
|
||||
// it on demand into a remote-tracking ref (never into refs/heads/*, which
|
||||
// would corrupt GetDefaultBranch's local-branch-name heuristic). It returns
|
||||
// (hash, true, nil) if name is a branch, (_, false, nil) if name is
|
||||
// confirmed not to be a branch, and a non-nil error for any other failure
|
||||
// (e.g. network/auth issues) that should not be silently treated as "not a
|
||||
// branch". When offline is true, no fetch is attempted and only an
|
||||
// already-fetched remote-tracking ref is consulted.
|
||||
func resolveBranch(repo *git.Repository, name string, offline bool) (plumbing.Hash, bool, error) {
|
||||
remoteTrackingRef := plumbing.NewRemoteReferenceName("origin", name)
|
||||
|
||||
if !offline {
|
||||
refSpec := gitCfg.RefSpec(fmt.Sprintf("+refs/heads/%s:%s", name, remoteTrackingRef))
|
||||
err := repo.Fetch(&git.FetchOptions{
|
||||
RemoteName: "origin",
|
||||
RefSpecs: []gitCfg.RefSpec{refSpec},
|
||||
Force: true,
|
||||
})
|
||||
if err != nil {
|
||||
var noMatch git.NoMatchingRefSpecError
|
||||
switch {
|
||||
case errors.Is(err, git.NoErrAlreadyUpToDate):
|
||||
case errors.As(err, &noMatch):
|
||||
return plumbing.ZeroHash, false, nil
|
||||
default:
|
||||
return plumbing.ZeroHash, false, errors.New(i18n.G("unable to fetch branch '%s': %s", name, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ref, err := repo.Reference(remoteTrackingRef, true)
|
||||
if err != nil {
|
||||
if offline {
|
||||
return plumbing.ZeroHash, false, errOfflineBranchUnresolved
|
||||
}
|
||||
return plumbing.ZeroHash, false, errors.New(i18n.G("unable to resolve fetched branch '%s': %s", name, err))
|
||||
}
|
||||
|
||||
return ref.Hash(), true, nil
|
||||
}
|
||||
|
||||
// errOfflineBranchUnresolved is returned by resolveBranch when offline mode
|
||||
// prevents fetching a branch that isn't already available locally.
|
||||
var errOfflineBranchUnresolved = errors.New(i18n.G("not fetched and cannot resolve offline"))
|
||||
|
||||
// Head retrieves latest HEAD metadata.
|
||||
func (r Recipe) Head() (*plumbing.Reference, error) {
|
||||
repo, err := git.PlainOpen(r.Dir)
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"coopcloud.tech/abra/pkg/test"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -36,3 +38,122 @@ func TestIsDirty(t *testing.T) {
|
||||
|
||||
assert.True(t, dirty)
|
||||
}
|
||||
|
||||
func TestEnsureVersionBranch(t *testing.T) {
|
||||
test.SetupWithUpstream()
|
||||
t.Cleanup(func() { test.Teardown() })
|
||||
|
||||
upstream, err := git.PlainOpen(test.UpstreamRecipeDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
featureRef, err := upstream.Reference(plumbing.NewBranchReferenceName(test.FeatureBranch), true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := Get(test.RecipeName)
|
||||
|
||||
isChaosCommit, err := r.EnsureVersion(test.FeatureBranch)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.True(t, isChaosCommit)
|
||||
|
||||
head, err := r.Head()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, featureRef.Hash(), head.Hash())
|
||||
}
|
||||
|
||||
func TestEnsureVersionBranchRefetchesOnRedeploy(t *testing.T) {
|
||||
test.SetupWithUpstream()
|
||||
t.Cleanup(func() { test.Teardown() })
|
||||
|
||||
r := Get(test.RecipeName)
|
||||
|
||||
if _, err := r.EnsureVersion(test.FeatureBranch); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := test.CreateBranchWithCommit(test.UpstreamRecipeDir, test.FeatureBranch, "feature2.txt", "more feature work"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
upstream, err := git.PlainOpen(test.UpstreamRecipeDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
featureRef, err := upstream.Reference(plumbing.NewBranchReferenceName(test.FeatureBranch), true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := r.EnsureVersion(test.FeatureBranch); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
head, err := r.Head()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, featureRef.Hash(), head.Hash())
|
||||
}
|
||||
|
||||
func TestEnsureVersionOfflineUnfetchedBranch(t *testing.T) {
|
||||
test.SetupWithUpstream()
|
||||
t.Cleanup(func() { test.Teardown() })
|
||||
|
||||
r := Get(test.RecipeName)
|
||||
|
||||
headBefore, err := r.Head()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = r.EnsureVersionCtx(EnsureContext{Offline: true}, test.FeatureBranch)
|
||||
assert.ErrorContains(t, err, "offline")
|
||||
|
||||
headAfter, err := r.Head()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, headBefore.Hash(), headAfter.Hash())
|
||||
}
|
||||
|
||||
func TestEnsureVersionUnknownNameErrors(t *testing.T) {
|
||||
test.SetupWithUpstream()
|
||||
t.Cleanup(func() { test.Teardown() })
|
||||
|
||||
r := Get(test.RecipeName)
|
||||
|
||||
_, err := r.EnsureVersion("does-not-exist-anywhere")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestEnsureBranchPinnedRecipe(t *testing.T) {
|
||||
test.SetupWithUpstream()
|
||||
t.Cleanup(func() { test.Teardown() })
|
||||
|
||||
upstream, err := git.PlainOpen(test.UpstreamRecipeDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
featureRef, err := upstream.Reference(plumbing.NewBranchReferenceName(test.FeatureBranch), true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := Get(test.RecipeName + ":" + test.FeatureBranch)
|
||||
|
||||
if err := r.Ensure(EnsureContext{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
head, err := r.Head()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, featureRef.Hash(), head.Hash())
|
||||
}
|
||||
|
||||
+103
-18
@@ -8,6 +8,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -121,7 +122,84 @@ type Features struct {
|
||||
SSO string `json:"sso"`
|
||||
}
|
||||
|
||||
// scpURLPattern matches SCP-style git URLs like git@host:path or git@host:port/path.
|
||||
// Captures: 1=host(:port)?, 2=path.
|
||||
var scpURLPattern = regexp.MustCompile(`^[\w.-]+@([\w.-]+(?::\d+)?):(.+)$`)
|
||||
|
||||
// NormalizeRecipeName canonicalizes a recipe identifier to a stable
|
||||
// "host/path" form (or returns short catalog names unchanged). Accepts:
|
||||
//
|
||||
// - https://host/path[.git][:version]
|
||||
// - http://host/path[.git][:version]
|
||||
// - ssh://git@host[:port]/path[.git][:version]
|
||||
// - git@host:path[.git][:version] (SCP-style)
|
||||
// - host/path[.git][:version] (already canonical)
|
||||
// - short-name[:version] (catalog recipe, pass through)
|
||||
//
|
||||
// The optional trailing :version suffix is preserved verbatim. The .git
|
||||
// suffix and trailing slashes on the path are stripped so the four URL
|
||||
// forms of the same repository collapse to one canonical value.
|
||||
func NormalizeRecipeName(input string) string {
|
||||
input = strings.TrimSpace(input)
|
||||
|
||||
// Split off the version suffix first, but only if it's not part of a
|
||||
// scheme (https://) or SCP-style git@host: prefix. The simplest way is
|
||||
// to detect those prefixes and treat the rest as the path-with-version.
|
||||
var (
|
||||
body = input
|
||||
version string
|
||||
)
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(input, "https://") || strings.HasPrefix(input, "http://") || strings.HasPrefix(input, "ssh://"):
|
||||
u, err := url.Parse(input)
|
||||
if err != nil {
|
||||
return input
|
||||
}
|
||||
host := u.Hostname() // strip port to keep canonical form colon-free
|
||||
p := strings.TrimPrefix(u.Path, "/")
|
||||
p, version = splitVersion(p)
|
||||
body = host + "/" + p
|
||||
case scpURLPattern.MatchString(input):
|
||||
m := scpURLPattern.FindStringSubmatch(input)
|
||||
host := m[1]
|
||||
if i := strings.Index(host, ":"); i >= 0 {
|
||||
host = host[:i] // strip port
|
||||
}
|
||||
p, v := splitVersion(m[2])
|
||||
body = host + "/" + p
|
||||
version = v
|
||||
default:
|
||||
body, version = splitVersion(input)
|
||||
}
|
||||
|
||||
body = strings.TrimSuffix(body, "/")
|
||||
body = strings.TrimSuffix(body, ".git")
|
||||
|
||||
if version != "" {
|
||||
return body + ":" + version
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
// splitVersion separates a trailing ":version" suffix from a recipe path.
|
||||
// It only treats the final colon-separated segment as a version when it is
|
||||
// non-empty and contains no slash (a slash means the colon belonged to the
|
||||
// path, e.g. a host:port or scheme separator).
|
||||
func splitVersion(s string) (string, string) {
|
||||
idx := strings.LastIndex(s, ":")
|
||||
if idx < 0 {
|
||||
return s, ""
|
||||
}
|
||||
candidate := s[idx+1:]
|
||||
if candidate == "" || strings.Contains(candidate, "/") {
|
||||
return s, ""
|
||||
}
|
||||
return s[:idx], candidate
|
||||
}
|
||||
|
||||
func Get(name string) Recipe {
|
||||
name = NormalizeRecipeName(name)
|
||||
version := ""
|
||||
versionRaw := ""
|
||||
if strings.Contains(name, ":") {
|
||||
@@ -209,6 +287,16 @@ func (r Recipe) String() string {
|
||||
return out
|
||||
}
|
||||
|
||||
// ShortName returns the final path segment of the recipe name, i.e. the
|
||||
// bare recipe name without any "host/org/" prefix carried by externally
|
||||
// sourced (git URL) recipes. For catalogue recipes it returns Name unchanged.
|
||||
func (r Recipe) ShortName() string {
|
||||
if i := strings.LastIndex(r.Name, "/"); i >= 0 {
|
||||
return r.Name[i+1:]
|
||||
}
|
||||
return r.Name
|
||||
}
|
||||
|
||||
func escapeRecipeName(recipeName string) string {
|
||||
recipeName = strings.ReplaceAll(recipeName, "/", "_")
|
||||
recipeName = strings.ReplaceAll(recipeName, ".", "_")
|
||||
@@ -592,15 +680,7 @@ func ReadReposMetadata(debug bool) (RepoCatalogue, error) {
|
||||
// sortRecipeVersions sorts the recipe semver versions
|
||||
func sortRecipeVersions(versions RecipeVersions) {
|
||||
sort.Slice(versions, func(i, j int) bool {
|
||||
version1, err := tagcmp.Parse(getVersionString(versions[i]))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
version2, err := tagcmp.Parse(getVersionString(versions[j]))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return version1.IsLessThan(version2)
|
||||
return safeCompareVersions(getVersionString(versions[i]), getVersionString(versions[j]))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -616,18 +696,23 @@ func getVersionString(versionMap map[string]map[string]ServiceMeta) string {
|
||||
// sortVersionStrings sorts a list of semver version strings
|
||||
func sortVersionStrings(versions []string) {
|
||||
sort.Slice(versions, func(i, j int) bool {
|
||||
version1, err := tagcmp.Parse(versions[i])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
version2, err := tagcmp.Parse(versions[j])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return version1.IsLessThan(version2)
|
||||
return safeCompareVersions(versions[i], versions[j])
|
||||
})
|
||||
}
|
||||
|
||||
// Invalid versions are sorted as less than valid ones.
|
||||
func safeCompareVersions(v1, v2 string) bool {
|
||||
version1, err1 := tagcmp.Parse(v1)
|
||||
if err1 != nil {
|
||||
return true
|
||||
}
|
||||
version2, err2 := tagcmp.Parse(v2)
|
||||
if err2 != nil {
|
||||
return false
|
||||
}
|
||||
return version1.IsLessThan(version2)
|
||||
}
|
||||
|
||||
// GetRecipeCatalogueVersions list the recipe versions listed in the recipe catalogue.
|
||||
func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]string, error) {
|
||||
var versions []string
|
||||
|
||||
@@ -88,6 +88,88 @@ func TestGet(t *testing.T) {
|
||||
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "https://mygit.org/myorg/cool-recipe",
|
||||
recipe: Recipe{
|
||||
Name: "mygit.org/myorg/cool-recipe",
|
||||
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
|
||||
GitURL: "https://mygit.org/myorg/cool-recipe.git",
|
||||
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
|
||||
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"),
|
||||
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"),
|
||||
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"),
|
||||
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "https://mygit.org/myorg/cool-recipe.git",
|
||||
recipe: Recipe{
|
||||
Name: "mygit.org/myorg/cool-recipe",
|
||||
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
|
||||
GitURL: "https://mygit.org/myorg/cool-recipe.git",
|
||||
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
|
||||
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"),
|
||||
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"),
|
||||
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"),
|
||||
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "https://mygit.org/myorg/cool-recipe.git:1.2.4",
|
||||
recipe: Recipe{
|
||||
Name: "mygit.org/myorg/cool-recipe",
|
||||
EnvVersion: "1.2.4",
|
||||
EnvVersionRaw: "1.2.4",
|
||||
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
|
||||
GitURL: "https://mygit.org/myorg/cool-recipe.git",
|
||||
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
|
||||
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"),
|
||||
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"),
|
||||
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"),
|
||||
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ssh://git@mygit.org/myorg/cool-recipe.git",
|
||||
recipe: Recipe{
|
||||
Name: "mygit.org/myorg/cool-recipe",
|
||||
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
|
||||
GitURL: "https://mygit.org/myorg/cool-recipe.git",
|
||||
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
|
||||
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"),
|
||||
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"),
|
||||
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"),
|
||||
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "git@mygit.org:myorg/cool-recipe.git",
|
||||
recipe: Recipe{
|
||||
Name: "mygit.org/myorg/cool-recipe",
|
||||
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
|
||||
GitURL: "https://mygit.org/myorg/cool-recipe.git",
|
||||
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
|
||||
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"),
|
||||
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"),
|
||||
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"),
|
||||
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "git@mygit.org:myorg/cool-recipe:1.2.4",
|
||||
recipe: Recipe{
|
||||
Name: "mygit.org/myorg/cool-recipe",
|
||||
EnvVersion: "1.2.4",
|
||||
EnvVersionRaw: "1.2.4",
|
||||
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
|
||||
GitURL: "https://mygit.org/myorg/cool-recipe.git",
|
||||
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
|
||||
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"),
|
||||
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"),
|
||||
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"),
|
||||
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
@@ -101,6 +183,49 @@ func TestGet(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRecipeName(t *testing.T) {
|
||||
cases := []struct {
|
||||
in, want string
|
||||
}{
|
||||
// catalog short names pass through
|
||||
{"foo", "foo"},
|
||||
{"foo:1.2.3", "foo:1.2.3"},
|
||||
|
||||
// bare host/path form
|
||||
{"mygit.org/myorg/cool-recipe", "mygit.org/myorg/cool-recipe"},
|
||||
{"mygit.org/myorg/cool-recipe.git", "mygit.org/myorg/cool-recipe"},
|
||||
{"mygit.org/myorg/cool-recipe.git:1.2.3", "mygit.org/myorg/cool-recipe:1.2.3"},
|
||||
{"mygit.org/myorg/cool-recipe/", "mygit.org/myorg/cool-recipe"},
|
||||
|
||||
// https://
|
||||
{"https://mygit.org/myorg/cool-recipe", "mygit.org/myorg/cool-recipe"},
|
||||
{"https://mygit.org/myorg/cool-recipe.git", "mygit.org/myorg/cool-recipe"},
|
||||
{"https://mygit.org/myorg/cool-recipe.git:1.2.3", "mygit.org/myorg/cool-recipe:1.2.3"},
|
||||
{"http://mygit.org/myorg/cool-recipe", "mygit.org/myorg/cool-recipe"},
|
||||
|
||||
// ssh://, with and without port
|
||||
{"ssh://git@mygit.org/myorg/cool-recipe.git", "mygit.org/myorg/cool-recipe"},
|
||||
{"ssh://git@mygit.org:2222/myorg/cool-recipe.git", "mygit.org/myorg/cool-recipe"},
|
||||
|
||||
// SCP-style git@host:path
|
||||
{"git@mygit.org:myorg/cool-recipe", "mygit.org/myorg/cool-recipe"},
|
||||
{"git@mygit.org:myorg/cool-recipe.git", "mygit.org/myorg/cool-recipe"},
|
||||
{"git@mygit.org:myorg/cool-recipe:1.2.3", "mygit.org/myorg/cool-recipe:1.2.3"},
|
||||
|
||||
// whitespace
|
||||
{" https://mygit.org/myorg/cool-recipe ", "mygit.org/myorg/cool-recipe"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.in, func(t *testing.T) {
|
||||
got := NormalizeRecipeName(tc.in)
|
||||
if got != tc.want {
|
||||
t.Errorf("NormalizeRecipeName(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) {
|
||||
test.Setup()
|
||||
t.Cleanup(func() { test.Teardown() })
|
||||
@@ -123,3 +248,41 @@ func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) {
|
||||
assert.NotEqual(t, label, timeout)
|
||||
}
|
||||
}
|
||||
|
||||
// This is not a full test of comparison, as that occurs in the external
|
||||
// tagcmp package. This is a sunny-day integration test
|
||||
func TestSortRecipeVersionsSortsTwoValidStrings(t *testing.T) {
|
||||
versions := RecipeVersions{
|
||||
{"2.0.0": {}}, // Index 0
|
||||
{"1.0.0": {}}, // Index 1
|
||||
}
|
||||
|
||||
sortRecipeVersions(versions)
|
||||
|
||||
assert.Equal(t, "1.0.0", getVersionString(versions[0]), "Version 1.0.0 should be first")
|
||||
assert.Equal(t, "2.0.0", getVersionString(versions[1]), "Version 2.0.0 should be second")
|
||||
}
|
||||
|
||||
func TestSortRecipeVersionsSortsInvalidVersionStringToTheBottom(t *testing.T) {
|
||||
versions := RecipeVersions{
|
||||
{"2.0.0": {}}, // Index 0
|
||||
{"invalid": {}}, // Index 1
|
||||
}
|
||||
|
||||
sortRecipeVersions(versions)
|
||||
|
||||
assert.Equal(t, "invalid", getVersionString(versions[0]), "The invalid version should be first")
|
||||
assert.Equal(t, "2.0.0", getVersionString(versions[1]), "Version 2.0.0 should be second")
|
||||
}
|
||||
|
||||
func TestSortRecipeVersionsSortsInvalidVersionStringToTheBottomInReverse(t *testing.T) {
|
||||
versions := RecipeVersions{
|
||||
{"invalid": {}}, // Index 0
|
||||
{"2.0.0": {}}, // Index 1
|
||||
}
|
||||
|
||||
sortRecipeVersions(versions)
|
||||
|
||||
assert.Equal(t, "invalid", getVersionString(versions[0]), "The invalid version should be first")
|
||||
assert.Equal(t, "2.0.0", getVersionString(versions[1]), "Version 2.0.0 should be second")
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: nginx:1.31.6
|
||||
image: nginx:1.31.8
|
||||
secrets:
|
||||
- test_pass_one
|
||||
- test_pass_two
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
"git.coopcloud.tech/toolshed/godotenv"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -76,6 +79,126 @@ func Setup() {
|
||||
}
|
||||
}
|
||||
|
||||
// UpstreamRecipeDir is the "remote" repo used by SetupWithUpstream, kept
|
||||
// outside recipes/ so Teardown's blanket removal of $ABRA_DIR still cleans
|
||||
// it up.
|
||||
var UpstreamRecipeDir = os.ExpandEnv("$ABRA_DIR/_upstream/test_recipe")
|
||||
|
||||
// FeatureBranch is the non-default branch created by SetupWithUpstream.
|
||||
const FeatureBranch = "feature"
|
||||
|
||||
// SetupWithUpstream is like Setup, but the recipe under $ABRA_DIR/recipes
|
||||
// is a real clone of a separate "upstream" repo (UpstreamRecipeDir) rather
|
||||
// than a git-inited copy of the fixture. The upstream repo additionally has
|
||||
// a FeatureBranch with one extra commit not present on main, so tests can
|
||||
// exercise resolving/fetching a non-default branch as a recipe :version.
|
||||
func SetupWithUpstream() {
|
||||
Teardown()
|
||||
|
||||
if err := os.Mkdir(os.ExpandEnv("$ABRA_DIR"), 0764); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Mkdir(os.ExpandEnv("$ABRA_DIR/servers"), 0700); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Mkdir(os.ExpandEnv("$ABRA_DIR/recipes"), 0764); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
serverSrcDir := os.ExpandEnv("$PWD/../../tests/resources/test_server")
|
||||
serverDestDir := os.ExpandEnv("$ABRA_DIR/servers/test_server")
|
||||
if err := os.CopyFS(serverDestDir, os.DirFS(serverSrcDir)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(path.Dir(UpstreamRecipeDir), 0764); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
recipeSrcDir := os.ExpandEnv("$PWD/../../tests/resources/test_recipe")
|
||||
if err := os.CopyFS(UpstreamRecipeDir, os.DirFS(recipeSrcDir)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path.Join(UpstreamRecipeDir, ".env.sample"), []byte("TYPE=test_recipe\n"), 0o644); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := gitPkg.Init(UpstreamRecipeDir, true, "tester", "helo@coopcloud.tech"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := CreateBranchWithCommit(UpstreamRecipeDir, FeatureBranch, "feature.txt", "feature work"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
recipeDestDir := os.ExpandEnv("$ABRA_DIR/recipes/test_recipe")
|
||||
if err := gitPkg.Clone(recipeDestDir, UpstreamRecipeDir); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateBranchWithCommit checks out branchName in repoPath (creating it off
|
||||
// the current HEAD if it doesn't exist yet, or extending it if it does),
|
||||
// writes fileName with contents, commits it, then returns to the repo's
|
||||
// default branch (leaving the new commit reachable only via the branch's
|
||||
// ref, matching how a contributor would push a feature branch upstream
|
||||
// without switching the repo's HEAD away from main).
|
||||
func CreateBranchWithCommit(repoPath, branchName, fileName, contents string) error {
|
||||
repo, err := git.PlainOpen(repoPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
head, err := repo.Head()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
branchRef := plumbing.NewBranchReferenceName(branchName)
|
||||
create := true
|
||||
if _, err := repo.Reference(branchRef, true); err == nil {
|
||||
create = false
|
||||
}
|
||||
|
||||
checkoutOpts := &git.CheckoutOptions{Branch: branchRef, Create: create}
|
||||
if create {
|
||||
checkoutOpts.Hash = head.Hash()
|
||||
}
|
||||
if err := worktree.Checkout(checkoutOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path.Join(repoPath, fileName), []byte(contents), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := worktree.Add(fileName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := worktree.Commit(fileName, &git.CommitOptions{
|
||||
Author: &object.Signature{Name: "tester", Email: "helo@coopcloud.tech"},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return worktree.Checkout(&git.CheckoutOptions{Branch: head.Name()})
|
||||
}
|
||||
|
||||
func AddEnv(envKey, envValue string) error {
|
||||
filePath := os.ExpandEnv(fmt.Sprintf("$ABRA_DIR/servers/%s/%s.env", ServerName, AppName))
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: nginx:1.31.6
|
||||
image: nginx:1.31.8
|
||||
networks:
|
||||
- proxy
|
||||
deploy:
|
||||
|
||||
+4
@@ -329,6 +329,10 @@ 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() {
|
||||
|
||||
+25
-3
@@ -139,8 +139,12 @@ 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); err != nil {
|
||||
if err := dw.fixAndBreakChainsOne(m, otp, visiting); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -148,7 +152,11 @@ func (dw *deltaSelector) fixAndBreakChains(objectsToPack []*ObjectToPack) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dw *deltaSelector) fixAndBreakChainsOne(objectsToPack map[plumbing.Hash]*ObjectToPack, otp *ObjectToPack) error {
|
||||
func (dw *deltaSelector) fixAndBreakChainsOne(
|
||||
objectsToPack map[plumbing.Hash]*ObjectToPack,
|
||||
otp *ObjectToPack,
|
||||
visiting map[plumbing.Hash]bool,
|
||||
) error {
|
||||
if !otp.Object.Type().IsDelta() {
|
||||
return nil
|
||||
}
|
||||
@@ -174,7 +182,21 @@ func (dw *deltaSelector) fixAndBreakChainsOne(objectsToPack map[plumbing.Hash]*O
|
||||
return dw.undeltify(otp)
|
||||
}
|
||||
|
||||
if err := dw.fixAndBreakChainsOne(objectsToPack, base); err != nil {
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+34
-2
@@ -474,6 +474,14 @@ 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
|
||||
}
|
||||
@@ -496,10 +504,32 @@ 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.
|
||||
@@ -536,8 +566,10 @@ func (w *TreeWalker) Next() (name string, entry TreeEntry, err error) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := pathutil.ValidTreePath(entry.Name); err != nil {
|
||||
return name, entry, err
|
||||
if !w.skipPathValidation {
|
||||
if err := pathutil.ValidTreePath(entry.Name); err != nil {
|
||||
return name, entry, err
|
||||
}
|
||||
}
|
||||
|
||||
if entry.Mode == filemode.Dir {
|
||||
|
||||
+6
@@ -106,6 +106,12 @@ 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
@@ -187,6 +187,13 @@ 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()
|
||||
|
||||
+379
-22
@@ -6,8 +6,12 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
@@ -73,7 +77,7 @@ func advertisedReferences(ctx context.Context, s *session, serviceName string) (
|
||||
s.endpoint.String(), infoRefsPath, serviceName,
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
req, err := newRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -177,6 +181,24 @@ 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{
|
||||
@@ -370,11 +392,20 @@ 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)
|
||||
}
|
||||
if r.URL.Scheme != "http" && r.URL.Scheme != "https" {
|
||||
// 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" {
|
||||
return fmt.Errorf("http redirect: unsupported scheme %q", r.URL.Scheme)
|
||||
}
|
||||
if r.URL.Scheme != s.endpoint.Protocol &&
|
||||
!(s.endpoint.Protocol == "http" && r.URL.Scheme == "https") {
|
||||
// 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) {
|
||||
return fmt.Errorf("http redirect: changes scheme from %q to %q", s.endpoint.Protocol, r.URL.Scheme)
|
||||
}
|
||||
|
||||
@@ -384,7 +415,20 @@ func (s *session) ModifyEndpointIfRedirect(res *http.Response) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if host != s.endpoint.Host || effectivePort(r.URL.Scheme, port) != effectivePort(s.endpoint.Protocol, s.endpoint.Port) {
|
||||
// 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) {
|
||||
s.endpoint.User = ""
|
||||
s.endpoint.Password = ""
|
||||
s.auth = nil
|
||||
@@ -393,7 +437,7 @@ func (s *session) ModifyEndpointIfRedirect(res *http.Response) error {
|
||||
s.endpoint.Host = host
|
||||
s.endpoint.Port = port
|
||||
|
||||
s.endpoint.Protocol = r.URL.Scheme
|
||||
s.endpoint.Protocol = scheme
|
||||
s.endpoint.Path = r.URL.Path[:len(r.URL.Path)-len(infoRefsPath)]
|
||||
return nil
|
||||
}
|
||||
@@ -419,19 +463,132 @@ func endpointPort(port string) (int, error) {
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func effectivePort(scheme string, port int) int {
|
||||
if port != 0 {
|
||||
return port
|
||||
}
|
||||
// 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")
|
||||
}
|
||||
|
||||
switch strings.ToLower(scheme) {
|
||||
case "http":
|
||||
return 80
|
||||
case "https":
|
||||
return 443
|
||||
default:
|
||||
return 0
|
||||
// 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()
|
||||
}
|
||||
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 {
|
||||
@@ -448,26 +605,215 @@ 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 {
|
||||
return next(req, via)
|
||||
if err := next(req, via); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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", req.URL)
|
||||
return fmt.Errorf("http redirect: redirects disabled to %s", redactedURL(req.URL))
|
||||
case "", FollowInitialRedirects:
|
||||
if !isInitialRequest(req) {
|
||||
return fmt.Errorf("http redirect: redirect on non-initial request to %s", req.URL)
|
||||
return fmt.Errorf("http redirect: redirect on non-initial request to %s", redactedURL(req.URL))
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("http redirect: invalid redirect policy %q", policy)
|
||||
}
|
||||
if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
|
||||
// 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") {
|
||||
return fmt.Errorf("http redirect: unsupported scheme %q", req.URL.Scheme)
|
||||
}
|
||||
if len(via) >= 10 {
|
||||
@@ -481,6 +827,17 @@ 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)
|
||||
@@ -598,6 +955,6 @@ func (e *Err) StatusCode() int {
|
||||
|
||||
func (e *Err) Error() string {
|
||||
return fmt.Sprintf("unexpected requesting %q status code: %d",
|
||||
e.Response.Request.URL, e.Response.StatusCode,
|
||||
redactedURL(e.Response.Request.URL), e.Response.StatusCode,
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ func (s *rpSession) doRequest(
|
||||
body = content
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
req, err := newRequest(method, url, body)
|
||||
if err != nil {
|
||||
return nil, plumbing.NewPermanentError(err)
|
||||
}
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ func (s *upSession) doRequest(
|
||||
body = content
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
req, err := newRequest(method, url, body)
|
||||
if err != nil {
|
||||
return nil, plumbing.NewPermanentError(err)
|
||||
}
|
||||
|
||||
+1
@@ -433,6 +433,7 @@ 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 {
|
||||
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
# 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
@@ -1,57 +0,0 @@
|
||||
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
@@ -1,35 +0,0 @@
|
||||
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
@@ -1,22 +0,0 @@
|
||||
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
@@ -1,512 +0,0 @@
|
||||
# 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
|
||||
|
||||
[](https://pkg.go.dev/github.com/klauspost/cpuid/v2)
|
||||
[](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
File diff suppressed because it is too large
Load Diff
-47
@@ -1,47 +0,0 @@
|
||||
// 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
@@ -1,72 +0,0 @@
|
||||
// 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
@@ -1,36 +0,0 @@
|
||||
// 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
@@ -1,250 +0,0 @@
|
||||
// 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
@@ -1,17 +0,0 @@
|
||||
// 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
@@ -1,45 +0,0 @@
|
||||
// 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
@@ -1,308 +0,0 @@
|
||||
// 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
@@ -1,129 +0,0 @@
|
||||
// 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
@@ -1,208 +0,0 @@
|
||||
// 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
@@ -1,16 +0,0 @@
|
||||
// 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
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) 2021 Klaus Post, released under MIT License. See LICENSE file.
|
||||
|
||||
//go:build nounsafe
|
||||
// +build nounsafe
|
||||
|
||||
package cpuid
|
||||
|
||||
var hwcap uint
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
// 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
@@ -1,15 +0,0 @@
|
||||
#!/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
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
# AGENTS.md - gotext Project Guidelines
|
||||
|
||||
This document is the **source of truth** for the `gotext` project's architecture, conventions, and development workflows. All contributors — human or AI agent — must adhere to these guidelines.
|
||||
|
||||
Agent-specific configuration files (e.g., `GEMINI.md`, `CLAUDE.md`) should reference this document rather than duplicating its content.
|
||||
|
||||
## Project Overview
|
||||
|
||||
`gotext` is a GNU Gettext implementation for Go (Golang). It provides a thread-safe, hierarchical translation system that supports PO/MO files, plural forms, and context-aware translations.
|
||||
|
||||
## Core Architecture
|
||||
|
||||
The project follows a hierarchical structure for managing translations:
|
||||
|
||||
1. **Global Package API (`gotext.go`)**: Provides convenient, global functions (e.g., `Get`, `GetD`, `GetN`) that rely on a shared `globalConfig`.
|
||||
2. **Locales (`locale.go`)**: Represent a specific language (e.g., "en_US", "es_ES"). A `Locale` container manages multiple `Domains`.
|
||||
3. **Domains (`domain.go`)**: The core translation engine for a specific set of messages. It handles:
|
||||
- Message retrieval (with support for plural forms and `msgctxt`).
|
||||
- Header parsing (including plural-form rules).
|
||||
- Thread-safety via `sync.RWMutex`.
|
||||
4. **Translators (`translator.go`)**: An interface for translation sources.
|
||||
- **PO (`po.go`)**: Parsers for human-readable Gettext Portable Object files.
|
||||
- **MO (`mo.go`)**: Parsers for binary Gettext Machine Object files.
|
||||
5. **Plural Forms (`plurals/`)**: A dedicated package for compiling and evaluating GNU Gettext plural form expressions.
|
||||
|
||||
## CLI Tools
|
||||
|
||||
- **xgotext (`cli/xgotext/`)**: A tool to scan Go source files, extract translatable strings (matching `Get`, `GetD`, etc.), and generate/update PO files.
|
||||
|
||||
## Key Files & Symbols
|
||||
|
||||
- `gotext.go`: `Configure`, `Get`, `GetD`, `GetN`.
|
||||
- `locale.go`: `Locale`, `NewLocale`, `AddDomain`.
|
||||
- `domain.go`: `Domain`, `Get`, `GetN`, `parseHeaders`.
|
||||
- `translator.go`: `Translator` interface.
|
||||
- `plurals/compiler.go`: `Compile` function for plural expressions.
|
||||
|
||||
## Development Workflows
|
||||
|
||||
### Branching & PRs
|
||||
|
||||
- **Feature Branches**: All development MUST happen in dedicated feature branches (e.g., `feature/description-of-change`, `fix/bug-description`, `refactor/topic`).
|
||||
- **Target Branch**: All Pull Requests MUST be made against the `master` branch.
|
||||
- **Review Requirement**: No direct commits to `master` are allowed. All changes must be reviewed via a PR.
|
||||
- **GitHub Integration**: If the `gh` CLI tool is installed and authenticated, it should be used for managing Pull Requests, Issues, and Reviews. Otherwise, these tasks must be performed manually through the GitHub web interface.
|
||||
|
||||
### Coding Standards
|
||||
|
||||
- **Idiomatic Go**: Follow standard Go conventions (`gofmt`, `go vet`).
|
||||
- **Thread Safety**: Ensure all shared state (like in `Domain`) is protected by appropriate synchronization primitives.
|
||||
- **Testing**:
|
||||
- Every new feature or bug fix must include corresponding tests.
|
||||
- Use the `fixtures/` directory for sample PO/MO files.
|
||||
- Run `go test ./...` to ensure no regressions.
|
||||
|
||||
### Pre-Submit Checklist
|
||||
|
||||
Before submitting a Pull Request, verify:
|
||||
|
||||
1. All existing tests pass: `go test ./...`
|
||||
2. Code passes static analysis: `go vet ./...`
|
||||
3. Code is properly formatted: `gofmt`
|
||||
4. New features or bug fixes include corresponding tests.
|
||||
5. Commit messages are clear and descriptive.
|
||||
+2
-45
@@ -1,46 +1,3 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
# CODE OF CONDUCT
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers 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, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at leonel.quinteros@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
The canonical text of this project's Code of Conduct lives at [docs/CODE_OF_CONDUCT.md](docs/CODE_OF_CONDUCT.md).
|
||||
+4
-14
@@ -1,19 +1,9 @@
|
||||
# CONTRIBUTING
|
||||
|
||||
This open source project welcomes everybody that wants to contribute to it by implementing new features, fixing bugs, testing, creating documentation or simply talk about it.
|
||||
Thank you for considering contributing to `gotext`! All kinds of contributions — features, bug fixes, tests, documentation, or discussions — are welcome.
|
||||
|
||||
Most contributions will start by creating a new Issue to discuss what is the contribution about and to agree on the steps to move forward.
|
||||
The canonical contribution guidelines live at [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md).
|
||||
|
||||
## Issues
|
||||
|
||||
All issues reports are welcome. Open a new Issue whenever you want to report a bug, request a change or make a proposal.
|
||||
|
||||
This should be your start point of contribution.
|
||||
|
||||
|
||||
## Pull Requests
|
||||
|
||||
If you have any changes that can be merged, feel free to send a Pull Request.
|
||||
|
||||
Usually, you'd want to create a new Issue to discuss about the change you want to merge and why it's needed or what it solves.
|
||||
Before contributing, please also read [AGENTS.md](AGENTS.md): it is the source of truth for project architecture, conventions, and development workflows (branching, PR process, coding standards, and the pre-submit checklist).
|
||||
|
||||
Start point: open an [Issue](https://github.com/leonelquinteros/gotext/issues) to discuss your idea, then send a Pull Request against `master`.
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
# GEMINI.md - Gemini CLI Instructions for gotext
|
||||
|
||||
> **Important**: Read and follow [`AGENTS.md`](./AGENTS.md) first. It is the source of truth for project architecture, conventions, and development workflows. The instructions below are specific to Gemini CLI and supplement — not replace — those guidelines.
|
||||
|
||||
## Gemini-Specific Instructions
|
||||
|
||||
- Always work in a feature branch. Never commit directly to `master`.
|
||||
- Before submitting, verify changes with existing and new tests (`go test ./...`).
|
||||
- Propose a PR to `master` after confirmation, using the `gh` CLI when available.
|
||||
+101
-208
@@ -9,258 +9,151 @@
|
||||
|
||||
[GNU gettext utilities](https://www.gnu.org/software/gettext) for Go.
|
||||
|
||||
`gotext` is a native Go implementation of the GNU Gettext utilities. It provides a thread-safe, flexible, and powerful way to handle internationalization (i18n) and localization (l10n) in your Go applications.
|
||||
|
||||
# Features
|
||||
---
|
||||
|
||||
- Implements GNU gettext support in native Go.
|
||||
- Complete support for [PO files](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html) including:
|
||||
- Support for multiline strings and headers.
|
||||
- Support for variables inside translation strings using Go's [fmt syntax](https://golang.org/pkg/fmt/).
|
||||
- Support for [pluralization rules](https://www.gnu.org/software/gettext/manual/html_node/Translating-plural-forms.html).
|
||||
- Support for [message contexts](https://www.gnu.org/software/gettext/manual/html_node/Contexts.html).
|
||||
- Support for MO files.
|
||||
- Thread-safe: This package is safe for concurrent use across multiple goroutines.
|
||||
- It works with UTF-8 encoding as it's the default for Go language.
|
||||
- Unit tests available.
|
||||
- Language codes are automatically simplified from the form `en_UK` to `en` if the first isn't available.
|
||||
- Ready to use inside Go templates.
|
||||
- Objects are serializable to []byte to store them in cache.
|
||||
- Support for Go Modules.
|
||||
**📖 [Read the Full Documentation](https://leonelquinteros.github.io/gotext/)**
|
||||
|
||||
---
|
||||
|
||||
# License
|
||||
### Table of Contents
|
||||
- [Features](#features)
|
||||
- [Installation](#installation)
|
||||
- [Getting Started](#getting-started)
|
||||
- [CLI Tool (xgotext)](#cli-tool-xgotext)
|
||||
- [Advanced Usage](#advanced-usage)
|
||||
- [Locale Object](#using-locale-object)
|
||||
- [Plural Forms](#use-plural-forms-of-translations)
|
||||
- [Dynamic Variables](#using-dynamic-variables-on-translations)
|
||||
- [Directory Structure](#locales-directories-structure)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
[MIT license](LICENSE)
|
||||
---
|
||||
|
||||
## Features
|
||||
- Native Go implementation of Gettext (the core library has no external dependencies).
|
||||
- Full support for **PO and MO files**.
|
||||
- **Pluralization rules** support via GNU Gettext plural-form expressions.
|
||||
- **Message context** support (`msgctxt`).
|
||||
- Thread-safe for concurrent use.
|
||||
- Works with UTF-8 by default.
|
||||
- Integrated CLI tool (`xgotext`) for string extraction.
|
||||
- Serializable objects for caching.
|
||||
- Plain-string results that plug directly into Go's `text/template` and `html/template` pipelines.
|
||||
|
||||
# Documentation
|
||||
---
|
||||
|
||||
Refer to package documentation at (https://pkg.go.dev/github.com/leonelquinteros/gotext)
|
||||
|
||||
|
||||
# Installation
|
||||
|
||||
```
|
||||
## Installation
|
||||
```bash
|
||||
go get github.com/leonelquinteros/gotext
|
||||
```
|
||||
|
||||
- There are no requirements or dependencies to use this package.
|
||||
- No need to install GNU gettext utilities (unless specific needs of CLI tools).
|
||||
- No need for environment variables. Some naming conventions are applied but not needed.
|
||||
---
|
||||
|
||||
|
||||
# Usage examples
|
||||
|
||||
## Using package for single language/domain settings
|
||||
|
||||
For quick/simple translations you can use the package level functions directly.
|
||||
## Getting Started
|
||||
For a quick start, use the package-level API:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Configure package
|
||||
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
|
||||
// Configure package: locales path, language, and domain
|
||||
gotext.Configure("/path/to/locales", "en_US", "default")
|
||||
|
||||
// Translate text from default domain
|
||||
fmt.Println(gotext.Get("My text on 'domain-name' domain"))
|
||||
// Simple translation
|
||||
fmt.Println(gotext.Get("Hello, world!"))
|
||||
|
||||
// Translate text from a different domain without reconfigure
|
||||
fmt.Println(gotext.GetD("domain2", "Another text on a different domain"))
|
||||
// Translation with variables
|
||||
fmt.Println(gotext.Get("Hello, %s!", "Gopher"))
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Using dynamic variables on translations
|
||||
For more details, see the [Getting Started Guide](docs/GETTING_STARTED.md).
|
||||
|
||||
All translation strings support dynamic variables to be inserted without translate.
|
||||
Use the fmt.Printf syntax (from Go's "fmt" package) to specify how to print the non-translated variable inside the translation string.
|
||||
---
|
||||
|
||||
## CLI Tool (xgotext)
|
||||
`gotext` includes a command-line tool to extract translatable strings from your Go source code.
|
||||
|
||||
**Install xgotext:**
|
||||
```bash
|
||||
go install github.com/leonelquinteros/gotext/cli/xgotext@latest
|
||||
```
|
||||
|
||||
**Extract strings:**
|
||||
```bash
|
||||
xgotext -in . -out locales/templates
|
||||
```
|
||||
The command generates `.pot` templates under `locales/templates`; merge those templates into language-specific `.po` files.
|
||||
|
||||
See the [xgotext Documentation](docs/xgotext.md) for full usage details.
|
||||
|
||||
---
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Using Locale object
|
||||
For managing multiple languages or domains independently:
|
||||
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Configure package
|
||||
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
|
||||
|
||||
// Set variables
|
||||
name := "John"
|
||||
|
||||
// Translate text with variables
|
||||
fmt.Println(gotext.Get("Hi, my name is %s", name))
|
||||
}
|
||||
|
||||
l := gotext.NewLocale("/path/to/locales", "es_UY")
|
||||
l.AddDomain("default")
|
||||
fmt.Println(l.Get("Translate this"))
|
||||
```
|
||||
|
||||
|
||||
## Using Locale object
|
||||
|
||||
When having multiple languages/domains/libraries at the same time, you can create Locale objects for each variation
|
||||
so you can handle each settings on their own.
|
||||
### Use plural forms of translations
|
||||
`gotext` handles complex pluralization rules defined in PO headers:
|
||||
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create Locale with library path and language code
|
||||
l := gotext.NewLocale("/path/to/locales/root/dir", "es_UY")
|
||||
|
||||
// Load domain '/path/to/locales/root/dir/es_UY/default.po'
|
||||
l.AddDomain("default")
|
||||
|
||||
// Translate text from default domain
|
||||
fmt.Println(l.Get("Translate this"))
|
||||
|
||||
// Load different domain
|
||||
l.AddDomain("translations")
|
||||
|
||||
// Translate text from domain
|
||||
fmt.Println(l.GetD("translations", "Translate this"))
|
||||
}
|
||||
// GetN(singular, plural, quantity, args...)
|
||||
fmt.Println(gotext.GetN("I have one apple.", "I have %d apples.", 5, 5))
|
||||
```
|
||||
See the [Plural Forms Guide](docs/PLURALS.md) for more examples.
|
||||
|
||||
This is also helpful for using inside templates (from the "text/template" package), where you can pass the Locale object to the template.
|
||||
If you set the Locale object as "Loc" in the template, then the template code would look like:
|
||||
|
||||
```
|
||||
{{ .Loc.Get "Translate this" }}
|
||||
```
|
||||
|
||||
|
||||
## Using the Po object to handle .po files and PO-formatted strings
|
||||
|
||||
For when you need to work with PO files and strings,
|
||||
you can directly use the Po object to parse it and access the translations in there in the same way.
|
||||
|
||||
### Using dynamic variables on translations
|
||||
Supports standard `fmt` package syntax:
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Set PO content
|
||||
str := `
|
||||
msgid "Translate this"
|
||||
msgstr "Translated text"
|
||||
|
||||
msgid "Another string"
|
||||
msgstr ""
|
||||
|
||||
msgid "One with var: %s"
|
||||
msgstr "This one sets the var: %s"
|
||||
`
|
||||
|
||||
// Create Po object
|
||||
po := gotext.NewPo()
|
||||
po.Parse(str)
|
||||
|
||||
fmt.Println(po.Get("Translate this"))
|
||||
}
|
||||
name := "John"
|
||||
fmt.Println(gotext.Get("Hi, my name is %s", name))
|
||||
```
|
||||
|
||||
### More advanced scenarios
|
||||
- **Embedded translation files** (`embed.FS`) and named `%(name)s` formatting: see the [Advanced Usage Guide](docs/ADVANCED.md).
|
||||
- **Languages fallback**: pass multiple languages to `Configure` as a colon-separated list (`gotext.Configure("/path/to/locales", "es_UY:es:en_US", "default")`).
|
||||
- **Serialization/caching** of parsed locales: `Locale.MarshalBinary` / `UnmarshalBinary`.
|
||||
|
||||
## Use plural forms of translations
|
||||
|
||||
PO format supports defining one or more plural forms for the same translation.
|
||||
Relying on the PO file headers, a Plural-Forms formula can be set on the translation file
|
||||
as defined in (https://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/Plural-forms.html)
|
||||
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Set PO content
|
||||
str := `
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
||||
# Header below
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
msgid "Translate this"
|
||||
msgstr "Translated text"
|
||||
|
||||
msgid "Another string"
|
||||
msgstr ""
|
||||
|
||||
msgid "One with var: %s"
|
||||
msgid_plural "Several with vars: %s"
|
||||
msgstr[0] "This one is the singular: %s"
|
||||
msgstr[1] "This one is the plural: %s"
|
||||
`
|
||||
|
||||
// Create Po object
|
||||
po := new(gotext.Po)
|
||||
po.Parse(str)
|
||||
|
||||
fmt.Println(po.GetN("One with var: %s", "Several with vars: %s", 54, v))
|
||||
// "This one is the plural: Variable"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# Locales directories structure
|
||||
|
||||
The package will assume a directories structure starting with a base path that will be provided to the package configuration
|
||||
or to object constructors depending on the use, but either will use the same convention to lookup inside the base path.
|
||||
|
||||
Inside the base directory where will be the language directories named using the language and country 2-letter codes (en_US, es_AR, ...).
|
||||
All package functions can lookup after the simplified version for each language in case the full code isn't present but the more general language code exists.
|
||||
So if the language set is `en_UK`, but there is no directory named after that code and there is a directory named `en`,
|
||||
all package functions will be able to resolve this generalization and provide translations for the more general library.
|
||||
|
||||
The language codes are assumed to be [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) codes (2-letter codes).
|
||||
That said, most functions will work with any coding standard as long the directory name matches the language code set on the configuration.
|
||||
|
||||
Then, there can be a `LC_MESSAGES` containing all PO files or the PO files themselves.
|
||||
A library directory structure can look like:
|
||||
---
|
||||
|
||||
## Locales directories structure
|
||||
The package expects a standard Gettext directory structure:
|
||||
```
|
||||
/path/to/locales
|
||||
/path/to/locales/en_US
|
||||
/path/to/locales/en_US/LC_MESSAGES
|
||||
/path/to/locales/en_US/LC_MESSAGES/default.po
|
||||
/path/to/locales/en_US/LC_MESSAGES/extras.po
|
||||
/path/to/locales/en_UK
|
||||
/path/to/locales/en_UK/LC_MESSAGES
|
||||
/path/to/locales/en_UK/LC_MESSAGES/default.po
|
||||
/path/to/locales/en_UK/LC_MESSAGES/extras.po
|
||||
/path/to/locales/en_AU
|
||||
/path/to/locales/en_AU/LC_MESSAGES
|
||||
/path/to/locales/en_AU/LC_MESSAGES/default.po
|
||||
/path/to/locales/en_AU/LC_MESSAGES/extras.po
|
||||
/path/to/locales/es
|
||||
/path/to/locales/es/default.po
|
||||
/path/to/locales/es/extras.po
|
||||
/path/to/locales/es_ES
|
||||
/path/to/locales/es_ES/default.po
|
||||
/path/to/locales/es_ES/extras.po
|
||||
/path/to/locales/fr
|
||||
/path/to/locales/fr/default.po
|
||||
/path/to/locales/fr/extras.po
|
||||
/en_US
|
||||
/LC_MESSAGES
|
||||
default.po
|
||||
/es_ES
|
||||
/LC_MESSAGES
|
||||
default.po
|
||||
```
|
||||
Translation files can live either inside an `LC_MESSAGES` subdirectory or directly under the language code (`/es_ES/default.po`); both layouts are supported.
|
||||
|
||||
And so on...
|
||||
It supports automatic language simplification (e.g., falling back from `en_UK` to `en`).
|
||||
|
||||
---
|
||||
|
||||
# Contribute
|
||||
## Contributing
|
||||
We welcome contributions of all kinds!
|
||||
- [Contributing Guidelines](CONTRIBUTING.md)
|
||||
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||
|
||||
- Please, contribute.
|
||||
- Use the package on your projects.
|
||||
- Report issues on Github.
|
||||
- Send pull requests for bugfixes and improvements.
|
||||
- Send proposals on Github issues.
|
||||
---
|
||||
|
||||
## License
|
||||
`gotext` is released under the [MIT License](LICENSE).
|
||||
|
||||
+317
-158
@@ -4,7 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"maps"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -64,9 +64,6 @@ func (m HeaderMap) Del(key string) {
|
||||
|
||||
// Get value for key from HeaderMap
|
||||
func (m HeaderMap) Get(key string) string {
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
v := m[key]
|
||||
if len(v) == 0 {
|
||||
return ""
|
||||
@@ -81,9 +78,6 @@ func (m HeaderMap) Set(key, value string) {
|
||||
|
||||
// Values returns all values for a given key from HeaderMap
|
||||
func (m HeaderMap) Values(key string) []string {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return m[key]
|
||||
}
|
||||
|
||||
@@ -100,29 +94,111 @@ func NewDomain() *Domain {
|
||||
return domain
|
||||
}
|
||||
|
||||
func cloneRefs(refs []string) []string {
|
||||
if refs == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
owned := make([]string, len(refs))
|
||||
copy(owned, refs)
|
||||
return owned
|
||||
}
|
||||
|
||||
func cloneHeaderMap(headers HeaderMap) HeaderMap {
|
||||
if headers == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
owned := make(HeaderMap, len(headers))
|
||||
for key, values := range headers {
|
||||
owned[key] = cloneRefs(values)
|
||||
}
|
||||
return owned
|
||||
}
|
||||
|
||||
func cloneTranslation(trans *Translation) *Translation {
|
||||
if trans == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
owned := NewTranslation()
|
||||
owned.ID = trans.ID
|
||||
owned.PluralID = trans.PluralID
|
||||
owned.dirty = trans.dirty
|
||||
owned.Refs = cloneRefs(trans.Refs)
|
||||
|
||||
maps.Copy(owned.Trs, trans.Trs)
|
||||
|
||||
return owned
|
||||
}
|
||||
|
||||
func cloneTranslations(translations map[string]*Translation) map[string]*Translation {
|
||||
if translations == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
owned := make(map[string]*Translation, len(translations))
|
||||
for id, trans := range translations {
|
||||
owned[id] = cloneTranslation(trans)
|
||||
}
|
||||
return owned
|
||||
}
|
||||
|
||||
func cloneContextTranslations(contexts map[string]map[string]*Translation) map[string]map[string]*Translation {
|
||||
if contexts == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
owned := make(map[string]map[string]*Translation, len(contexts))
|
||||
for context, translations := range contexts {
|
||||
owned[context] = cloneTranslations(translations)
|
||||
}
|
||||
return owned
|
||||
}
|
||||
|
||||
func (do *Domain) hasTranslation(id string) bool {
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
return do.translations[id] != nil
|
||||
}
|
||||
|
||||
func (do *Domain) hasContextTranslation(id, context string) bool {
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
return do.contextTranslations[context][id] != nil
|
||||
}
|
||||
|
||||
// SetPluralResolver sets a custom plural resolver function
|
||||
func (do *Domain) SetPluralResolver(f func(int) int) {
|
||||
do.pluralMutex.Lock()
|
||||
defer do.pluralMutex.Unlock()
|
||||
|
||||
do.customPluralResolver = f
|
||||
}
|
||||
|
||||
func (do *Domain) pluralForm(n int) int {
|
||||
// do we really need locking here? not sure how this plurals.Expression works, so sticking with it for now
|
||||
// Snapshot plural state before calling an expression or custom resolver.
|
||||
do.pluralMutex.RLock()
|
||||
defer do.pluralMutex.RUnlock()
|
||||
pluralforms := do.pluralforms
|
||||
customPluralResolver := do.customPluralResolver
|
||||
do.pluralMutex.RUnlock()
|
||||
|
||||
if pluralforms != nil {
|
||||
return pluralforms.Eval(uint32(n))
|
||||
}
|
||||
|
||||
// Failure fallback
|
||||
if do.pluralforms == nil {
|
||||
if do.customPluralResolver != nil {
|
||||
return do.customPluralResolver(n)
|
||||
}
|
||||
|
||||
/* Use the Germanic plural rule. */
|
||||
if n == 1 {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
if customPluralResolver != nil {
|
||||
return customPluralResolver(n)
|
||||
}
|
||||
return do.pluralforms.Eval(uint32(n))
|
||||
|
||||
/* Use the Germanic plural rule. */
|
||||
if n == 1 {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// parseHeaders retrieves data from previously parsed headers. it's called by both Mo and Po when parsing
|
||||
@@ -136,18 +212,16 @@ func (do *Domain) parseHeaders() {
|
||||
languageKey := "Language"
|
||||
pluralFormsKey := "Plural-Forms"
|
||||
|
||||
rawLines := strings.Split(raw, "\n")
|
||||
for _, line := range rawLines {
|
||||
for line := range strings.SplitSeq(raw, "\n") {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
colonIdx := strings.Index(line, ":")
|
||||
if colonIdx < 0 {
|
||||
key, value, ok := strings.Cut(line, ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
key := line[:colonIdx]
|
||||
lowerKey := strings.ToLower(key)
|
||||
if lowerKey == strings.ToLower(languageKey) {
|
||||
languageKey = key
|
||||
@@ -155,7 +229,7 @@ func (do *Domain) parseHeaders() {
|
||||
pluralFormsKey = key
|
||||
}
|
||||
|
||||
value := strings.TrimSpace(line[colonIdx+1:])
|
||||
value = strings.TrimSpace(value)
|
||||
do.Headers.Add(key, value)
|
||||
}
|
||||
|
||||
@@ -168,22 +242,18 @@ func (do *Domain) parseHeaders() {
|
||||
return
|
||||
}
|
||||
|
||||
// Split plural form header value
|
||||
pfs := strings.Split(do.PluralForms, ";")
|
||||
|
||||
// Parse values
|
||||
for _, i := range pfs {
|
||||
vs := strings.SplitN(i, "=", 2)
|
||||
if len(vs) != 2 {
|
||||
for i := range strings.SplitSeq(do.PluralForms, ";") {
|
||||
key, value, ok := strings.Cut(i, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch strings.TrimSpace(vs[0]) {
|
||||
switch strings.TrimSpace(key) {
|
||||
case "nplurals":
|
||||
do.nplurals, _ = strconv.Atoi(vs[1])
|
||||
do.nplurals, _ = strconv.Atoi(value)
|
||||
|
||||
case "plural":
|
||||
do.plural = vs[1]
|
||||
do.plural = value
|
||||
|
||||
if expr, err := plurals.Compile(do.plural); err == nil {
|
||||
do.pluralforms = expr
|
||||
@@ -221,17 +291,19 @@ func (do *Domain) DropStaleTranslations() {
|
||||
|
||||
// SetRefs set source references for a given translation
|
||||
func (do *Domain) SetRefs(str string, refs []string) {
|
||||
ownedRefs := cloneRefs(refs)
|
||||
|
||||
do.trMutex.Lock()
|
||||
do.pluralMutex.Lock()
|
||||
defer do.trMutex.Unlock()
|
||||
defer do.pluralMutex.Unlock()
|
||||
|
||||
if trans, ok := do.translations[str]; ok {
|
||||
trans.Refs = refs
|
||||
if trans, ok := do.translations[str]; ok && trans != nil {
|
||||
trans.SetRefs(ownedRefs)
|
||||
} else {
|
||||
trans = NewTranslation()
|
||||
trans := NewTranslation()
|
||||
trans.ID = str
|
||||
trans.SetRefs(refs)
|
||||
trans.SetRefs(ownedRefs)
|
||||
do.translations[str] = trans
|
||||
}
|
||||
}
|
||||
@@ -243,8 +315,8 @@ func (do *Domain) GetRefs(str string) []string {
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
if do.translations != nil {
|
||||
if trans, ok := do.translations[str]; ok {
|
||||
return trans.Refs
|
||||
if trans, ok := do.translations[str]; ok && trans != nil {
|
||||
return cloneRefs(trans.Refs)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -268,7 +340,7 @@ func (do *Domain) Set(id, str string) {
|
||||
}
|
||||
|
||||
// Get retrieves the Translation for the given string.
|
||||
func (do *Domain) Get(str string, vars ...interface{}) string {
|
||||
func (do *Domain) Get(str string, vars ...any) string {
|
||||
// Sync read
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
@@ -284,8 +356,8 @@ func (do *Domain) Get(str string, vars ...interface{}) string {
|
||||
}
|
||||
|
||||
// Append retrieves the Translation for the given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) Append(b []byte, str string, vars ...interface{}) []byte {
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) Append(b []byte, str string, vars ...any) []byte {
|
||||
// Sync read
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
@@ -322,40 +394,48 @@ func (do *Domain) SetN(id, plural string, n int, str string) {
|
||||
}
|
||||
|
||||
// GetN retrieves the (N)th plural form of Translation for the given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) GetN(str, plural string, n int, vars ...interface{}) string {
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) GetN(str, plural string, n int, vars ...any) string {
|
||||
// Get plural form before acquiring the translation lock so a custom
|
||||
// resolver is never called while a project lock is held.
|
||||
pluralForm := do.pluralForm(n)
|
||||
|
||||
// Sync read
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
if do.translations != nil {
|
||||
if _, ok := do.translations[str]; ok {
|
||||
return FormatString(do.translations[str].GetN(do.pluralForm(n)), vars...)
|
||||
return FormatString(do.translations[str].GetN(pluralForm), vars...)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse plural forms to distinguish between plural and singular
|
||||
if do.pluralForm(n) == 0 {
|
||||
if pluralForm == 0 {
|
||||
return FormatString(str, vars...)
|
||||
}
|
||||
return FormatString(plural, vars...)
|
||||
}
|
||||
|
||||
// AppendN adds the (N)th plural form of Translation for the given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte {
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) AppendN(b []byte, str, plural string, n int, vars ...any) []byte {
|
||||
// Get plural form before acquiring the translation lock so a custom
|
||||
// resolver is never called while a project lock is held.
|
||||
pluralForm := do.pluralForm(n)
|
||||
|
||||
// Sync read
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
if do.translations != nil {
|
||||
if _, ok := do.translations[str]; ok {
|
||||
return Appendf(b, do.translations[str].GetN(do.pluralForm(n)), vars...)
|
||||
return Appendf(b, do.translations[str].GetN(pluralForm), vars...)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse plural forms to distinguish between plural and singular
|
||||
if do.pluralForm(n) == 0 {
|
||||
if pluralForm == 0 {
|
||||
return Appendf(b, str, vars...)
|
||||
}
|
||||
return Appendf(b, plural, vars...)
|
||||
@@ -388,8 +468,8 @@ func (do *Domain) SetC(id, ctx, str string) {
|
||||
}
|
||||
|
||||
// GetC retrieves the corresponding Translation for a given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) GetC(str, ctx string, vars ...interface{}) string {
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) GetC(str, ctx string, vars ...any) string {
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
@@ -408,8 +488,8 @@ func (do *Domain) GetC(str, ctx string, vars ...interface{}) string {
|
||||
}
|
||||
|
||||
// AppendC retrieves the corresponding Translation for a given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) AppendC(b []byte, str, ctx string, vars ...interface{}) []byte {
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) AppendC(b []byte, str, ctx string, vars ...any) []byte {
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
@@ -443,12 +523,14 @@ func (do *Domain) SetNC(id, plural, ctx string, n int, str string) {
|
||||
} else {
|
||||
trans = NewTranslation()
|
||||
trans.ID = id
|
||||
trans.PluralID = plural
|
||||
trans.SetN(pluralForm, str)
|
||||
context[id] = trans
|
||||
}
|
||||
} else {
|
||||
trans := NewTranslation()
|
||||
trans.ID = id
|
||||
trans.PluralID = plural
|
||||
trans.SetN(pluralForm, str)
|
||||
do.contextTranslations[ctx] = map[string]*Translation{
|
||||
id: trans,
|
||||
@@ -457,19 +539,22 @@ func (do *Domain) SetNC(id, plural, ctx string, n int, str string) {
|
||||
}
|
||||
|
||||
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) GetNC(str, plural string, n int, ctx string, vars ...any) string {
|
||||
if do.hasContextTranslation(str, ctx) {
|
||||
// Get plural form before acquiring the translation lock so a custom
|
||||
// resolver is never called while a project lock is held.
|
||||
pluralForm := do.pluralForm(n)
|
||||
|
||||
if do.contextTranslations != nil {
|
||||
if _, ok := do.contextTranslations[ctx]; ok {
|
||||
if do.contextTranslations[ctx] != nil {
|
||||
if _, ok := do.contextTranslations[ctx][str]; ok {
|
||||
return FormatString(do.contextTranslations[ctx][str].GetN(do.pluralForm(n)), vars...)
|
||||
}
|
||||
do.trMutex.RLock()
|
||||
if translations, ok := do.contextTranslations[ctx]; ok && translations != nil {
|
||||
if trans, ok := translations[str]; ok && trans != nil {
|
||||
translated := trans.GetN(pluralForm)
|
||||
do.trMutex.RUnlock()
|
||||
return FormatString(translated, vars...)
|
||||
}
|
||||
}
|
||||
do.trMutex.RUnlock()
|
||||
}
|
||||
|
||||
if n == 1 {
|
||||
@@ -479,19 +564,22 @@ func (do *Domain) GetNC(str, plural string, n int, ctx string, vars ...interface
|
||||
}
|
||||
|
||||
// AppendNC retrieves the (N)th plural form of Translation for the given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte {
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...any) []byte {
|
||||
if do.hasContextTranslation(str, ctx) {
|
||||
// Get plural form before acquiring the translation lock so a custom
|
||||
// resolver is never called while a project lock is held.
|
||||
pluralForm := do.pluralForm(n)
|
||||
|
||||
if do.contextTranslations != nil {
|
||||
if _, ok := do.contextTranslations[ctx]; ok {
|
||||
if do.contextTranslations[ctx] != nil {
|
||||
if _, ok := do.contextTranslations[ctx][str]; ok {
|
||||
return Appendf(b, do.contextTranslations[ctx][str].GetN(do.pluralForm(n)), vars...)
|
||||
}
|
||||
do.trMutex.RLock()
|
||||
if translations, ok := do.contextTranslations[ctx]; ok && translations != nil {
|
||||
if trans, ok := translations[str]; ok && trans != nil {
|
||||
translated := trans.GetN(pluralForm)
|
||||
do.trMutex.RUnlock()
|
||||
return Appendf(b, translated, vars...)
|
||||
}
|
||||
}
|
||||
do.trMutex.RUnlock()
|
||||
}
|
||||
|
||||
if n == 1 {
|
||||
@@ -507,6 +595,14 @@ func (do *Domain) IsTranslated(str string) bool {
|
||||
|
||||
// IsTranslatedN reports whether a plural string is translated
|
||||
func (do *Domain) IsTranslatedN(str string, n int) bool {
|
||||
if !do.hasTranslation(str) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get plural form before acquiring the translation lock so a custom
|
||||
// resolver is never called while a project lock is held.
|
||||
pluralForm := do.pluralForm(n)
|
||||
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
@@ -514,10 +610,10 @@ func (do *Domain) IsTranslatedN(str string, n int) bool {
|
||||
return false
|
||||
}
|
||||
tr, ok := do.translations[str]
|
||||
if !ok {
|
||||
if !ok || tr == nil {
|
||||
return false
|
||||
}
|
||||
return tr.IsTranslatedN(do.pluralForm(n))
|
||||
return tr.IsTranslatedN(pluralForm)
|
||||
}
|
||||
|
||||
// IsTranslatedC reports whether a context string is translated
|
||||
@@ -527,6 +623,14 @@ func (do *Domain) IsTranslatedC(str, ctx string) bool {
|
||||
|
||||
// IsTranslatedNC reports whether a plural context string is translated
|
||||
func (do *Domain) IsTranslatedNC(str string, n int, ctx string) bool {
|
||||
if !do.hasContextTranslation(str, ctx) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get plural form before acquiring the translation lock so a custom
|
||||
// resolver is never called while a project lock is held.
|
||||
pluralForm := do.pluralForm(n)
|
||||
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
@@ -534,36 +638,24 @@ func (do *Domain) IsTranslatedNC(str string, n int, ctx string) bool {
|
||||
return false
|
||||
}
|
||||
translations, ok := do.contextTranslations[ctx]
|
||||
if !ok {
|
||||
if !ok || translations == nil {
|
||||
return false
|
||||
}
|
||||
tr, ok := translations[str]
|
||||
if !ok {
|
||||
if !ok || tr == nil {
|
||||
return false
|
||||
}
|
||||
return tr.IsTranslatedN(do.pluralForm(n))
|
||||
return tr.IsTranslatedN(pluralForm)
|
||||
}
|
||||
|
||||
// GetTranslations returns a copy of every translation in the domain. It does not support contexts.
|
||||
func (do *Domain) GetTranslations() map[string]*Translation {
|
||||
all := make(map[string]*Translation, len(do.translations))
|
||||
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
all := make(map[string]*Translation, len(do.translations))
|
||||
for msgID, trans := range do.translations {
|
||||
newTrans := NewTranslation()
|
||||
newTrans.ID = trans.ID
|
||||
newTrans.PluralID = trans.PluralID
|
||||
newTrans.dirty = trans.dirty
|
||||
if len(trans.Refs) > 0 {
|
||||
newTrans.Refs = make([]string, len(trans.Refs))
|
||||
copy(newTrans.Refs, trans.Refs)
|
||||
}
|
||||
for k, v := range trans.Trs {
|
||||
newTrans.Trs[k] = v
|
||||
}
|
||||
all[msgID] = newTrans
|
||||
all[msgID] = cloneTranslation(trans)
|
||||
}
|
||||
|
||||
return all
|
||||
@@ -571,32 +663,18 @@ func (do *Domain) GetTranslations() map[string]*Translation {
|
||||
|
||||
// GetCtxTranslations returns a copy of every translation in the domain with context
|
||||
func (do *Domain) GetCtxTranslations() map[string]map[string]*Translation {
|
||||
all := make(map[string]map[string]*Translation, len(do.contextTranslations))
|
||||
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
all := make(map[string]map[string]*Translation, len(do.contextTranslations))
|
||||
for ctx, translations := range do.contextTranslations {
|
||||
for msgID, trans := range translations {
|
||||
newTrans := NewTranslation()
|
||||
newTrans.ID = trans.ID
|
||||
newTrans.PluralID = trans.PluralID
|
||||
newTrans.dirty = trans.dirty
|
||||
if len(trans.Refs) > 0 {
|
||||
newTrans.Refs = make([]string, len(trans.Refs))
|
||||
copy(newTrans.Refs, trans.Refs)
|
||||
}
|
||||
for k, v := range trans.Trs {
|
||||
newTrans.Trs[k] = v
|
||||
}
|
||||
|
||||
if all[ctx] == nil {
|
||||
all[ctx] = make(map[string]*Translation)
|
||||
}
|
||||
|
||||
all[ctx][msgID] = newTrans
|
||||
all[ctx][msgID] = cloneTranslation(trans)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return all
|
||||
@@ -604,10 +682,11 @@ func (do *Domain) GetCtxTranslations() map[string]map[string]*Translation {
|
||||
|
||||
// SourceReference is a struct to hold source reference information
|
||||
type SourceReference struct {
|
||||
path string
|
||||
line int
|
||||
context string
|
||||
trans *Translation
|
||||
path string
|
||||
line int
|
||||
context string
|
||||
hasContext bool
|
||||
trans *Translation
|
||||
}
|
||||
|
||||
func extractPathAndLine(ref string) (string, int) {
|
||||
@@ -627,6 +706,11 @@ func extractPathAndLine(ref string) (string, int) {
|
||||
// MarshalText implements encoding.TextMarshaler interface
|
||||
// Assists round-trip of POT/PO content
|
||||
func (do *Domain) MarshalText() ([]byte, error) {
|
||||
do.trMutex.RLock()
|
||||
do.pluralMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
defer do.pluralMutex.RUnlock()
|
||||
|
||||
var buf bytes.Buffer
|
||||
if len(do.headerComments) > 0 {
|
||||
buf.WriteString(strings.Join(do.headerComments, "\n"))
|
||||
@@ -681,31 +765,29 @@ func (do *Domain) MarshalText() ([]byte, error) {
|
||||
v := do.Headers[k]
|
||||
|
||||
for _, value := range v {
|
||||
buf.WriteString("\n\"" + k + ": " + value + "\\n\"")
|
||||
encoded := EscapeSpecialCharacters(k + ": " + value + "\n")
|
||||
buf.WriteString("\n\"" + encoded + "\"")
|
||||
}
|
||||
}
|
||||
|
||||
// Just as with headers, output translations in consistent order (to minimise diffs between round-trips), with (first) source reference taking priority, followed by context and finally ID
|
||||
references := make([]SourceReference, 0)
|
||||
for name, ctx := range do.contextTranslations {
|
||||
for id, trans := range ctx {
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
for _, trans := range ctx {
|
||||
if len(trans.Refs) > 0 {
|
||||
path, line := extractPathAndLine(trans.Refs[0])
|
||||
references = append(references, SourceReference{
|
||||
path,
|
||||
line,
|
||||
name,
|
||||
trans,
|
||||
path: path,
|
||||
line: line,
|
||||
context: name,
|
||||
hasContext: true,
|
||||
trans: trans,
|
||||
})
|
||||
} else {
|
||||
references = append(references, SourceReference{
|
||||
"",
|
||||
0,
|
||||
name,
|
||||
trans,
|
||||
context: name,
|
||||
hasContext: true,
|
||||
trans: trans,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -719,17 +801,13 @@ func (do *Domain) MarshalText() ([]byte, error) {
|
||||
if len(trans.Refs) > 0 {
|
||||
path, line := extractPathAndLine(trans.Refs[0])
|
||||
references = append(references, SourceReference{
|
||||
path,
|
||||
line,
|
||||
"",
|
||||
trans,
|
||||
path: path,
|
||||
line: line,
|
||||
trans: trans,
|
||||
})
|
||||
} else {
|
||||
references = append(references, SourceReference{
|
||||
"",
|
||||
0,
|
||||
"",
|
||||
trans,
|
||||
trans: trans,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -754,6 +832,9 @@ func (do *Domain) MarshalText() ([]byte, error) {
|
||||
if references[i].context > references[j].context {
|
||||
return false
|
||||
}
|
||||
if references[i].hasContext != references[j].hasContext {
|
||||
return !references[i].hasContext
|
||||
}
|
||||
return references[i].trans.ID < references[j].trans.ID
|
||||
})
|
||||
|
||||
@@ -765,7 +846,7 @@ func (do *Domain) MarshalText() ([]byte, error) {
|
||||
buf.WriteByte(byte('\n'))
|
||||
}
|
||||
|
||||
if ref.context == "" {
|
||||
if !ref.hasContext {
|
||||
buf.WriteString("\nmsgid \"" + EscapeSpecialCharacters(trans.ID) + "\"")
|
||||
} else {
|
||||
buf.WriteString("\nmsgctxt \"" + EscapeSpecialCharacters(ref.context) + "\"\nmsgid \"" + EscapeSpecialCharacters(trans.ID) + "\"")
|
||||
@@ -774,9 +855,16 @@ func (do *Domain) MarshalText() ([]byte, error) {
|
||||
if trans.PluralID == "" {
|
||||
buf.WriteString("\nmsgstr \"" + EscapeSpecialCharacters(trans.Trs[0]) + "\"")
|
||||
} else {
|
||||
buf.WriteString("\nmsgid_plural \"" + trans.PluralID + "\"")
|
||||
for i, tr := range trans.Trs {
|
||||
buf.WriteString("\nmsgstr[" + EscapeSpecialCharacters(strconv.Itoa(i)) + "] \"" + tr + "\"")
|
||||
buf.WriteString("\nmsgid_plural \"" + EscapeSpecialCharacters(trans.PluralID) + "\"")
|
||||
|
||||
pluralIndexes := make([]int, 0, len(trans.Trs))
|
||||
for i := range trans.Trs {
|
||||
pluralIndexes = append(pluralIndexes, i)
|
||||
}
|
||||
sort.Ints(pluralIndexes)
|
||||
|
||||
for _, i := range pluralIndexes {
|
||||
buf.WriteString("\nmsgstr[" + strconv.Itoa(i) + "] \"" + EscapeSpecialCharacters(trans.Trs[i]) + "\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -786,8 +874,23 @@ func (do *Domain) MarshalText() ([]byte, error) {
|
||||
|
||||
// EscapeSpecialCharacters escapes special characters in a string
|
||||
func EscapeSpecialCharacters(s string) string {
|
||||
s = regexp.MustCompile(`([^\\])(")`).ReplaceAllString(s, "$1\\\"") // Escape non-escaped double quotation marks
|
||||
var escaped strings.Builder
|
||||
escaped.Grow(len(s))
|
||||
|
||||
for i := range len(s) {
|
||||
switch s[i] {
|
||||
case '\\':
|
||||
escaped.WriteByte('\\')
|
||||
escaped.WriteByte('\\')
|
||||
case '"':
|
||||
escaped.WriteByte('\\')
|
||||
escaped.WriteByte('"')
|
||||
default:
|
||||
escaped.WriteByte(s[i])
|
||||
}
|
||||
}
|
||||
|
||||
s = escaped.String()
|
||||
if strings.Count(s, "\n") == 0 {
|
||||
return s
|
||||
}
|
||||
@@ -821,16 +924,26 @@ func EscapeSpecialCharacters(s string) string {
|
||||
return strings.Join(data, "\n")
|
||||
}
|
||||
|
||||
func (do *Domain) translatorEncodingSnapshot() *TranslatorEncoding {
|
||||
do.trMutex.RLock()
|
||||
do.pluralMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
defer do.pluralMutex.RUnlock()
|
||||
|
||||
return &TranslatorEncoding{
|
||||
Headers: cloneHeaderMap(do.Headers),
|
||||
Language: do.Language,
|
||||
PluralForms: do.PluralForms,
|
||||
Nplurals: do.nplurals,
|
||||
Plural: do.plural,
|
||||
Translations: cloneTranslations(do.translations),
|
||||
Contexts: cloneContextTranslations(do.contextTranslations),
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalBinary implements encoding.BinaryMarshaler interface
|
||||
func (do *Domain) MarshalBinary() ([]byte, error) {
|
||||
obj := new(TranslatorEncoding)
|
||||
obj.Headers = do.Headers
|
||||
obj.Language = do.Language
|
||||
obj.PluralForms = do.PluralForms
|
||||
obj.Nplurals = do.nplurals
|
||||
obj.Plural = do.plural
|
||||
obj.Translations = do.translations
|
||||
obj.Contexts = do.contextTranslations
|
||||
obj := do.translatorEncodingSnapshot()
|
||||
|
||||
var buff bytes.Buffer
|
||||
encoder := gob.NewEncoder(&buff)
|
||||
@@ -845,22 +958,68 @@ func (do *Domain) UnmarshalBinary(data []byte) error {
|
||||
obj := new(TranslatorEncoding)
|
||||
|
||||
decoder := gob.NewDecoder(buff)
|
||||
err := decoder.Decode(obj)
|
||||
if err != nil {
|
||||
if err := decoder.Decode(obj); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
do.Headers = obj.Headers
|
||||
headers := cloneHeaderMap(obj.Headers)
|
||||
if headers == nil {
|
||||
headers = make(HeaderMap)
|
||||
}
|
||||
translations := cloneTranslations(obj.Translations)
|
||||
if translations == nil {
|
||||
translations = make(map[string]*Translation)
|
||||
}
|
||||
contextTranslations := cloneContextTranslations(obj.Contexts)
|
||||
if contextTranslations == nil {
|
||||
contextTranslations = make(map[string]map[string]*Translation)
|
||||
}
|
||||
|
||||
for id, trans := range translations {
|
||||
if trans == nil {
|
||||
trans = NewTranslation()
|
||||
trans.ID = id
|
||||
translations[id] = trans
|
||||
}
|
||||
}
|
||||
for context, translationsForContext := range contextTranslations {
|
||||
if translationsForContext == nil {
|
||||
contextTranslations[context] = make(map[string]*Translation)
|
||||
continue
|
||||
}
|
||||
for id, trans := range translationsForContext {
|
||||
if trans == nil {
|
||||
trans = NewTranslation()
|
||||
trans.ID = id
|
||||
translationsForContext[id] = trans
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var pluralforms plurals.Expression
|
||||
if expr, err := plurals.Compile(obj.Plural); err == nil {
|
||||
pluralforms = expr
|
||||
}
|
||||
|
||||
do.trMutex.Lock()
|
||||
do.pluralMutex.Lock()
|
||||
|
||||
do.Headers = headers
|
||||
do.Language = obj.Language
|
||||
do.PluralForms = obj.PluralForms
|
||||
do.nplurals = obj.Nplurals
|
||||
do.plural = obj.Plural
|
||||
do.translations = obj.Translations
|
||||
do.contextTranslations = obj.Contexts
|
||||
do.pluralforms = pluralforms
|
||||
do.translations = translations
|
||||
do.contextTranslations = contextTranslations
|
||||
do.pluralTranslations = make(map[string]*Translation)
|
||||
do.headerComments = make([]string, 0)
|
||||
do.trBuffer = nil
|
||||
do.ctxBuffer = ""
|
||||
do.refBuffer = ""
|
||||
|
||||
if expr, err := plurals.Compile(do.plural); err == nil {
|
||||
do.pluralforms = expr
|
||||
}
|
||||
do.pluralMutex.Unlock()
|
||||
do.trMutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+173
-111
@@ -55,51 +55,59 @@ func init() {
|
||||
domain: "default",
|
||||
languages: []string{FallbackLocale},
|
||||
library: "/usr/local/share/locale",
|
||||
locales: nil,
|
||||
locales: make([]*Locale, 0),
|
||||
}
|
||||
|
||||
// Register Translator types for gob encoding
|
||||
gob.Register(TranslatorEncoding{})
|
||||
}
|
||||
|
||||
// loadLocales creates a new Locale object for every language (specified using Configure)
|
||||
// at package level based on the configuration of global configuration .
|
||||
// It is called when trying to use Get or GetD methods.
|
||||
// loadLocales ensures package-level locales and their configured domain are loaded.
|
||||
func loadLocales(rebuildCache bool) {
|
||||
globalConfig.Lock()
|
||||
|
||||
if globalConfig.locales == nil || rebuildCache {
|
||||
var locales []*Locale
|
||||
locales := make([]*Locale, 0, len(globalConfig.languages))
|
||||
for _, language := range globalConfig.languages {
|
||||
locales = append(locales, NewLocale(globalConfig.library, language))
|
||||
}
|
||||
globalConfig.locales = locales
|
||||
}
|
||||
|
||||
for _, locale := range globalConfig.locales {
|
||||
if _, ok := locale.Domains[globalConfig.domain]; !ok || rebuildCache {
|
||||
locale.AddDomain(globalConfig.domain)
|
||||
}
|
||||
locale.SetDomain(globalConfig.domain)
|
||||
}
|
||||
|
||||
locales := append([]*Locale(nil), globalConfig.locales...)
|
||||
domain := globalConfig.domain
|
||||
globalConfig.Unlock()
|
||||
|
||||
for _, locale := range locales {
|
||||
if locale == nil {
|
||||
continue
|
||||
}
|
||||
if rebuildCache || !locale.hasDomain(domain) {
|
||||
locale.AddDomain(domain)
|
||||
}
|
||||
locale.SetDomain(domain)
|
||||
}
|
||||
}
|
||||
|
||||
// GetDomain is the domain getter for the package configuration
|
||||
func GetDomain() string {
|
||||
var dom string
|
||||
globalConfig.RLock()
|
||||
if globalConfig.locales != nil {
|
||||
// All locales have the same domain
|
||||
dom = globalConfig.locales[0].GetDomain()
|
||||
}
|
||||
if dom == "" {
|
||||
dom = globalConfig.domain
|
||||
domain := globalConfig.domain
|
||||
var locale *Locale
|
||||
for _, candidate := range globalConfig.locales {
|
||||
if candidate != nil {
|
||||
locale = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
globalConfig.RUnlock()
|
||||
|
||||
return dom
|
||||
if locale != nil {
|
||||
if localeDomain := locale.GetDomain(); localeDomain != "" {
|
||||
return localeDomain
|
||||
}
|
||||
}
|
||||
return domain
|
||||
}
|
||||
|
||||
// SetDomain sets the name for the domain to be used at package level.
|
||||
@@ -107,8 +115,8 @@ func GetDomain() string {
|
||||
func SetDomain(dom string) {
|
||||
globalConfig.Lock()
|
||||
globalConfig.domain = dom
|
||||
if globalConfig.locales != nil {
|
||||
for _, locale := range globalConfig.locales {
|
||||
for _, locale := range globalConfig.locales {
|
||||
if locale != nil {
|
||||
locale.SetDomain(dom)
|
||||
}
|
||||
}
|
||||
@@ -131,8 +139,10 @@ func GetLanguage() string {
|
||||
// GetLanguages returns all languages that have been supplied.
|
||||
func GetLanguages() []string {
|
||||
globalConfig.RLock()
|
||||
defer globalConfig.RUnlock()
|
||||
return globalConfig.languages
|
||||
languages := make([]string, len(globalConfig.languages))
|
||||
copy(languages, globalConfig.languages)
|
||||
globalConfig.RUnlock()
|
||||
return languages
|
||||
}
|
||||
|
||||
// SetLanguage sets the language code (or colon separated language codes) to be used at package level.
|
||||
@@ -140,7 +150,7 @@ func GetLanguages() []string {
|
||||
func SetLanguage(lang string) {
|
||||
globalConfig.Lock()
|
||||
var languages []string
|
||||
for _, language := range strings.Split(lang, ":") {
|
||||
for language := range strings.SplitSeq(lang, ":") {
|
||||
languages = append(languages, SimplifiedLocale(language))
|
||||
}
|
||||
globalConfig.languages = languages
|
||||
@@ -171,17 +181,20 @@ func SetLibrary(lib string) {
|
||||
// GetLocales returns the locales that have been set for the package configuration.
|
||||
func GetLocales() []*Locale {
|
||||
globalConfig.RLock()
|
||||
defer globalConfig.RUnlock()
|
||||
return globalConfig.locales
|
||||
locales := make([]*Locale, len(globalConfig.locales))
|
||||
copy(locales, globalConfig.locales)
|
||||
globalConfig.RUnlock()
|
||||
return locales
|
||||
}
|
||||
|
||||
// GetStorage is the locale storage getter for the package configuration.
|
||||
//
|
||||
// Deprecated: Storage has been renamed to Locale for consistency, use GetLocales instead.
|
||||
func GetStorage() *Locale {
|
||||
locales := GetLocales()
|
||||
if len(locales) > 0 {
|
||||
return locales[0]
|
||||
for _, locale := range GetLocales() {
|
||||
if locale != nil {
|
||||
return locale
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -190,24 +203,40 @@ func GetStorage() *Locale {
|
||||
// NewLocale(). This makes it possible to attach custom Domain objects from in-memory po/mo.
|
||||
// The library, language and domain of the first Locale will set the default global configuration.
|
||||
func SetLocales(locales []*Locale) {
|
||||
ownedLocales := make([]*Locale, len(locales))
|
||||
copy(ownedLocales, locales)
|
||||
|
||||
globalConfig.Lock()
|
||||
defer globalConfig.Unlock()
|
||||
globalConfig.locales = ownedLocales
|
||||
|
||||
globalConfig.locales = locales
|
||||
globalConfig.library = locales[0].path
|
||||
globalConfig.domain = locales[0].defaultDomain
|
||||
languages := make([]string, 0, len(ownedLocales))
|
||||
var first *Locale
|
||||
for _, locale := range ownedLocales {
|
||||
if locale == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var languages []string
|
||||
for _, locale := range locales {
|
||||
locale.RLock()
|
||||
if first == nil {
|
||||
first = locale
|
||||
globalConfig.library = locale.path
|
||||
globalConfig.domain = locale.defaultDomain
|
||||
}
|
||||
languages = append(languages, locale.lang)
|
||||
locale.RUnlock()
|
||||
}
|
||||
globalConfig.languages = languages
|
||||
globalConfig.Unlock()
|
||||
}
|
||||
|
||||
// SetStorage allows overriding the global Locale object with one built manually with NewLocale().
|
||||
//
|
||||
// Deprecated: Storage has been renamed to Locale for consistency, use SetLocales instead.
|
||||
func SetStorage(locale *Locale) {
|
||||
if locale == nil {
|
||||
SetLocales(nil)
|
||||
return
|
||||
}
|
||||
SetLocales([]*Locale{locale})
|
||||
}
|
||||
|
||||
@@ -219,7 +248,7 @@ func Configure(lib, lang, dom string) {
|
||||
globalConfig.Lock()
|
||||
globalConfig.library = lib
|
||||
var languages []string
|
||||
for _, language := range strings.Split(lang, ":") {
|
||||
for language := range strings.SplitSeq(lang, ":") {
|
||||
languages = append(languages, SimplifiedLocale(language))
|
||||
}
|
||||
globalConfig.languages = languages
|
||||
@@ -230,114 +259,137 @@ func Configure(lib, lang, dom string) {
|
||||
}
|
||||
|
||||
// Get uses the default domain globally set to return the corresponding Translation of a given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func Get(str string, vars ...interface{}) string {
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func Get(str string, vars ...any) string {
|
||||
return GetD(GetDomain(), str, vars...)
|
||||
}
|
||||
|
||||
// GetN retrieves the (N)th plural form of Translation for the given string in the default domain.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetN(str, plural string, n int, vars ...interface{}) string {
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetN(str, plural string, n int, vars ...any) string {
|
||||
return GetND(GetDomain(), str, plural, n, vars...)
|
||||
}
|
||||
|
||||
// GetD returns the corresponding Translation in the given domain for a given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetD(dom, str string, vars ...interface{}) string {
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetD(dom, str string, vars ...any) string {
|
||||
// Try to load default package Locales
|
||||
loadLocales(false)
|
||||
|
||||
globalConfig.RLock()
|
||||
defer globalConfig.RUnlock()
|
||||
|
||||
var tr string
|
||||
for i, locale := range globalConfig.locales {
|
||||
if _, ok := locale.Domains[dom]; !ok {
|
||||
locale.AddDomain(dom)
|
||||
}
|
||||
if !locale.IsTranslatedD(dom, str) && i < (len(globalConfig.locales)-1) {
|
||||
locales := GetLocales()
|
||||
var fallback *Locale
|
||||
for _, locale := range locales {
|
||||
if locale == nil {
|
||||
continue
|
||||
}
|
||||
tr = locale.GetD(dom, str, vars...)
|
||||
break
|
||||
fallback = locale
|
||||
if !locale.hasDomain(dom) {
|
||||
locale.AddDomain(dom)
|
||||
}
|
||||
if locale.IsTranslatedD(dom, str) {
|
||||
return locale.GetD(dom, str, vars...)
|
||||
}
|
||||
}
|
||||
return tr
|
||||
if fallback != nil {
|
||||
return fallback.GetD(dom, str, vars...)
|
||||
}
|
||||
return FormatString(str, vars...)
|
||||
}
|
||||
|
||||
// GetND retrieves the (N)th plural form of Translation in the given domain for a given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetND(dom, str, plural string, n int, vars ...interface{}) string {
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetND(dom, str, plural string, n int, vars ...any) string {
|
||||
// Try to load default package Locales
|
||||
loadLocales(false)
|
||||
|
||||
globalConfig.RLock()
|
||||
defer globalConfig.RUnlock()
|
||||
|
||||
var tr string
|
||||
for i, locale := range globalConfig.locales {
|
||||
if _, ok := locale.Domains[dom]; !ok {
|
||||
locale.AddDomain(dom)
|
||||
}
|
||||
if !locale.IsTranslatedND(dom, str, n) && i < (len(globalConfig.locales)-1) {
|
||||
locales := GetLocales()
|
||||
var fallback *Locale
|
||||
for _, locale := range locales {
|
||||
if locale == nil {
|
||||
continue
|
||||
}
|
||||
tr = locale.GetND(dom, str, plural, n, vars...)
|
||||
break
|
||||
fallback = locale
|
||||
if !locale.hasDomain(dom) {
|
||||
locale.AddDomain(dom)
|
||||
}
|
||||
if locale.IsTranslatedND(dom, str, n) {
|
||||
return locale.GetND(dom, str, plural, n, vars...)
|
||||
}
|
||||
}
|
||||
return tr
|
||||
if fallback != nil {
|
||||
return fallback.GetND(dom, str, plural, n, vars...)
|
||||
}
|
||||
if n == 1 {
|
||||
return FormatString(str, vars...)
|
||||
}
|
||||
return FormatString(plural, vars...)
|
||||
}
|
||||
|
||||
// GetC uses the default domain globally set to return the corresponding Translation of the given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetC(str, ctx string, vars ...interface{}) string {
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetC(str, ctx string, vars ...any) string {
|
||||
return GetDC(GetDomain(), str, ctx, vars...)
|
||||
}
|
||||
|
||||
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the default domain.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetNC(str, plural string, n int, ctx string, vars ...any) string {
|
||||
return GetNDC(GetDomain(), str, plural, n, ctx, vars...)
|
||||
}
|
||||
|
||||
// GetDC returns the corresponding Translation in the given domain for the given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetDC(dom, str, ctx string, vars ...interface{}) string {
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetDC(dom, str, ctx string, vars ...any) string {
|
||||
// Try to load default package Locales
|
||||
loadLocales(false)
|
||||
|
||||
globalConfig.RLock()
|
||||
defer globalConfig.RUnlock()
|
||||
|
||||
var tr string
|
||||
for i, locale := range globalConfig.locales {
|
||||
if !locale.IsTranslatedDC(dom, str, ctx) && i < (len(globalConfig.locales)-1) {
|
||||
locales := GetLocales()
|
||||
var fallback *Locale
|
||||
for _, locale := range locales {
|
||||
if locale == nil {
|
||||
continue
|
||||
}
|
||||
tr = locale.GetDC(dom, str, ctx, vars...)
|
||||
break
|
||||
fallback = locale
|
||||
if !locale.hasDomain(dom) {
|
||||
locale.AddDomain(dom)
|
||||
}
|
||||
if locale.IsTranslatedDC(dom, str, ctx) {
|
||||
return locale.GetDC(dom, str, ctx, vars...)
|
||||
}
|
||||
}
|
||||
return tr
|
||||
if fallback != nil {
|
||||
return fallback.GetDC(dom, str, ctx, vars...)
|
||||
}
|
||||
return FormatString(str, vars...)
|
||||
}
|
||||
|
||||
// GetNDC retrieves the (N)th plural form of Translation in the given domain for a given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetNDC(dom, str, plural string, n int, ctx string, vars ...any) string {
|
||||
// Try to load default package Locales
|
||||
loadLocales(false)
|
||||
|
||||
// Return Translation
|
||||
globalConfig.RLock()
|
||||
defer globalConfig.RUnlock()
|
||||
|
||||
var tr string
|
||||
for i, locale := range globalConfig.locales {
|
||||
if !locale.IsTranslatedNDC(dom, str, n, ctx) && i < (len(globalConfig.locales)-1) {
|
||||
locales := GetLocales()
|
||||
var fallback *Locale
|
||||
for _, locale := range locales {
|
||||
if locale == nil {
|
||||
continue
|
||||
}
|
||||
tr = locale.GetNDC(dom, str, plural, n, ctx, vars...)
|
||||
break
|
||||
fallback = locale
|
||||
if !locale.hasDomain(dom) {
|
||||
locale.AddDomain(dom)
|
||||
}
|
||||
if locale.IsTranslatedNDC(dom, str, n, ctx) {
|
||||
return locale.GetNDC(dom, str, plural, n, ctx, vars...)
|
||||
}
|
||||
}
|
||||
return tr
|
||||
if fallback != nil {
|
||||
return fallback.GetNDC(dom, str, plural, n, ctx, vars...)
|
||||
}
|
||||
if n == 1 {
|
||||
return FormatString(str, vars...)
|
||||
}
|
||||
return FormatString(plural, vars...)
|
||||
}
|
||||
|
||||
// IsTranslated reports whether a string is translated in given languages.
|
||||
@@ -366,18 +418,23 @@ func IsTranslatedND(dom, str string, n int, langs ...string) bool {
|
||||
}
|
||||
|
||||
loadLocales(false)
|
||||
|
||||
globalConfig.RLock()
|
||||
defer globalConfig.RUnlock()
|
||||
|
||||
locales := GetLocales()
|
||||
for _, lang := range langs {
|
||||
lang = SimplifiedLocale(lang)
|
||||
|
||||
for _, supportedLocale := range globalConfig.locales {
|
||||
if lang != supportedLocale.GetActualLanguage(dom) {
|
||||
for _, supportedLocale := range locales {
|
||||
if supportedLocale == nil {
|
||||
continue
|
||||
}
|
||||
return supportedLocale.IsTranslatedND(dom, str, n)
|
||||
if lang != supportedLocale.GetLanguage() && lang != supportedLocale.GetActualLanguage(dom) {
|
||||
continue
|
||||
}
|
||||
if !supportedLocale.hasDomain(dom) {
|
||||
supportedLocale.AddDomain(dom)
|
||||
}
|
||||
if supportedLocale.IsTranslatedND(dom, str, n) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
@@ -398,7 +455,7 @@ func IsTranslatedNC(str string, n int, ctx string, langs ...string) bool {
|
||||
// IsTranslatedDC reports whether a domain context string is translated in given languages.
|
||||
// When the langs argument is omitted, the output of GetLanguages is used.
|
||||
func IsTranslatedDC(dom, str, ctx string, langs ...string) bool {
|
||||
return IsTranslatedNDC(dom, str, 0, ctx, langs...)
|
||||
return IsTranslatedNDC(dom, str, 1, ctx, langs...)
|
||||
}
|
||||
|
||||
// IsTranslatedNDC reports whether a plural domain context string is translated in any of given languages.
|
||||
@@ -409,18 +466,23 @@ func IsTranslatedNDC(dom, str string, n int, ctx string, langs ...string) bool {
|
||||
}
|
||||
|
||||
loadLocales(false)
|
||||
|
||||
globalConfig.RLock()
|
||||
defer globalConfig.RUnlock()
|
||||
|
||||
locales := GetLocales()
|
||||
for _, lang := range langs {
|
||||
lang = SimplifiedLocale(lang)
|
||||
|
||||
for _, locale := range globalConfig.locales {
|
||||
if lang != locale.GetActualLanguage(dom) {
|
||||
for _, supportedLocale := range locales {
|
||||
if supportedLocale == nil {
|
||||
continue
|
||||
}
|
||||
return locale.IsTranslatedNDC(dom, str, n, ctx)
|
||||
if lang != supportedLocale.GetLanguage() && lang != supportedLocale.GetActualLanguage(dom) {
|
||||
continue
|
||||
}
|
||||
if !supportedLocale.hasDomain(dom) {
|
||||
supportedLocale.AddDomain(dom)
|
||||
}
|
||||
if supportedLocale.IsTranslatedNDC(dom, str, n, ctx) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
+31
-27
@@ -29,16 +29,21 @@ func SimplifiedLocale(lang string) string {
|
||||
}
|
||||
|
||||
// FormatString applies text formatting only when needed to parse variables.
|
||||
func FormatString(str string, vars ...interface{}) string {
|
||||
func FormatString(str string, vars ...any) string {
|
||||
if len(vars) > 0 {
|
||||
return fmt.Sprintf(str, vars...)
|
||||
return FormatStringWithArgs(str, vars)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// FormatStringWithArgs applies text formatting as a proxy for fmt.Sprintf.
|
||||
func FormatStringWithArgs(str string, vars []any) string {
|
||||
return fmt.Sprintf(str, vars...)
|
||||
}
|
||||
|
||||
// Appendf applies text formatting only when needed to parse variables.
|
||||
func Appendf(b []byte, str string, vars ...interface{}) []byte {
|
||||
func Appendf(b []byte, str string, vars ...any) []byte {
|
||||
if len(vars) > 0 {
|
||||
return fmt.Appendf(b, str, vars...)
|
||||
}
|
||||
@@ -47,50 +52,49 @@ func Appendf(b []byte, str string, vars ...interface{}) []byte {
|
||||
}
|
||||
|
||||
// NPrintf support named format
|
||||
// NPrintf("%(name)s is Type %(type)s", map[string]interface{}{"name": "Gotext", "type": "struct"})
|
||||
func NPrintf(format string, params map[string]interface{}) {
|
||||
// NPrintf("%(name)s is Type %(type)s", map[string]any{"name": "Gotext", "type": "struct"})
|
||||
func NPrintf(format string, params map[string]any) {
|
||||
f, p := parseSprintf(format, params)
|
||||
fmt.Printf(f, p...)
|
||||
}
|
||||
|
||||
// Sprintf support named format
|
||||
//
|
||||
// Sprintf("%(name)s is Type %(type)s", map[string]interface{}{"name": "Gotext", "type": "struct"})
|
||||
func Sprintf(format string, params map[string]interface{}) string {
|
||||
// Sprintf("%(name)s is Type %(type)s", map[string]any{"name": "Gotext", "type": "struct"})
|
||||
func Sprintf(format string, params map[string]any) string {
|
||||
f, p := parseSprintf(format, params)
|
||||
return fmt.Sprintf(f, p...)
|
||||
}
|
||||
|
||||
func parseSprintf(format string, params map[string]interface{}) (string, []interface{}) {
|
||||
func parseSprintf(format string, params map[string]any) (string, []any) {
|
||||
f, n := reformatSprintf(format)
|
||||
var p []interface{}
|
||||
for _, v := range n {
|
||||
p = append(p, params[v])
|
||||
var p []any
|
||||
if len(n) > 0 {
|
||||
p = make([]any, len(n))
|
||||
for i, v := range n {
|
||||
p[i] = params[v]
|
||||
}
|
||||
}
|
||||
return f, p
|
||||
}
|
||||
|
||||
func reformatSprintf(f string) (string, []string) {
|
||||
m := re.FindAllStringSubmatch(f, -1)
|
||||
i := re.FindAllStringSubmatchIndex(f, -1)
|
||||
matches := re.FindAllStringSubmatchIndex(f, -1)
|
||||
ord := make([]string, 0, len(matches))
|
||||
pair := make([]int, 1, 1+2*len(matches))
|
||||
pair[0] = 0
|
||||
|
||||
ord := []string{}
|
||||
for _, v := range m {
|
||||
ord = append(ord, v[1])
|
||||
}
|
||||
|
||||
pair := []int{0}
|
||||
for _, v := range i {
|
||||
pair = append(pair, v[2]-1)
|
||||
pair = append(pair, v[3]+1)
|
||||
for _, match := range matches {
|
||||
ord = append(ord, f[match[2]:match[3]])
|
||||
pair = append(pair, match[2]-1, match[3]+1)
|
||||
}
|
||||
pair = append(pair, len(f))
|
||||
plen := len(pair)
|
||||
|
||||
out := ""
|
||||
for n := 0; n < plen; n += 2 {
|
||||
out += f[pair[n]:pair[n+1]]
|
||||
var out strings.Builder
|
||||
out.Grow(len(f))
|
||||
for n := 0; n < len(pair); n += 2 {
|
||||
out.WriteString(f[pair[n]:pair[n+1]])
|
||||
}
|
||||
|
||||
return out, ord
|
||||
return out.String(), ord
|
||||
}
|
||||
|
||||
+214
-163
@@ -9,6 +9,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"io/fs"
|
||||
"maps"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
@@ -22,8 +23,6 @@ multiple languages at the same time by working with this object.
|
||||
Example:
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
)
|
||||
@@ -90,95 +89,114 @@ func NewLocaleFSWithPath(l string, filesystem fs.FS, p string) *Locale {
|
||||
return loc
|
||||
}
|
||||
|
||||
func (l *Locale) findExt(dom, ext string) string {
|
||||
filename := path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext)
|
||||
if l.fileExists(filename) {
|
||||
return filename
|
||||
func (l *Locale) snapshotPathLanguageFS() (string, string, fs.FS) {
|
||||
if l == nil {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
if len(l.lang) > 2 {
|
||||
filename = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext)
|
||||
if l.fileExists(filename) {
|
||||
return filename
|
||||
}
|
||||
}
|
||||
|
||||
filename = path.Join(l.path, l.lang, dom+"."+ext)
|
||||
if l.fileExists(filename) {
|
||||
return filename
|
||||
}
|
||||
|
||||
if len(l.lang) > 2 {
|
||||
filename = path.Join(l.path, l.lang[:2], dom+"."+ext)
|
||||
if l.fileExists(filename) {
|
||||
return filename
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
l.RLock()
|
||||
p := l.path
|
||||
lang := l.lang
|
||||
filesystem := l.fs
|
||||
l.RUnlock()
|
||||
return p, lang, filesystem
|
||||
}
|
||||
|
||||
// GetActualLanguage inspects the filesystem and decides whether to strip
|
||||
// a CC part of the ll_CC locale string.
|
||||
func (l *Locale) GetActualLanguage(dom string) string {
|
||||
extensions := []string{"mo", "po"}
|
||||
var fp string
|
||||
for _, ext := range extensions {
|
||||
// 'll' (or 'll_CC') exists, and it was specified as-is
|
||||
fp = path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext)
|
||||
if l.fileExists(fp) {
|
||||
return l.lang
|
||||
}
|
||||
// 'll' exists, but 'll_CC' was specified
|
||||
if len(l.lang) > 2 {
|
||||
fp = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext)
|
||||
if l.fileExists(fp) {
|
||||
return l.lang[:2]
|
||||
}
|
||||
}
|
||||
// 'll' (or 'll_CC') exists outside of LC_category, and it was specified as-is
|
||||
fp = path.Join(l.path, l.lang, dom+"."+ext)
|
||||
if l.fileExists(fp) {
|
||||
return l.lang
|
||||
}
|
||||
// 'll' exists outside of LC_category, but 'll_CC' was specified
|
||||
if len(l.lang) > 2 {
|
||||
fp = path.Join(l.path, l.lang[:2], dom+"."+ext)
|
||||
if l.fileExists(fp) {
|
||||
return l.lang[:2]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (l *Locale) fileExists(filename string) bool {
|
||||
if l.fs != nil {
|
||||
_, err := fs.Stat(l.fs, filename)
|
||||
func fileExistsIn(filesystem fs.FS, filename string) bool {
|
||||
if filesystem != nil {
|
||||
_, err := fs.Stat(filesystem, filename)
|
||||
return err == nil
|
||||
}
|
||||
_, err := os.Stat(filename)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (l *Locale) findExtWithFS(dom, ext string) (fs.FS, string) {
|
||||
p, lang, filesystem := l.snapshotPathLanguageFS()
|
||||
|
||||
filename := path.Join(p, lang, "LC_MESSAGES", dom+"."+ext)
|
||||
if fileExistsIn(filesystem, filename) {
|
||||
return filesystem, filename
|
||||
}
|
||||
|
||||
if len(lang) > 2 {
|
||||
filename = path.Join(p, lang[:2], "LC_MESSAGES", dom+"."+ext)
|
||||
if fileExistsIn(filesystem, filename) {
|
||||
return filesystem, filename
|
||||
}
|
||||
}
|
||||
|
||||
filename = path.Join(p, lang, dom+"."+ext)
|
||||
if fileExistsIn(filesystem, filename) {
|
||||
return filesystem, filename
|
||||
}
|
||||
|
||||
if len(lang) > 2 {
|
||||
filename = path.Join(p, lang[:2], dom+"."+ext)
|
||||
if fileExistsIn(filesystem, filename) {
|
||||
return filesystem, filename
|
||||
}
|
||||
}
|
||||
|
||||
return filesystem, ""
|
||||
}
|
||||
|
||||
// GetActualLanguage inspects the filesystem and decides whether to strip
|
||||
// a CC part of the ll_CC locale string.
|
||||
func (l *Locale) GetActualLanguage(dom string) string {
|
||||
p, lang, filesystem := l.snapshotPathLanguageFS()
|
||||
extensions := []string{"po", "mo"}
|
||||
var fp string
|
||||
for _, ext := range extensions {
|
||||
// 'll' (or 'll_CC') exists, and it was specified as-is
|
||||
fp = path.Join(p, lang, "LC_MESSAGES", dom+"."+ext)
|
||||
if fileExistsIn(filesystem, fp) {
|
||||
return lang
|
||||
}
|
||||
// 'll' exists, but 'll_CC' was specified
|
||||
if len(lang) > 2 {
|
||||
fp = path.Join(p, lang[:2], "LC_MESSAGES", dom+"."+ext)
|
||||
if fileExistsIn(filesystem, fp) {
|
||||
return lang[:2]
|
||||
}
|
||||
}
|
||||
// 'll' (or 'll_CC') exists outside of LC_category, and it was specified as-is
|
||||
fp = path.Join(p, lang, dom+"."+ext)
|
||||
if fileExistsIn(filesystem, fp) {
|
||||
return lang
|
||||
}
|
||||
// 'll' exists outside of LC_category, but 'll_CC' was specified
|
||||
if len(lang) > 2 {
|
||||
fp = path.Join(p, lang[:2], dom+"."+ext)
|
||||
if fileExistsIn(filesystem, fp) {
|
||||
return lang[:2]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// AddDomain creates a new domain for a given locale object and initializes the Po object.
|
||||
// If the domain exists, it gets reloaded.
|
||||
func (l *Locale) AddDomain(dom string) {
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var poObj Translator
|
||||
|
||||
file := l.findExt(dom, "po")
|
||||
filesystem, file := l.findExtWithFS(dom, "po")
|
||||
if file != "" {
|
||||
poObj = NewPoFS(l.fs)
|
||||
poObj = NewPoFS(filesystem)
|
||||
// Parse file.
|
||||
poObj.ParseFile(file)
|
||||
} else {
|
||||
file = l.findExt(dom, "mo")
|
||||
filesystem, file = l.findExtWithFS(dom, "mo")
|
||||
if file != "" {
|
||||
poObj = NewMoFS(l.fs)
|
||||
poObj = NewMoFS(filesystem)
|
||||
// Parse file.
|
||||
poObj.ParseFile(file)
|
||||
} else {
|
||||
// fallback return if no file found with
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -212,9 +230,34 @@ func (l *Locale) AddTranslator(dom string, tr Translator) {
|
||||
|
||||
l.Unlock()
|
||||
}
|
||||
func (l *Locale) translatorFor(dom string) Translator {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
l.RLock()
|
||||
translator := l.Domains[dom]
|
||||
l.RUnlock()
|
||||
return translator
|
||||
}
|
||||
|
||||
func (l *Locale) hasDomain(dom string) bool {
|
||||
if l == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
l.RLock()
|
||||
_, ok := l.Domains[dom]
|
||||
l.RUnlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetDomain is the domain getter for Locale configuration
|
||||
func (l *Locale) GetDomain() string {
|
||||
if l == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
l.RLock()
|
||||
dom := l.defaultDomain
|
||||
l.RUnlock()
|
||||
@@ -223,6 +266,10 @@ func (l *Locale) GetDomain() string {
|
||||
|
||||
// SetDomain sets the name for the domain to be used.
|
||||
func (l *Locale) SetDomain(dom string) {
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
|
||||
l.Lock()
|
||||
l.defaultDomain = dom
|
||||
l.Unlock()
|
||||
@@ -230,6 +277,10 @@ func (l *Locale) SetDomain(dom string) {
|
||||
|
||||
// GetLanguage is the lang getter for Locale configuration
|
||||
func (l *Locale) GetLanguage() string {
|
||||
if l == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
l.RLock()
|
||||
lang := l.lang
|
||||
l.RUnlock()
|
||||
@@ -237,51 +288,37 @@ func (l *Locale) GetLanguage() string {
|
||||
}
|
||||
|
||||
// Get uses a domain "default" to return the corresponding Translation of a given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) Get(str string, vars ...interface{}) string {
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) Get(str string, vars ...any) string {
|
||||
return l.GetD(l.GetDomain(), str, vars...)
|
||||
}
|
||||
|
||||
// GetN retrieves the (N)th plural form of Translation for the given string in the "default" domain.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetN(str, plural string, n int, vars ...interface{}) string {
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetN(str, plural string, n int, vars ...any) string {
|
||||
return l.GetND(l.GetDomain(), str, plural, n, vars...)
|
||||
}
|
||||
|
||||
// GetD returns the corresponding Translation in the given domain for the given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetD(dom, str string, vars ...interface{}) string {
|
||||
// Sync read
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
if l.Domains != nil {
|
||||
if _, ok := l.Domains[dom]; ok {
|
||||
if l.Domains[dom] != nil {
|
||||
return l.Domains[dom].Get(str, vars...)
|
||||
}
|
||||
}
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetD(dom, str string, vars ...any) string {
|
||||
translator := l.translatorFor(dom)
|
||||
if translator != nil {
|
||||
return translator.Get(str, vars...)
|
||||
}
|
||||
|
||||
return FormatString(str, vars...)
|
||||
}
|
||||
|
||||
// GetND retrieves the (N)th plural form of Translation in the given domain for the given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetND(dom, str, plural string, n int, vars ...interface{}) string {
|
||||
// Sync read
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
if l.Domains != nil {
|
||||
if _, ok := l.Domains[dom]; ok {
|
||||
if l.Domains[dom] != nil {
|
||||
return l.Domains[dom].GetN(str, plural, n, vars...)
|
||||
}
|
||||
}
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetND(dom, str, plural string, n int, vars ...any) string {
|
||||
translator := l.translatorFor(dom)
|
||||
if translator != nil {
|
||||
return translator.GetN(str, plural, n, vars...)
|
||||
}
|
||||
|
||||
// Use western default rule (plural > 1) to handle missing domain default result.
|
||||
// Use western default rule (n != 1) to handle missing domain default result.
|
||||
if n == 1 {
|
||||
return FormatString(str, vars...)
|
||||
}
|
||||
@@ -289,51 +326,37 @@ func (l *Locale) GetND(dom, str, plural string, n int, vars ...interface{}) stri
|
||||
}
|
||||
|
||||
// GetC uses a domain "default" to return the corresponding Translation of the given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetC(str, ctx string, vars ...interface{}) string {
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetC(str, ctx string, vars ...any) string {
|
||||
return l.GetDC(l.GetDomain(), str, ctx, vars...)
|
||||
}
|
||||
|
||||
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the "default" domain.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetNC(str, plural string, n int, ctx string, vars ...any) string {
|
||||
return l.GetNDC(l.GetDomain(), str, plural, n, ctx, vars...)
|
||||
}
|
||||
|
||||
// GetDC returns the corresponding Translation in the given domain for the given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetDC(dom, str, ctx string, vars ...interface{}) string {
|
||||
// Sync read
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
if l.Domains != nil {
|
||||
if _, ok := l.Domains[dom]; ok {
|
||||
if l.Domains[dom] != nil {
|
||||
return l.Domains[dom].GetC(str, ctx, vars...)
|
||||
}
|
||||
}
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetDC(dom, str, ctx string, vars ...any) string {
|
||||
translator := l.translatorFor(dom)
|
||||
if translator != nil {
|
||||
return translator.GetC(str, ctx, vars...)
|
||||
}
|
||||
|
||||
return FormatString(str, vars...)
|
||||
}
|
||||
|
||||
// GetNDC retrieves the (N)th plural form of Translation in the given domain for the given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||
// Sync read
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
if l.Domains != nil {
|
||||
if _, ok := l.Domains[dom]; ok {
|
||||
if l.Domains[dom] != nil {
|
||||
return l.Domains[dom].GetNC(str, plural, n, ctx, vars...)
|
||||
}
|
||||
}
|
||||
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...any) string {
|
||||
translator := l.translatorFor(dom)
|
||||
if translator != nil {
|
||||
return translator.GetNC(str, plural, n, ctx, vars...)
|
||||
}
|
||||
|
||||
// Use western default rule (plural > 1) to handle missing domain default result.
|
||||
// Use western default rule (n != 1) to handle missing domain default result.
|
||||
if n == 1 {
|
||||
return FormatString(str, vars...)
|
||||
}
|
||||
@@ -343,13 +366,26 @@ func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...inte
|
||||
// GetTranslations returns a copy of all translations in all domains of this locale. It does not support contexts.
|
||||
func (l *Locale) GetTranslations() map[string]*Translation {
|
||||
all := make(map[string]*Translation)
|
||||
if l == nil {
|
||||
return all
|
||||
}
|
||||
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
translators := make([]Translator, 0, len(l.Domains))
|
||||
for _, translator := range l.Domains {
|
||||
for msgID, trans := range translator.GetDomain().GetTranslations() {
|
||||
all[msgID] = trans
|
||||
translators = append(translators, translator)
|
||||
}
|
||||
l.RUnlock()
|
||||
|
||||
for _, translator := range translators {
|
||||
if translator == nil {
|
||||
continue
|
||||
}
|
||||
domain := translator.GetDomain()
|
||||
if domain == nil {
|
||||
continue
|
||||
}
|
||||
maps.Copy(all, domain.GetTranslations())
|
||||
}
|
||||
|
||||
return all
|
||||
@@ -357,7 +393,7 @@ func (l *Locale) GetTranslations() map[string]*Translation {
|
||||
|
||||
// IsTranslated reports whether a string is translated
|
||||
func (l *Locale) IsTranslated(str string) bool {
|
||||
return l.IsTranslatedND(l.GetDomain(), str, 0)
|
||||
return l.IsTranslatedND(l.GetDomain(), str, 1)
|
||||
}
|
||||
|
||||
// IsTranslatedN reports whether a plural string is translated
|
||||
@@ -367,27 +403,25 @@ func (l *Locale) IsTranslatedN(str string, n int) bool {
|
||||
|
||||
// IsTranslatedD reports whether a domain string is translated
|
||||
func (l *Locale) IsTranslatedD(dom, str string) bool {
|
||||
return l.IsTranslatedND(dom, str, 0)
|
||||
return l.IsTranslatedND(dom, str, 1)
|
||||
}
|
||||
|
||||
// IsTranslatedND reports whether a plural domain string is translated
|
||||
func (l *Locale) IsTranslatedND(dom, str string, n int) bool {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
if l.Domains == nil {
|
||||
translator := l.translatorFor(dom)
|
||||
if translator == nil {
|
||||
return false
|
||||
}
|
||||
translator, ok := l.Domains[dom]
|
||||
if !ok {
|
||||
domain := translator.GetDomain()
|
||||
if domain == nil {
|
||||
return false
|
||||
}
|
||||
return translator.GetDomain().IsTranslatedN(str, n)
|
||||
return domain.IsTranslatedN(str, n)
|
||||
}
|
||||
|
||||
// IsTranslatedC reports whether a context string is translated
|
||||
func (l *Locale) IsTranslatedC(str, ctx string) bool {
|
||||
return l.IsTranslatedNDC(l.GetDomain(), str, 0, ctx)
|
||||
return l.IsTranslatedNDC(l.GetDomain(), str, 1, ctx)
|
||||
}
|
||||
|
||||
// IsTranslatedNC reports whether a plural context string is translated
|
||||
@@ -397,22 +431,20 @@ func (l *Locale) IsTranslatedNC(str string, n int, ctx string) bool {
|
||||
|
||||
// IsTranslatedDC reports whether a domain context string is translated
|
||||
func (l *Locale) IsTranslatedDC(dom, str, ctx string) bool {
|
||||
return l.IsTranslatedNDC(dom, str, 0, ctx)
|
||||
return l.IsTranslatedNDC(dom, str, 1, ctx)
|
||||
}
|
||||
|
||||
// IsTranslatedNDC reports whether a plural domain context string is translated
|
||||
func (l *Locale) IsTranslatedNDC(dom string, str string, n int, ctx string) bool {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
if l.Domains == nil {
|
||||
translator := l.translatorFor(dom)
|
||||
if translator == nil {
|
||||
return false
|
||||
}
|
||||
translator, ok := l.Domains[dom]
|
||||
if !ok {
|
||||
domain := translator.GetDomain()
|
||||
if domain == nil {
|
||||
return false
|
||||
}
|
||||
return translator.GetDomain().IsTranslatedNC(str, n, ctx)
|
||||
return domain.IsTranslatedNC(str, n, ctx)
|
||||
}
|
||||
|
||||
// LocaleEncoding is used as intermediary storage to encode Locale objects to Gob.
|
||||
@@ -425,18 +457,35 @@ type LocaleEncoding struct {
|
||||
|
||||
// MarshalBinary implements encoding BinaryMarshaler interface
|
||||
func (l *Locale) MarshalBinary() ([]byte, error) {
|
||||
obj := new(LocaleEncoding)
|
||||
obj.DefaultDomain = l.defaultDomain
|
||||
obj.Domains = make(map[string][]byte)
|
||||
for k, v := range l.Domains {
|
||||
var err error
|
||||
obj.Domains[k], err = v.MarshalBinary()
|
||||
var localePath, lang, defaultDomain string
|
||||
domains := make(map[string]Translator)
|
||||
if l != nil {
|
||||
l.RLock()
|
||||
localePath = l.path
|
||||
lang = l.lang
|
||||
defaultDomain = l.defaultDomain
|
||||
domains = make(map[string]Translator, len(l.Domains))
|
||||
for name, translator := range l.Domains {
|
||||
if translator != nil {
|
||||
domains[name] = translator
|
||||
}
|
||||
}
|
||||
l.RUnlock()
|
||||
}
|
||||
|
||||
obj := &LocaleEncoding{
|
||||
Path: localePath,
|
||||
Lang: lang,
|
||||
Domains: make(map[string][]byte, len(domains)),
|
||||
DefaultDomain: defaultDomain,
|
||||
}
|
||||
for name, translator := range domains {
|
||||
data, err := translator.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
obj.Domains[name] = data
|
||||
}
|
||||
obj.Lang = l.lang
|
||||
obj.Path = l.path
|
||||
|
||||
var buff bytes.Buffer
|
||||
encoder := gob.NewEncoder(&buff)
|
||||
@@ -445,7 +494,7 @@ func (l *Locale) MarshalBinary() ([]byte, error) {
|
||||
return buff.Bytes(), err
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements encoding BinaryUnmarshaler interface
|
||||
// UnmarshalBinary implements encoding.BinaryUnmarshaler interface
|
||||
func (l *Locale) UnmarshalBinary(data []byte) error {
|
||||
buff := bytes.NewBuffer(data)
|
||||
obj := new(LocaleEncoding)
|
||||
@@ -456,12 +505,7 @@ func (l *Locale) UnmarshalBinary(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
l.defaultDomain = obj.DefaultDomain
|
||||
l.lang = obj.Lang
|
||||
l.path = obj.Path
|
||||
|
||||
// Decode Domains
|
||||
l.Domains = make(map[string]Translator)
|
||||
domains := make(map[string]Translator, len(obj.Domains))
|
||||
for k, v := range obj.Domains {
|
||||
var tr TranslatorEncoding
|
||||
buff := bytes.NewBuffer(v)
|
||||
@@ -471,8 +515,15 @@ func (l *Locale) UnmarshalBinary(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Domains[k] = tr.GetTranslator()
|
||||
domains[k] = tr.GetTranslator()
|
||||
}
|
||||
|
||||
l.Lock()
|
||||
l.defaultDomain = obj.DefaultDomain
|
||||
l.lang = obj.Lang
|
||||
l.path = obj.Path
|
||||
l.Domains = domains
|
||||
l.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
site_name: gotext
|
||||
site_description: GNU gettext utilities for Go
|
||||
site_author: Leonel Quinteros
|
||||
repo_url: https://github.com/leonelquinteros/gotext
|
||||
edit_uri: edit/master/docs/
|
||||
|
||||
theme:
|
||||
name: material
|
||||
palette:
|
||||
primary: indigo
|
||||
accent: indigo
|
||||
features:
|
||||
- navigation.tabs
|
||||
- navigation.sections
|
||||
- navigation.top
|
||||
- search.suggest
|
||||
- search.highlight
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Getting Started: GETTING_STARTED.md
|
||||
- User Guides:
|
||||
- Plural Forms: PLURALS.md
|
||||
- xgotext CLI: xgotext.md
|
||||
- Character Encoding: ENCODING.md
|
||||
- Advanced Usage: ADVANCED.md
|
||||
- Best Practices: BEST_PRACTICES.md
|
||||
- API Reference: https://pkg.go.dev/github.com/leonelquinteros/gotext
|
||||
- Contributing:
|
||||
- Guidelines: CONTRIBUTING.md
|
||||
- Code of Conduct: CODE_OF_CONDUCT.md
|
||||
- License: LICENSE.md
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- pymdownx.highlight:
|
||||
anchor_linenums: true
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.snippets
|
||||
- pymdownx.superfences
|
||||
+65
-97
@@ -76,42 +76,42 @@ func (mo *Mo) GetDomain() *Domain {
|
||||
}
|
||||
|
||||
// Get returns the translation for the given string
|
||||
func (mo *Mo) Get(str string, vars ...interface{}) string {
|
||||
func (mo *Mo) Get(str string, vars ...any) string {
|
||||
return mo.domain.Get(str, vars...)
|
||||
}
|
||||
|
||||
// Append a translation string into the domain
|
||||
func (mo *Mo) Append(b []byte, str string, vars ...interface{}) []byte {
|
||||
func (mo *Mo) Append(b []byte, str string, vars ...any) []byte {
|
||||
return mo.domain.Append(b, str, vars...)
|
||||
}
|
||||
|
||||
// GetN returns the translation for the given string and plural form
|
||||
func (mo *Mo) GetN(str, plural string, n int, vars ...interface{}) string {
|
||||
func (mo *Mo) GetN(str, plural string, n int, vars ...any) string {
|
||||
return mo.domain.GetN(str, plural, n, vars...)
|
||||
}
|
||||
|
||||
// AppendN appends a translation string for the given plural form into the domain
|
||||
func (mo *Mo) AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte {
|
||||
func (mo *Mo) AppendN(b []byte, str, plural string, n int, vars ...any) []byte {
|
||||
return mo.domain.AppendN(b, str, plural, n, vars...)
|
||||
}
|
||||
|
||||
// GetC returns the translation for the given string and context
|
||||
func (mo *Mo) GetC(str, ctx string, vars ...interface{}) string {
|
||||
func (mo *Mo) GetC(str, ctx string, vars ...any) string {
|
||||
return mo.domain.GetC(str, ctx, vars...)
|
||||
}
|
||||
|
||||
// AppendC appends a translation string for the given context into the domain
|
||||
func (mo *Mo) AppendC(b []byte, str, ctx string, vars ...interface{}) []byte {
|
||||
func (mo *Mo) AppendC(b []byte, str, ctx string, vars ...any) []byte {
|
||||
return mo.domain.AppendC(b, str, ctx, vars...)
|
||||
}
|
||||
|
||||
// GetNC returns the translation for the given string, plural form and context
|
||||
func (mo *Mo) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||
func (mo *Mo) GetNC(str, plural string, n int, ctx string, vars ...any) string {
|
||||
return mo.domain.GetNC(str, plural, n, ctx, vars...)
|
||||
}
|
||||
|
||||
// AppendNC appends a translation string for the given plural form and context into the domain
|
||||
func (mo *Mo) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte {
|
||||
func (mo *Mo) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...any) []byte {
|
||||
return mo.domain.AppendNC(b, str, plural, n, ctx, vars...)
|
||||
}
|
||||
|
||||
@@ -163,22 +163,18 @@ func (mo *Mo) Parse(buf []byte) {
|
||||
defer mo.domain.trMutex.Unlock()
|
||||
defer mo.domain.pluralMutex.Unlock()
|
||||
|
||||
r := bytes.NewReader(buf)
|
||||
|
||||
var magicNumber uint32
|
||||
if err := binary.Read(r, binary.LittleEndian, &magicNumber); err != nil {
|
||||
if len(buf) < 28 {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
|
||||
var bo binary.ByteOrder
|
||||
switch magicNumber {
|
||||
switch binary.LittleEndian.Uint32(buf[:4]) {
|
||||
case MoMagicLittleEndian:
|
||||
bo = binary.LittleEndian
|
||||
case MoMagicBigEndian:
|
||||
bo = binary.BigEndian
|
||||
default:
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", "invalid magic number")
|
||||
}
|
||||
|
||||
var header struct {
|
||||
@@ -187,82 +183,58 @@ func (mo *Mo) Parse(buf []byte) {
|
||||
MsgIDCount uint32
|
||||
MsgIDOffset uint32
|
||||
MsgStrOffset uint32
|
||||
HashSize uint32
|
||||
HashOffset uint32
|
||||
}
|
||||
if err := binary.Read(r, bo, &header); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
if v := header.MajorVersion; v != 0 && v != 1 {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", "invalid version number")
|
||||
}
|
||||
if v := header.MinorVersion; v != 0 && v != 1 {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", "invalid version number")
|
||||
}
|
||||
|
||||
msgIDStart := make([]uint32, header.MsgIDCount)
|
||||
msgIDLen := make([]uint32, header.MsgIDCount)
|
||||
if _, err := r.Seek(int64(header.MsgIDOffset), 0); err != nil {
|
||||
bufLen := uint64(len(buf))
|
||||
header.MajorVersion = bo.Uint16(buf[4:6])
|
||||
header.MinorVersion = bo.Uint16(buf[6:8])
|
||||
header.MsgIDCount = bo.Uint32(buf[8:12])
|
||||
header.MsgIDOffset = bo.Uint32(buf[12:16])
|
||||
header.MsgStrOffset = bo.Uint32(buf[16:20])
|
||||
|
||||
if header.MajorVersion != 0 && header.MajorVersion != 1 {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
for i := 0; i < int(header.MsgIDCount); i++ {
|
||||
if err := binary.Read(r, bo, &msgIDLen[i]); err != nil {
|
||||
if header.MinorVersion != 0 && header.MinorVersion != 1 {
|
||||
return
|
||||
}
|
||||
|
||||
directorySize := uint64(header.MsgIDCount) * 8
|
||||
if uint64(header.MsgIDOffset)+directorySize > bufLen ||
|
||||
uint64(header.MsgStrOffset)+directorySize > bufLen {
|
||||
return
|
||||
}
|
||||
|
||||
for i := uint32(0); i < header.MsgIDCount; i++ {
|
||||
msgIDEntry := int(uint64(header.MsgIDOffset) + uint64(i)*8)
|
||||
msgIDLen := bo.Uint32(buf[msgIDEntry:])
|
||||
msgIDStart := bo.Uint32(buf[msgIDEntry+4:])
|
||||
if uint64(msgIDStart) > bufLen ||
|
||||
uint64(msgIDLen) > bufLen-uint64(msgIDStart) {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
if err := binary.Read(r, bo, &msgIDStart[i]); err != nil {
|
||||
|
||||
msgStrEntry := int(uint64(header.MsgStrOffset) + uint64(i)*8)
|
||||
msgStrLen := bo.Uint32(buf[msgStrEntry:])
|
||||
msgStrStart := bo.Uint32(buf[msgStrEntry+4:])
|
||||
if uint64(msgStrStart) > bufLen ||
|
||||
uint64(msgStrLen) > bufLen-uint64(msgStrStart) {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
msgStrStart := make([]int32, header.MsgIDCount)
|
||||
msgStrLen := make([]int32, header.MsgIDCount)
|
||||
if _, err := r.Seek(int64(header.MsgStrOffset), 0); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
for i := 0; i < int(header.MsgIDCount); i++ {
|
||||
if err := binary.Read(r, bo, &msgStrLen[i]); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
if err := binary.Read(r, bo, &msgStrStart[i]); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
}
|
||||
for i := uint32(0); i < header.MsgIDCount; i++ {
|
||||
msgIDEntry := int(uint64(header.MsgIDOffset) + uint64(i)*8)
|
||||
msgIDLen := bo.Uint32(buf[msgIDEntry:])
|
||||
msgIDStart := bo.Uint32(buf[msgIDEntry+4:])
|
||||
msgIDEnd := int(uint64(msgIDStart) + uint64(msgIDLen))
|
||||
|
||||
for i := 0; i < int(header.MsgIDCount); i++ {
|
||||
if _, err := r.Seek(int64(msgIDStart[i]), 0); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
msgIDData := make([]byte, msgIDLen[i])
|
||||
if _, err := r.Read(msgIDData); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
msgStrEntry := int(uint64(header.MsgStrOffset) + uint64(i)*8)
|
||||
msgStrLen := bo.Uint32(buf[msgStrEntry:])
|
||||
msgStrStart := bo.Uint32(buf[msgStrEntry+4:])
|
||||
msgStrEnd := int(uint64(msgStrStart) + uint64(msgStrLen))
|
||||
|
||||
if _, err := r.Seek(int64(msgStrStart[i]), 0); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
msgStrData := make([]byte, msgStrLen[i])
|
||||
if _, err := r.Read(msgStrData); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
|
||||
if len(msgIDData) == 0 {
|
||||
mo.addTranslation(msgIDData, msgStrData)
|
||||
} else {
|
||||
mo.addTranslation(msgIDData, msgStrData)
|
||||
}
|
||||
mo.addTranslation(buf[int(msgIDStart):msgIDEnd], buf[int(msgStrStart):msgStrEnd])
|
||||
}
|
||||
|
||||
// Parse headers
|
||||
@@ -279,36 +251,32 @@ func (mo *Mo) addTranslation(msgid, msgstr []byte) {
|
||||
translation := NewTranslation()
|
||||
var msgctxt []byte
|
||||
var msgidPlural []byte
|
||||
hasContext := false
|
||||
|
||||
d := bytes.Split(msgid, []byte(EotSeparator))
|
||||
if len(d) == 1 {
|
||||
msgid = d[0]
|
||||
} else {
|
||||
msgid, msgctxt = d[1], d[0]
|
||||
if eotIndex := bytes.IndexByte(msgid, EotSeparator[0]); eotIndex >= 0 {
|
||||
hasContext = true
|
||||
msgctxt = msgid[:eotIndex]
|
||||
msgid = msgid[eotIndex+len(EotSeparator):]
|
||||
}
|
||||
|
||||
dd := bytes.Split(msgid, []byte(NulSeparator))
|
||||
if len(dd) > 1 {
|
||||
msgid = dd[0]
|
||||
dd = dd[1:]
|
||||
if nulIndex := bytes.IndexByte(msgid, NulSeparator[0]); nulIndex >= 0 {
|
||||
msgidPlural = msgid[nulIndex+1:]
|
||||
msgid = msgid[:nulIndex]
|
||||
}
|
||||
|
||||
translation.ID = string(msgid)
|
||||
|
||||
msgidPlural = bytes.Join(dd, []byte(NulSeparator))
|
||||
if len(msgidPlural) > 0 {
|
||||
translation.PluralID = string(msgidPlural)
|
||||
}
|
||||
|
||||
ddd := bytes.Split(msgstr, []byte(NulSeparator))
|
||||
if len(ddd) > 0 {
|
||||
for i, s := range ddd {
|
||||
translation.Trs[i] = string(s)
|
||||
}
|
||||
i := 0
|
||||
for s := range bytes.SplitSeq(msgstr, []byte(NulSeparator)) {
|
||||
translation.Trs[i] = string(s)
|
||||
i++
|
||||
}
|
||||
|
||||
if len(msgctxt) > 0 {
|
||||
// With context...
|
||||
if hasContext {
|
||||
// With context, including an explicitly empty context...
|
||||
if _, ok := mo.domain.contextTranslations[string(msgctxt)]; !ok {
|
||||
mo.domain.contextTranslations[string(msgctxt)] = make(map[string]*Translation)
|
||||
}
|
||||
|
||||
+362
-178
@@ -12,18 +12,12 @@ package plurals
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type match struct {
|
||||
openPos int
|
||||
closePos int
|
||||
}
|
||||
|
||||
var pat = regexp.MustCompile(`(\?|:|\|\||&&|==|!=|>=|>|<=|<|%|\d+|n)`)
|
||||
|
||||
type testToken interface {
|
||||
compile(tokens []string) (test test, err error)
|
||||
}
|
||||
@@ -31,30 +25,30 @@ type testToken interface {
|
||||
type cmpTestBuilder func(val uint32, flipped bool) test
|
||||
type logicTestBuild func(left test, right test) test
|
||||
|
||||
var ternaryToken ternaryStruct
|
||||
func compileTernary(tokens []string, depth int) (expr Expression, err error) {
|
||||
if depth > maxParseDepth {
|
||||
return nil, errors.New("expression nesting is too deep")
|
||||
}
|
||||
|
||||
type ternaryStruct struct{}
|
||||
|
||||
func (ternaryStruct) compile(tokens []string) (expr Expression, err error) {
|
||||
main, err := splitTokens(tokens, "?")
|
||||
if err != nil {
|
||||
return expr, err
|
||||
return nil, err
|
||||
}
|
||||
test, err := compileTest(strings.Join(main.Left, ""))
|
||||
test, err := compileTestDepth(strings.Join(main.Left, ""), depth+1)
|
||||
if err != nil {
|
||||
return expr, err
|
||||
return nil, err
|
||||
}
|
||||
actions, err := splitTokens(main.Right, ":")
|
||||
actions, err := splitTernaryTokens(main.Right)
|
||||
if err != nil {
|
||||
return expr, err
|
||||
return nil, err
|
||||
}
|
||||
trueAction, err := compileExpression(strings.Join(actions.Left, ""))
|
||||
trueAction, err := compileExpressionDepth(strings.Join(actions.Left, ""), depth+1)
|
||||
if err != nil {
|
||||
return expr, err
|
||||
return nil, err
|
||||
}
|
||||
falseAction, err := compileExpression(strings.Join(actions.Right, ""))
|
||||
falseAction, err := compileExpressionDepth(strings.Join(actions.Right, ""), depth+1)
|
||||
if err != nil {
|
||||
return expr, nil
|
||||
return nil, err
|
||||
}
|
||||
return ternary{
|
||||
test: test,
|
||||
@@ -63,93 +57,145 @@ func (ternaryStruct) compile(tokens []string) (expr Expression, err error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
const maxParseDepth = 1024
|
||||
|
||||
func splitTernaryTokens(tokens []string) (s splitted, err error) {
|
||||
depth := 0
|
||||
for index, token := range tokens {
|
||||
switch token {
|
||||
case "?":
|
||||
depth++
|
||||
case ":":
|
||||
if depth == 0 {
|
||||
return splitted{
|
||||
Left: tokens[:index],
|
||||
Right: tokens[index+1:],
|
||||
}, nil
|
||||
}
|
||||
depth--
|
||||
}
|
||||
}
|
||||
return s, errors.New("':' not found for ternary expression")
|
||||
}
|
||||
|
||||
var constToken constValStruct
|
||||
|
||||
type constValStruct struct{}
|
||||
|
||||
func (constValStruct) compile(tokens []string) (expr Expression, err error) {
|
||||
if len(tokens) == 0 {
|
||||
return expr, errors.New("got nothing instead of constant")
|
||||
return nil, errors.New("got nothing instead of constant")
|
||||
}
|
||||
if len(tokens) != 1 {
|
||||
return expr, fmt.Errorf("invalid constant: %s", strings.Join(tokens, ""))
|
||||
return nil, fmt.Errorf("invalid constant: %s", strings.Join(tokens, ""))
|
||||
}
|
||||
i, err := strconv.Atoi(tokens[0])
|
||||
if err != nil {
|
||||
return expr, err
|
||||
return nil, err
|
||||
}
|
||||
return constValue{value: i}, nil
|
||||
}
|
||||
|
||||
func compileLogicTest(tokens []string, sep string, builder logicTestBuild) (test test, err error) {
|
||||
func compileLogicTestDepth(tokens []string, sep string, builder logicTestBuild, depth int) (test test, err error) {
|
||||
if depth > maxParseDepth {
|
||||
return nil, errors.New("expression nesting is too deep")
|
||||
}
|
||||
split, err := splitTokens(tokens, sep)
|
||||
if err != nil {
|
||||
return test, err
|
||||
return nil, err
|
||||
}
|
||||
left, err := compileTest(strings.Join(split.Left, ""))
|
||||
left, err := compileTestDepth(strings.Join(split.Left, ""), depth+1)
|
||||
if err != nil {
|
||||
return test, err
|
||||
return nil, err
|
||||
}
|
||||
right, err := compileTest(strings.Join(split.Right, ""))
|
||||
right, err := compileTestDepth(strings.Join(split.Right, ""), depth+1)
|
||||
if err != nil {
|
||||
return test, err
|
||||
return nil, err
|
||||
}
|
||||
return builder(left, right), nil
|
||||
}
|
||||
|
||||
var orToken orStruct
|
||||
|
||||
type orStruct struct{}
|
||||
|
||||
func (orStruct) compile(tokens []string) (test test, err error) {
|
||||
return compileLogicTest(tokens, "||", buildOr)
|
||||
}
|
||||
func buildOr(left test, right test) test {
|
||||
return or{left: left, right: right}
|
||||
}
|
||||
|
||||
var andToken andStruct
|
||||
|
||||
type andStruct struct{}
|
||||
|
||||
func (andStruct) compile(tokens []string) (test test, err error) {
|
||||
return compileLogicTest(tokens, "&&", buildAnd)
|
||||
}
|
||||
func buildAnd(left test, right test) test {
|
||||
return and{left: left, right: right}
|
||||
}
|
||||
|
||||
func unwrapSingleGroup(tokens []string) ([]string, error) {
|
||||
for len(tokens) == 1 && strings.HasPrefix(tokens[0], "(") {
|
||||
inner, err := tokenize(tokens[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(inner) == 1 && inner[0] == tokens[0] {
|
||||
break
|
||||
}
|
||||
tokens = inner
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
func isSimpleN(tokens []string) bool {
|
||||
tokens, err := unwrapSingleGroup(tokens)
|
||||
if err != nil || len(tokens) != 1 {
|
||||
return false
|
||||
}
|
||||
return tokens[0] == "n"
|
||||
}
|
||||
|
||||
func parseLiteralTokens(tokens []string) (uint32, error) {
|
||||
tokens, err := unwrapSingleGroup(tokens)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(tokens) != 1 {
|
||||
return 0, errors.New("expected a single integer")
|
||||
}
|
||||
return parseUint32(tokens[0])
|
||||
}
|
||||
|
||||
func containsSimpleN(tokens []string) bool {
|
||||
for _, token := range tokens {
|
||||
if isSimpleN([]string{token}) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func compileMod(tokens []string) (math math, err error) {
|
||||
split, err := splitTokens(tokens, "%")
|
||||
if err != nil {
|
||||
return math, err
|
||||
}
|
||||
if len(split.Left) != 1 || split.Left[0] != "n" {
|
||||
if !isSimpleN(split.Left) {
|
||||
return math, errors.New("modulus operation requires 'n' as left operand")
|
||||
}
|
||||
if len(split.Right) != 1 {
|
||||
return math, errors.New("modulus operation requires simple integer as right operand")
|
||||
}
|
||||
i, err := parseUint32(split.Right[0])
|
||||
i, err := parseLiteralTokens(split.Right)
|
||||
if err != nil {
|
||||
return math, err
|
||||
}
|
||||
return mod{value: uint32(i)}, nil
|
||||
if i == 0 {
|
||||
return math, errors.New("modulus operation requires a non-zero integer as right operand")
|
||||
}
|
||||
return mod{value: i}, nil
|
||||
}
|
||||
|
||||
func subPipe(modTokens []string, actionTokens []string, builder cmpTestBuilder, flipped bool) (test test, err error) {
|
||||
modifier, err := compileMod(modTokens)
|
||||
if err != nil {
|
||||
return test, err
|
||||
return nil, err
|
||||
}
|
||||
if len(actionTokens) != 1 {
|
||||
return test, errors.New("can only get modulus of integer")
|
||||
if len(actionTokens) == 0 {
|
||||
return nil, errors.New("can only get modulus of integer")
|
||||
}
|
||||
i, err := parseUint32(actionTokens[0])
|
||||
i, err := parseLiteralTokens(actionTokens)
|
||||
if err != nil {
|
||||
return test, err
|
||||
return nil, err
|
||||
}
|
||||
action := builder(uint32(i), flipped)
|
||||
action := builder(i, flipped)
|
||||
return pipe{
|
||||
modifier: modifier,
|
||||
action: action,
|
||||
@@ -159,31 +205,37 @@ func subPipe(modTokens []string, actionTokens []string, builder cmpTestBuilder,
|
||||
func compileEquality(tokens []string, sep string, builder cmpTestBuilder) (test test, err error) {
|
||||
split, err := splitTokens(tokens, sep)
|
||||
if err != nil {
|
||||
return test, err
|
||||
return nil, err
|
||||
}
|
||||
if len(split.Left) == 1 && split.Left[0] == "n" {
|
||||
if len(split.Right) != 1 {
|
||||
return test, errors.New("test can only compare n to integers")
|
||||
}
|
||||
i, err := parseUint32(split.Right[0])
|
||||
left, err := unwrapSingleGroup(split.Left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
right, err := unwrapSingleGroup(split.Right)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isSimpleN(left) {
|
||||
i, err := parseLiteralTokens(right)
|
||||
if err != nil {
|
||||
return test, err
|
||||
return nil, err
|
||||
}
|
||||
return builder(i, false), nil
|
||||
} else if len(split.Right) == 1 && split.Right[0] == "n" {
|
||||
if len(split.Left) != 1 {
|
||||
return test, errors.New("test can only compare n to integers")
|
||||
}
|
||||
i, err := parseUint32(split.Left[0])
|
||||
}
|
||||
if isSimpleN(right) {
|
||||
i, err := parseLiteralTokens(left)
|
||||
if err != nil {
|
||||
return test, err
|
||||
return nil, err
|
||||
}
|
||||
return builder(i, true), nil
|
||||
} else if contains(split.Left, "n") && contains(split.Left, "%") {
|
||||
return subPipe(split.Left, split.Right, builder, false)
|
||||
}
|
||||
return test, errors.New("equality test must have 'n' as one of the two tests")
|
||||
|
||||
if containsSimpleN(left) && slices.Contains(left, "%") {
|
||||
return subPipe(left, right, builder, false)
|
||||
}
|
||||
if containsSimpleN(right) && slices.Contains(right, "%") {
|
||||
return subPipe(right, left, builder, true)
|
||||
}
|
||||
return nil, errors.New("equality test must have 'n' as one of the two tests")
|
||||
}
|
||||
|
||||
var eqToken eqStruct
|
||||
@@ -258,8 +310,8 @@ type testTokenDef struct {
|
||||
}
|
||||
|
||||
var precedence = []testTokenDef{
|
||||
{op: "||", token: orToken},
|
||||
{op: "&&", token: andToken},
|
||||
{op: "||"},
|
||||
{op: "&&"},
|
||||
{op: "==", token: eqToken},
|
||||
{op: "!=", token: neqToken},
|
||||
{op: ">=", token: gteToken},
|
||||
@@ -273,20 +325,10 @@ type splitted struct {
|
||||
Right []string
|
||||
}
|
||||
|
||||
// Find index of token in list of tokens
|
||||
func index(tokens []string, sep string) int {
|
||||
for index, token := range tokens {
|
||||
if token == sep {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Split a list of tokens by a token into a splitted struct holding the tokens
|
||||
// before and after the token to be split by.
|
||||
func splitTokens(tokens []string, sep string) (s splitted, err error) {
|
||||
index := index(tokens, sep)
|
||||
index := slices.Index(tokens, sep)
|
||||
if index == -1 {
|
||||
return s, fmt.Errorf("'%s' not found in ['%s']", sep, strings.Join(tokens, "','"))
|
||||
}
|
||||
@@ -296,135 +338,277 @@ func splitTokens(tokens []string, sep string) (s splitted, err error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Scan a string for parenthesis
|
||||
func scan(s string) <-chan match {
|
||||
ch := make(chan match)
|
||||
go func() {
|
||||
depth := 0
|
||||
opener := 0
|
||||
for index, char := range s {
|
||||
switch char {
|
||||
case '(':
|
||||
if depth == 0 {
|
||||
opener = index
|
||||
}
|
||||
depth++
|
||||
case ')':
|
||||
depth--
|
||||
if depth == 0 {
|
||||
ch <- match{
|
||||
openPos: opener,
|
||||
closePos: index + 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func trimLexicalWhitespace(s string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if unicode.IsSpace(r) {
|
||||
return -1
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
return ch
|
||||
return r
|
||||
}, s)
|
||||
}
|
||||
|
||||
// Split the string into tokens
|
||||
func split(s string) <-chan string {
|
||||
ch := make(chan string)
|
||||
go func() {
|
||||
s = strings.ReplaceAll(s, " ", "")
|
||||
if !strings.Contains(s, "(") {
|
||||
ch <- s
|
||||
} else {
|
||||
last := 0
|
||||
end := len(s)
|
||||
for info := range scan(s) {
|
||||
if last != info.openPos {
|
||||
ch <- s[last:info.openPos]
|
||||
}
|
||||
ch <- s[info.openPos:info.closePos]
|
||||
last = info.closePos
|
||||
func lexicalToken(s string, index int) (token string, next int, err error) {
|
||||
if index >= len(s) {
|
||||
return "", index, errors.New("unexpected end of expression")
|
||||
}
|
||||
|
||||
switch s[index] {
|
||||
case '?', ':', '%', '(', ')':
|
||||
return s[index : index+1], index + 1, nil
|
||||
case '|':
|
||||
if index+1 >= len(s) || s[index+1] != '|' {
|
||||
return "", index, errors.New("logical OR must use '||'")
|
||||
}
|
||||
return "||", index + 2, nil
|
||||
case '&':
|
||||
if index+1 >= len(s) || s[index+1] != '&' {
|
||||
return "", index, errors.New("logical AND must use '&&'")
|
||||
}
|
||||
return "&&", index + 2, nil
|
||||
case '=':
|
||||
if index+1 >= len(s) || s[index+1] != '=' {
|
||||
return "", index, errors.New("equality must use '=='")
|
||||
}
|
||||
return "==", index + 2, nil
|
||||
case '!':
|
||||
if index+1 >= len(s) || s[index+1] != '=' {
|
||||
return "", index, errors.New("inequality must use '!='")
|
||||
}
|
||||
return "!=", index + 2, nil
|
||||
case '>':
|
||||
if index+1 < len(s) && s[index+1] == '=' {
|
||||
return ">=", index + 2, nil
|
||||
}
|
||||
return ">", index + 1, nil
|
||||
case '<':
|
||||
if index+1 < len(s) && s[index+1] == '=' {
|
||||
return "<=", index + 2, nil
|
||||
}
|
||||
return "<", index + 1, nil
|
||||
case 'n':
|
||||
return "n", index + 1, nil
|
||||
default:
|
||||
if s[index] >= '0' && s[index] <= '9' {
|
||||
next = index + 1
|
||||
for next < len(s) && s[next] >= '0' && s[next] <= '9' {
|
||||
next++
|
||||
}
|
||||
if last != end {
|
||||
ch <- s[last:]
|
||||
return s[index:next], next, nil
|
||||
}
|
||||
return "", index, fmt.Errorf("invalid character %q in expression", s[index])
|
||||
}
|
||||
}
|
||||
|
||||
func validateParentheses(s string) error {
|
||||
depth := 0
|
||||
for index := 0; index < len(s); {
|
||||
token, next, err := lexicalToken(s, index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch token {
|
||||
case "(":
|
||||
depth++
|
||||
case ")":
|
||||
depth--
|
||||
if depth < 0 {
|
||||
return errors.New("unmatched closing parenthesis")
|
||||
}
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
return ch
|
||||
index = next
|
||||
}
|
||||
if depth != 0 {
|
||||
return errors.New("unmatched opening parenthesis")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stripRedundantOuterParens(s string) string {
|
||||
if len(s) < 2 || s[0] != '(' {
|
||||
return s
|
||||
}
|
||||
|
||||
leading := 0
|
||||
for leading < len(s) && s[leading] == '(' {
|
||||
leading++
|
||||
}
|
||||
|
||||
openers := make([]int, 0, leading)
|
||||
outer := leading
|
||||
for index := range s {
|
||||
switch s[index] {
|
||||
case '(':
|
||||
openers = append(openers, index)
|
||||
case ')':
|
||||
if len(openers) == 0 {
|
||||
return s
|
||||
}
|
||||
opener := openers[len(openers)-1]
|
||||
openers = openers[:len(openers)-1]
|
||||
if opener < outer && index != len(s)-1-opener {
|
||||
outer = opener
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(openers) != 0 {
|
||||
return s
|
||||
}
|
||||
return s[outer : len(s)-outer]
|
||||
}
|
||||
|
||||
// Tokenizes a string into a list of strings, tokens grouped by parenthesis are
|
||||
// not split! If the string starts with ( and ends in ), those are stripped.
|
||||
func tokenize(s string) []string {
|
||||
/*
|
||||
TODO: Properly detect if the string starts with a ( and ends with a )
|
||||
and that those two form a matching pair.
|
||||
func tokenize(s string) ([]string, error) {
|
||||
s = trimLexicalWhitespace(s)
|
||||
if s == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
if err := validateParentheses(s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s = stripRedundantOuterParens(s)
|
||||
if s == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
Eg: (foo) -> true; (foo)(bar) -> false;
|
||||
*/
|
||||
if len(s) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
if s[0] == '(' && s[len(s)-1] == ')' {
|
||||
s = s[1 : len(s)-1]
|
||||
}
|
||||
ret := []string{}
|
||||
for chunk := range split(s) {
|
||||
if len(chunk) != 0 {
|
||||
if chunk[0] == '(' && chunk[len(chunk)-1] == ')' {
|
||||
ret = append(ret, chunk)
|
||||
} else {
|
||||
for _, token := range pat.FindAllStringSubmatch(chunk, -1) {
|
||||
ret = append(ret, token[0])
|
||||
for index := 0; index < len(s); {
|
||||
if s[index] == '(' {
|
||||
start := index
|
||||
depth := 0
|
||||
groupEnd := false
|
||||
for index < len(s) {
|
||||
switch s[index] {
|
||||
case '(':
|
||||
depth++
|
||||
case ')':
|
||||
depth--
|
||||
if depth == 0 {
|
||||
index++
|
||||
ret = append(ret, s[start:index])
|
||||
groupEnd = true
|
||||
}
|
||||
}
|
||||
if groupEnd {
|
||||
break
|
||||
}
|
||||
index++
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Empty chunk in string '%s'\n", s)
|
||||
if depth != 0 {
|
||||
return nil, errors.New("unmatched opening parenthesis")
|
||||
}
|
||||
continue
|
||||
}
|
||||
token, next, err := lexicalToken(s, index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token == ")" {
|
||||
return nil, errors.New("unmatched closing parenthesis")
|
||||
}
|
||||
ret = append(ret, token)
|
||||
index = next
|
||||
}
|
||||
return ret
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Compile a string containing a plural form expression to a Expression object.
|
||||
func Compile(s string) (expr Expression, err error) {
|
||||
if s == "0" {
|
||||
return constValue{value: 0}, nil
|
||||
s = trimLexicalWhitespace(s)
|
||||
if s == "" {
|
||||
return nil, errors.New("empty expression")
|
||||
}
|
||||
if !strings.Contains(s, "?") {
|
||||
s += "?1:0"
|
||||
}
|
||||
return compileExpression(s)
|
||||
return compileExpressionDepth(s, 0)
|
||||
}
|
||||
|
||||
// Check if a token is in a slice of strings
|
||||
func contains(haystack []string, needle string) bool {
|
||||
for _, s := range haystack {
|
||||
if s == needle {
|
||||
return true
|
||||
// Compiles an expression (ternary, numeric leaf, or test).
|
||||
func compileExpressionDepth(s string, depth int) (expr Expression, err error) {
|
||||
if depth > maxParseDepth {
|
||||
return nil, errors.New("expression nesting is too deep")
|
||||
}
|
||||
tokens, err := tokenize(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if slices.Contains(tokens, "?") {
|
||||
return compileTernary(tokens, depth)
|
||||
}
|
||||
for _, tokenDef := range precedence {
|
||||
if !slices.Contains(tokens, tokenDef.op) {
|
||||
continue
|
||||
}
|
||||
condition, err := compileTestDepth(s, depth+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return testValue{condition: condition}, nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Compiles an expression (ternary or constant)
|
||||
func compileExpression(s string) (expr Expression, err error) {
|
||||
tokens := tokenize(s)
|
||||
if contains(tokens, "?") {
|
||||
return ternaryToken.compile(tokens)
|
||||
if isSimpleN(tokens) {
|
||||
return variableValue{}, nil
|
||||
}
|
||||
if slices.Contains(tokens, "%") {
|
||||
modifier, err := compileMod(tokens)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mathValue{value: modifier}, nil
|
||||
}
|
||||
return constToken.compile(tokens)
|
||||
}
|
||||
|
||||
// Compiles a test (comparison)
|
||||
func compileTest(s string) (test test, err error) {
|
||||
tokens := tokenize(s)
|
||||
func compileTestDepth(s string, depth int) (test test, err error) {
|
||||
if depth > maxParseDepth {
|
||||
return nil, errors.New("expression nesting is too deep")
|
||||
}
|
||||
tokens, err := tokenize(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, tokenDef := range precedence {
|
||||
if contains(tokens, tokenDef.op) {
|
||||
if !slices.Contains(tokens, tokenDef.op) {
|
||||
continue
|
||||
}
|
||||
switch tokenDef.op {
|
||||
case "||":
|
||||
return compileLogicTestDepth(tokens, "||", buildOr, depth)
|
||||
case "&&":
|
||||
return compileLogicTestDepth(tokens, "&&", buildAnd, depth)
|
||||
default:
|
||||
return tokenDef.token.compile(tokens)
|
||||
}
|
||||
}
|
||||
return test, errors.New("cannot compile")
|
||||
if slices.Contains(tokens, "%") {
|
||||
m, err := compileMod(tokens)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pipe{
|
||||
modifier: m,
|
||||
action: notequal{value: 0},
|
||||
}, nil
|
||||
}
|
||||
if len(tokens) == 1 && strings.HasPrefix(tokens[0], "(") {
|
||||
inner, err := tokenize(tokens[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(inner) == 1 && inner[0] == tokens[0] {
|
||||
return nil, errors.New("cannot compile parenthesized test")
|
||||
}
|
||||
return compileTestDepth(tokens[0], depth+1)
|
||||
}
|
||||
if len(tokens) == 1 && tokens[0] == "n" {
|
||||
return notequal{value: 0}, nil
|
||||
}
|
||||
if value, err := parseLiteralTokens(tokens); err == nil {
|
||||
return literalTest{value: value}, nil
|
||||
}
|
||||
return nil, errors.New("cannot compile")
|
||||
}
|
||||
|
||||
func parseUint32(s string) (ui uint32, err error) {
|
||||
s = trimLexicalWhitespace(s)
|
||||
i, err := strconv.ParseUint(s, 10, 32)
|
||||
if err != nil {
|
||||
return ui, err
|
||||
|
||||
+34
@@ -20,6 +20,37 @@ func (c constValue) Eval(n uint32) int {
|
||||
return c.value
|
||||
}
|
||||
|
||||
type variableValue struct{}
|
||||
|
||||
func (variableValue) Eval(n uint32) int {
|
||||
return int(n)
|
||||
}
|
||||
|
||||
type mathValue struct {
|
||||
value math
|
||||
}
|
||||
|
||||
func (m mathValue) Eval(n uint32) int {
|
||||
if m.value == nil {
|
||||
return -1
|
||||
}
|
||||
return int(m.value.calc(n))
|
||||
}
|
||||
|
||||
type testValue struct {
|
||||
condition test
|
||||
}
|
||||
|
||||
func (t testValue) Eval(n uint32) int {
|
||||
if t.condition == nil {
|
||||
return -1
|
||||
}
|
||||
if t.condition.test(n) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type test interface {
|
||||
test(n uint32) bool
|
||||
}
|
||||
@@ -31,6 +62,9 @@ type ternary struct {
|
||||
}
|
||||
|
||||
func (t ternary) Eval(n uint32) int {
|
||||
if t.test == nil {
|
||||
return -1
|
||||
}
|
||||
if t.test.test(n) {
|
||||
if t.trueExpr == nil {
|
||||
return -1
|
||||
|
||||
+3
@@ -15,5 +15,8 @@ type mod struct {
|
||||
}
|
||||
|
||||
func (m mod) calc(n uint32) uint32 {
|
||||
if m.value == 0 {
|
||||
return 0
|
||||
}
|
||||
return n % m.value
|
||||
}
|
||||
|
||||
+17
@@ -22,6 +22,14 @@ func (e notequal) test(n uint32) bool {
|
||||
return n != e.value
|
||||
}
|
||||
|
||||
type literalTest struct {
|
||||
value uint32
|
||||
}
|
||||
|
||||
func (l literalTest) test(n uint32) bool {
|
||||
return l.value != 0
|
||||
}
|
||||
|
||||
type gt struct {
|
||||
value uint32
|
||||
flipped bool
|
||||
@@ -77,6 +85,9 @@ type and struct {
|
||||
}
|
||||
|
||||
func (e and) test(n uint32) bool {
|
||||
if e.left == nil || e.right == nil {
|
||||
return false
|
||||
}
|
||||
if !e.left.test(n) {
|
||||
return false
|
||||
}
|
||||
@@ -89,6 +100,9 @@ type or struct {
|
||||
}
|
||||
|
||||
func (e or) test(n uint32) bool {
|
||||
if e.left == nil || e.right == nil {
|
||||
return false
|
||||
}
|
||||
if e.left.test(n) {
|
||||
return true
|
||||
}
|
||||
@@ -101,5 +115,8 @@ type pipe struct {
|
||||
}
|
||||
|
||||
func (e pipe) test(n uint32) bool {
|
||||
if e.modifier == nil || e.action == nil {
|
||||
return false
|
||||
}
|
||||
return e.action.test(e.modifier.calc(n))
|
||||
}
|
||||
|
||||
+305
-78
@@ -42,6 +42,10 @@ type Po struct {
|
||||
|
||||
domain *Domain
|
||||
fs fs.FS
|
||||
|
||||
parseBufferHasID bool
|
||||
parseBufferHasPlural bool
|
||||
parseBufferHasContext bool
|
||||
}
|
||||
|
||||
type parseState int
|
||||
@@ -52,6 +56,7 @@ const (
|
||||
msgID
|
||||
msgIDPlural
|
||||
msgStr
|
||||
msgStrInvalid
|
||||
)
|
||||
|
||||
// NewPo should always be used to instantiate a new Po object
|
||||
@@ -94,7 +99,7 @@ func (po *Po) GetRefs(str string) []string {
|
||||
|
||||
// SetPluralResolver sets the plural resolver function
|
||||
func (po *Po) SetPluralResolver(f func(int) int) {
|
||||
po.domain.customPluralResolver = f
|
||||
po.domain.SetPluralResolver(f)
|
||||
}
|
||||
|
||||
// Set translation
|
||||
@@ -103,12 +108,12 @@ func (po *Po) Set(id, str string) {
|
||||
}
|
||||
|
||||
// Get translation
|
||||
func (po *Po) Get(str string, vars ...interface{}) string {
|
||||
func (po *Po) Get(str string, vars ...any) string {
|
||||
return po.domain.Get(str, vars...)
|
||||
}
|
||||
|
||||
// Append translation
|
||||
func (po *Po) Append(b []byte, str string, vars ...interface{}) []byte {
|
||||
func (po *Po) Append(b []byte, str string, vars ...any) []byte {
|
||||
return po.domain.Append(b, str, vars...)
|
||||
}
|
||||
|
||||
@@ -118,12 +123,12 @@ func (po *Po) SetN(id, plural string, n int, str string) {
|
||||
}
|
||||
|
||||
// GetN gets the plural translation
|
||||
func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string {
|
||||
func (po *Po) GetN(str, plural string, n int, vars ...any) string {
|
||||
return po.domain.GetN(str, plural, n, vars...)
|
||||
}
|
||||
|
||||
// AppendN appends the plural translation
|
||||
func (po *Po) AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte {
|
||||
func (po *Po) AppendN(b []byte, str, plural string, n int, vars ...any) []byte {
|
||||
return po.domain.AppendN(b, str, plural, n, vars...)
|
||||
}
|
||||
|
||||
@@ -133,12 +138,12 @@ func (po *Po) SetC(id, ctx, str string) {
|
||||
}
|
||||
|
||||
// GetC gets the translation for a given context
|
||||
func (po *Po) GetC(str, ctx string, vars ...interface{}) string {
|
||||
func (po *Po) GetC(str, ctx string, vars ...any) string {
|
||||
return po.domain.GetC(str, ctx, vars...)
|
||||
}
|
||||
|
||||
// AppendC appends the translation for a given context
|
||||
func (po *Po) AppendC(b []byte, str, ctx string, vars ...interface{}) []byte {
|
||||
func (po *Po) AppendC(b []byte, str, ctx string, vars ...any) []byte {
|
||||
return po.domain.AppendC(b, str, ctx, vars...)
|
||||
}
|
||||
|
||||
@@ -148,12 +153,12 @@ func (po *Po) SetNC(id, plural, ctx string, n int, str string) {
|
||||
}
|
||||
|
||||
// GetNC gets the plural translation for a given context
|
||||
func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||
func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...any) string {
|
||||
return po.domain.GetNC(str, plural, n, ctx, vars...)
|
||||
}
|
||||
|
||||
// AppendNC appends the plural translation for a given context
|
||||
func (po *Po) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte {
|
||||
func (po *Po) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...any) []byte {
|
||||
return po.domain.AppendNC(b, str, plural, n, ctx, vars...)
|
||||
}
|
||||
|
||||
@@ -214,16 +219,17 @@ func (po *Po) Parse(buf []byte) {
|
||||
defer po.domain.trMutex.Unlock()
|
||||
defer po.domain.pluralMutex.Unlock()
|
||||
|
||||
// Get lines
|
||||
lines := strings.Split(string(buf), "\n")
|
||||
|
||||
// Init buffer
|
||||
po.domain.trBuffer = NewTranslation()
|
||||
po.domain.ctxBuffer = ""
|
||||
po.domain.refBuffer = ""
|
||||
po.parseBufferHasID = false
|
||||
po.parseBufferHasPlural = false
|
||||
po.parseBufferHasContext = false
|
||||
|
||||
state := head
|
||||
for _, l := range lines {
|
||||
activeMsgStrIndex := 0
|
||||
for l := range strings.Lines(string(buf)) {
|
||||
// Trim spaces
|
||||
l = strings.TrimSpace(l)
|
||||
|
||||
@@ -233,44 +239,71 @@ func (po *Po) Parse(buf []byte) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Buffer context and continue
|
||||
if strings.HasPrefix(l, "msgctxt") {
|
||||
po.parseContext(l)
|
||||
state = msgCtxt
|
||||
// Multi line strings and headers
|
||||
if strings.HasPrefix(l, "\"") {
|
||||
if !po.parseString(l, state, activeMsgStrIndex) && state != head {
|
||||
state = msgStrInvalid
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Buffer msgid and continue
|
||||
if strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") {
|
||||
po.parseID(l)
|
||||
state = msgID
|
||||
// Buffer context and continue
|
||||
if po.hasKeywordBoundary(l, "msgctxt") {
|
||||
if po.parseContext(l) {
|
||||
state = msgCtxt
|
||||
} else {
|
||||
state = msgStrInvalid
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for plural form
|
||||
if strings.HasPrefix(l, "msgid_plural") {
|
||||
po.parsePluralID(l)
|
||||
po.domain.pluralTranslations[po.domain.trBuffer.PluralID] = po.domain.trBuffer
|
||||
state = msgIDPlural
|
||||
if po.hasKeywordBoundary(l, "msgid_plural") {
|
||||
if state != msgID || !po.parsePluralID(l) {
|
||||
state = msgStrInvalid
|
||||
} else {
|
||||
state = msgIDPlural
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Buffer msgid and continue
|
||||
if po.hasKeywordBoundary(l, "msgid") {
|
||||
if po.parseID(l) {
|
||||
state = msgID
|
||||
} else {
|
||||
state = msgStrInvalid
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Save Translation
|
||||
if strings.HasPrefix(l, "msgstr") {
|
||||
po.parseMessage(l)
|
||||
state = msgStr
|
||||
continue
|
||||
}
|
||||
if po.hasKeywordBoundary(l, "msgstr") {
|
||||
if state != msgID && state != msgIDPlural && state != msgStr {
|
||||
activeMsgStrIndex = 0
|
||||
state = msgStrInvalid
|
||||
continue
|
||||
}
|
||||
|
||||
// Multi line strings and headers
|
||||
if strings.HasPrefix(l, "\"") && strings.HasSuffix(l, "\"") {
|
||||
po.parseString(l, state)
|
||||
var ok bool
|
||||
activeMsgStrIndex, ok = po.parseMessage(l)
|
||||
if ok {
|
||||
state = msgStr
|
||||
} else {
|
||||
activeMsgStrIndex = 0
|
||||
state = msgStrInvalid
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Save last Translation buffer.
|
||||
po.saveBuffer()
|
||||
// Save last Translation buffer, but do not synthesize an entry after an
|
||||
// invalid directive. Preserve the historical empty-input entry.
|
||||
if po.parseBufferHasID {
|
||||
po.saveBuffer()
|
||||
} else if len(buf) == 0 {
|
||||
po.domain.translations[""] = po.domain.trBuffer
|
||||
}
|
||||
|
||||
// Parse headers
|
||||
po.domain.parseHeaders()
|
||||
@@ -285,22 +318,30 @@ func (po *Po) Parse(buf []byte) {
|
||||
// saveBuffer takes the context and Translation buffers
|
||||
// and saves it on the translations collection
|
||||
func (po *Po) saveBuffer() {
|
||||
// With no context...
|
||||
if po.domain.ctxBuffer == "" {
|
||||
po.domain.translations[po.domain.trBuffer.ID] = po.domain.trBuffer
|
||||
} else {
|
||||
// With context...
|
||||
if !po.parseBufferHasID {
|
||||
return
|
||||
}
|
||||
|
||||
// Store a completed plural ID after all of its continuation lines have
|
||||
// been decoded.
|
||||
if po.parseBufferHasPlural {
|
||||
po.domain.pluralTranslations[po.domain.trBuffer.PluralID] = po.domain.trBuffer
|
||||
}
|
||||
|
||||
if po.parseBufferHasContext {
|
||||
if _, ok := po.domain.contextTranslations[po.domain.ctxBuffer]; !ok {
|
||||
po.domain.contextTranslations[po.domain.ctxBuffer] = make(map[string]*Translation)
|
||||
}
|
||||
po.domain.contextTranslations[po.domain.ctxBuffer][po.domain.trBuffer.ID] = po.domain.trBuffer
|
||||
|
||||
// Cleanup current context buffer if needed
|
||||
if po.domain.trBuffer.ID != "" {
|
||||
po.domain.ctxBuffer = ""
|
||||
}
|
||||
// Context applies only to the record it prefixes, even when its ID is empty.
|
||||
po.domain.ctxBuffer = ""
|
||||
po.parseBufferHasContext = false
|
||||
} else {
|
||||
po.domain.translations[po.domain.trBuffer.ID] = po.domain.trBuffer
|
||||
}
|
||||
|
||||
po.parseBufferHasID = false
|
||||
po.parseBufferHasPlural = false
|
||||
// Flush Translation buffer
|
||||
if po.domain.refBuffer == "" {
|
||||
po.domain.trBuffer = NewTranslation()
|
||||
@@ -328,68 +369,106 @@ func (po *Po) parseComment(l string, state parseState) {
|
||||
|
||||
// parseContext takes a line starting with "msgctxt",
|
||||
// saves the current Translation buffer and creates a new context.
|
||||
func (po *Po) parseContext(l string) {
|
||||
func (po *Po) parseContext(l string) bool {
|
||||
value, ok := po.parseQuotedDirective(l, "msgctxt")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Save current Translation buffer.
|
||||
po.saveBuffer()
|
||||
|
||||
// Buffer context
|
||||
po.domain.ctxBuffer, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgctxt")))
|
||||
// Buffer context, including an explicitly empty context.
|
||||
po.domain.ctxBuffer = value
|
||||
po.parseBufferHasContext = true
|
||||
return true
|
||||
}
|
||||
|
||||
// parseID takes a line starting with "msgid",
|
||||
// saves the current Translation and creates a new msgid buffer.
|
||||
func (po *Po) parseID(l string) {
|
||||
func (po *Po) parseID(l string) bool {
|
||||
value, ok := po.parseQuotedDirective(l, "msgid")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Save current Translation buffer.
|
||||
po.saveBuffer()
|
||||
|
||||
// Set id
|
||||
po.domain.trBuffer.ID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid")))
|
||||
po.domain.trBuffer.ID = value
|
||||
po.parseBufferHasID = true
|
||||
return true
|
||||
}
|
||||
|
||||
// parsePluralID saves the plural id buffer from a line starting with "msgid_plural"
|
||||
func (po *Po) parsePluralID(l string) {
|
||||
po.domain.trBuffer.PluralID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid_plural")))
|
||||
func (po *Po) parsePluralID(l string) bool {
|
||||
value, ok := po.parseQuotedDirective(l, "msgid_plural")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
po.domain.trBuffer.PluralID = value
|
||||
po.parseBufferHasPlural = true
|
||||
return true
|
||||
}
|
||||
|
||||
// parseMessage takes a line starting with "msgstr" and saves it into the current buffer.
|
||||
func (po *Po) parseMessage(l string) {
|
||||
l = strings.TrimSpace(strings.TrimPrefix(l, "msgstr"))
|
||||
|
||||
func (po *Po) parseMessage(l string) (int, bool) {
|
||||
if !po.hasKeywordBoundary(l, "msgstr") {
|
||||
return 0, false
|
||||
}
|
||||
l = strings.TrimSpace(l[len("msgstr"):])
|
||||
// Check for indexed Translation forms
|
||||
if strings.HasPrefix(l, "[") {
|
||||
idx := strings.Index(l, "]")
|
||||
if idx == -1 {
|
||||
idx := strings.IndexByte(l, ']')
|
||||
if idx <= 1 || !isASCIIPluralIndex(l[1:idx]) {
|
||||
// Skip wrong index formatting
|
||||
return
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// Parse index
|
||||
i, err := strconv.Atoi(l[1:idx])
|
||||
if err != nil {
|
||||
// Skip wrong index formatting
|
||||
return
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// Parse Translation string
|
||||
po.domain.trBuffer.Trs[i], _ = strconv.Unquote(strings.TrimSpace(l[idx+1:]))
|
||||
clean, ok := decodePOQuotedString(strings.TrimSpace(l[idx+1:]))
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
po.domain.trBuffer.Trs[i] = clean
|
||||
|
||||
// Loop
|
||||
return
|
||||
return i, true
|
||||
}
|
||||
|
||||
// Save single Translation form under 0 index
|
||||
po.domain.trBuffer.Trs[0], _ = strconv.Unquote(l)
|
||||
clean, ok := decodePOQuotedString(l)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
po.domain.trBuffer.Trs[0] = clean
|
||||
return 0, true
|
||||
}
|
||||
|
||||
// parseString takes a well formatted string without prefix
|
||||
// and creates headers or attach multi-line strings when corresponding
|
||||
func (po *Po) parseString(l string, state parseState) {
|
||||
clean, _ := strconv.Unquote(l)
|
||||
func (po *Po) parseString(l string, state parseState, activeMsgStrIndex int) bool {
|
||||
if state == msgStrInvalid {
|
||||
return false
|
||||
}
|
||||
|
||||
clean, ok := decodePOQuotedString(l)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
switch state {
|
||||
case msgStr:
|
||||
// Append to last Translation found
|
||||
po.domain.trBuffer.Trs[len(po.domain.trBuffer.Trs)-1] += clean
|
||||
// Append to active Translation form
|
||||
po.domain.trBuffer.Trs[activeMsgStrIndex] += clean
|
||||
|
||||
case msgID:
|
||||
// Multiline msgid - Append to current id
|
||||
@@ -402,26 +481,174 @@ func (po *Po) parseString(l string, state parseState) {
|
||||
case msgCtxt:
|
||||
// Multiline context - Append to current context
|
||||
po.domain.ctxBuffer += clean
|
||||
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// isValidLine checks for line prefixes to detect valid syntax.
|
||||
func (po *Po) isValidLine(l string) bool {
|
||||
// Check prefix
|
||||
valid := []string{
|
||||
"\"",
|
||||
"msgctxt",
|
||||
"msgid",
|
||||
"msgid_plural",
|
||||
"msgstr",
|
||||
if strings.HasPrefix(l, "\"") {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, v := range valid {
|
||||
if strings.HasPrefix(l, v) {
|
||||
for _, keyword := range []string{
|
||||
"msgctxt",
|
||||
"msgid_plural",
|
||||
"msgid",
|
||||
"msgstr",
|
||||
} {
|
||||
if po.hasKeywordBoundary(l, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (po *Po) hasKeywordBoundary(l, keyword string) bool {
|
||||
if !strings.HasPrefix(l, keyword) {
|
||||
return false
|
||||
}
|
||||
if len(l) == len(keyword) {
|
||||
return true
|
||||
}
|
||||
|
||||
next := l[len(keyword)]
|
||||
if keyword == "msgstr" && next == '[' {
|
||||
return true
|
||||
}
|
||||
return next == ' ' || next == '\t'
|
||||
}
|
||||
|
||||
func (po *Po) parseQuotedDirective(l, keyword string) (string, bool) {
|
||||
if !po.hasKeywordBoundary(l, keyword) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
value, ok := decodePOQuotedString(strings.TrimSpace(l[len(keyword):]))
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
|
||||
// decodePOQuotedString decodes a GNU PO double-quoted string.
|
||||
func decodePOQuotedString(source string) (string, bool) {
|
||||
if len(source) < 2 || source[0] != '"' {
|
||||
return "", false
|
||||
}
|
||||
|
||||
var decoded []byte
|
||||
for i := 1; i < len(source); {
|
||||
switch source[i] {
|
||||
case '"':
|
||||
if i != len(source)-1 {
|
||||
return "", false
|
||||
}
|
||||
if decoded == nil {
|
||||
return source[1:i], true
|
||||
}
|
||||
return string(decoded), true
|
||||
|
||||
case '\\':
|
||||
if decoded == nil {
|
||||
decoded = make([]byte, 0, len(source)-2)
|
||||
decoded = append(decoded, source[1:i]...)
|
||||
}
|
||||
i++
|
||||
if i >= len(source) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
switch source[i] {
|
||||
case 'a':
|
||||
decoded = append(decoded, '\a')
|
||||
i++
|
||||
case 'b':
|
||||
decoded = append(decoded, '\b')
|
||||
i++
|
||||
case 'f':
|
||||
decoded = append(decoded, '\f')
|
||||
i++
|
||||
case 'n':
|
||||
decoded = append(decoded, '\n')
|
||||
i++
|
||||
case 'r':
|
||||
decoded = append(decoded, '\r')
|
||||
i++
|
||||
case 't':
|
||||
decoded = append(decoded, '\t')
|
||||
i++
|
||||
case 'v':
|
||||
decoded = append(decoded, '\v')
|
||||
i++
|
||||
case '\\':
|
||||
decoded = append(decoded, '\\')
|
||||
i++
|
||||
case '"':
|
||||
decoded = append(decoded, '"')
|
||||
i++
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7':
|
||||
var value byte
|
||||
for digits := 0; i < len(source) && digits < 3; digits++ {
|
||||
if source[i] < '0' || source[i] > '7' {
|
||||
break
|
||||
}
|
||||
value = value<<3 | (source[i] - '0')
|
||||
i++
|
||||
}
|
||||
decoded = append(decoded, value)
|
||||
case 'x':
|
||||
i++
|
||||
if i >= len(source) || !isPOHexDigit(source[i]) {
|
||||
return "", false
|
||||
}
|
||||
var value byte
|
||||
for i < len(source) && isPOHexDigit(source[i]) {
|
||||
value = value<<4 | poHexValue(source[i])
|
||||
i++
|
||||
}
|
||||
decoded = append(decoded, value)
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
|
||||
default:
|
||||
if decoded != nil {
|
||||
decoded = append(decoded, source[i])
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func isPOHexDigit(c byte) bool {
|
||||
return c >= '0' && c <= '9' ||
|
||||
c >= 'a' && c <= 'f' ||
|
||||
c >= 'A' && c <= 'F'
|
||||
}
|
||||
|
||||
func poHexValue(c byte) byte {
|
||||
switch {
|
||||
case c >= '0' && c <= '9':
|
||||
return c - '0'
|
||||
case c >= 'a' && c <= 'f':
|
||||
return c - 'a' + 10
|
||||
default:
|
||||
return c - 'A' + 10
|
||||
}
|
||||
}
|
||||
|
||||
func isASCIIPluralIndex(index string) bool {
|
||||
if index == "" {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(index); i++ {
|
||||
if index[i] < '0' || index[i] > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
+6
-14
@@ -49,11 +49,8 @@ func (t *Translation) Set(str string) {
|
||||
|
||||
// Get returns the string of the translation
|
||||
func (t *Translation) Get() string {
|
||||
// Look for Translation index 0
|
||||
if _, ok := t.Trs[0]; ok {
|
||||
if t.Trs[0] != "" {
|
||||
return t.Trs[0]
|
||||
}
|
||||
if tr := t.Trs[0]; tr != "" {
|
||||
return tr
|
||||
}
|
||||
|
||||
// Return untranslated id by default
|
||||
@@ -68,11 +65,8 @@ func (t *Translation) SetN(n int, str string) {
|
||||
|
||||
// GetN returns the string of the plural translation
|
||||
func (t *Translation) GetN(n int) string {
|
||||
// Look for Translation index
|
||||
if _, ok := t.Trs[n]; ok {
|
||||
if t.Trs[n] != "" {
|
||||
return t.Trs[n]
|
||||
}
|
||||
if tr := t.Trs[n]; tr != "" {
|
||||
return tr
|
||||
}
|
||||
|
||||
// Return untranslated singular if corresponding
|
||||
@@ -86,12 +80,10 @@ func (t *Translation) GetN(n int) string {
|
||||
|
||||
// IsTranslated reports whether a string is translated
|
||||
func (t *Translation) IsTranslated() bool {
|
||||
tr, ok := t.Trs[0]
|
||||
return tr != "" && ok
|
||||
return t.Trs[0] != ""
|
||||
}
|
||||
|
||||
// IsTranslatedN reports whether a plural string is translated
|
||||
func (t *Translation) IsTranslatedN(n int) bool {
|
||||
tr, ok := t.Trs[n]
|
||||
return tr != "" && ok
|
||||
return t.Trs[n] != ""
|
||||
}
|
||||
|
||||
+60
-14
@@ -9,18 +9,20 @@ import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"github.com/leonelquinteros/gotext/plurals"
|
||||
)
|
||||
|
||||
// Translator interface is used by Locale and Po objects.Translator
|
||||
// Translator interface is used by Locale and Po objects.
|
||||
// It contains all methods needed to parse translation sources and obtain corresponding translations.
|
||||
// Also implements gob.GobEncoder/gob.DobDecoder interfaces to allow serialization of Locale objects.
|
||||
// It includes encoding.BinaryMarshaler and encoding.BinaryUnmarshaler methods for Locale serialization.
|
||||
type Translator interface {
|
||||
ParseFile(f string)
|
||||
Parse(buf []byte)
|
||||
Get(str string, vars ...interface{}) string
|
||||
GetN(str, plural string, n int, vars ...interface{}) string
|
||||
GetC(str, ctx string, vars ...interface{}) string
|
||||
GetNC(str, plural string, n int, ctx string, vars ...interface{}) string
|
||||
Get(str string, vars ...any) string
|
||||
GetN(str, plural string, n int, vars ...any) string
|
||||
GetC(str, ctx string, vars ...any) string
|
||||
GetNC(str, plural string, n int, ctx string, vars ...any) string
|
||||
|
||||
MarshalBinary() ([]byte, error)
|
||||
UnmarshalBinary([]byte) error
|
||||
@@ -31,10 +33,10 @@ type Translator interface {
|
||||
// It contains all methods needed to parse translation sources and to append entries to the object.
|
||||
type AppendTranslator interface {
|
||||
Translator
|
||||
Append(b []byte, str string, vars ...interface{}) []byte
|
||||
AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte
|
||||
AppendC(b []byte, str, ctx string, vars ...interface{}) []byte
|
||||
AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte
|
||||
Append(b []byte, str string, vars ...any) []byte
|
||||
AppendN(b []byte, str, plural string, n int, vars ...any) []byte
|
||||
AppendC(b []byte, str, ctx string, vars ...any) []byte
|
||||
AppendNC(b []byte, str, plural string, n int, ctx string, vars ...any) []byte
|
||||
}
|
||||
|
||||
// TranslatorEncoding is used as intermediary storage to encode Translator objects to Gob.
|
||||
@@ -63,14 +65,58 @@ type TranslatorEncoding struct {
|
||||
// deserialize into a Po-compatible object.
|
||||
func (te *TranslatorEncoding) GetTranslator() Translator {
|
||||
po := NewPo()
|
||||
po.domain = NewDomain()
|
||||
po.domain.Headers = te.Headers
|
||||
if te == nil {
|
||||
return po
|
||||
}
|
||||
|
||||
headers := te.Headers
|
||||
if headers == nil {
|
||||
headers = make(HeaderMap)
|
||||
}
|
||||
translations := te.Translations
|
||||
if translations == nil {
|
||||
translations = make(map[string]*Translation)
|
||||
}
|
||||
for id, translation := range translations {
|
||||
if translation == nil {
|
||||
translation = NewTranslation()
|
||||
translation.ID = id
|
||||
translations[id] = translation
|
||||
}
|
||||
}
|
||||
contexts := te.Contexts
|
||||
if contexts == nil {
|
||||
contexts = make(map[string]map[string]*Translation)
|
||||
}
|
||||
for context, translationsForContext := range contexts {
|
||||
if translationsForContext == nil {
|
||||
contexts[context] = make(map[string]*Translation)
|
||||
continue
|
||||
}
|
||||
for id, translation := range translationsForContext {
|
||||
if translation == nil {
|
||||
translation = NewTranslation()
|
||||
translation.ID = id
|
||||
translationsForContext[id] = translation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
po.domain.Headers = headers
|
||||
po.domain.Language = te.Language
|
||||
po.domain.PluralForms = te.PluralForms
|
||||
po.domain.nplurals = te.Nplurals
|
||||
po.domain.plural = te.Plural
|
||||
po.domain.translations = te.Translations
|
||||
po.domain.contextTranslations = te.Contexts
|
||||
po.domain.translations = translations
|
||||
po.domain.contextTranslations = contexts
|
||||
|
||||
if expr, err := plurals.Compile(te.Plural); err == nil {
|
||||
po.domain.pluralforms = expr
|
||||
}
|
||||
|
||||
po.Headers = po.domain.Headers
|
||||
po.Language = po.domain.Language
|
||||
po.PluralForms = po.domain.PluralForms
|
||||
|
||||
return po
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.27@sha256:512690a5660563b57d37ecc31129e7f136e831db2aed24a1dbeb8ad7380dc0fa
|
||||
FROM golang:1.27@sha256:820153096fda4de6ac1b7194d12e416b7dd8f4e9a33d17e0345b111b945162d8
|
||||
|
||||
ENV GOOS=linux
|
||||
ENV GOARCH=arm
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.27@sha256:512690a5660563b57d37ecc31129e7f136e831db2aed24a1dbeb8ad7380dc0fa
|
||||
FROM golang:1.27@sha256:820153096fda4de6ac1b7194d12e416b7dd8f4e9a33d17e0345b111b945162d8
|
||||
|
||||
ENV GOOS=linux
|
||||
ENV GOARCH=arm64
|
||||
|
||||
+23
-4
@@ -1,18 +1,35 @@
|
||||
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 ./...
|
||||
go test -race -timeout 15s -tags sha1cd_asmtest ./...
|
||||
SHA1CD_TEST_NOSHANI=1 go test -race -timeout 15s -tags sha1cd_asmtest ./test/
|
||||
|
||||
.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:
|
||||
go test -tags gofuzz -fuzz=. -fuzztime=$(FUZZ_TIME) ./test/
|
||||
@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
|
||||
|
||||
# Cross build project in arm/v7.
|
||||
build-arm:
|
||||
@@ -24,9 +41,11 @@ build-arm64:
|
||||
docker build -t sha1cd-arm64 -f Dockerfile.arm64 .
|
||||
docker run --rm sha1cd-arm64
|
||||
|
||||
# Build with cgo disabled.
|
||||
# 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-nocgo:
|
||||
CGO_ENABLED=0 go build ./cgo
|
||||
CGO_ENABLED=0 go build ./...
|
||||
CGO_ENABLED=0 go vet ./...
|
||||
|
||||
# Run cross-compilation to assure supported architectures.
|
||||
cross-build: build-arm build-arm64 build-nocgo
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// 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
@@ -0,0 +1,63 @@
|
||||
//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
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
//go:build !noasm && gc && amd64
|
||||
|
||||
#include "textflag.h"
|
||||
|
||||
// func cpuid(eaxArg, ecxArg uint32) (eax, ebx, ecx, edx uint32)
|
||||
TEXT ·cpuid(SB), NOSPLIT, $0-24
|
||||
MOVL eaxArg+0(FP), AX
|
||||
MOVL ecxArg+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() (eax, edx uint32)
|
||||
TEXT ·xgetbv(SB), NOSPLIT, $0-8
|
||||
MOVL $0, CX
|
||||
XGETBV
|
||||
MOVL AX, eax+0(FP)
|
||||
MOVL DX, edx+4(FP)
|
||||
RET
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
//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
@@ -0,0 +1,12 @@
|
||||
//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
@@ -0,0 +1,9 @@
|
||||
//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
@@ -0,0 +1,29 @@
|
||||
//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
@@ -0,0 +1,18 @@
|
||||
//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
@@ -0,0 +1,32 @@
|
||||
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
|
||||
}
|
||||
+28
-18
@@ -4,30 +4,39 @@
|
||||
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 hasSHANI = (runtime.GOARCH == "amd64" &&
|
||||
cpuid.CPU.Supports(cpuid.AVX) &&
|
||||
cpuid.CPU.Supports(cpuid.SHA) &&
|
||||
cpuid.CPU.Supports(cpuid.SSE3) &&
|
||||
cpuid.CPU.Supports(cpuid.SSE4))
|
||||
// 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
|
||||
|
||||
// blockAMD64 hashes the message p into the current state in h.
|
||||
// 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.
|
||||
// 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) {
|
||||
if forceGeneric || !hasSHANI {
|
||||
switch {
|
||||
case forceGeneric:
|
||||
blockGeneric(dig, p)
|
||||
case hasSHANI:
|
||||
blockSHANI(dig, p)
|
||||
case hasAVX2:
|
||||
blockAVX2(dig, p)
|
||||
default:
|
||||
blockGeneric(dig, p)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func blockSHANI(dig *digest, p []byte) {
|
||||
m1 := [shared.Rounds]uint32{}
|
||||
cs := [shared.PreStepState][shared.WordBuffers]uint32{}
|
||||
|
||||
@@ -37,14 +46,15 @@ func block(dig *digest, p []byte) {
|
||||
chunk := p[:shared.Chunk]
|
||||
|
||||
blockAMD64(dig.h[:], chunk, m1[:], cs[:])
|
||||
rectifyCompressionState(&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
|
||||
|
||||
col := checkCollision(&m1, &cs, &dig.h)
|
||||
if col {
|
||||
dig.col = true
|
||||
|
||||
blockAMD64(dig.h[:], chunk, m1[:], cs[:])
|
||||
blockAMD64(dig.h[:], chunk, m1[:], cs[:])
|
||||
blockAMD64(dig.h[:], chunk, m1[:], cs[:])
|
||||
blockAMD64(dig.h[:], chunk, m1[:], cs[:])
|
||||
}
|
||||
}
|
||||
|
||||
p = p[shared.Chunk:]
|
||||
|
||||
+44
-49
@@ -12,27 +12,30 @@
|
||||
// - 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 VPEXTRD's that each go through the store port.
|
||||
// move, instead of issuing four PEXTRD's that each go through the store port.
|
||||
#define LOADCS(abcd, e, index, target) \
|
||||
VPSHUFD $0x1B, abcd, X8; \
|
||||
VMOVDQU X8, ((index*20)+0)(target); \
|
||||
PSHUFD $0x1B, abcd, X8; \
|
||||
MOVOU X8, ((index*20)+0)(target); \
|
||||
MOVL e, ((index*20)+16)(target);
|
||||
|
||||
#define LOADM1(m1, index, target) \
|
||||
VPSHUFD $0x1B, m1, X8; \
|
||||
VMOVDQU X8, ((index*16)+0)(target);
|
||||
PSHUFD $0x1B, m1, X8; \
|
||||
MOVOU X8, ((index*16)+0)(target);
|
||||
|
||||
// func blockAMD64(h []uint32, p []byte, m1 []uint32, cs [][5]uint32)
|
||||
// Requires: AVX, SHA, SSE2, SSE4.1, SSSE3
|
||||
// Requires: 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
|
||||
CMPQ DX, $0x00
|
||||
JEQ done
|
||||
ADDQ SI, DX
|
||||
|
||||
// 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
|
||||
|
||||
// Allocate space on the stack for saving ABCD and E0, and align it to 16 bytes
|
||||
LEAQ 15(SP), AX
|
||||
@@ -42,52 +45,51 @@ TEXT ·blockAMD64(SB), NOSPLIT, $80-96
|
||||
|
||||
// Load initial hash state
|
||||
PINSRD $0x03, 16(DI), X5
|
||||
VMOVDQU (DI), X0
|
||||
MOVOU (DI), X0
|
||||
PAND upper_mask<>+0(SB), X5
|
||||
PSHUFD $0x1b, X0, X0
|
||||
VMOVDQA shuffle_mask<>+0(SB), X7
|
||||
MOVO shuffle_mask<>+0(SB), X7
|
||||
|
||||
loop:
|
||||
// Save ABCD and E working values
|
||||
VMOVDQA X5, (AX)
|
||||
VMOVDQA X0, 16(AX)
|
||||
MOVO X5, (AX)
|
||||
MOVO X0, 16(AX)
|
||||
|
||||
// LOAD CS 0
|
||||
VPEXTRD $3, X5, R12
|
||||
PEXTRD $3, X5, R12
|
||||
LOADCS(X0, R12, 0, R15)
|
||||
|
||||
// Rounds 0-3
|
||||
VMOVDQU (SI), X1
|
||||
MOVOU (SI), X1
|
||||
PSHUFB X7, X1
|
||||
PADDD X1, X5
|
||||
VMOVDQA X0, X6
|
||||
MOVO X0, X6
|
||||
SHA1RNDS4 $0x00, X5, X0
|
||||
LOADM1(X1, 0, R13)
|
||||
|
||||
// Rounds 4-7
|
||||
VMOVDQU 16(SI), X2
|
||||
MOVOU 16(SI), X2
|
||||
PSHUFB X7, X2
|
||||
SHA1NEXTE X2, X6
|
||||
VMOVDQA X0, X5
|
||||
MOVO X0, X5
|
||||
SHA1RNDS4 $0x00, X6, X0
|
||||
SHA1MSG1 X2, X1
|
||||
LOADM1(X2, 1, R13)
|
||||
|
||||
// Rounds 8-11
|
||||
VMOVDQU 32(SI), X3
|
||||
MOVOU 32(SI), X3
|
||||
PSHUFB X7, X3
|
||||
SHA1NEXTE X3, X5
|
||||
VMOVDQA X0, X6
|
||||
MOVO X0, X6
|
||||
SHA1RNDS4 $0x00, X5, X0
|
||||
SHA1MSG1 X3, X2
|
||||
PXOR X3, X1
|
||||
LOADM1(X3, 2, R13)
|
||||
|
||||
// Rounds 12-15
|
||||
VMOVDQU 48(SI), X4
|
||||
MOVOU 48(SI), X4
|
||||
PSHUFB X7, X4
|
||||
SHA1NEXTE X4, X6
|
||||
VMOVDQA X0, X5
|
||||
MOVO X0, X5
|
||||
SHA1MSG2 X4, X1
|
||||
SHA1RNDS4 $0x00, X6, X0
|
||||
SHA1MSG1 X4, X3
|
||||
@@ -96,7 +98,7 @@ loop:
|
||||
|
||||
// Rounds 16-19
|
||||
SHA1NEXTE X1, X5
|
||||
VMOVDQA X0, X6
|
||||
MOVO X0, X6
|
||||
SHA1MSG2 X1, X2
|
||||
SHA1RNDS4 $0x00, X5, X0
|
||||
SHA1MSG1 X1, X4
|
||||
@@ -105,7 +107,7 @@ loop:
|
||||
|
||||
// Rounds 20-23
|
||||
SHA1NEXTE X2, X6
|
||||
VMOVDQA X0, X5
|
||||
MOVO X0, X5
|
||||
SHA1MSG2 X2, X3
|
||||
SHA1RNDS4 $0x01, X6, X0
|
||||
SHA1MSG1 X2, X1
|
||||
@@ -114,7 +116,7 @@ loop:
|
||||
|
||||
// Rounds 24-27
|
||||
SHA1NEXTE X3, X5
|
||||
VMOVDQA X0, X6
|
||||
MOVO X0, X6
|
||||
SHA1MSG2 X3, X4
|
||||
SHA1RNDS4 $0x01, X5, X0
|
||||
SHA1MSG1 X3, X2
|
||||
@@ -123,7 +125,7 @@ loop:
|
||||
|
||||
// Rounds 28-31
|
||||
SHA1NEXTE X4, X6
|
||||
VMOVDQA X0, X5
|
||||
MOVO X0, X5
|
||||
SHA1MSG2 X4, X1
|
||||
SHA1RNDS4 $0x01, X6, X0
|
||||
SHA1MSG1 X4, X3
|
||||
@@ -132,7 +134,7 @@ loop:
|
||||
|
||||
// Rounds 32-35
|
||||
SHA1NEXTE X1, X5
|
||||
VMOVDQA X0, X6
|
||||
MOVO X0, X6
|
||||
SHA1MSG2 X1, X2
|
||||
SHA1RNDS4 $0x01, X5, X0
|
||||
SHA1MSG1 X1, X4
|
||||
@@ -141,7 +143,7 @@ loop:
|
||||
|
||||
// Rounds 36-39
|
||||
SHA1NEXTE X2, X6
|
||||
VMOVDQA X0, X5
|
||||
MOVO X0, X5
|
||||
SHA1MSG2 X2, X3
|
||||
SHA1RNDS4 $0x01, X6, X0
|
||||
SHA1MSG1 X2, X1
|
||||
@@ -150,7 +152,7 @@ loop:
|
||||
|
||||
// Rounds 40-43
|
||||
SHA1NEXTE X3, X5
|
||||
VMOVDQA X0, X6
|
||||
MOVO X0, X6
|
||||
SHA1MSG2 X3, X4
|
||||
SHA1RNDS4 $0x02, X5, X0
|
||||
SHA1MSG1 X3, X2
|
||||
@@ -159,7 +161,7 @@ loop:
|
||||
|
||||
// Rounds 44-47
|
||||
SHA1NEXTE X4, X6
|
||||
VMOVDQA X0, X5
|
||||
MOVO X0, X5
|
||||
SHA1MSG2 X4, X1
|
||||
SHA1RNDS4 $0x02, X6, X0
|
||||
SHA1MSG1 X4, X3
|
||||
@@ -168,21 +170,20 @@ loop:
|
||||
|
||||
// Rounds 48-51
|
||||
SHA1NEXTE X1, X5
|
||||
VMOVDQA X0, X6
|
||||
MOVO 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.
|
||||
VPEXTRD $3, X0, R12
|
||||
PEXTRD $3, X0, R12
|
||||
ROLL $30, R12
|
||||
|
||||
// Rounds 52-55
|
||||
SHA1NEXTE X2, X6
|
||||
VMOVDQA X0, X5
|
||||
MOVO X0, X5
|
||||
SHA1MSG2 X2, X3
|
||||
SHA1RNDS4 $0x02, X6, X0
|
||||
SHA1MSG1 X2, X1
|
||||
@@ -194,21 +195,20 @@ loop:
|
||||
|
||||
// Rounds 56-59
|
||||
SHA1NEXTE X3, X5
|
||||
VMOVDQA X0, X6
|
||||
MOVO 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.
|
||||
VPEXTRD $3, X0, R12
|
||||
PEXTRD $3, X0, R12
|
||||
ROLL $30, R12
|
||||
|
||||
// Rounds 60-63
|
||||
SHA1NEXTE X4, X6
|
||||
VMOVDQA X0, X5
|
||||
MOVO X0, X5
|
||||
SHA1MSG2 X4, X1
|
||||
SHA1RNDS4 $0x03, X6, X0
|
||||
SHA1MSG1 X4, X3
|
||||
@@ -220,7 +220,7 @@ loop:
|
||||
|
||||
// Rounds 64-67
|
||||
SHA1NEXTE X1, X5
|
||||
VMOVDQA X0, X6
|
||||
MOVO X0, X6
|
||||
SHA1MSG2 X1, X2
|
||||
SHA1RNDS4 $0x03, X5, X0
|
||||
SHA1MSG1 X1, X4
|
||||
@@ -229,7 +229,7 @@ loop:
|
||||
|
||||
// Rounds 68-71
|
||||
SHA1NEXTE X2, X6
|
||||
VMOVDQA X0, X5
|
||||
MOVO X0, X5
|
||||
SHA1MSG2 X2, X3
|
||||
SHA1RNDS4 $0x03, X6, X0
|
||||
PXOR X2, X4
|
||||
@@ -237,14 +237,14 @@ loop:
|
||||
|
||||
// Rounds 72-75
|
||||
SHA1NEXTE X3, X5
|
||||
VMOVDQA X0, X6
|
||||
MOVO X0, X6
|
||||
SHA1MSG2 X3, X4
|
||||
SHA1RNDS4 $0x03, X5, X0
|
||||
LOADM1(X3, 18, R13)
|
||||
|
||||
// Rounds 76-79
|
||||
SHA1NEXTE X4, X6
|
||||
VMOVDQA X0, X5
|
||||
MOVO X0, X5
|
||||
SHA1RNDS4 $0x03, X6, X0
|
||||
LOADM1(X4, 19, R13)
|
||||
|
||||
@@ -252,14 +252,9 @@ loop:
|
||||
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
|
||||
VMOVDQU X0, (DI)
|
||||
MOVOU X0, (DI)
|
||||
PEXTRD $0x03, X5, 16(DI)
|
||||
|
||||
done:
|
||||
|
||||
+15
-11
@@ -4,15 +4,17 @@
|
||||
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 = (runtime.GOARCH == "arm64" && cpuid.CPU.Supports(cpuid.SHA1))
|
||||
var hasSHA1 = cpu.ARM64.HasSHA1
|
||||
|
||||
// blockARM64 hashes the message p into the current state in h.
|
||||
// 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.
|
||||
// Both m1 and cs are used to store intermediate results which are used by the collision detection logic.
|
||||
//
|
||||
//go:noescape
|
||||
@@ -34,13 +36,15 @@ func block(dig *digest, p []byte) {
|
||||
|
||||
blockARM64(dig.h[:], chunk, m1[:], cs[:])
|
||||
|
||||
rectifyCompressionState(&m1, &cs)
|
||||
col := checkCollision(&m1, &cs, &dig.h)
|
||||
if col {
|
||||
dig.col = true
|
||||
// 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
|
||||
|
||||
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:]
|
||||
|
||||
+2
-4
@@ -37,15 +37,13 @@ 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
|
||||
|
||||
loop:
|
||||
CMP R16, R21
|
||||
BLS end
|
||||
// The caller passes exactly one block; skip hashing only if p is shorter.
|
||||
CBZ R2, end
|
||||
|
||||
// Load block (p) into 16-bytes vectors.
|
||||
VLD1.P 16(R1), [V4.B16]
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
//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
@@ -0,0 +1,55 @@
|
||||
//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
@@ -0,0 +1,415 @@
|
||||
//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
|
||||
+32
-46
@@ -20,7 +20,9 @@ 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) {
|
||||
var w [16]uint32
|
||||
// Expand directly into the schedule retained for collision detection.
|
||||
// Every word is overwritten for each block, including rehashes.
|
||||
var m1 [shared.Rounds]uint32
|
||||
|
||||
// cs stores the pre-step compression state for only the steps required for the
|
||||
// collision detection, which are 0, 58 and 65.
|
||||
@@ -29,7 +31,6 @@ 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.
|
||||
@@ -51,36 +52,27 @@ func blockGeneric(dig *digest, p []byte) {
|
||||
for ; i < 16; i++ {
|
||||
// load step
|
||||
j := i * 4
|
||||
w[i] = uint32(p[j])<<24 | uint32(p[j+1])<<16 | uint32(p[j+2])<<8 | uint32(p[j+3])
|
||||
m1[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 + w[i&0xf] + shared.K0
|
||||
t := bits.RotateLeft32(a, 5) + f + e + m1[i] + 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 := w[(i-3)&0xf] ^ w[(i-8)&0xf] ^ w[(i-14)&0xf] ^ w[(i)&0xf]
|
||||
w[i&0xf] = tmp<<1 | tmp>>(32-1)
|
||||
tmp := m1[i-3] ^ m1[i-8] ^ m1[i-14] ^ m1[i-16]
|
||||
m1[i] = tmp<<1 | tmp>>(32-1)
|
||||
|
||||
f := b&c | (^b)&d
|
||||
t := bits.RotateLeft32(a, 5) + f + e + w[i&0xf] + shared.K0
|
||||
t := bits.RotateLeft32(a, 5) + f + e + m1[i] + 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 := w[(i-3)&0xf] ^ w[(i-8)&0xf] ^ w[(i-14)&0xf] ^ w[(i)&0xf]
|
||||
w[i&0xf] = tmp<<1 | tmp>>(32-1)
|
||||
tmp := m1[i-3] ^ m1[i-8] ^ m1[i-14] ^ m1[i-16]
|
||||
m1[i] = tmp<<1 | tmp>>(32-1)
|
||||
|
||||
f := b ^ c ^ d
|
||||
t := bits.RotateLeft32(a, 5) + f + e + w[i&0xf] + shared.K1
|
||||
t := bits.RotateLeft32(a, 5) + f + e + m1[i] + 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 {
|
||||
@@ -88,15 +80,12 @@ func blockGeneric(dig *digest, p []byte) {
|
||||
cs[1] = [shared.WordBuffers]uint32{a, b, c, d, e}
|
||||
}
|
||||
|
||||
tmp := w[(i-3)&0xf] ^ w[(i-8)&0xf] ^ w[(i-14)&0xf] ^ w[(i)&0xf]
|
||||
w[i&0xf] = tmp<<1 | tmp>>(32-1)
|
||||
tmp := m1[i-3] ^ m1[i-8] ^ m1[i-14] ^ m1[i-16]
|
||||
m1[i] = tmp<<1 | tmp>>(32-1)
|
||||
|
||||
f := ((b | c) & d) | (b & c)
|
||||
t := bits.RotateLeft32(a, 5) + f + e + w[i&0xf] + shared.K2
|
||||
t := bits.RotateLeft32(a, 5) + f + e + m1[i] + 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 {
|
||||
@@ -104,15 +93,12 @@ func blockGeneric(dig *digest, p []byte) {
|
||||
cs[2] = [shared.WordBuffers]uint32{a, b, c, d, e}
|
||||
}
|
||||
|
||||
tmp := w[(i-3)&0xf] ^ w[(i-8)&0xf] ^ w[(i-14)&0xf] ^ w[(i)&0xf]
|
||||
w[i&0xf] = tmp<<1 | tmp>>(32-1)
|
||||
tmp := m1[i-3] ^ m1[i-8] ^ m1[i-14] ^ m1[i-16]
|
||||
m1[i] = tmp<<1 | tmp>>(32-1)
|
||||
|
||||
f := b ^ c ^ d
|
||||
t := bits.RotateLeft32(a, 5) + f + e + w[i&0xf] + shared.K3
|
||||
t := bits.RotateLeft32(a, 5) + f + e + m1[i] + 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
|
||||
@@ -128,7 +114,7 @@ func blockGeneric(dig *digest, p []byte) {
|
||||
|
||||
if hi == 1 {
|
||||
h := [shared.WordBuffers]uint32{h0, h1, h2, h3, h4}
|
||||
col := checkCollision(&m1, &cs, &h)
|
||||
col := checkCollision(&m1, &cs, &h, ubc.CalculateDvMask(&m1))
|
||||
if col {
|
||||
dig.col = true
|
||||
hi++
|
||||
@@ -147,8 +133,9 @@ func checkCollision(
|
||||
m1 *[shared.Rounds]uint32,
|
||||
cs *[shared.PreStepState][shared.WordBuffers]uint32,
|
||||
h *[shared.WordBuffers]uint32,
|
||||
mask uint32,
|
||||
) bool {
|
||||
if mask := ubc.CalculateDvMask(m1); mask != 0 {
|
||||
if mask != 0 {
|
||||
dvs := ubc.SHA1_dvs()
|
||||
|
||||
for i := 0; dvs[i].DvType != 0; i++ {
|
||||
@@ -290,23 +277,22 @@ func rectifyCompressionState(
|
||||
return
|
||||
}
|
||||
|
||||
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]
|
||||
// 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.
|
||||
|
||||
// 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
|
||||
|
||||
cs57 := func3(cs[1], 56)
|
||||
cs[1] = func3(cs57, 57)
|
||||
cs[2] = func4(cs[2], 64)
|
||||
// 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
|
||||
}
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
//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
@@ -0,0 +1,52 @@
|
||||
//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
@@ -0,0 +1,111 @@
|
||||
//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
@@ -0,0 +1,14 @@
|
||||
//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)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user