Compare commits
21 Commits
chore-deps
...
type_remot
| Author | SHA1 | Date | |
|---|---|---|---|
| c5bfff8e64 | |||
| ebc7c9eeee | |||
| 12d4252acb | |||
| 776693acc0 | |||
| 5dea5f7746 | |||
| 1d9a289888 | |||
| c7bd55e371 | |||
| 4276337b0f | |||
| 90ca856b64 | |||
| f2dd65491d | |||
| 0e902ed897 | |||
| db001c1ba4 | |||
| e4215c09aa | |||
| e0e6dcb710 | |||
| e7ddb74a08 | |||
| 24a5e6334f | |||
| 9d8eb2317e | |||
| 5945ea8e1b | |||
| e170d1c971 | |||
| 5eba3abb1b | |||
| df5a38e887 |
@ -15,7 +15,7 @@ WORKDIR /app
|
||||
|
||||
RUN CGO_ENABLED=0 make build
|
||||
|
||||
FROM alpine:3.22
|
||||
FROM alpine:3.23
|
||||
|
||||
RUN apk add --no-cache \
|
||||
ca-certificates \
|
||||
|
||||
14
Makefile
14
Makefile
@ -1,5 +1,5 @@
|
||||
ABRA := ./cmd/abra
|
||||
XGETTEXT := ./bin/xgettext-go
|
||||
XGETTEXT := xgettext-go
|
||||
COMMIT := $(shell git rev-list -1 HEAD)
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
GOVERSION := 1.26
|
||||
@ -62,8 +62,8 @@ update-po:
|
||||
done
|
||||
|
||||
.PHONY: update-pot
|
||||
update-pot: $(XGETTEXT)
|
||||
@${XGETTEXT} \
|
||||
update-pot: xgettext-go
|
||||
@$(XGETTEXT) \
|
||||
-o pkg/i18n/locales/$(DOMAIN).pot \
|
||||
--keyword=i18n.G \
|
||||
--keyword-ctx=i18n.GC \
|
||||
@ -71,10 +71,10 @@ update-pot: $(XGETTEXT)
|
||||
--add-comments-tag="translators" \
|
||||
$$(find . -name "*.go" -not -path "*vendor*" | sort)
|
||||
|
||||
${XGETTEXT}:
|
||||
@mkdir -p ./bin && \
|
||||
wget -O ./bin/xgettext-go https://git.coopcloud.tech/toolshed/xgettext-go/raw/branch/main/xgettext-go && \
|
||||
chmod +x ./bin/xgettext-go
|
||||
.PHONY: xgettext-go
|
||||
xgettext-go:
|
||||
@command -v $(XGETTEXT) >/dev/null 2>&1 || \
|
||||
go install git.coopcloud.tech/toolshed/xgettext-go@latest
|
||||
|
||||
.PHONY: update-pot-po-metadata
|
||||
update-pot-po-metadata:
|
||||
|
||||
@ -114,7 +114,9 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
}
|
||||
|
||||
if !isChaosCommit && !tagcmp.IsParsable(toDeployVersion) {
|
||||
log.Fatal(i18n.G("unable to parse deploy version: %s", toDeployVersion))
|
||||
log.Warnf(i18n.G("version '%s' is not a semver tag; deploying as chaos", toDeployVersion))
|
||||
isChaosCommit = true
|
||||
internal.Chaos = true
|
||||
}
|
||||
|
||||
if !internal.Chaos {
|
||||
@ -151,11 +153,12 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
|
||||
stackName := app.StackName()
|
||||
deployOpts := stack.Deploy{
|
||||
Composefiles: composeFiles,
|
||||
Namespace: stackName,
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
Detach: false,
|
||||
Composefiles: composeFiles,
|
||||
Namespace: stackName,
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
Detach: false,
|
||||
SendRegistryAuth: true,
|
||||
}
|
||||
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||
if err != nil {
|
||||
|
||||
@ -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
|
||||
@ -289,9 +301,13 @@ func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secr
|
||||
// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
|
||||
func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
|
||||
if appDomain == "" && !internal.NoInput {
|
||||
shortName := recipe.Name
|
||||
if i := strings.LastIndex(shortName, "/"); i >= 0 {
|
||||
shortName = shortName[i+1:]
|
||||
}
|
||||
prompt := &survey.Input{
|
||||
Message: i18n.G("Specify app domain"),
|
||||
Default: fmt.Sprintf("%s.%s", recipe.Name, server),
|
||||
Default: fmt.Sprintf("%s.%s", shortName, server),
|
||||
}
|
||||
if err := survey.AskOne(prompt, &appDomain); err != nil {
|
||||
return err
|
||||
@ -302,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
|
||||
}
|
||||
|
||||
|
||||
@ -166,11 +166,12 @@ beforehand. See "abra app backup" for more.`),
|
||||
|
||||
stackName := app.StackName()
|
||||
deployOpts := stack.Deploy{
|
||||
Composefiles: composeFiles,
|
||||
Namespace: stackName,
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
Detach: false,
|
||||
Composefiles: composeFiles,
|
||||
Namespace: stackName,
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
Detach: false,
|
||||
SendRegistryAuth: true,
|
||||
}
|
||||
|
||||
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||
|
||||
@ -178,11 +178,12 @@ beforehand. See "abra app backup" for more.`),
|
||||
|
||||
stackName := app.StackName()
|
||||
deployOpts := stack.Deploy{
|
||||
Composefiles: composeFiles,
|
||||
Namespace: stackName,
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
Detach: false,
|
||||
Composefiles: composeFiles,
|
||||
Namespace: stackName,
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
Detach: false,
|
||||
SendRegistryAuth: true,
|
||||
}
|
||||
|
||||
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -16,7 +16,7 @@ func main() {
|
||||
Version = "dev"
|
||||
}
|
||||
if Commit == "" {
|
||||
Commit = " "
|
||||
Commit = "unknown-commit"
|
||||
}
|
||||
|
||||
cli.Run(Version, Commit)
|
||||
|
||||
28
devbox.json
Normal file
28
devbox.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.17.2/.schema/devbox.schema.json",
|
||||
"packages": [
|
||||
"go@1.26",
|
||||
"gnumake@latest",
|
||||
"gettext@latest"
|
||||
],
|
||||
"env": {
|
||||
"GOPATH": "$PWD/.devbox",
|
||||
"PATH": "$PWD/.devbox/bin:$PATH"
|
||||
},
|
||||
"shell": {
|
||||
"init_hook": [
|
||||
"mkdir -p .devbox/bin"
|
||||
],
|
||||
"scripts": {
|
||||
"xgettext-go": [
|
||||
"go install git.coopcloud.tech/toolshed/xgettext-go@latest"
|
||||
],
|
||||
"build": [
|
||||
"make build"
|
||||
],
|
||||
"test": [
|
||||
"make test"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
265
devbox.lock
Normal file
265
devbox.lock
Normal file
@ -0,0 +1,265 @@
|
||||
{
|
||||
"lockfile_version": "1",
|
||||
"packages": {
|
||||
"gettext@latest": {
|
||||
"last_modified": "2026-05-21T08:15:18Z",
|
||||
"resolved": "github:NixOS/nixpkgs/4a29d733e8a7d5b824c3d8c958a946a9867b3eb2#gettext",
|
||||
"source": "devbox-search",
|
||||
"version": "1.0",
|
||||
"systems": {
|
||||
"aarch64-darwin": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/i5aw4v15lklq5r5w5clvcy8dxc2igjck-gettext-1.0",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "man",
|
||||
"path": "/nix/store/17g5cqxvbkxivmfd39ppvyc0rx1wpcn8-gettext-1.0-man",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "doc",
|
||||
"path": "/nix/store/kfx0dvvkmyw91jbqnp58yhzlxqi1xyi0-gettext-1.0-doc"
|
||||
},
|
||||
{
|
||||
"name": "info",
|
||||
"path": "/nix/store/d14xspsnm4xj9w8yicrj50xmhs8bb5s2-gettext-1.0-info"
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/i5aw4v15lklq5r5w5clvcy8dxc2igjck-gettext-1.0"
|
||||
},
|
||||
"aarch64-linux": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/jdqf3kqpj22agyhakrk5gklvzc6fxp3l-gettext-1.0",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "man",
|
||||
"path": "/nix/store/axz6675qpbr93bbs1hfz60m0dm1i01mg-gettext-1.0-man",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "doc",
|
||||
"path": "/nix/store/5wckc0cg4l9wbmaqsi08rrmc09q7mxym-gettext-1.0-doc"
|
||||
},
|
||||
{
|
||||
"name": "info",
|
||||
"path": "/nix/store/fd0d7h9y3kvp9i3x172lsj3x5ffz6sh7-gettext-1.0-info"
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/jdqf3kqpj22agyhakrk5gklvzc6fxp3l-gettext-1.0"
|
||||
},
|
||||
"x86_64-darwin": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/f46icmrv0rpaykfxs313d4g17g0jxn02-gettext-1.0",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "man",
|
||||
"path": "/nix/store/nk8dbg526a7lmsr4x41nbj3hizjk7f6k-gettext-1.0-man",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "doc",
|
||||
"path": "/nix/store/w0wgn2f7ha2kp5ank9ymf1l8mlfki30a-gettext-1.0-doc"
|
||||
},
|
||||
{
|
||||
"name": "info",
|
||||
"path": "/nix/store/cvp75qjcdjh8xrhzhrrdc62dyfil1f5j-gettext-1.0-info"
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/f46icmrv0rpaykfxs313d4g17g0jxn02-gettext-1.0"
|
||||
},
|
||||
"x86_64-linux": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/rb8rna9gkhs0ybl6z2p904myslh8llg8-gettext-1.0",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "man",
|
||||
"path": "/nix/store/ck224axif94df161wk7b0wzwm035747f-gettext-1.0-man",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "doc",
|
||||
"path": "/nix/store/2lp7blpw8ahc683287gbnp6vn4i9p9q4-gettext-1.0-doc"
|
||||
},
|
||||
{
|
||||
"name": "info",
|
||||
"path": "/nix/store/33xaaniyh3qk6mq75dzx7v9c55n21i3p-gettext-1.0-info"
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/rb8rna9gkhs0ybl6z2p904myslh8llg8-gettext-1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"github:NixOS/nixpkgs/nixpkgs-unstable": {
|
||||
"last_modified": "2026-05-27T10:28:13Z",
|
||||
"resolved": "github:NixOS/nixpkgs/4100e830e085863741bc69b156ec4ccd53ab5be0?lastModified=1779877693&narHash=sha256-NOF9NAREhxr50bbBfVcVOq%2BArCMSoe8dP79Pk2uyARk%3D"
|
||||
},
|
||||
"gnumake@latest": {
|
||||
"last_modified": "2026-05-27T10:28:13Z",
|
||||
"resolved": "github:NixOS/nixpkgs/4100e830e085863741bc69b156ec4ccd53ab5be0#gnumake",
|
||||
"source": "devbox-search",
|
||||
"version": "4.4.1",
|
||||
"systems": {
|
||||
"aarch64-darwin": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/8wwiw8pwyhrkzyq28hqzxfl4z84lks81-gnumake-4.4.1",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "man",
|
||||
"path": "/nix/store/wdm9prq35jlgwq6rjyhqxy4jzy833sfr-gnumake-4.4.1-man",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "info",
|
||||
"path": "/nix/store/6dbp1ysgxsrrn7xgjgf523faqrm605a5-gnumake-4.4.1-info"
|
||||
},
|
||||
{
|
||||
"name": "doc",
|
||||
"path": "/nix/store/sl2ygvgjg4ml8dhyfxj5kh84x2bs1mhc-gnumake-4.4.1-doc"
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/8wwiw8pwyhrkzyq28hqzxfl4z84lks81-gnumake-4.4.1"
|
||||
},
|
||||
"aarch64-linux": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/9ngw1ippk25jjj5fjxv36xbp6iq7rxdx-gnumake-4.4.1",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "man",
|
||||
"path": "/nix/store/dg3aa71vii59scbfhi02cdxnps64x6ql-gnumake-4.4.1-man",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "debug",
|
||||
"path": "/nix/store/mdv67w2xvchbs76zjdzppdkk64z5avki-gnumake-4.4.1-debug"
|
||||
},
|
||||
{
|
||||
"name": "doc",
|
||||
"path": "/nix/store/bybbj9avk3g107mwhgvkzkpm6mkbl8gv-gnumake-4.4.1-doc"
|
||||
},
|
||||
{
|
||||
"name": "info",
|
||||
"path": "/nix/store/bxdqxx2q13sj15ss8z5l33jd31x55j5l-gnumake-4.4.1-info"
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/9ngw1ippk25jjj5fjxv36xbp6iq7rxdx-gnumake-4.4.1"
|
||||
},
|
||||
"x86_64-darwin": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/60y2hs45l18g56ykw9789b5vhhi2ls5q-gnumake-4.4.1",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "man",
|
||||
"path": "/nix/store/m2ba4bwmfgh994xbg1iqm6zj1mdrq8h7-gnumake-4.4.1-man",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "info",
|
||||
"path": "/nix/store/apljbm6plyajbfrnwif0a2d4nx6w0cpf-gnumake-4.4.1-info"
|
||||
},
|
||||
{
|
||||
"name": "doc",
|
||||
"path": "/nix/store/s9y438l347nky6rkl1wfk2y3b0d9i636-gnumake-4.4.1-doc"
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/60y2hs45l18g56ykw9789b5vhhi2ls5q-gnumake-4.4.1"
|
||||
},
|
||||
"x86_64-linux": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/d3bwqm6bymhy3pdgbvf7vxjqfp31m3j1-gnumake-4.4.1",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "man",
|
||||
"path": "/nix/store/bmvqa5ym318mymhxlwwmxq8wxal958p4-gnumake-4.4.1-man",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "debug",
|
||||
"path": "/nix/store/q683z843wi7xs96pngh1kinnx88vn2if-gnumake-4.4.1-debug"
|
||||
},
|
||||
{
|
||||
"name": "doc",
|
||||
"path": "/nix/store/wxg27bzsljjw4k72m9b2v6a0fx5a68s0-gnumake-4.4.1-doc"
|
||||
},
|
||||
{
|
||||
"name": "info",
|
||||
"path": "/nix/store/03d41qgjsrhcr3fzq2z99ry07rahnj0v-gnumake-4.4.1-info"
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/d3bwqm6bymhy3pdgbvf7vxjqfp31m3j1-gnumake-4.4.1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"go@1.26": {
|
||||
"last_modified": "2026-05-21T08:15:18Z",
|
||||
"resolved": "github:NixOS/nixpkgs/4a29d733e8a7d5b824c3d8c958a946a9867b3eb2#go",
|
||||
"source": "devbox-search",
|
||||
"version": "1.26.3",
|
||||
"systems": {
|
||||
"aarch64-darwin": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/7ycp8j45iay38g9mjaxmy4jhwdsrb47y-go-1.26.3",
|
||||
"default": true
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/7ycp8j45iay38g9mjaxmy4jhwdsrb47y-go-1.26.3"
|
||||
},
|
||||
"aarch64-linux": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/jkcwcbwvhgzmxg59798z4clmj4bfv42i-go-1.26.3",
|
||||
"default": true
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/jkcwcbwvhgzmxg59798z4clmj4bfv42i-go-1.26.3"
|
||||
},
|
||||
"x86_64-darwin": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/3f2jzvxmhhlarjqxy1p7i9r5l34siz29-go-1.26.3",
|
||||
"default": true
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/3f2jzvxmhhlarjqxy1p7i9r5l34siz29-go-1.26.3"
|
||||
},
|
||||
"x86_64-linux": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/33fw5m31lfcnk4ff2f0df7j2bxnh8lgk-go-1.26.3",
|
||||
"default": true
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/33fw5m31lfcnk4ff2f0df7j2bxnh8lgk-go-1.26.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1778443072,
|
||||
"narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
37
flake.nix
Normal file
37
flake.nix
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
description = "The Co-op Cloud utility belt";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
packages = rec {
|
||||
abra = pkgs.callPackage ./package.nix { };
|
||||
default = abra;
|
||||
};
|
||||
apps = rec {
|
||||
abra = flake-utils.lib.mkApp { drv = self.packages.${system}.abra; };
|
||||
default = abra;
|
||||
};
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
go_1_26
|
||||
gnumake
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
22
go.mod
22
go.mod
@ -3,7 +3,7 @@ module coopcloud.tech/abra
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca
|
||||
coopcloud.tech/tagcmp v0.0.0-20260515102403-c26951b55977
|
||||
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
@ -13,14 +13,14 @@ require (
|
||||
github.com/docker/cli v28.4.0+incompatible
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/go-git/go-git/v5 v5.17.2
|
||||
github.com/go-git/go-git/v5 v5.19.1
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/leonelquinteros/gotext v1.7.2
|
||||
github.com/moby/sys/signal v0.7.1
|
||||
github.com/moby/term v0.5.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/schollz/progressbar/v3 v3.19.0
|
||||
golang.org/x/term v0.41.0
|
||||
golang.org/x/term v0.43.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gotest.tools/v3 v3.5.2
|
||||
)
|
||||
@ -60,7 +60,7 @@ require (
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.8.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.9.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
@ -96,7 +96,7 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/runc v1.1.13 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.1.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
@ -124,10 +124,10 @@ require (
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/text v0.36.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
|
||||
@ -155,9 +155,9 @@ require (
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
golang.org/x/sys v0.42.0
|
||||
golang.org/x/sys v0.45.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-i18n
|
||||
replace github.com/spf13/cobra => github.com/decentral1se/cobra v1.10.2
|
||||
|
||||
44
go.sum
44
go.sum
@ -22,8 +22,8 @@ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIA
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca h1:gSD53tBAsbIGq4SnFfq+mEep6foekQ2a5ea7b38qkm0=
|
||||
coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
|
||||
coopcloud.tech/tagcmp v0.0.0-20260515102403-c26951b55977 h1:J7I0HFjwVAj/kkX6lwSTHmlXDRjQRsdIFNUUqu55ADY=
|
||||
coopcloud.tech/tagcmp v0.0.0-20260515102403-c26951b55977/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
@ -308,8 +308,8 @@ github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjI
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decentral1se/cobra v1.10.2-i18n h1:XR+6AHHfnf4k5NM9f09oLMrEVwz3rkQIAIcqgL8R08g=
|
||||
github.com/decentral1se/cobra v1.10.2-i18n/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/decentral1se/cobra v1.10.2 h1:MZ8Ifi/jRels9sZrpSccDbUlK++3b2HlBODfv0Bh6x0=
|
||||
github.com/decentral1se/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/decentral1se/passgen v1.0.1 h1:j2AxK/kHKxDHWZZfkJj8Wgae9+O+DYEqR5sjKthIYKA=
|
||||
github.com/decentral1se/passgen v1.0.1/go.mod h1:530V+lNoPhKtkrX2fIVsIfLhkl47CuiOM7HRgi7C+SU=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
@ -389,12 +389,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.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
|
||||
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
|
||||
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
|
||||
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.17.2 h1:B+nkdlxdYrvyFK4GPXVU8w1U+YkbsgciIR7f2sZJ104=
|
||||
github.com/go-git/go-git/v5 v5.17.2/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
|
||||
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
|
||||
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
||||
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=
|
||||
@ -753,8 +753,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.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
||||
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@ -966,8 +966,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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
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=
|
||||
@ -978,8 +978,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@ -1043,8 +1043,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.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
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=
|
||||
@ -1140,13 +1140,13 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
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=
|
||||
@ -1156,8 +1156,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.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
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=
|
||||
|
||||
54
package.nix
Normal file
54
package.nix
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
buildGo126Module,
|
||||
fetchgit,
|
||||
lib,
|
||||
installShellFiles,
|
||||
}:
|
||||
|
||||
buildGo126Module rec {
|
||||
pname = "abra";
|
||||
version = "0.13.0-beta";
|
||||
rev = "06a57ded025a43c80f94d4e65299add8a31830dc";
|
||||
|
||||
src = fetchgit {
|
||||
url = "https://git.coopcloud.tech/toolshed/abra.git";
|
||||
tag = version;
|
||||
hash = "sha256-rgoK0TY0WLSQ39lPvVM80zW/qJF40VFBSxYDOaKXZQo=";
|
||||
};
|
||||
|
||||
vendorHash = null;
|
||||
|
||||
nativeBuildInputs = [
|
||||
installShellFiles
|
||||
];
|
||||
|
||||
env.CGO_ENABLED = 0;
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
go build -ldflags="-s -w -X 'main.Commit=${rev}' -X 'main.Version=${version}'" ./cmd/abra
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
install -D abra $out/bin/abra
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
postInstall = ''
|
||||
export ABRA_DIR="$out"
|
||||
$out/bin/abra autocomplete bash >abra.bash
|
||||
$out/bin/abra autocomplete fish >abra.fish
|
||||
$out/bin/abra autocomplete zsh >abra.zsh
|
||||
installShellCompletion abra.{bash,fish,zsh}
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "The Co-op Cloud utility belt";
|
||||
homepage = "https://docs.coopcloud.tech/abra";
|
||||
changelog = "https://git.coopcloud.tech/toolshed/abra/releases/tag/${version}";
|
||||
mainProgram = "abra";
|
||||
license = licenses.gpl3Plus;
|
||||
maintainers = "devydave";
|
||||
};
|
||||
}
|
||||
@ -390,13 +390,21 @@ func TemplateAppEnvSample(r recipe.Recipe, appName, server, domain string) error
|
||||
return err
|
||||
}
|
||||
|
||||
shortName := r.Name
|
||||
if i := strings.LastIndex(shortName, "/"); i >= 0 {
|
||||
shortName = shortName[i+1:]
|
||||
}
|
||||
newContents := strings.Replace(
|
||||
string(read),
|
||||
fmt.Sprintf("%s.example.com", r.Name),
|
||||
fmt.Sprintf("%s.example.com", 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 +415,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 {
|
||||
|
||||
@ -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-04-11 11:34+0200\n"
|
||||
"POT-Creation-Date: 2026-05-30 11:14+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
@ -19,6 +20,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
|
||||
@ -78,6 +84,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 {
|
||||
@ -87,6 +99,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
|
||||
@ -163,6 +187,9 @@ func (r Recipe) EnsureVersion(version string) (bool, error) {
|
||||
|
||||
hash, err := repo.ResolveRevision(plumbing.Revision(version))
|
||||
if err != nil {
|
||||
if isRemoteBranch(repo, version) {
|
||||
log.Fatal(i18n.G("'%s' is a branch name; ':<version>' only supports tags or commit hashes, not branches", version))
|
||||
}
|
||||
log.Fatal(i18n.G("unable to resolve '%s': %s", version, err))
|
||||
}
|
||||
|
||||
@ -479,6 +506,28 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
||||
return versions, uniqueWarnings, nil
|
||||
}
|
||||
|
||||
// isRemoteBranch reports whether name matches a branch on any configured
|
||||
// remote. Used to give a clearer error when a user passes a branch as the
|
||||
// ":version" suffix, which is unsupported.
|
||||
func isRemoteBranch(repo *git.Repository, name string) bool {
|
||||
remotes, err := repo.Remotes()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, remote := range remotes {
|
||||
refs, err := remote.List(&git.ListOptions{})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, ref := range refs {
|
||||
if ref.Name().IsBranch() && ref.Name().Short() == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Head retrieves latest HEAD metadata.
|
||||
func (r Recipe) Head() (*plumbing.Reference, error) {
|
||||
repo, err := git.PlainOpen(r.Dir)
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -121,7 +122,83 @@ 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 if it
|
||||
// looks like one (no slashes, contains a digit or '+').
|
||||
func splitVersion(s string) (string, string) {
|
||||
idx := strings.LastIndex(s, ":")
|
||||
if idx < 0 {
|
||||
return s, ""
|
||||
}
|
||||
candidate := s[idx+1:]
|
||||
if candidate == "" || strings.ContainsAny(candidate, "/") {
|
||||
return s, ""
|
||||
}
|
||||
return s[:idx], candidate
|
||||
}
|
||||
|
||||
func Get(name string) Recipe {
|
||||
name = NormalizeRecipeName(name)
|
||||
version := ""
|
||||
versionRaw := ""
|
||||
if strings.Contains(name, ":") {
|
||||
|
||||
@ -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() })
|
||||
|
||||
@ -3,7 +3,7 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: nginx:1.21.0
|
||||
image: nginx:1.31.1
|
||||
secrets:
|
||||
- test_pass_one
|
||||
- test_pass_two
|
||||
|
||||
@ -3,7 +3,7 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: nginx:1.29.0
|
||||
image: nginx:1.31.1
|
||||
networks:
|
||||
- proxy
|
||||
deploy:
|
||||
|
||||
6
vendor/coopcloud.tech/tagcmp/.drone.yml
vendored
6
vendor/coopcloud.tech/tagcmp/.drone.yml
vendored
@ -3,16 +3,16 @@ kind: pipeline
|
||||
name: coopcloud.tech/tagcmp
|
||||
steps:
|
||||
- name: gofmt
|
||||
image: golang:1.21
|
||||
image: golang:1.26
|
||||
commands:
|
||||
- test -z "$(gofmt -l .)"
|
||||
|
||||
- name: go build
|
||||
image: golang:1.21
|
||||
image: golang:1.26
|
||||
commands:
|
||||
- go build -v .
|
||||
|
||||
- name: go test
|
||||
image: golang:1.21
|
||||
image: golang:1.26
|
||||
commands:
|
||||
- go test . -cover
|
||||
|
||||
6
vendor/coopcloud.tech/tagcmp/renovate.json
vendored
Normal file
6
vendor/coopcloud.tech/tagcmp/renovate.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
]
|
||||
}
|
||||
4
vendor/github.com/go-git/go-billy/v5/README.md
generated
vendored
4
vendor/github.com/go-git/go-billy/v5/README.md
generated
vendored
@ -5,6 +5,10 @@ Billy implements an interface based on the `os` standard library, allowing to de
|
||||
|
||||
Billy was born as part of [go-git/go-git](https://github.com/go-git/go-git) project.
|
||||
|
||||
## Version support
|
||||
|
||||
go-billy v5 is in maintenance mode. Users should upgrade to [go-billy v6](https://pkg.go.dev/github.com/go-git/go-billy/v6) where possible.
|
||||
|
||||
## Installation
|
||||
|
||||
```go
|
||||
|
||||
208
vendor/github.com/go-git/go-billy/v5/helper/chroot/chroot.go
generated
vendored
208
vendor/github.com/go-git/go-billy/v5/helper/chroot/chroot.go
generated
vendored
@ -3,19 +3,25 @@ package chroot
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/go-git/go-billy/v5"
|
||||
"github.com/go-git/go-billy/v5/helper/polyfill"
|
||||
)
|
||||
|
||||
// ChrootHelper is a helper to implement billy.Chroot.
|
||||
// It is not a security boundary, callers that need containment should use a
|
||||
// filesystem implementation that enforces paths at the OS boundary instead.
|
||||
type ChrootHelper struct {
|
||||
underlying billy.Filesystem
|
||||
base string
|
||||
}
|
||||
|
||||
const maxFollowedSymlinks = 8 // Aligns with POSIX_SYMLOOP_MAX
|
||||
|
||||
// New creates a new filesystem wrapping up the given 'fs'.
|
||||
// The created filesystem has its base in the given ChrootHelperectory of the
|
||||
// underlying filesystem.
|
||||
@ -34,15 +40,184 @@ func (fs *ChrootHelper) underlyingPath(filename string) (string, error) {
|
||||
return fs.Join(fs.Root(), filename), nil
|
||||
}
|
||||
|
||||
func isCrossBoundaries(path string) bool {
|
||||
path = filepath.ToSlash(path)
|
||||
path = filepath.Clean(path)
|
||||
func (fs *ChrootHelper) followedPath(filename string, followFinal bool, op string) (string, error) {
|
||||
fullpath, err := fs.underlyingPath(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return strings.HasPrefix(path, ".."+string(filepath.Separator))
|
||||
sl, ok := fs.underlying.(billy.Symlink)
|
||||
if !ok {
|
||||
return fullpath, nil
|
||||
}
|
||||
|
||||
rel, err := fs.relativeToRoot(fullpath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fullpath, err = fs.resolveFollowedPath(rel, followFinal, op, sl)
|
||||
if errors.Is(err, billy.ErrNotSupported) {
|
||||
return fs.underlyingPath(filename)
|
||||
}
|
||||
|
||||
return fullpath, err
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) resolveFollowedPath(rel string, followFinal bool, op string, sl billy.Symlink) (string, error) {
|
||||
if rel == "" {
|
||||
return fs.resolveFollowedRoot(followFinal, op, sl)
|
||||
}
|
||||
|
||||
parts := splitRelativePath(rel)
|
||||
resolved := ""
|
||||
followed := 0
|
||||
|
||||
for len(parts) > 0 {
|
||||
part := parts[0]
|
||||
parts = parts[1:]
|
||||
|
||||
currentRel := joinRelativePath(resolved, part)
|
||||
currentPath := fs.Join(fs.Root(), currentRel)
|
||||
if len(parts) == 0 && !followFinal {
|
||||
return currentPath, nil
|
||||
}
|
||||
|
||||
fi, err := sl.Lstat(currentPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fs.Join(fs.Root(), joinRelativePath(append([]string{currentRel}, parts...)...)), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
if fi.Mode()&os.ModeSymlink == 0 {
|
||||
resolved = currentRel
|
||||
continue
|
||||
}
|
||||
|
||||
followed++
|
||||
if followed > maxFollowedSymlinks {
|
||||
return "", symlinkLoopError(op, currentPath)
|
||||
}
|
||||
|
||||
target, err := sl.Readlink(currentPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
targetRel, err := fs.linkTargetRel(currentPath, target)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if targetRel == currentRel {
|
||||
return "", symlinkLoopError(op, currentPath)
|
||||
}
|
||||
|
||||
parts = append(splitRelativePath(targetRel), parts...)
|
||||
resolved = ""
|
||||
}
|
||||
|
||||
return fs.Join(fs.Root(), resolved), nil
|
||||
}
|
||||
|
||||
func symlinkLoopError(op, path string) error {
|
||||
return &os.PathError{Op: op, Path: path, Err: syscall.ELOOP}
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) resolveFollowedRoot(followFinal bool, op string, sl billy.Symlink) (string, error) {
|
||||
root := fs.Join(fs.Root(), "")
|
||||
if !followFinal {
|
||||
return root, nil
|
||||
}
|
||||
|
||||
fi, err := sl.Lstat(root)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return root, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
if fi.Mode()&os.ModeSymlink == 0 {
|
||||
return root, nil
|
||||
}
|
||||
|
||||
target, err := sl.Readlink(root)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
targetRel, err := fs.linkTargetRel(root, target)
|
||||
if err != nil {
|
||||
return root, err
|
||||
}
|
||||
if targetRel == "" {
|
||||
return "", symlinkLoopError(op, root)
|
||||
}
|
||||
|
||||
return fs.resolveFollowedPath(targetRel, followFinal, op, sl)
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) relativeToRoot(filename string) (string, error) {
|
||||
rel, err := filepath.Rel(filepath.Clean(fs.Root()), filepath.Clean(filename))
|
||||
if err != nil || isCrossBoundaries(rel) {
|
||||
return "", billy.ErrCrossedBoundary
|
||||
}
|
||||
|
||||
if rel == "." {
|
||||
return "", nil
|
||||
}
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) linkTargetRel(linkPath, target string) (string, error) {
|
||||
target = filepath.FromSlash(target)
|
||||
if filepath.IsAbs(target) || strings.HasPrefix(target, string(filepath.Separator)) {
|
||||
return fs.relativeToRoot(target)
|
||||
}
|
||||
|
||||
return fs.relativeToRoot(fs.Join(filepath.Dir(linkPath), target))
|
||||
}
|
||||
|
||||
func splitRelativePath(filename string) []string {
|
||||
filename = filepath.Clean(filename)
|
||||
if filename == "" || filename == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(filepath.ToSlash(filename), "/")
|
||||
}
|
||||
|
||||
func joinRelativePath(elem ...string) string {
|
||||
parts := make([]string, 0, len(elem))
|
||||
for _, part := range elem {
|
||||
if part == "" || part == "." {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, part)
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(parts...)
|
||||
}
|
||||
|
||||
func isCreateExclusive(flag int) bool {
|
||||
return flag&os.O_CREATE != 0 && flag&os.O_EXCL != 0
|
||||
}
|
||||
|
||||
func isCrossBoundaries(name string) bool {
|
||||
name = filepath.ToSlash(name)
|
||||
name = strings.TrimLeft(name, "/")
|
||||
name = path.Clean(name)
|
||||
|
||||
return name == ".." || strings.HasPrefix(name, "../")
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) Create(filename string) (billy.File, error) {
|
||||
fullpath, err := fs.underlyingPath(filename)
|
||||
fullpath, err := fs.followedPath(filename, true, "create")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -56,7 +231,7 @@ func (fs *ChrootHelper) Create(filename string) (billy.File, error) {
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) Open(filename string) (billy.File, error) {
|
||||
fullpath, err := fs.underlyingPath(filename)
|
||||
fullpath, err := fs.followedPath(filename, true, "open")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -70,7 +245,7 @@ func (fs *ChrootHelper) Open(filename string) (billy.File, error) {
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) OpenFile(filename string, flag int, mode os.FileMode) (billy.File, error) {
|
||||
fullpath, err := fs.underlyingPath(filename)
|
||||
fullpath, err := fs.followedPath(filename, !isCreateExclusive(flag), "open")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -84,12 +259,16 @@ func (fs *ChrootHelper) OpenFile(filename string, flag int, mode os.FileMode) (b
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) Stat(filename string) (os.FileInfo, error) {
|
||||
fullpath, err := fs.underlyingPath(filename)
|
||||
fullpath, err := fs.followedPath(filename, true, "stat")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fs.underlying.Stat(fullpath)
|
||||
fi, err := fs.underlying.Stat(fullpath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileInfo{FileInfo: fi, name: filepath.Base(filename)}, nil
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) Rename(from, to string) error {
|
||||
@ -135,7 +314,7 @@ func (fs *ChrootHelper) TempFile(dir, prefix string) (billy.File, error) {
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) ReadDir(path string) ([]os.FileInfo, error) {
|
||||
fullpath, err := fs.underlyingPath(path)
|
||||
fullpath, err := fs.followedPath(path, true, "readdir")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -241,6 +420,11 @@ type file struct {
|
||||
name string
|
||||
}
|
||||
|
||||
type fileInfo struct {
|
||||
os.FileInfo
|
||||
name string
|
||||
}
|
||||
|
||||
func newFile(fs billy.Filesystem, f billy.File, filename string) billy.File {
|
||||
filename = fs.Join(fs.Root(), filename)
|
||||
filename, _ = filepath.Rel(fs.Root(), filename)
|
||||
@ -254,3 +438,7 @@ func newFile(fs billy.Filesystem, f billy.File, filename string) billy.File {
|
||||
func (f *file) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (fi fileInfo) Name() string {
|
||||
return fi.name
|
||||
}
|
||||
|
||||
5
vendor/github.com/go-git/go-billy/v5/osfs/os.go
generated
vendored
5
vendor/github.com/go-git/go-billy/v5/osfs/os.go
generated
vendored
@ -24,6 +24,9 @@ var Default = &ChrootOS{}
|
||||
// New returns a new OS filesystem.
|
||||
// By default paths are deduplicated, but still enforced
|
||||
// under baseDir. For more info refer to WithDeduplicatePath.
|
||||
//
|
||||
// New returns ChrootOS by default for v5 compatibility. Users should prefer
|
||||
// New with WithBoundOS.
|
||||
func New(baseDir string, opts ...Option) billy.Filesystem {
|
||||
o := &options{
|
||||
deduplicatePath: true,
|
||||
@ -47,6 +50,8 @@ func WithBoundOS() Option {
|
||||
}
|
||||
|
||||
// WithChrootOS returns the option of using a Chroot filesystem OS.
|
||||
//
|
||||
// Deprecated: use WithBoundOS instead.
|
||||
func WithChrootOS() Option {
|
||||
return func(o *options) {
|
||||
o.Type = ChrootOSFS
|
||||
|
||||
100
vendor/github.com/go-git/go-billy/v5/osfs/os_bound.go
generated
vendored
100
vendor/github.com/go-git/go-billy/v5/osfs/os_bound.go
generated
vendored
@ -20,6 +20,7 @@
|
||||
package osfs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -29,6 +30,31 @@ import (
|
||||
"github.com/go-git/go-billy/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrBaseDirCannotBeRemoved is returned when removing the BoundOS base dir.
|
||||
ErrBaseDirCannotBeRemoved = errors.New("base dir cannot be removed")
|
||||
|
||||
// ErrBaseDirCannotBeRenamed is returned when renaming the BoundOS base dir.
|
||||
ErrBaseDirCannotBeRenamed = errors.New("base dir cannot be renamed")
|
||||
|
||||
dotPrefixes = dotPathPrefixes()
|
||||
dotSeparators = dotPathSeparators()
|
||||
)
|
||||
|
||||
func dotPathPrefixes() []string {
|
||||
if filepath.Separator == '\\' {
|
||||
return []string{"./", ".\\"}
|
||||
}
|
||||
return []string{"./"}
|
||||
}
|
||||
|
||||
func dotPathSeparators() string {
|
||||
if filepath.Separator == '\\' {
|
||||
return `/\`
|
||||
}
|
||||
return `/`
|
||||
}
|
||||
|
||||
// BoundOS is a fs implementation based on the OS filesystem which is bound to
|
||||
// a base dir.
|
||||
// Prefer this fs implementation over ChrootOS.
|
||||
@ -54,6 +80,7 @@ func (fs *BoundOS) Create(filename string) (billy.File, error) {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
|
||||
filename = fs.expandDot(filename)
|
||||
fn, err := fs.abs(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -62,6 +89,7 @@ func (fs *BoundOS) OpenFile(filename string, flag int, perm os.FileMode) (billy.
|
||||
}
|
||||
|
||||
func (fs *BoundOS) ReadDir(path string) ([]os.FileInfo, error) {
|
||||
path = fs.expandDot(path)
|
||||
dir, err := fs.abs(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -71,6 +99,12 @@ func (fs *BoundOS) ReadDir(path string) ([]os.FileInfo, error) {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) Rename(from, to string) error {
|
||||
if fs.isBaseDir(from) {
|
||||
return ErrBaseDirCannotBeRenamed
|
||||
}
|
||||
from = fs.expandDot(from)
|
||||
to = fs.expandDot(to)
|
||||
|
||||
f, err := fs.abs(from)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -89,6 +123,7 @@ func (fs *BoundOS) Rename(from, to string) error {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) MkdirAll(path string, perm os.FileMode) error {
|
||||
path = fs.expandDot(path)
|
||||
dir, err := fs.abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -101,6 +136,7 @@ func (fs *BoundOS) Open(filename string) (billy.File, error) {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) Stat(filename string) (os.FileInfo, error) {
|
||||
filename = fs.expandDot(filename)
|
||||
filename, err := fs.abs(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -109,6 +145,11 @@ func (fs *BoundOS) Stat(filename string) (os.FileInfo, error) {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) Remove(filename string) error {
|
||||
if fs.isBaseDir(filename) {
|
||||
return ErrBaseDirCannotBeRemoved
|
||||
}
|
||||
filename = fs.expandDot(filename)
|
||||
|
||||
fn, err := fs.abs(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -122,6 +163,7 @@ func (fs *BoundOS) Remove(filename string) error {
|
||||
func (fs *BoundOS) TempFile(dir, prefix string) (billy.File, error) {
|
||||
if dir != "" {
|
||||
var err error
|
||||
dir = fs.expandDot(dir)
|
||||
dir, err = fs.abs(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -144,6 +186,11 @@ func (fs *BoundOS) Join(elem ...string) string {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) RemoveAll(path string) error {
|
||||
if fs.isBaseDir(path) {
|
||||
return ErrBaseDirCannotBeRemoved
|
||||
}
|
||||
path = fs.expandDot(path)
|
||||
|
||||
dir, err := fs.abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -152,6 +199,7 @@ func (fs *BoundOS) RemoveAll(path string) error {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) Symlink(target, link string) error {
|
||||
link = fs.expandDot(link)
|
||||
ln, err := fs.abs(link)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -164,6 +212,7 @@ func (fs *BoundOS) Symlink(target, link string) error {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) Lstat(filename string) (os.FileInfo, error) {
|
||||
filename = fs.expandDot(filename)
|
||||
filename = filepath.Clean(filename)
|
||||
if !filepath.IsAbs(filename) {
|
||||
filename = filepath.Join(fs.baseDir, filename)
|
||||
@ -175,6 +224,7 @@ func (fs *BoundOS) Lstat(filename string) (os.FileInfo, error) {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) Readlink(link string) (string, error) {
|
||||
link = fs.expandDot(link)
|
||||
if !filepath.IsAbs(link) {
|
||||
link = filepath.Clean(filepath.Join(fs.baseDir, link))
|
||||
}
|
||||
@ -185,6 +235,7 @@ func (fs *BoundOS) Readlink(link string) (string, error) {
|
||||
}
|
||||
|
||||
func (fs *BoundOS) Chmod(path string, mode os.FileMode) error {
|
||||
path = fs.expandDot(path)
|
||||
abspath, err := fs.abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -199,7 +250,7 @@ func (fs *BoundOS) Chroot(path string) (billy.Filesystem, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return New(joined), nil
|
||||
return New(joined, WithBoundOS()), nil
|
||||
}
|
||||
|
||||
// Root returns the current base dir of the billy.Filesystem.
|
||||
@ -220,6 +271,37 @@ func (fs *BoundOS) createDir(fullpath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *BoundOS) expandDot(path string) string {
|
||||
if path == "." {
|
||||
return fs.baseDir
|
||||
}
|
||||
for _, prefix := range dotPrefixes {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
path = strings.TrimLeft(strings.TrimPrefix(path, prefix), dotSeparators)
|
||||
if path == "" {
|
||||
return fs.baseDir
|
||||
}
|
||||
return path
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func (fs *BoundOS) isBaseDir(path string) bool {
|
||||
if path == "" || filepath.Clean(path) == "." {
|
||||
return true
|
||||
}
|
||||
path = fs.expandDot(path)
|
||||
if filepath.Clean(path) == filepath.Clean(fs.baseDir) {
|
||||
return true
|
||||
}
|
||||
abspath, err := fs.abs(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return filepath.Clean(abspath) == filepath.Clean(fs.baseDir)
|
||||
}
|
||||
|
||||
// abs transforms filename to an absolute path, taking into account the base dir.
|
||||
// Relative paths won't be allowed to ascend the base dir, so `../file` will become
|
||||
// `/working-dir/file`.
|
||||
@ -233,7 +315,7 @@ func (fs *BoundOS) abs(filename string) (string, error) {
|
||||
|
||||
path, err := securejoin.SecureJoin(fs.baseDir, filename)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
return "", err
|
||||
}
|
||||
|
||||
if fs.deduplicatePath {
|
||||
@ -246,24 +328,12 @@ func (fs *BoundOS) abs(filename string) (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// insideBaseDir checks whether filename is located within
|
||||
// the fs.baseDir.
|
||||
func (fs *BoundOS) insideBaseDir(filename string) (bool, error) {
|
||||
if filename == fs.baseDir {
|
||||
return true, nil
|
||||
}
|
||||
if !strings.HasPrefix(filename, fs.baseDir+string(filepath.Separator)) {
|
||||
return false, fmt.Errorf("path outside base dir")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// insideBaseDirEval checks whether filename is contained within
|
||||
// a dir that is within the fs.baseDir, by first evaluating any symlinks
|
||||
// that either filename or fs.baseDir may contain.
|
||||
func (fs *BoundOS) insideBaseDirEval(filename string) (bool, error) {
|
||||
// "/" contains all others.
|
||||
if fs.baseDir == "/" {
|
||||
if fs.baseDir == "/" || fs.baseDir == filename {
|
||||
return true, nil
|
||||
}
|
||||
dir, err := filepath.EvalSymlinks(filepath.Dir(filename))
|
||||
|
||||
10
vendor/github.com/go-git/go-billy/v5/osfs/os_chroot.go
generated
vendored
10
vendor/github.com/go-git/go-billy/v5/osfs/os_chroot.go
generated
vendored
@ -14,6 +14,8 @@ import (
|
||||
// ChrootOS is a legacy filesystem based on a "soft chroot" of the os filesystem.
|
||||
// Although this is still the default os filesystem, consider using BoundOS instead.
|
||||
//
|
||||
// Deprecated: use New with WithBoundOS instead.
|
||||
//
|
||||
// Behaviours of note:
|
||||
// 1. A "soft chroot" translates the base dir to "/" for the purposes of the
|
||||
// fs abstraction.
|
||||
@ -24,6 +26,14 @@ import (
|
||||
type ChrootOS struct{}
|
||||
|
||||
func newChrootOS(baseDir string) billy.Filesystem {
|
||||
if baseDir != "" {
|
||||
resolved, err := filepath.EvalSymlinks(baseDir)
|
||||
if err != nil {
|
||||
return chroot.New(&ChrootOS{}, baseDir)
|
||||
}
|
||||
baseDir = resolved
|
||||
}
|
||||
|
||||
return chroot.New(&ChrootOS{}, baseDir)
|
||||
}
|
||||
|
||||
|
||||
43
vendor/github.com/go-git/go-billy/v5/util/util.go
generated
vendored
43
vendor/github.com/go-git/go-billy/v5/util/util.go
generated
vendored
@ -16,8 +16,6 @@ import (
|
||||
// can but returns the first error it encounters. If the path does not exist,
|
||||
// RemoveAll returns nil (no error).
|
||||
func RemoveAll(fs billy.Basic, path string) error {
|
||||
fs, path = getUnderlyingAndPath(fs, path)
|
||||
|
||||
if r, ok := fs.(removerAll); ok {
|
||||
return r.RemoveAll(path)
|
||||
}
|
||||
@ -39,7 +37,7 @@ func removeAll(fs billy.Basic, path string) error {
|
||||
}
|
||||
|
||||
// Otherwise, is this a directory we need to recurse into?
|
||||
dir, serr := fs.Stat(path)
|
||||
dir, serr := lstat(fs, path)
|
||||
if serr != nil {
|
||||
if errors.Is(serr, os.ErrNotExist) {
|
||||
return nil
|
||||
@ -48,8 +46,8 @@ func removeAll(fs billy.Basic, path string) error {
|
||||
return serr
|
||||
}
|
||||
|
||||
if !dir.IsDir() {
|
||||
// Not a directory; return the error from Remove.
|
||||
if dir.Mode()&os.ModeSymlink != 0 || !dir.IsDir() {
|
||||
// Not a directory we should recurse into; return the error from Remove.
|
||||
return err
|
||||
}
|
||||
|
||||
@ -62,7 +60,7 @@ func removeAll(fs billy.Basic, path string) error {
|
||||
fis, err := dirfs.ReadDir(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// Race. It was deleted between the Lstat and Open.
|
||||
// Race. It was deleted between the Lstat and ReadDir.
|
||||
// Return nil per RemoveAll's docs.
|
||||
return nil
|
||||
}
|
||||
@ -91,7 +89,18 @@ func removeAll(fs billy.Basic, path string) error {
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func lstat(filesystem billy.Basic, path string) (os.FileInfo, error) {
|
||||
if sl, ok := filesystem.(billy.Symlink); ok {
|
||||
// Avoid following a symlink substituted after the initial Remove fails.
|
||||
fi, err := sl.Lstat(path)
|
||||
if err == nil || !errors.Is(err, billy.ErrNotSupported) {
|
||||
return fi, err
|
||||
}
|
||||
}
|
||||
|
||||
return filesystem.Stat(path)
|
||||
}
|
||||
|
||||
// WriteFile writes data to a file named by filename in the given filesystem.
|
||||
@ -123,8 +132,10 @@ func WriteFile(fs billy.Basic, filename string, data []byte, perm os.FileMode) (
|
||||
// We generate random temporary file names so that there's a good
|
||||
// chance the file doesn't exist yet - keeps the number of tries in
|
||||
// TempFile to a minimum.
|
||||
var rand uint32
|
||||
var randmu sync.Mutex
|
||||
var (
|
||||
rand uint32
|
||||
randmu sync.Mutex
|
||||
)
|
||||
|
||||
func reseed() uint32 {
|
||||
return uint32(time.Now().UnixNano() + int64(os.Getpid()))
|
||||
@ -220,22 +231,6 @@ func getTempDir(fs billy.Basic) string {
|
||||
return ".tmp"
|
||||
}
|
||||
|
||||
type underlying interface {
|
||||
Underlying() billy.Basic
|
||||
}
|
||||
|
||||
func getUnderlyingAndPath(fs billy.Basic, path string) (billy.Basic, string) {
|
||||
u, ok := fs.(underlying)
|
||||
if !ok {
|
||||
return fs, path
|
||||
}
|
||||
if ch, ok := fs.(billy.Chroot); ok {
|
||||
path = fs.Join(ch.Root(), path)
|
||||
}
|
||||
|
||||
return u.Underlying(), path
|
||||
}
|
||||
|
||||
// ReadFile reads the named file and returns the contents from the given filesystem.
|
||||
// A successful call returns err == nil, not err == EOF.
|
||||
// Because ReadFile reads the whole file, it does not treat an EOF from Read
|
||||
|
||||
31
vendor/github.com/go-git/go-git/v5/config/config.go
generated
vendored
31
vendor/github.com/go-git/go-git/v5/config/config.go
generated
vendored
@ -61,6 +61,16 @@ type Config struct {
|
||||
CommentChar string
|
||||
// RepositoryFormatVersion identifies the repository format and layout version.
|
||||
RepositoryFormatVersion format.RepositoryFormatVersion
|
||||
// ProtectNTFS controls whether NTFS-specific path protections are
|
||||
// applied (e.g. rejecting .git trailing spaces/periods, alternate
|
||||
// data streams, 8.3 short names). When unset, defaults to true on
|
||||
// Windows.
|
||||
ProtectNTFS OptBool
|
||||
// ProtectHFS controls whether HFS+-specific path protections are
|
||||
// applied (e.g. rejecting .git with Unicode zero-width or
|
||||
// directional characters that HFS+ would normalize away).
|
||||
// When unset, defaults to true on macOS.
|
||||
ProtectHFS OptBool
|
||||
}
|
||||
|
||||
User struct {
|
||||
@ -266,6 +276,8 @@ const (
|
||||
repositoryFormatVersionKey = "repositoryformatversion"
|
||||
objectFormat = "objectformat"
|
||||
mirrorKey = "mirror"
|
||||
protectNTFSKey = "protectNTFS"
|
||||
protectHFSKey = "protectHFS"
|
||||
|
||||
// DefaultPackWindow holds the number of previous objects used to
|
||||
// generate deltas. The value 10 is the same used by git command.
|
||||
@ -309,6 +321,14 @@ func (c *Config) unmarshalCore() {
|
||||
|
||||
c.Core.Worktree = s.Options.Get(worktreeKey)
|
||||
c.Core.CommentChar = s.Options.Get(commentCharKey)
|
||||
|
||||
if parsed := parseConfigBool(s.Options.Get(protectNTFSKey)); parsed.IsSet() {
|
||||
c.Core.ProtectNTFS = parsed
|
||||
}
|
||||
|
||||
if parsed := parseConfigBool(s.Options.Get(protectHFSKey)); parsed.IsSet() {
|
||||
c.Core.ProtectHFS = parsed
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) unmarshalUser() {
|
||||
@ -379,7 +399,8 @@ func unmarshalSubmodules(fc *format.Config, submodules map[string]*Submodule) {
|
||||
m := &Submodule{}
|
||||
m.unmarshal(sub)
|
||||
|
||||
if m.Validate() == ErrModuleBadPath {
|
||||
if err := m.Validate(); errors.Is(err, ErrModuleBadPath) ||
|
||||
errors.Is(err, ErrModuleBadName) {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -436,6 +457,14 @@ func (c *Config) marshalCore() {
|
||||
if c.Core.Worktree != "" {
|
||||
s.SetOption(worktreeKey, c.Core.Worktree)
|
||||
}
|
||||
|
||||
if c.Core.ProtectNTFS.IsSet() {
|
||||
s.SetOption(protectNTFSKey, c.Core.ProtectNTFS.FormatBool())
|
||||
}
|
||||
|
||||
if c.Core.ProtectHFS.IsSet() {
|
||||
s.SetOption(protectHFSKey, c.Core.ProtectHFS.FormatBool())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) marshalExtensions() {
|
||||
|
||||
52
vendor/github.com/go-git/go-git/v5/config/modules.go
generated
vendored
52
vendor/github.com/go-git/go-git/v5/config/modules.go
generated
vendored
@ -3,8 +3,11 @@ package config
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5/internal/pathutil"
|
||||
format "github.com/go-git/go-git/v5/plumbing/format/config"
|
||||
)
|
||||
|
||||
@ -12,6 +15,7 @@ var (
|
||||
ErrModuleEmptyURL = errors.New("module config: empty URL")
|
||||
ErrModuleEmptyPath = errors.New("module config: empty path")
|
||||
ErrModuleBadPath = errors.New("submodule has an invalid path")
|
||||
ErrModuleBadName = errors.New("ignoring suspicious submodule name")
|
||||
)
|
||||
|
||||
var (
|
||||
@ -94,6 +98,10 @@ type Submodule struct {
|
||||
|
||||
// Validate validates the fields and sets the default values.
|
||||
func (m *Submodule) Validate() error {
|
||||
if err := validSubmoduleName(m.Name); err != nil {
|
||||
return fmt.Errorf("%w: %q", ErrModuleBadName, m.Name)
|
||||
}
|
||||
|
||||
if m.Path == "" {
|
||||
return ErrModuleEmptyPath
|
||||
}
|
||||
@ -109,6 +117,50 @@ func (m *Submodule) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validSubmoduleName mirrors canonical Git's check_submodule_name in
|
||||
// submodule-config.c [1]: reject empty names and any name with a ".."
|
||||
// path component, using both '/' and '\\' as separators so the rule
|
||||
// is consistent across platforms. The component check is delegated to
|
||||
// `pathutil.IsHFSDot` and `pathutil.IsNTFSDot` with `.` as the needle,
|
||||
// which both cover the bare ".." case and reject components that
|
||||
// resolve to ".." after HFS+ Unicode normalisation (ignored code
|
||||
// points, e.g. `.<U+200C>.`) or NTFS trailing-space/dot/ADS
|
||||
// canonicalisation (e.g. `.. `, `..::$INDEX_ALLOCATION`).
|
||||
// `.gitmodules` is attacker-controlled by definition, so both checks
|
||||
// run unconditionally regardless of host OS.
|
||||
//
|
||||
// The additional checks (bare ".", NUL byte, leading or trailing
|
||||
// separator, drive-letter prefix) close go-git-specific edge cases
|
||||
// the canonical loop does not exercise: canonical Git treats names
|
||||
// as opaque C strings, while Go strings carry NULs through and the
|
||||
// billy filesystem layer is path-aware in ways Git's working storage
|
||||
// is not.
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/submodule-config.c#L214-L237
|
||||
func validSubmoduleName(name string) error {
|
||||
if name == "" || name == "." {
|
||||
return ErrModuleBadName
|
||||
}
|
||||
for _, seg := range strings.FieldsFunc(name, isPathSep) {
|
||||
if pathutil.IsHFSDot(seg, ".") || pathutil.IsNTFSDot(seg, ".", "") {
|
||||
return ErrModuleBadName
|
||||
}
|
||||
}
|
||||
// go-git-specific defensive checks beyond canonical Git.
|
||||
if strings.ContainsRune(name, 0) {
|
||||
return ErrModuleBadName
|
||||
}
|
||||
if isPathSep(rune(name[0])) || isPathSep(rune(name[len(name)-1])) {
|
||||
return ErrModuleBadName
|
||||
}
|
||||
if len(name) >= 2 && name[1] == ':' {
|
||||
return ErrModuleBadName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isPathSep(r rune) bool { return r == '/' || r == '\\' }
|
||||
|
||||
func (m *Submodule) unmarshal(s *format.Subsection) {
|
||||
m.raw = s
|
||||
|
||||
|
||||
82
vendor/github.com/go-git/go-git/v5/config/optbool.go
generated
vendored
Normal file
82
vendor/github.com/go-git/go-git/v5/config/optbool.go
generated
vendored
Normal file
@ -0,0 +1,82 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// OptBool is a tri-state boolean: unset, explicitly false, or explicitly true.
|
||||
// Its zero value (OptBoolUnset) means the setting was not specified, which
|
||||
// allows merge logic based on reflect.Value.IsZero to skip unset fields while
|
||||
// still letting an explicit "false" override a previously set "true".
|
||||
type OptBool byte
|
||||
|
||||
const (
|
||||
// OptBoolUnset indicates the setting was not specified.
|
||||
OptBoolUnset OptBool = iota
|
||||
// OptBoolFalse indicates the setting was explicitly set to false.
|
||||
OptBoolFalse
|
||||
// OptBoolTrue indicates the setting was explicitly set to true.
|
||||
OptBoolTrue
|
||||
)
|
||||
|
||||
// NewOptBool converts a plain bool into an OptBool.
|
||||
func NewOptBool(v bool) OptBool {
|
||||
if v {
|
||||
return OptBoolTrue
|
||||
}
|
||||
return OptBoolFalse
|
||||
}
|
||||
|
||||
// IsTrue returns whether the value is explicitly true.
|
||||
func (o OptBool) IsTrue() bool { return o == OptBoolTrue }
|
||||
|
||||
// IsSet returns whether the value was explicitly specified (true or false).
|
||||
func (o OptBool) IsSet() bool { return o != OptBoolUnset }
|
||||
|
||||
func (o OptBool) String() string {
|
||||
switch o {
|
||||
case OptBoolTrue:
|
||||
return "true"
|
||||
case OptBoolFalse:
|
||||
return "false"
|
||||
default:
|
||||
return "unset"
|
||||
}
|
||||
}
|
||||
|
||||
// FormatBool returns the strconv-formatted value. Only meaningful when IsSet.
|
||||
func (o OptBool) FormatBool() string {
|
||||
return strconv.FormatBool(o.IsTrue())
|
||||
}
|
||||
|
||||
// parseConfigBool mirrors upstream Git's git_parse_maybe_bool: it
|
||||
// accepts true/yes/on (→ OptBoolTrue) and false/no/off (→
|
||||
// OptBoolFalse) case-insensitively, plus any decimal integer (zero
|
||||
// → OptBoolFalse, non-zero → OptBoolTrue). Empty or otherwise
|
||||
// unrecognised values return OptBoolUnset, leaving the caller's
|
||||
// platform default in place. The empty-string handling is the only
|
||||
// intentional divergence from upstream, which returns false for
|
||||
// empty: in our unmarshalCore caller, an empty value means the key
|
||||
// is unset and the platform default should apply.
|
||||
//
|
||||
// Reference: upstream Git git_parse_maybe_bool_text at parse.c
|
||||
// L157-L173 and git_parse_maybe_bool at parse.c L174-L182 in tag
|
||||
// v2.54.0[1].
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/parse.c#L157-L182
|
||||
func parseConfigBool(v string) OptBool {
|
||||
switch strings.ToLower(v) {
|
||||
case "true", "yes", "on":
|
||||
return OptBoolTrue
|
||||
case "false", "no", "off":
|
||||
return OptBoolFalse
|
||||
}
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
if i != 0 {
|
||||
return OptBoolTrue
|
||||
}
|
||||
return OptBoolFalse
|
||||
}
|
||||
return OptBoolUnset
|
||||
}
|
||||
21
vendor/github.com/go-git/go-git/v5/internal/pathutil/dotgit.go
generated
vendored
Normal file
21
vendor/github.com/go-git/go-git/v5/internal/pathutil/dotgit.go
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
package pathutil
|
||||
|
||||
import "strings"
|
||||
|
||||
// IsDotGitName reports whether name is `.git` or its 8.3 NTFS short
|
||||
// alias `git~1`, case-insensitively. Both are forbidden as path
|
||||
// components (and as submodule names) because they refer to the
|
||||
// repository's own metadata directory.
|
||||
//
|
||||
// File names that do not conform to the 8.3 format (up to eight
|
||||
// characters for the basename, three for the file extension) are
|
||||
// associated with a so-called "short name" on NTFS — at least on
|
||||
// the `C:` drive by default — which means that `git~1/` is a valid
|
||||
// way to refer to `.git/`.
|
||||
func IsDotGitName(name string) bool {
|
||||
switch strings.ToLower(name) {
|
||||
case ".git", "git~1":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
99
vendor/github.com/go-git/go-git/v5/internal/pathutil/hfs.go
generated
vendored
Normal file
99
vendor/github.com/go-git/go-git/v5/internal/pathutil/hfs.go
generated
vendored
Normal file
@ -0,0 +1,99 @@
|
||||
package pathutil
|
||||
|
||||
import "unicode"
|
||||
|
||||
// hfsIgnoredCodepoints contains Unicode code points that HFS+ ignores
|
||||
// during path normalization. A path component containing these
|
||||
// characters between the bytes of ".git" (or ".gitmodules", etc.)
|
||||
// will be treated as that name by HFS+, so they have to be filtered
|
||||
// out before comparison.
|
||||
//
|
||||
// See upstream Git utf8.c next_hfs_char in tag v2.54.0[1].
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/utf8.c#L703-L740
|
||||
var hfsIgnoredCodepoints = map[rune]struct{}{
|
||||
0x200c: {}, // ZERO WIDTH NON-JOINER
|
||||
0x200d: {}, // ZERO WIDTH JOINER
|
||||
0x200e: {}, // LEFT-TO-RIGHT MARK
|
||||
0x200f: {}, // RIGHT-TO-LEFT MARK
|
||||
0x202a: {}, // LEFT-TO-RIGHT EMBEDDING
|
||||
0x202b: {}, // RIGHT-TO-LEFT EMBEDDING
|
||||
0x202c: {}, // POP DIRECTIONAL FORMATTING
|
||||
0x202d: {}, // LEFT-TO-RIGHT OVERRIDE
|
||||
0x202e: {}, // RIGHT-TO-LEFT OVERRIDE
|
||||
0x206a: {}, // INHIBIT SYMMETRIC SWAPPING
|
||||
0x206b: {}, // ACTIVATE SYMMETRIC SWAPPING
|
||||
0x206c: {}, // INHIBIT ARABIC FORM SHAPING
|
||||
0x206d: {}, // ACTIVATE ARABIC FORM SHAPING
|
||||
0x206e: {}, // NATIONAL DIGIT SHAPES
|
||||
0x206f: {}, // NOMINAL DIGIT SHAPES
|
||||
0xfeff: {}, // ZERO WIDTH NO-BREAK SPACE
|
||||
}
|
||||
|
||||
// IsHFSDot reports whether part would be treated as ".<needle>" on an
|
||||
// HFS+ filesystem after stripping ignored Unicode code points and
|
||||
// folding ASCII to lower case. The needle is the lowercase ASCII
|
||||
// suffix without the leading dot (e.g. "git", "gitmodules"). It
|
||||
// mirrors upstream Git's is_hfs_dot_generic and is the building
|
||||
// block of IsHFSDotGit / IsHFSDotGitmodules.
|
||||
//
|
||||
// Reference: upstream Git utf8.c is_hfs_dot_generic at L741-L774 and
|
||||
// the dotgit family at L784-L809 in tag v2.54.0[1].
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/utf8.c#L741-L809
|
||||
func IsHFSDot(part, needle string) bool {
|
||||
runes := []rune(part)
|
||||
i := 0
|
||||
|
||||
// skip ignored code points, then expect '.'
|
||||
for i < len(runes) {
|
||||
if _, ok := hfsIgnoredCodepoints[runes[i]]; !ok {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i >= len(runes) || runes[i] != '.' {
|
||||
return false
|
||||
}
|
||||
i++
|
||||
|
||||
// match needle case-insensitively, skipping ignored code points
|
||||
for _, expected := range needle {
|
||||
for i < len(runes) {
|
||||
if _, ok := hfsIgnoredCodepoints[runes[i]]; !ok {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i >= len(runes) {
|
||||
return false
|
||||
}
|
||||
r := runes[i]
|
||||
if r > 127 {
|
||||
return false
|
||||
}
|
||||
if unicode.ToLower(r) != expected {
|
||||
return false
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
// skip trailing ignored code points
|
||||
for i < len(runes) {
|
||||
if _, ok := hfsIgnoredCodepoints[runes[i]]; !ok {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
// must be at end of component
|
||||
return i == len(runes)
|
||||
}
|
||||
|
||||
// IsHFSDotGit reports whether part is an HFS+ equivalent of ".git".
|
||||
func IsHFSDotGit(part string) bool { return IsHFSDot(part, "git") }
|
||||
|
||||
// IsHFSDotGitmodules reports whether part is an HFS+ equivalent of
|
||||
// ".gitmodules", catching attempts to plant the file via Unicode
|
||||
// code points that HFS+ would strip during normalisation.
|
||||
func IsHFSDotGitmodules(part string) bool { return IsHFSDot(part, "gitmodules") }
|
||||
187
vendor/github.com/go-git/go-git/v5/internal/pathutil/ntfs.go
generated
vendored
Normal file
187
vendor/github.com/go-git/go-git/v5/internal/pathutil/ntfs.go
generated
vendored
Normal file
@ -0,0 +1,187 @@
|
||||
package pathutil
|
||||
|
||||
import "strings"
|
||||
|
||||
// IsNTFSDotGit ports upstream Git's is_ntfs_dotgit. It detects path
|
||||
// components that NTFS would resolve to ".git": the canonical name
|
||||
// itself and its 8.3 short-name alias "git~1", each followed by any
|
||||
// number of trailing spaces or periods (which NTFS silently trims)
|
||||
// and an optional Alternate Data Stream suffix (":<stream>"). The
|
||||
// bare strings ".git" and "git~1" also match, mirroring upstream.
|
||||
//
|
||||
// Reference: upstream Git path.c is_ntfs_dotgit at L1415-L1449
|
||||
// in tag v2.54.0[1].
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/path.c#L1415-L1449
|
||||
func IsNTFSDotGit(part string) bool {
|
||||
var i int
|
||||
switch {
|
||||
case len(part) >= 4 && part[0] == '.' &&
|
||||
asciiToLower(part[1]) == 'g' &&
|
||||
asciiToLower(part[2]) == 'i' &&
|
||||
asciiToLower(part[3]) == 't':
|
||||
i = 4
|
||||
case len(part) >= 5 &&
|
||||
asciiToLower(part[0]) == 'g' &&
|
||||
asciiToLower(part[1]) == 'i' &&
|
||||
asciiToLower(part[2]) == 't' &&
|
||||
part[3] == '~' && part[4] == '1':
|
||||
i = 5
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
for ; i < len(part); i++ {
|
||||
c := part[i]
|
||||
if c == ':' {
|
||||
return true
|
||||
}
|
||||
if c != '.' && c != ' ' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// WindowsValidPath reports whether part is a valid Windows / NTFS
|
||||
// path component for the worktree filesystem abstraction. It rejects
|
||||
// NTFS-disguised variants of `.git` and `git~1` (trailing spaces,
|
||||
// periods, Alternate Data Streams) and Windows reserved device
|
||||
// names. Bare `.git` and `git~1` are allowed at this layer; the
|
||||
// caller decides whether they are permissible at the current path
|
||||
// position.
|
||||
func WindowsValidPath(part string) bool {
|
||||
if IsNTFSDotGit(part) && !IsDotGitName(part) {
|
||||
return false
|
||||
}
|
||||
return !isWindowsReservedName(part)
|
||||
}
|
||||
|
||||
// windowsReservedNames lists the Windows reserved device names.
|
||||
// A path component is reserved if its base name (ignoring trailing
|
||||
// spaces, extensions, and NTFS Alternate Data Streams) matches one of
|
||||
// these case-insensitively.
|
||||
//
|
||||
// See upstream Git compat/mingw.c is_valid_win32_path().
|
||||
var windowsReservedNames = []string{
|
||||
"CON", "PRN", "AUX", "NUL",
|
||||
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||
"CONIN$", "CONOUT$",
|
||||
}
|
||||
|
||||
func isWindowsReservedName(part string) bool {
|
||||
for _, name := range windowsReservedNames {
|
||||
if len(part) < len(name) {
|
||||
continue
|
||||
}
|
||||
if !strings.EqualFold(part[:len(name)], name) {
|
||||
continue
|
||||
}
|
||||
// Exact match or followed by space, dot, colon (ADS), or separator.
|
||||
if len(part) == len(name) {
|
||||
return true
|
||||
}
|
||||
switch part[len(name)] {
|
||||
case ' ', '.', ':':
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsNTFSDot ports upstream Git's is_ntfs_dot_generic. It detects NTFS
|
||||
// path-component variants of a dotfile name that attackers can use to
|
||||
// bypass case-insensitive comparisons against the canonical name on
|
||||
// Windows. The dotgit parameter is the lowercase name without the
|
||||
// leading dot (e.g. "gitmodules"); shortnamePrefix is the canonical
|
||||
// 6-character NTFS short-name prefix used as a fall-back match
|
||||
// (e.g. "gi7eba" for ".gitmodules").
|
||||
//
|
||||
// Reference: upstream Git path.c is_ntfs_dot_generic at L1451-L1507
|
||||
// in tag v2.54.0[1].
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/path.c#L1451-L1507
|
||||
func IsNTFSDot(name, dotgit, shortnamePrefix string) bool {
|
||||
// onlySpacesAndPeriods returns true when the suffix from start
|
||||
// onwards consists only of trailing spaces and periods, possibly
|
||||
// terminated by a NTFS Alternate Data Stream colon. Mirrors the
|
||||
// only_spaces_and_periods label in upstream's is_ntfs_dot_generic.
|
||||
onlySpacesAndPeriods := func(start int) bool {
|
||||
for i := start; i < len(name); i++ {
|
||||
c := name[i]
|
||||
if c == ':' {
|
||||
return true
|
||||
}
|
||||
if c != ' ' && c != '.' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Pattern 1: ".<dotgit>" prefix + trailing spaces / periods / ADS.
|
||||
if len(name) >= len(dotgit)+1 && name[0] == '.' &&
|
||||
strings.EqualFold(name[1:1+len(dotgit)], dotgit) {
|
||||
if onlySpacesAndPeriods(len(dotgit) + 1) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 2: standard NTFS short name <dotgit[:6]>~[1-4].
|
||||
if len(dotgit) >= 6 && len(name) >= 8 &&
|
||||
strings.EqualFold(name[:6], dotgit[:6]) &&
|
||||
name[6] == '~' && name[7] >= '1' && name[7] <= '4' {
|
||||
if onlySpacesAndPeriods(8) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 3: fall-back NTFS short name keyed by shortnamePrefix.
|
||||
if len(shortnamePrefix) < 6 || len(name) < 8 {
|
||||
return false
|
||||
}
|
||||
sawTilde := false
|
||||
i := 0
|
||||
for i < 8 {
|
||||
c := name[i]
|
||||
switch {
|
||||
case sawTilde:
|
||||
if c < '0' || c > '9' {
|
||||
return false
|
||||
}
|
||||
case c == '~':
|
||||
i++
|
||||
if i >= len(name) || name[i] < '1' || name[i] > '9' {
|
||||
return false
|
||||
}
|
||||
sawTilde = true
|
||||
case i >= 6:
|
||||
return false
|
||||
case c&0x80 != 0:
|
||||
return false
|
||||
default:
|
||||
if asciiToLower(c) != shortnamePrefix[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
return onlySpacesAndPeriods(8)
|
||||
}
|
||||
|
||||
// IsNTFSDotGitmodules reports whether part is an NTFS-equivalent of
|
||||
// ".gitmodules" — the file name (or any of its variants that NTFS
|
||||
// would resolve to it) that attackers can use to plant submodule
|
||||
// configuration disguised as a symlink. The 6-character canonical
|
||||
// short-name prefix "gi7eba" mirrors upstream Git's is_ntfs_dotgitmodules.
|
||||
func IsNTFSDotGitmodules(part string) bool {
|
||||
return IsNTFSDot(part, "gitmodules", "gi7eba")
|
||||
}
|
||||
|
||||
func asciiToLower(c byte) byte {
|
||||
if c >= 'A' && c <= 'Z' {
|
||||
return c + ('a' - 'A')
|
||||
}
|
||||
return c
|
||||
}
|
||||
66
vendor/github.com/go-git/go-git/v5/internal/pathutil/tree.go
generated
vendored
Normal file
66
vendor/github.com/go-git/go-git/v5/internal/pathutil/tree.go
generated
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
package pathutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrInvalidPath is returned by ValidTreePath when its argument is
|
||||
// not a safe path to materialise into the worktree.
|
||||
var ErrInvalidPath = fmt.Errorf("invalid path")
|
||||
|
||||
// ValidTreePath rejects path strings that, if materialised into a
|
||||
// worktree, would let an attacker-controlled tree entry escape the
|
||||
// worktree or rewrite repository metadata. It rejects:
|
||||
//
|
||||
// - control characters (< 0x20, 0x7f);
|
||||
// - empty paths and "." / ".." components;
|
||||
// - Windows volume name prefixes (e.g. C:);
|
||||
// - .git, its 8.3 NTFS short-name git~1, plus their HFS+ and NTFS
|
||||
// variants — at every position, not just the root.
|
||||
//
|
||||
// HFS+/NTFS variants of `.git` are always rejected at this layer
|
||||
// regardless of runtime config: tree paths are canonical UTF-8 with
|
||||
// no zero-width characters or NTFS short-name forms, so an entry
|
||||
// that looks like a disguised `.git` is suspicious anywhere. Windows
|
||||
// reserved device names (CON, NUL, etc.) are not policed here — they
|
||||
// are legitimate filenames on non-Windows filesystems and upstream
|
||||
// Git accepts them. The wrapper layer (validPath in package git)
|
||||
// rejects them at materialisation time when core.protectNTFS is on.
|
||||
//
|
||||
// Mirrors upstream Git's verify_path_internal at read-cache.c#L987
|
||||
// in tag v2.54.0[1] with protect_hfs / protect_ntfs treated as
|
||||
// always-on for `.git`-disguise detection (tree paths are not
|
||||
// application-supplied) and is_valid_win32_path left to the wrapper.
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/read-cache.c#L987
|
||||
func ValidTreePath(p string) error {
|
||||
for i := 0; i < len(p); i++ {
|
||||
if p[i] < 0x20 || p[i] == 0x7f {
|
||||
return fmt.Errorf("%w %q: contains control character", ErrInvalidPath, p)
|
||||
}
|
||||
}
|
||||
|
||||
parts := strings.FieldsFunc(p, func(r rune) bool { return r == '\\' || r == '/' })
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("%w: %q", ErrInvalidPath, p)
|
||||
}
|
||||
|
||||
// Volume names are not supported, in both formats: \\ and <DRIVE_LETTER>:.
|
||||
if vol := filepath.VolumeName(p); vol != "" {
|
||||
return fmt.Errorf("%w: %q", ErrInvalidPath, p)
|
||||
}
|
||||
|
||||
for _, part := range parts {
|
||||
if part == "." || part == ".." {
|
||||
return fmt.Errorf("%w %q: cannot use %q", ErrInvalidPath, p, part)
|
||||
}
|
||||
|
||||
if IsDotGitName(part) || IsHFSDotGit(part) || IsNTFSDotGit(part) {
|
||||
return fmt.Errorf("%w component: %q", ErrInvalidPath, p)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
37
vendor/github.com/go-git/go-git/v5/internal/url/url.go
generated
vendored
37
vendor/github.com/go-git/go-git/v5/internal/url/url.go
generated
vendored
@ -2,12 +2,14 @@ package url
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
isSchemeRegExp = regexp.MustCompile(`^[^:]+://`)
|
||||
|
||||
// Ref: https://github.com/git/git/blob/master/Documentation/urls.txt#L37
|
||||
// Ref: https://github.com/git/git/blob/v2.54.0/Documentation/urls.adoc#L41-L48
|
||||
scpLikeUrlRegExp = regexp.MustCompile(`^(?:(?P<user>[^@]+)@)?(?P<host>[^:\s]+):(?:(?P<port>[0-9]{1,5}):)?(?P<path>[^\\].*)$`)
|
||||
)
|
||||
|
||||
@ -20,7 +22,38 @@ func MatchesScheme(url string) bool {
|
||||
// MatchesScpLike returns true if the given string matches an SCP-like
|
||||
// format scheme.
|
||||
func MatchesScpLike(url string) bool {
|
||||
return scpLikeUrlRegExp.MatchString(url)
|
||||
if !scpLikeUrlRegExp.MatchString(url) {
|
||||
return false
|
||||
}
|
||||
// Mirror canonical Git's url_is_local_not_ssh in connect.c[1] for
|
||||
// the cases the regex above cannot disambiguate by itself: a URL
|
||||
// is treated as a local path (not SCP-style SSH) when a `/`
|
||||
// precedes the first `:` (e.g. `./relative:path`,
|
||||
// `/abs/with:colon/file`), or — on Windows only — when it has a
|
||||
// DOS drive prefix like `C:foo` where the host is a single
|
||||
// ASCII letter.
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/connect.c#L710-L716
|
||||
if before, _, _ := strings.Cut(url, ":"); strings.Contains(before, "/") {
|
||||
return false
|
||||
}
|
||||
if runtime.GOOS == "windows" && hasDosDrivePrefix(url) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// hasDosDrivePrefix reports whether s begins with `<letter>:` (a
|
||||
// Windows drive prefix such as `C:` or `c:`). Mirrors canonical Git's
|
||||
// win32_has_dos_drive_prefix[1].
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/compat/win32/path-utils.c#L20-L29
|
||||
func hasDosDrivePrefix(s string) bool {
|
||||
if len(s) < 2 || s[1] != ':' {
|
||||
return false
|
||||
}
|
||||
c := s[0]
|
||||
return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')
|
||||
}
|
||||
|
||||
// FindScpLikeComponents returns the user, host, port and path of the
|
||||
|
||||
164
vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/decoder.go
generated
vendored
164
vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/decoder.go
generated
vendored
@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/hash"
|
||||
"github.com/go-git/go-git/v5/utils/binary"
|
||||
@ -25,35 +26,88 @@ const (
|
||||
objectIDLength = hash.Size
|
||||
)
|
||||
|
||||
// Byte sizes of the idx v2 layout elements, used by the size formula
|
||||
// in [validateIdxV2Size]. See [gitformat-pack] for the canonical
|
||||
// layout.
|
||||
//
|
||||
// [gitformat-pack]: https://git-scm.com/docs/gitformat-pack
|
||||
const (
|
||||
headerLen = 8 // magic + version
|
||||
fanoutLen = fanout * 4 // uint32 per bucket
|
||||
crc32Len = 4 // CRC32 per object
|
||||
offset32Len = 4 // 32-bit offset per object
|
||||
offset64Len = 8 // 64-bit overflow offset
|
||||
trailerHashes = 2 // pack checksum + idx checksum, each hashsz
|
||||
)
|
||||
|
||||
// statInput is the optional shape the [Decoder] probes for at the
|
||||
// start of [Decoder.Decode] to learn the on-disk length of the idx
|
||||
// blob, which it uses to validate the canonical-Git size formula
|
||||
// before any allocations driven by the fanout table. Callers that
|
||||
// pass an [*os.File] or a `billy.File` backed by an `*os.File`
|
||||
// (the production call sites in `storage/filesystem`) satisfy it
|
||||
// directly; arbitrary [io.Reader]s do not, and decode for them
|
||||
// retains the pre-existing behaviour of erroring out at the
|
||||
// truncated-payload boundary instead.
|
||||
//
|
||||
// The interface is intentionally unexported so the public
|
||||
// [NewDecoder] signature stays compatible with v5.
|
||||
type statInput interface {
|
||||
Stat() (fs.FileInfo, error)
|
||||
}
|
||||
|
||||
// Decoder reads and decodes idx files from an input stream.
|
||||
type Decoder struct {
|
||||
io.Reader
|
||||
h hash.Hash
|
||||
src io.Reader
|
||||
h hash.Hash
|
||||
}
|
||||
|
||||
// NewDecoder builds a new idx stream decoder, that reads from r.
|
||||
func NewDecoder(r io.Reader) *Decoder {
|
||||
h := hash.New(crypto.SHA1)
|
||||
tr := io.TeeReader(r, h)
|
||||
return &Decoder{tr, h}
|
||||
return &Decoder{tr, r, h}
|
||||
}
|
||||
|
||||
// Decode reads from the stream and decode the content into the MemoryIndex struct.
|
||||
func (d *Decoder) Decode(idx *MemoryIndex) error {
|
||||
idxSize := int64(-1)
|
||||
if in, ok := d.src.(statInput); ok {
|
||||
fi, err := in.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: stat input: %w", ErrMalformedIdxFile, err)
|
||||
}
|
||||
idxSize = fi.Size()
|
||||
}
|
||||
|
||||
if err := validateHeader(d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
flow := []func(*MemoryIndex, io.Reader) error{
|
||||
headerFlow := []func(*MemoryIndex, io.Reader) error{
|
||||
readVersion,
|
||||
readFanout,
|
||||
}
|
||||
for _, f := range headerFlow {
|
||||
if err := f(idx, d); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if idxSize >= 0 {
|
||||
if err := validateIdxV2Size(idx, idxSize); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
bodyFlow := []func(*MemoryIndex, io.Reader) error{
|
||||
readObjectNames,
|
||||
readCRC32,
|
||||
readOffsets,
|
||||
readPackChecksum,
|
||||
}
|
||||
|
||||
for _, f := range flow {
|
||||
for _, f := range bodyFlow {
|
||||
if err := f(idx, d); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -199,3 +253,103 @@ func readIdxChecksum(idx *MemoryIndex, r io.Reader) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateIdxV2Size enforces the size formula used by canonical Git
|
||||
// load_idx for idx v2 files: the on-disk length must lie within
|
||||
// [minSize, maxSize] where
|
||||
//
|
||||
// perObject = hashsz + crc32Len + offset32Len
|
||||
// minSize = headerLen + fanoutLen + trailerHashes*hashsz + nr*perObject
|
||||
// maxSize = minSize + (nr-1)*offset64Len when nr > 0
|
||||
//
|
||||
// with nr taken from the last fanout entry and hashsz fixed at
|
||||
// [objectIDLength] (SHA-1 in v5). Multiplications use a self-checking
|
||||
// overflow guard so inputs whose claimed object count overflows the
|
||||
// formula are rejected rather than wrapping into a smaller value.
|
||||
func validateIdxV2Size(idx *MemoryIndex, idxSize int64) error {
|
||||
nr := int64(idx.Fanout[fanout-1])
|
||||
hashsz := int64(objectIDLength)
|
||||
|
||||
minSize := minIdxV2Size(nr, hashsz)
|
||||
maxSize := maxIdxV2Size(nr, hashsz)
|
||||
if minSize < 0 || maxSize < 0 {
|
||||
return fmt.Errorf("%w: object count %d is inconsistent with file size", ErrMalformedIdxFile, nr)
|
||||
}
|
||||
|
||||
if idxSize < minSize || idxSize > maxSize {
|
||||
return fmt.Errorf("%w: file size %d is inconsistent with object count %d", ErrMalformedIdxFile, idxSize, nr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// minIdxV2Size returns the minimum on-disk size of an idx v2 file
|
||||
// holding nr objects with the given hash size, mirroring the
|
||||
// computation in canonical Git load_idx. Returns -1 when any
|
||||
// intermediate multiplication or addition would overflow int64.
|
||||
func minIdxV2Size(nr, hashsz int64) int64 {
|
||||
perObject := hashsz + crc32Len + offset32Len
|
||||
fixed := int64(headerLen+fanoutLen) + trailerHashes*hashsz
|
||||
|
||||
objects, ok := mulInt64(nr, perObject)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
sum, ok := addInt64(fixed, objects)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
// maxIdxV2Size returns the maximum on-disk size of an idx v2 file
|
||||
// holding nr objects with the given hash size, mirroring the
|
||||
// computation in canonical Git load_idx. Returns -1 on overflow.
|
||||
func maxIdxV2Size(nr, hashsz int64) int64 {
|
||||
minSize := minIdxV2Size(nr, hashsz)
|
||||
if minSize < 0 {
|
||||
return -1
|
||||
}
|
||||
if nr == 0 {
|
||||
return minSize
|
||||
}
|
||||
overflow, ok := mulInt64(nr-1, offset64Len)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
sum, ok := addInt64(minSize, overflow)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
// mulInt64 returns a*b and whether the result fits in an int64 without
|
||||
// overflow. Negative operands or overflow yield ok=false. The overflow
|
||||
// check uses the standard self-inverse identity: a*b/b == a only when
|
||||
// the multiplication did not wrap.
|
||||
func mulInt64(a, b int64) (int64, bool) {
|
||||
if a < 0 || b < 0 {
|
||||
return 0, false
|
||||
}
|
||||
if a == 0 || b == 0 {
|
||||
return 0, true
|
||||
}
|
||||
c := a * b
|
||||
if c/b != a {
|
||||
return 0, false
|
||||
}
|
||||
return c, true
|
||||
}
|
||||
|
||||
// addInt64 returns a+b and whether the result fits in an int64 without
|
||||
// overflow. Negative operands or overflow yield ok=false.
|
||||
func addInt64(a, b int64) (int64, bool) {
|
||||
if a < 0 || b < 0 {
|
||||
return 0, false
|
||||
}
|
||||
c := a + b
|
||||
if c < a {
|
||||
return 0, false
|
||||
}
|
||||
return c, true
|
||||
}
|
||||
|
||||
29
vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/idxfile.go
generated
vendored
29
vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/idxfile.go
generated
vendored
@ -2,6 +2,7 @@ package idxfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"sync"
|
||||
@ -126,7 +127,10 @@ func (idx *MemoryIndex) FindOffset(h plumbing.Hash) (int64, error) {
|
||||
return 0, plumbing.ErrObjectNotFound
|
||||
}
|
||||
|
||||
offset := idx.getOffset(k, i)
|
||||
offset, err := idx.getOffset(k, i)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Save the offset for reverse lookup
|
||||
idx.mu.Lock()
|
||||
@ -141,17 +145,19 @@ func (idx *MemoryIndex) FindOffset(h plumbing.Hash) (int64, error) {
|
||||
|
||||
const isO64Mask = uint64(1) << 31
|
||||
|
||||
func (idx *MemoryIndex) getOffset(firstLevel, secondLevel int) uint64 {
|
||||
func (idx *MemoryIndex) getOffset(firstLevel, secondLevel int) (uint64, error) {
|
||||
offset := secondLevel << 2
|
||||
ofs := encbin.BigEndian.Uint32(idx.Offset32[firstLevel][offset : offset+4])
|
||||
|
||||
if (uint64(ofs) & isO64Mask) != 0 {
|
||||
offset := 8 * (uint64(ofs) & ^isO64Mask)
|
||||
n := encbin.BigEndian.Uint64(idx.Offset64[offset : offset+8])
|
||||
return n
|
||||
if l := uint64(len(idx.Offset64)); l < 8 || offset > l-8 {
|
||||
return 0, fmt.Errorf("%w: offset64 index out of range", ErrMalformedIdxFile)
|
||||
}
|
||||
return encbin.BigEndian.Uint64(idx.Offset64[offset : offset+8]), nil
|
||||
}
|
||||
|
||||
return uint64(ofs)
|
||||
return uint64(ofs), nil
|
||||
}
|
||||
|
||||
// FindCRC32 implements the Index interface.
|
||||
@ -209,8 +215,11 @@ func (idx *MemoryIndex) genOffsetHash() error {
|
||||
mappedFirstLevel := idx.FanoutMapping[firstLevel]
|
||||
for secondLevel := uint32(0); i < fanoutValue; i++ {
|
||||
copy(hash[:], idx.Names[mappedFirstLevel][secondLevel*objectIDLength:])
|
||||
offset := int64(idx.getOffset(mappedFirstLevel, int(secondLevel)))
|
||||
offsetHash[offset] = hash
|
||||
off, err := idx.getOffset(mappedFirstLevel, int(secondLevel))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
offsetHash[int64(off)] = hash
|
||||
secondLevel++
|
||||
}
|
||||
}
|
||||
@ -291,7 +300,11 @@ func (i *idxfileEntryIter) Next() (*Entry, error) {
|
||||
mappedFirstLevel := i.idx.FanoutMapping[i.firstLevel]
|
||||
entry := new(Entry)
|
||||
copy(entry.Hash[:], i.idx.Names[mappedFirstLevel][i.secondLevel*objectIDLength:])
|
||||
entry.Offset = i.idx.getOffset(mappedFirstLevel, i.secondLevel)
|
||||
var err error
|
||||
entry.Offset, err = i.idx.getOffset(mappedFirstLevel, i.secondLevel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry.CRC32 = i.idx.getCRC32(mappedFirstLevel, i.secondLevel)
|
||||
|
||||
i.secondLevel++
|
||||
|
||||
18
vendor/github.com/go-git/go-git/v5/plumbing/format/objfile/reader.go
generated
vendored
18
vendor/github.com/go-git/go-git/v5/plumbing/format/objfile/reader.go
generated
vendored
@ -11,9 +11,10 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrClosed = errors.New("objfile: already closed")
|
||||
ErrHeader = errors.New("objfile: invalid header")
|
||||
ErrNegativeSize = errors.New("objfile: negative object size")
|
||||
ErrClosed = errors.New("objfile: already closed")
|
||||
ErrHeader = errors.New("objfile: invalid header")
|
||||
ErrHeaderNotRead = errors.New("objfile: Header must be called before Read")
|
||||
ErrNegativeSize = errors.New("objfile: negative object size")
|
||||
)
|
||||
|
||||
// Reader reads and decodes compressed objfile data from a provided io.Reader.
|
||||
@ -100,12 +101,23 @@ func (r *Reader) prepareForRead(t plumbing.ObjectType, size int64) {
|
||||
//
|
||||
// If Read encounters the end of the data stream it will return err == io.EOF,
|
||||
// either in the current call if n > 0 or in a subsequent call.
|
||||
//
|
||||
// Read returns ErrHeaderNotRead if Header has not been called successfully.
|
||||
func (r *Reader) Read(p []byte) (n int, err error) {
|
||||
if r.multi == nil {
|
||||
return 0, ErrHeaderNotRead
|
||||
}
|
||||
return r.multi.Read(p)
|
||||
}
|
||||
|
||||
// Hash returns the hash of the object data stream that has been read so far.
|
||||
// It returns the zero plumbing.Hash if Header has not been called
|
||||
// successfully — guarding against the nil hasher that prepareForRead has
|
||||
// not yet allocated.
|
||||
func (r *Reader) Hash() plumbing.Hash {
|
||||
if r.multi == nil {
|
||||
return plumbing.ZeroHash
|
||||
}
|
||||
return r.hasher.Sum()
|
||||
}
|
||||
|
||||
|
||||
3
vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/diff_delta.go
generated
vendored
3
vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/diff_delta.go
generated
vendored
@ -19,9 +19,6 @@ const (
|
||||
// https://github.com/git/git/blob/f7466e94375b3be27f229c78873f0acf8301c0a5/diff-delta.c#L428
|
||||
// Max size of a copy operation (64KB).
|
||||
maxCopySize = 64 * 1024
|
||||
|
||||
// Min size of a copy operation.
|
||||
minCopySize = 4
|
||||
)
|
||||
|
||||
// GetDelta returns an EncodedObject of type OFSDeltaObject. Base and Target object,
|
||||
|
||||
8
vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/fsobject.go
generated
vendored
8
vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/fsobject.go
generated
vendored
@ -78,7 +78,13 @@ func (o *FSObject) Reader() (io.ReadCloser, error) {
|
||||
_ = f.Close()
|
||||
return nil, err
|
||||
}
|
||||
return ioutil.NewReadCloserWithCloser(r, f.Close), nil
|
||||
// Cap the lazy stream at the resolved object size: well-formed
|
||||
// content reaches EOF inside the bound, an inflated stream that
|
||||
// runs past surfaces ErrInflatedSizeMismatch on the byte just
|
||||
// past the limit. For delta-resolved objects o.size is the
|
||||
// expanded size, which is what the caller is reading here.
|
||||
bounded := newBoundedReadCloser(r, o.size)
|
||||
return ioutil.NewReadCloserWithCloser(bounded, f.Close), nil
|
||||
}
|
||||
r, err := p.getObjectContent(o.offset)
|
||||
if err != nil {
|
||||
|
||||
21
vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/packfile.go
generated
vendored
21
vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/packfile.go
generated
vendored
@ -126,11 +126,17 @@ func (p *Packfile) nextObjectHeader() (*ObjectHeader, error) {
|
||||
return h, err
|
||||
}
|
||||
|
||||
func (p *Packfile) getDeltaObjectSize(buf *bytes.Buffer) int64 {
|
||||
func (p *Packfile) getDeltaObjectSize(buf *bytes.Buffer) (int64, error) {
|
||||
delta := buf.Bytes()
|
||||
_, delta = decodeLEB128(delta) // skip src size
|
||||
sz, _ := decodeLEB128(delta)
|
||||
return int64(sz)
|
||||
_, delta, err := decodeLEB128(delta) // skip src size
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sz, _, err := decodeLEB128(delta)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int64(sz), nil
|
||||
}
|
||||
|
||||
func (p *Packfile) getObjectSize(h *ObjectHeader) (int64, error) {
|
||||
@ -145,7 +151,7 @@ func (p *Packfile) getObjectSize(h *ObjectHeader) (int64, error) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return p.getDeltaObjectSize(buf), nil
|
||||
return p.getDeltaObjectSize(buf)
|
||||
default:
|
||||
return 0, ErrInvalidObject.AddDetails("type %q", h.Type)
|
||||
}
|
||||
@ -233,7 +239,10 @@ func (p *Packfile) getNextObject(h *ObjectHeader, hash plumbing.Hash) (plumbing.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
size = p.getDeltaObjectSize(buf)
|
||||
size, err = p.getDeltaObjectSize(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if size <= smallObjectThreshold {
|
||||
var obj = new(plumbing.MemoryObject)
|
||||
obj.SetSize(size)
|
||||
|
||||
72
vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/parser.go
generated
vendored
72
vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/parser.go
generated
vendored
@ -26,6 +26,45 @@ var (
|
||||
ErrDeltaNotCached = errors.New("delta could not be found in cache")
|
||||
)
|
||||
|
||||
// maxObjectPreallocBytes caps the up-front size hint passed to
|
||||
// bytes.Buffer.Grow when staging an object's contents, so a malformed length
|
||||
// cannot trigger a huge or out-of-range allocation. The buffer still grows
|
||||
// dynamically as data is written; this is purely a hint cap.
|
||||
const maxObjectPreallocBytes = 1 << 30 // 1 GiB
|
||||
|
||||
// maxObjectsPrealloc caps the up-front capacity reserved from the pack's
|
||||
// declared object count, so a header advertising an absurd quantity cannot
|
||||
// trigger a multi-gigabyte allocation. The slice and maps still grow
|
||||
// organically beyond this hint.
|
||||
const maxObjectsPrealloc = 1 << 16 // 64 Ki entries
|
||||
|
||||
// Match upstream Git's pack depth ceiling: pack-objects.h OE_DEPTH_BITS,
|
||||
// enforced in builtin/pack-objects.c as (1 << OE_DEPTH_BITS) - 1.
|
||||
const maxDeltaChainDepth = 4095
|
||||
|
||||
// growHint returns a non-negative int64 size, clamped to a sane upper bound,
|
||||
// suitable for passing to bytes.Buffer.Grow.
|
||||
func growHint(n int64) int {
|
||||
switch {
|
||||
case n <= 0:
|
||||
return 0
|
||||
case n > maxObjectPreallocBytes:
|
||||
return maxObjectPreallocBytes
|
||||
default:
|
||||
return int(n)
|
||||
}
|
||||
}
|
||||
|
||||
// objectsHint returns a non-negative count, clamped to maxObjectsPrealloc,
|
||||
// suitable for passing to make() as the capacity hint for slices or maps
|
||||
// sized from a pack's declared object count.
|
||||
func objectsHint(n uint32) int {
|
||||
if n > maxObjectsPrealloc {
|
||||
return maxObjectsPrealloc
|
||||
}
|
||||
return int(n)
|
||||
}
|
||||
|
||||
// Observer interface is implemented by index encoders.
|
||||
type Observer interface {
|
||||
// OnHeader is called when a new packfile is opened.
|
||||
@ -166,9 +205,10 @@ func (p *Parser) init() error {
|
||||
}
|
||||
|
||||
p.count = c
|
||||
p.oiByHash = make(map[plumbing.Hash]*objectInfo, p.count)
|
||||
p.oiByOffset = make(map[int64]*objectInfo, p.count)
|
||||
p.oi = make([]*objectInfo, p.count)
|
||||
hint := objectsHint(p.count)
|
||||
p.oiByHash = make(map[plumbing.Hash]*objectInfo, hint)
|
||||
p.oiByOffset = make(map[int64]*objectInfo, hint)
|
||||
p.oi = make([]*objectInfo, 0, hint)
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -261,7 +301,7 @@ func (p *Parser) indexObjects() error {
|
||||
}
|
||||
if delta && !p.scanner.IsSeekable {
|
||||
buf.Reset()
|
||||
buf.Grow(int(oh.Length))
|
||||
buf.Grow(growHint(oh.Length))
|
||||
writers = append(writers, buf)
|
||||
}
|
||||
|
||||
@ -306,7 +346,7 @@ func (p *Parser) indexObjects() error {
|
||||
}
|
||||
|
||||
p.oiByOffset[oh.Offset] = ota
|
||||
p.oi[i] = ota
|
||||
p.oi = append(p.oi, ota)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -317,8 +357,12 @@ func (p *Parser) resolveDeltas() error {
|
||||
defer sync.PutBytesBuffer(buf)
|
||||
|
||||
for _, obj := range p.oi {
|
||||
if err := checkDeltaChainDepth(obj); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
buf.Grow(int(obj.Length))
|
||||
buf.Grow(growHint(obj.Length))
|
||||
err := p.get(obj, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -337,6 +381,9 @@ func (p *Parser) resolveDeltas() error {
|
||||
// create it once and reuse across all children.
|
||||
r := bytes.NewReader(buf.Bytes())
|
||||
for _, child := range obj.Children {
|
||||
if err := checkDeltaChainDepth(child); err != nil {
|
||||
return err
|
||||
}
|
||||
// Even though we are discarding the output, we still need to read it to
|
||||
// so that the scanner can advance to the next object, and the SHA1 can be
|
||||
// calculated.
|
||||
@ -356,6 +403,17 @@ func (p *Parser) resolveDeltas() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkDeltaChainDepth(o *objectInfo) error {
|
||||
var depth int
|
||||
for current := o; current != nil && current.DiskType.IsDelta(); current = current.Parent {
|
||||
depth++
|
||||
if depth > maxDeltaChainDepth {
|
||||
return fmt.Errorf("%w: delta chain depth exceeds %d", ErrMalformedPackFile, maxDeltaChainDepth)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) resolveExternalRef(o *objectInfo) {
|
||||
if ref, ok := p.oiByHash[o.SHA1]; ok && ref.ExternalRef {
|
||||
p.oiByHash[o.SHA1] = o
|
||||
@ -405,7 +463,7 @@ func (p *Parser) get(o *objectInfo, buf *bytes.Buffer) (err error) {
|
||||
if o.DiskType.IsDelta() {
|
||||
b := sync.GetBytesBuffer()
|
||||
defer sync.PutBytesBuffer(b)
|
||||
buf.Grow(int(o.Length))
|
||||
buf.Grow(growHint(o.Length))
|
||||
err := p.get(o.Parent, b)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
113
vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/patch_delta.go
generated
vendored
113
vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/patch_delta.go
generated
vendored
@ -31,10 +31,15 @@ const (
|
||||
// premptively made available for a patch operation.
|
||||
maxPatchPreemptionSize uint = 65536
|
||||
|
||||
// minDeltaSize defines the smallest size for a delta.
|
||||
minDeltaSize = 4
|
||||
// minDeltaSize is the smallest valid delta: a 1-byte srcSz LEB128
|
||||
// header followed by a 1-byte targetSz LEB128 header (the
|
||||
// shortest case being targetSz=0 with no operations).
|
||||
minDeltaSize = 2
|
||||
)
|
||||
|
||||
// uintBits is the bit width of uint on the current platform (32 or 64).
|
||||
const uintBits = 32 << (^uint(0) >> 63)
|
||||
|
||||
type offset struct {
|
||||
mask byte
|
||||
shift uint
|
||||
@ -142,7 +147,7 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo
|
||||
baseBuf := bufio.NewReader(baseRd)
|
||||
basePos := uint(0)
|
||||
|
||||
for {
|
||||
for remainingTargetSz > 0 {
|
||||
cmd, err := deltaBuf.ReadByte()
|
||||
if err == io.EOF {
|
||||
_ = dstWr.CloseWithError(ErrInvalidDelta)
|
||||
@ -166,9 +171,9 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo
|
||||
return
|
||||
}
|
||||
|
||||
if invalidSize(sz, targetSz) ||
|
||||
if invalidSize(sz, remainingTargetSz) ||
|
||||
invalidOffsetSize(offset, sz, srcSz) {
|
||||
_ = dstWr.Close()
|
||||
_ = dstWr.CloseWithError(ErrInvalidDelta)
|
||||
return
|
||||
}
|
||||
|
||||
@ -210,7 +215,7 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo
|
||||
|
||||
case isCopyFromDelta(cmd):
|
||||
sz := uint(cmd) // cmd is the size itself
|
||||
if invalidSize(sz, targetSz) {
|
||||
if invalidSize(sz, remainingTargetSz) {
|
||||
_ = dstWr.CloseWithError(ErrInvalidDelta)
|
||||
return
|
||||
}
|
||||
@ -225,40 +230,48 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo
|
||||
_ = dstWr.CloseWithError(ErrDeltaCmd)
|
||||
return
|
||||
}
|
||||
|
||||
if remainingTargetSz <= 0 {
|
||||
_ = dstWr.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Mirror upstream's `data != top` post-loop check: every byte
|
||||
// of the delta payload must be consumed.
|
||||
if _, err := deltaBuf.ReadByte(); err == nil {
|
||||
_ = dstWr.CloseWithError(ErrInvalidDelta)
|
||||
return
|
||||
} else if err != io.EOF {
|
||||
_ = dstWr.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = dstWr.Close()
|
||||
}()
|
||||
|
||||
return dstRd, nil
|
||||
}
|
||||
|
||||
func patchDelta(dst *bytes.Buffer, src, delta []byte) error {
|
||||
if len(delta) < minCopySize {
|
||||
return ErrInvalidDelta
|
||||
srcSz, delta, err := decodeLEB128(delta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrInvalidDelta, err)
|
||||
}
|
||||
|
||||
srcSz, delta := decodeLEB128(delta)
|
||||
if srcSz != uint(len(src)) {
|
||||
return ErrInvalidDelta
|
||||
}
|
||||
|
||||
targetSz, delta := decodeLEB128(delta)
|
||||
targetSz, delta, err := decodeLEB128(delta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrInvalidDelta, err)
|
||||
}
|
||||
remainingTargetSz := targetSz
|
||||
|
||||
var cmd byte
|
||||
|
||||
growSz := min(targetSz, maxPatchPreemptionSize)
|
||||
dst.Grow(int(growSz))
|
||||
for {
|
||||
|
||||
for remainingTargetSz > 0 {
|
||||
if len(delta) == 0 {
|
||||
return ErrInvalidDelta
|
||||
}
|
||||
|
||||
cmd = delta[0]
|
||||
cmd := delta[0]
|
||||
delta = delta[1:]
|
||||
|
||||
switch {
|
||||
@ -275,16 +288,16 @@ func patchDelta(dst *bytes.Buffer, src, delta []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if invalidSize(sz, targetSz) ||
|
||||
if invalidSize(sz, remainingTargetSz) ||
|
||||
invalidOffsetSize(offset, sz, srcSz) {
|
||||
break
|
||||
return ErrInvalidDelta
|
||||
}
|
||||
dst.Write(src[offset : offset+sz])
|
||||
remainingTargetSz -= sz
|
||||
|
||||
case isCopyFromDelta(cmd):
|
||||
sz := uint(cmd) // cmd is the size itself
|
||||
if invalidSize(sz, targetSz) {
|
||||
if invalidSize(sz, remainingTargetSz) {
|
||||
return ErrInvalidDelta
|
||||
}
|
||||
|
||||
@ -299,10 +312,12 @@ func patchDelta(dst *bytes.Buffer, src, delta []byte) error {
|
||||
default:
|
||||
return ErrDeltaCmd
|
||||
}
|
||||
}
|
||||
|
||||
if remainingTargetSz <= 0 {
|
||||
break
|
||||
}
|
||||
// Mirror upstream's `data != top` post-loop check: every byte of
|
||||
// the delta payload must be consumed.
|
||||
if len(delta) != 0 {
|
||||
return ErrInvalidDelta
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -354,7 +369,7 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader,
|
||||
baselr := io.LimitReader(sr, 0).(*io.LimitedReader)
|
||||
deltalr := io.LimitReader(deltaBuf, 0).(*io.LimitedReader)
|
||||
|
||||
for {
|
||||
for remainingTargetSz > 0 {
|
||||
buf := *bufp
|
||||
cmd, err := deltaBuf.ReadByte()
|
||||
if err == io.EOF {
|
||||
@ -374,9 +389,9 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader,
|
||||
return 0, plumbing.ZeroHash, err
|
||||
}
|
||||
|
||||
if invalidSize(sz, targetSz) ||
|
||||
if invalidSize(sz, remainingTargetSz) ||
|
||||
invalidOffsetSize(offset, sz, srcSz) {
|
||||
return 0, plumbing.ZeroHash, err
|
||||
return 0, plumbing.ZeroHash, ErrInvalidDelta
|
||||
}
|
||||
|
||||
if _, err := sr.Seek(int64(offset), io.SeekStart); err != nil {
|
||||
@ -389,7 +404,7 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader,
|
||||
remainingTargetSz -= sz
|
||||
} else if isCopyFromDelta(cmd) {
|
||||
sz := uint(cmd) // cmd is the size itself
|
||||
if invalidSize(sz, targetSz) {
|
||||
if invalidSize(sz, remainingTargetSz) {
|
||||
return 0, plumbing.ZeroHash, ErrInvalidDelta
|
||||
}
|
||||
deltalr.N = int64(sz)
|
||||
@ -399,30 +414,41 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader,
|
||||
|
||||
remainingTargetSz -= sz
|
||||
} else {
|
||||
return 0, plumbing.ZeroHash, err
|
||||
}
|
||||
if remainingTargetSz <= 0 {
|
||||
break
|
||||
return 0, plumbing.ZeroHash, ErrDeltaCmd
|
||||
}
|
||||
}
|
||||
|
||||
// Mirror upstream's `data != top` post-loop check: every byte of
|
||||
// the delta payload must be consumed.
|
||||
if _, err := deltaBuf.ReadByte(); err == nil {
|
||||
return 0, plumbing.ZeroHash, ErrInvalidDelta
|
||||
} else if err != io.EOF {
|
||||
return 0, plumbing.ZeroHash, err
|
||||
}
|
||||
|
||||
return targetSz, hasher.Sum(), nil
|
||||
}
|
||||
|
||||
// Decodes a number encoded as an unsigned LEB128 at the start of some
|
||||
// binary data and returns the decoded number and the rest of the
|
||||
// stream.
|
||||
// binary data and returns the decoded number, the rest of the stream,
|
||||
// and an error if the encoded value does not fit in a uint.
|
||||
//
|
||||
// This must be called twice on the delta data buffer, first to get the
|
||||
// expected source buffer size, and again to get the target buffer size.
|
||||
func decodeLEB128(input []byte) (uint, []byte) {
|
||||
func decodeLEB128(input []byte) (uint, []byte, error) {
|
||||
if len(input) == 0 {
|
||||
return 0, input
|
||||
return 0, input, nil
|
||||
}
|
||||
|
||||
var num, sz uint
|
||||
var b byte
|
||||
for {
|
||||
// A continuation byte at shift > uintBits-7 cannot contribute
|
||||
// without overflowing the accumulator.
|
||||
if sz*7 > uintBits-7 {
|
||||
return 0, input, ErrLengthOverflow
|
||||
}
|
||||
|
||||
b = input[sz]
|
||||
num |= (uint(b) & payload) << (sz * 7) // concats 7 bits chunks
|
||||
sz++
|
||||
@ -432,12 +458,16 @@ func decodeLEB128(input []byte) (uint, []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
return num, input[sz:]
|
||||
return num, input[sz:], nil
|
||||
}
|
||||
|
||||
func decodeLEB128ByteReader(input io.ByteReader) (uint, error) {
|
||||
var num, sz uint
|
||||
for {
|
||||
if sz*7 > uintBits-7 {
|
||||
return 0, ErrLengthOverflow
|
||||
}
|
||||
|
||||
b, err := input.ReadByte()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@ -529,8 +559,9 @@ func decodeSize(cmd byte, delta []byte) (uint, []byte, error) {
|
||||
return sz, delta, nil
|
||||
}
|
||||
|
||||
func invalidSize(sz, targetSz uint) bool {
|
||||
return sz > targetSz
|
||||
// invalidSize reports whether sz exceeds the remaining target size.
|
||||
func invalidSize(sz, remaining uint) bool {
|
||||
return sz > remaining
|
||||
}
|
||||
|
||||
func invalidOffsetSize(offset, sz, srcSz uint) bool {
|
||||
|
||||
154
vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/scanner.go
generated
vendored
154
vendor/github.com/go-git/go-git/v5/plumbing/format/packfile/scanner.go
generated
vendored
@ -29,8 +29,100 @@ var (
|
||||
ErrSeekNotSupported = NewError("not seek support")
|
||||
// ErrMalformedPackFile is returned by the parser when the pack file is corrupted.
|
||||
ErrMalformedPackFile = errors.New("malformed PACK file")
|
||||
// ErrLengthOverflow is returned when a variable-length integer would not
|
||||
// fit into its accumulator because the input declares more continuation
|
||||
// bytes than the type can hold.
|
||||
ErrLengthOverflow = errors.New("variable-length integer overflow")
|
||||
// ErrInflatedSizeMismatch is returned when a packfile object inflates to
|
||||
// more bytes than the size declared in its object header. A well-formed
|
||||
// packfile never produces more data than the declared size; exceeding it
|
||||
// indicates a structurally invalid entry.
|
||||
ErrInflatedSizeMismatch = errors.New("packfile: inflated object exceeds declared size")
|
||||
)
|
||||
|
||||
// boundedWriter passes writes through to w up to limit bytes total, then
|
||||
// returns ErrInflatedSizeMismatch. It is used to enforce that a packfile
|
||||
// object's inflated length does not exceed the size declared in its header.
|
||||
type boundedWriter struct {
|
||||
w io.Writer
|
||||
limit int64
|
||||
n int64
|
||||
}
|
||||
|
||||
// Write forwards p to the underlying writer while keeping the running total
|
||||
// at or below limit. On overrun it forwards the legal prefix and reports
|
||||
// the number of bytes actually consumed alongside ErrInflatedSizeMismatch,
|
||||
// matching the contract in io.Writer. A write error from the underlying
|
||||
// writer during overrun-handling is joined with ErrInflatedSizeMismatch so
|
||||
// it is not silently dropped.
|
||||
func (b *boundedWriter) Write(p []byte) (int, error) {
|
||||
if b.n+int64(len(p)) > b.limit {
|
||||
remain := int(b.limit - b.n)
|
||||
err := error(ErrInflatedSizeMismatch)
|
||||
if remain > 0 {
|
||||
n, werr := b.w.Write(p[:remain])
|
||||
b.n += int64(n)
|
||||
if werr != nil {
|
||||
err = errors.Join(ErrInflatedSizeMismatch, werr)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
n, err := b.w.Write(p)
|
||||
b.n += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// boundedReadCloser wraps a ReadCloser and reports ErrInflatedSizeMismatch
|
||||
// once more than limit bytes have been read. It is used by the on-demand
|
||||
// object reader returned from FSObject.Reader so that a lazy Read of a
|
||||
// packfile object cannot stream past its declared inflated size.
|
||||
//
|
||||
// The implementation builds on io.LimitedReader with the standard
|
||||
// overrun-detection trick: request limit+1 bytes from the underlying so
|
||||
// that the moment the sentinel byte materializes (LimitedReader.N drops
|
||||
// to zero) we know the source produced more than limit bytes.
|
||||
type boundedReadCloser struct {
|
||||
lr io.LimitedReader
|
||||
closer io.Closer
|
||||
overrun bool
|
||||
}
|
||||
|
||||
// newBoundedReadCloser wraps rc so that the cumulative bytes returned from
|
||||
// Read never exceed limit. The first call that would have returned a byte
|
||||
// past limit instead returns ErrInflatedSizeMismatch; subsequent calls
|
||||
// keep returning the same error. A negative limit is treated as zero, so
|
||||
// the first byte produced by rc surfaces ErrInflatedSizeMismatch.
|
||||
func newBoundedReadCloser(rc io.ReadCloser, limit int64) *boundedReadCloser {
|
||||
if limit < 0 {
|
||||
limit = 0
|
||||
}
|
||||
return &boundedReadCloser{
|
||||
lr: io.LimitedReader{R: rc, N: limit + 1},
|
||||
closer: rc,
|
||||
}
|
||||
}
|
||||
|
||||
// Read forwards Read up to the configured byte limit. When the underlying
|
||||
// stream produces the limit+1 sentinel byte, the legal prefix is returned
|
||||
// alongside ErrInflatedSizeMismatch; on subsequent calls only the error
|
||||
// is returned.
|
||||
func (b *boundedReadCloser) Read(p []byte) (int, error) {
|
||||
if b.overrun {
|
||||
return 0, ErrInflatedSizeMismatch
|
||||
}
|
||||
n, err := b.lr.Read(p)
|
||||
if b.lr.N == 0 {
|
||||
b.overrun = true
|
||||
return n - 1, ErrInflatedSizeMismatch
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Close closes the underlying ReadCloser.
|
||||
func (b *boundedReadCloser) Close() error { return b.closer.Close() }
|
||||
|
||||
// ObjectHeader contains the information related to the object, this information
|
||||
// is collected from the previous bytes to the content of the object.
|
||||
type ObjectHeader struct {
|
||||
@ -220,6 +312,13 @@ func (s *Scanner) nextObjectHeader() (*ObjectHeader, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// An OFS-delta references a base object that appears earlier
|
||||
// in the pack; the negative offset must be strictly positive
|
||||
// and not larger than the current object's offset.
|
||||
if no <= 0 || no > h.Offset {
|
||||
return nil, fmt.Errorf("%w: invalid OFS delta offset", ErrMalformedPackFile)
|
||||
}
|
||||
|
||||
h.OffsetReference = h.Offset - no
|
||||
case plumbing.REFDeltaObject:
|
||||
var err error
|
||||
@ -303,6 +402,13 @@ func (s *Scanner) readLength(first byte) (int64, error) {
|
||||
shift := firstLengthBits
|
||||
var err error
|
||||
for c&maskContinue > 0 {
|
||||
// Mirrors unpack_object_header_buffer in canonical Git's
|
||||
// packfile.c: a continuation byte at shift > 64-7 cannot
|
||||
// contribute without overflowing an int64.
|
||||
if shift > 64-lengthBits {
|
||||
return 0, fmt.Errorf("%w: %w", ErrMalformedPackFile, ErrLengthOverflow)
|
||||
}
|
||||
|
||||
if c, err = s.r.ReadByte(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -315,10 +421,18 @@ func (s *Scanner) readLength(first byte) (int64, error) {
|
||||
}
|
||||
|
||||
// NextObject writes the content of the next object into the reader, returns
|
||||
// the number of bytes written, the CRC32 of the content and an error, if any
|
||||
// the number of bytes written, the CRC32 of the content and an error, if any.
|
||||
//
|
||||
// When a prior NextObjectHeader has stashed the object header in
|
||||
// pendingObject, the inflated stream is bounded by the header's declared
|
||||
// length and surfaces ErrInflatedSizeMismatch on overrun.
|
||||
func (s *Scanner) NextObject(w io.Writer) (written int64, crc32 uint32, err error) {
|
||||
declaredSize := int64(-1)
|
||||
if s.pendingObject != nil {
|
||||
declaredSize = s.pendingObject.Length
|
||||
}
|
||||
s.pendingObject = nil
|
||||
written, err = s.copyObject(w)
|
||||
written, err = s.copyObject(w, declaredSize)
|
||||
|
||||
s.r.Flush()
|
||||
crc32 = s.crc.Sum32()
|
||||
@ -327,23 +441,39 @@ func (s *Scanner) NextObject(w io.Writer) (written int64, crc32 uint32, err erro
|
||||
return
|
||||
}
|
||||
|
||||
// ReadObject returns a reader for the object content and an error
|
||||
// ReadObject returns a reader for the object content and an error.
|
||||
//
|
||||
// When a prior NextObjectHeader has stashed the object header in
|
||||
// pendingObject, the returned reader is bounded by the header's declared
|
||||
// length so callers cannot stream past the declared inflated size; an
|
||||
// overrun surfaces ErrInflatedSizeMismatch on the byte just past the
|
||||
// limit.
|
||||
func (s *Scanner) ReadObject() (io.ReadCloser, error) {
|
||||
declaredSize := int64(-1)
|
||||
if s.pendingObject != nil {
|
||||
declaredSize = s.pendingObject.Length
|
||||
}
|
||||
s.pendingObject = nil
|
||||
zr, err := sync.GetZlibReader(s.r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("zlib reset error: %s", err)
|
||||
}
|
||||
|
||||
return ioutil.NewReadCloserWithCloser(zr.Reader, func() error {
|
||||
rc := ioutil.NewReadCloserWithCloser(zr.Reader, func() error {
|
||||
sync.PutZlibReader(zr)
|
||||
return nil
|
||||
}), nil
|
||||
})
|
||||
if declaredSize >= 0 {
|
||||
return newBoundedReadCloser(rc, declaredSize), nil
|
||||
}
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// ReadRegularObject reads and write a non-deltified object
|
||||
// from it zlib stream in an object entry in the packfile.
|
||||
func (s *Scanner) copyObject(w io.Writer) (n int64, err error) {
|
||||
// copyObject inflates a non-deltified object's zlib stream into w. When
|
||||
// declaredSize is non-negative, the write sink is wrapped in a
|
||||
// boundedWriter so an overrun surfaces ErrInflatedSizeMismatch instead
|
||||
// of being silently appended.
|
||||
func (s *Scanner) copyObject(w io.Writer, declaredSize int64) (n int64, err error) {
|
||||
zr, err := sync.GetZlibReader(s.r)
|
||||
defer sync.PutZlibReader(zr)
|
||||
|
||||
@ -352,8 +482,14 @@ func (s *Scanner) copyObject(w io.Writer) (n int64, err error) {
|
||||
}
|
||||
|
||||
defer ioutil.CheckClose(zr.Reader, &err)
|
||||
|
||||
sink := w
|
||||
if declaredSize >= 0 {
|
||||
sink = &boundedWriter{w: w, limit: declaredSize}
|
||||
}
|
||||
|
||||
buf := sync.GetByteSlice()
|
||||
n, err = io.CopyBuffer(w, zr.Reader, *buf)
|
||||
n, err = io.CopyBuffer(sink, zr.Reader, *buf)
|
||||
sync.PutByteSlice(buf)
|
||||
return
|
||||
}
|
||||
|
||||
207
vendor/github.com/go-git/go-git/v5/plumbing/object/commit.go
generated
vendored
207
vendor/github.com/go-git/go-git/v5/plumbing/object/commit.go
generated
vendored
@ -5,7 +5,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
@ -20,6 +20,7 @@ const (
|
||||
beginpgp string = "-----BEGIN PGP SIGNATURE-----"
|
||||
endpgp string = "-----END PGP SIGNATURE-----"
|
||||
headerpgp string = "gpgsig"
|
||||
headerpgp256 string = "gpgsig-sha256"
|
||||
headerencoding string = "encoding"
|
||||
|
||||
// https://github.com/git/git/blob/bcb6cae2966cc407ca1afc77413b3ef11103c175/Documentation/gitformat-signature.txt#L153
|
||||
@ -41,6 +42,11 @@ type MessageEncoding string
|
||||
// in time, such as a timestamp, the author of the changes since the last
|
||||
// commit, a pointer to the previous commit(s), etc.
|
||||
// http://shafiulazam.com/gitbook/1_the_git_object_model.html
|
||||
//
|
||||
// When a Commit is populated by Decode it retains a reference to the source
|
||||
// plumbing.EncodedObject so that EncodeWithoutSignature can reproduce the
|
||||
// exact bytes the signature was computed over. Refer to EncodeWithoutSignature
|
||||
// for more information.
|
||||
type Commit struct {
|
||||
// Hash of the commit object.
|
||||
Hash plumbing.Hash
|
||||
@ -66,6 +72,9 @@ type Commit struct {
|
||||
ExtraHeaders []ExtraHeader
|
||||
|
||||
s storer.EncodedObjectStorer
|
||||
// src holds the encoded object this Commit was decoded from, used by
|
||||
// EncodeWithoutSignature to recover the canonical signed bytes.
|
||||
src plumbing.EncodedObject
|
||||
}
|
||||
|
||||
// ExtraHeader holds any non-standard header
|
||||
@ -98,8 +107,8 @@ func (h ExtraHeader) Format(f fmt.State, verb rune) {
|
||||
func parseExtraHeader(line []byte) (ExtraHeader, bool) {
|
||||
split := bytes.SplitN(line, []byte{' '}, 2)
|
||||
|
||||
out := ExtraHeader {
|
||||
Key: string(bytes.TrimRight(split[0], "\n")),
|
||||
out := ExtraHeader{
|
||||
Key: string(bytes.TrimRight(split[0], "\n")),
|
||||
Value: "",
|
||||
}
|
||||
|
||||
@ -181,6 +190,11 @@ func (c *Commit) NumParents() int {
|
||||
|
||||
var ErrParentNotFound = errors.New("commit parent not found")
|
||||
|
||||
// ErrMalformedCommit is returned when a commit object cannot be decoded
|
||||
// because its standard headers (tree, parent, author, committer) are missing,
|
||||
// duplicated, or out of order.
|
||||
var ErrMalformedCommit = errors.New("malformed commit")
|
||||
|
||||
// Parent returns the ith parent of a commit.
|
||||
func (c *Commit) Parent(i int) (*Commit, error) {
|
||||
if len(c.ParentHashes) == 0 || i > len(c.ParentHashes)-1 {
|
||||
@ -227,14 +241,23 @@ func (c *Commit) Type() plumbing.ObjectType {
|
||||
return plumbing.CommitObject
|
||||
}
|
||||
|
||||
func (c *Commit) reset() {
|
||||
storer := c.s
|
||||
*c = Commit{
|
||||
Encoding: defaultUtf8CommitMessageEncoding,
|
||||
s: storer,
|
||||
}
|
||||
}
|
||||
|
||||
// Decode transforms a plumbing.EncodedObject into a Commit struct.
|
||||
func (c *Commit) Decode(o plumbing.EncodedObject) (err error) {
|
||||
if o.Type() != plumbing.CommitObject {
|
||||
return ErrUnsupportedObject
|
||||
}
|
||||
|
||||
c.reset()
|
||||
c.Hash = o.Hash()
|
||||
c.Encoding = defaultUtf8CommitMessageEncoding
|
||||
c.src = o
|
||||
|
||||
reader, err := o.Reader()
|
||||
if err != nil {
|
||||
@ -245,97 +268,17 @@ func (c *Commit) Decode(o plumbing.EncodedObject) (err error) {
|
||||
r := sync.GetBufioReader(reader)
|
||||
defer sync.PutBufioReader(r)
|
||||
|
||||
var message bool
|
||||
var mergetag bool
|
||||
var pgpsig bool
|
||||
var msgbuf bytes.Buffer
|
||||
var extraheader *ExtraHeader = nil
|
||||
for {
|
||||
line, err := r.ReadBytes('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
s := &commitScanner{r: r, c: c}
|
||||
for state := scanTree; state != nil; {
|
||||
state, err = state(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if mergetag {
|
||||
if len(line) > 0 && line[0] == ' ' {
|
||||
line = bytes.TrimLeft(line, " ")
|
||||
c.MergeTag += string(line)
|
||||
continue
|
||||
} else {
|
||||
mergetag = false
|
||||
}
|
||||
}
|
||||
|
||||
if pgpsig {
|
||||
if len(line) > 0 && line[0] == ' ' {
|
||||
line = bytes.TrimLeft(line, " ")
|
||||
c.PGPSignature += string(line)
|
||||
continue
|
||||
} else {
|
||||
pgpsig = false
|
||||
}
|
||||
}
|
||||
|
||||
if extraheader != nil {
|
||||
if len(line) > 0 && line[0] == ' ' {
|
||||
extraheader.Value += string(line[1:])
|
||||
continue
|
||||
} else {
|
||||
extraheader.Value = strings.TrimRight(extraheader.Value, "\n")
|
||||
c.ExtraHeaders = append(c.ExtraHeaders, *extraheader)
|
||||
extraheader = nil
|
||||
}
|
||||
}
|
||||
|
||||
if !message {
|
||||
original_line := line
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
message = true
|
||||
continue
|
||||
}
|
||||
|
||||
split := bytes.SplitN(line, []byte{' '}, 2)
|
||||
|
||||
var data []byte
|
||||
if len(split) == 2 {
|
||||
data = split[1]
|
||||
}
|
||||
|
||||
switch string(split[0]) {
|
||||
case "tree":
|
||||
c.TreeHash = plumbing.NewHash(string(data))
|
||||
case "parent":
|
||||
c.ParentHashes = append(c.ParentHashes, plumbing.NewHash(string(data)))
|
||||
case "author":
|
||||
c.Author.Decode(data)
|
||||
case "committer":
|
||||
c.Committer.Decode(data)
|
||||
case headermergetag:
|
||||
c.MergeTag += string(data) + "\n"
|
||||
mergetag = true
|
||||
case headerencoding:
|
||||
c.Encoding = MessageEncoding(data)
|
||||
case headerpgp:
|
||||
c.PGPSignature += string(data) + "\n"
|
||||
pgpsig = true
|
||||
default:
|
||||
h, maybecontinued := parseExtraHeader(original_line)
|
||||
if maybecontinued {
|
||||
extraheader = &h
|
||||
} else {
|
||||
c.ExtraHeaders = append(c.ExtraHeaders, h)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
msgbuf.Write(line)
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
c.Message = msgbuf.String()
|
||||
if !s.sawTree {
|
||||
return fmt.Errorf("%w: missing tree header", ErrMalformedCommit)
|
||||
}
|
||||
c.Message = s.msgbuf.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -344,11 +287,73 @@ func (c *Commit) Encode(o plumbing.EncodedObject) error {
|
||||
return c.encode(o, true)
|
||||
}
|
||||
|
||||
// EncodeWithoutSignature export a Commit into a plumbing.EncodedObject without the signature (correspond to the payload of the PGP signature).
|
||||
// EncodeWithoutSignature exports a Commit into a plumbing.EncodedObject
|
||||
// without any signature headers, producing the payload that PGP/GPG
|
||||
// signatures are computed over.
|
||||
//
|
||||
// Behaviour depends on how the Commit was created:
|
||||
//
|
||||
// - For Commits populated by Decode whose exported fields still match the
|
||||
// source object, the payload is streamed from the raw source bytes with
|
||||
// gpgsig and gpgsig-sha256 headers (and their continuation lines)
|
||||
// stripped verbatim. This preserves the exact bytes the signature was
|
||||
// computed over, regardless of any normalization performed by Decode.
|
||||
//
|
||||
// - For Commits constructed in memory, or for decoded Commits whose
|
||||
// exported fields have been mutated, the payload is derived from the
|
||||
// current struct fields. Mutation is detected by re-decoding the source
|
||||
// object and comparing exported fields; if any differ, the in-memory
|
||||
// representation prevails.
|
||||
func (c *Commit) EncodeWithoutSignature(o plumbing.EncodedObject) error {
|
||||
if c.matchesSource() {
|
||||
return stripObjectSignatures(o, c.src, plumbing.CommitObject)
|
||||
}
|
||||
return c.encode(o, false)
|
||||
}
|
||||
|
||||
// matchesSource reports whether c.src is set and re-decoding it produces a
|
||||
// Commit whose payload-affecting exported fields are identical to those of
|
||||
// c. It is the auto-detection used by EncodeWithoutSignature to decide
|
||||
// between the raw bytes and the struct-encoded payload.
|
||||
//
|
||||
// PGPSignature is intentionally excluded from the comparison: neither path
|
||||
// emits it, so mutating it must not trigger a switch to struct-encode (which
|
||||
// would change the byte layout the caller is trying to verify against).
|
||||
func (c *Commit) matchesSource() bool {
|
||||
if c.src == nil {
|
||||
return false
|
||||
}
|
||||
fresh := &Commit{}
|
||||
if err := fresh.Decode(c.src); err != nil {
|
||||
return false
|
||||
}
|
||||
return c.Hash == fresh.Hash &&
|
||||
signatureEqual(c.Author, fresh.Author) &&
|
||||
signatureEqual(c.Committer, fresh.Committer) &&
|
||||
c.MergeTag == fresh.MergeTag &&
|
||||
c.Message == fresh.Message &&
|
||||
c.TreeHash == fresh.TreeHash &&
|
||||
c.Encoding == fresh.Encoding &&
|
||||
slices.Equal(c.ParentHashes, fresh.ParentHashes) &&
|
||||
slices.Equal(c.ExtraHeaders, fresh.ExtraHeaders)
|
||||
}
|
||||
|
||||
func signatureEqual(a, b Signature) bool {
|
||||
return a.Name == b.Name &&
|
||||
a.Email == b.Email &&
|
||||
a.When.Unix() == b.When.Unix() &&
|
||||
a.When.Format("-0700") == b.When.Format("-0700")
|
||||
}
|
||||
|
||||
func isStandardHeader(key string) bool {
|
||||
switch key {
|
||||
case "tree", "parent", "author", "committer",
|
||||
headerencoding, headermergetag, headerpgp, headerpgp256:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
||||
o.SetType(plumbing.CommitObject)
|
||||
w, err := o.Writer()
|
||||
@ -407,7 +412,9 @@ func (c *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
||||
}
|
||||
|
||||
for _, header := range c.ExtraHeaders {
|
||||
|
||||
if isStandardHeader(header.Key) {
|
||||
continue
|
||||
}
|
||||
if _, err = fmt.Fprintf(w, "\n%s", header); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -478,9 +485,21 @@ func (c *Commit) String() string {
|
||||
)
|
||||
}
|
||||
|
||||
// ErrMultipleSignatures is returned by Verify when the commit carries more
|
||||
// than one armored signature block. Mirrors upstream's parse_gpg_output
|
||||
// rejection of GOODSIG/BADSIG status lines after the first
|
||||
// (gpg-interface.c:257-269): multi-signature commits are intentionally
|
||||
// unsupported because their provenance cannot be reduced to a single
|
||||
// authoritative signer.
|
||||
var ErrMultipleSignatures = errors.New("commit has multiple signatures")
|
||||
|
||||
// Verify performs PGP verification of the commit with a provided armored
|
||||
// keyring and returns openpgp.Entity associated with verifying key on success.
|
||||
func (c *Commit) Verify(armoredKeyRing string) (*openpgp.Entity, error) {
|
||||
if countSignatureBlocks([]byte(c.PGPSignature)) > 1 {
|
||||
return nil, ErrMultipleSignatures
|
||||
}
|
||||
|
||||
keyRingReader := strings.NewReader(armoredKeyRing)
|
||||
keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader)
|
||||
if err != nil {
|
||||
|
||||
377
vendor/github.com/go-git/go-git/v5/plumbing/object/commit_scanner.go
generated
vendored
Normal file
377
vendor/github.com/go-git/go-git/v5/plumbing/object/commit_scanner.go
generated
vendored
Normal file
@ -0,0 +1,377 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// commitScanner holds the working state of the commit decoder driven by the
|
||||
// stateFn loop in (*Commit).Decode. Each commitState reads one or more lines
|
||||
// from r, updates the in-progress *Commit and the scanner's bookkeeping, and
|
||||
// returns the state that should run next (or nil to stop).
|
||||
type commitScanner struct {
|
||||
r *bufio.Reader
|
||||
c *Commit
|
||||
msgbuf bytes.Buffer
|
||||
|
||||
// pending holds a line that was read but the current state decided to
|
||||
// hand back to the next state, paired with the io.EOF flag that was
|
||||
// returned when the line was originally read.
|
||||
pending []byte
|
||||
pendingErr error
|
||||
|
||||
// First-occurrence tracking: once the corresponding field has been
|
||||
// decoded, subsequent occurrences are silently dropped (matches
|
||||
// upstream's find_commit_header / first-wins semantics).
|
||||
//
|
||||
// gpgsig is not tracked here: upstream's parse_buffer_signed_by_header
|
||||
// (commit.c:1186) accumulates every occurrence into one signature buffer,
|
||||
// so we do the same on the scanner side to keep verification payloads
|
||||
// byte-aligned. gpgsig-sha256 is recognized and skipped without exposing a
|
||||
// new field in v5.
|
||||
sawTree, sawAuthor, sawCommitter bool
|
||||
sawEncoding, sawMergetag bool
|
||||
|
||||
// extra is the multi-line ExtraHeader currently being assembled.
|
||||
extra *ExtraHeader
|
||||
}
|
||||
|
||||
// commitState is one step of the decoder state machine. Each function reads
|
||||
// the lines it needs, mutates *Commit via s.c, and returns the next state to
|
||||
// run (or nil to terminate the loop).
|
||||
type commitState func(*commitScanner) (commitState, error)
|
||||
|
||||
// readLine returns the next line from the buffer, transparently consuming any
|
||||
// line that was previously pushed back by a state that decided not to handle
|
||||
// it.
|
||||
func (s *commitScanner) readLine() ([]byte, error) {
|
||||
if s.pending != nil {
|
||||
line, err := s.pending, s.pendingErr
|
||||
s.pending, s.pendingErr = nil, nil
|
||||
return line, err
|
||||
}
|
||||
line, err := s.r.ReadBytes('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
return line, err
|
||||
}
|
||||
return line, err
|
||||
}
|
||||
|
||||
// pushBack stashes an unconsumed line so the next state's readLine call sees
|
||||
// it. Only one line can be pushed back at a time.
|
||||
func (s *commitScanner) pushBack(line []byte, err error) {
|
||||
s.pending = line
|
||||
s.pendingErr = err
|
||||
}
|
||||
|
||||
// scanTree expects the first non-empty header to be `tree HASH`. Anything
|
||||
// else (or an empty buffer) is rejected with ErrMalformedCommit, matching
|
||||
// upstream's `bogus commit object` check.
|
||||
func scanTree(s *commitScanner) (commitState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 || isBlankLine(line) {
|
||||
return nil, fmt.Errorf("%w: missing tree header", ErrMalformedCommit)
|
||||
}
|
||||
|
||||
key, data := splitHeader(line)
|
||||
if key != "tree" {
|
||||
return nil, fmt.Errorf("%w: tree header must be first", ErrMalformedCommit)
|
||||
}
|
||||
h, herr := parseObjectIDHex(data, ErrMalformedCommit, "tree")
|
||||
if herr != nil {
|
||||
return nil, herr
|
||||
}
|
||||
s.c.TreeHash = h
|
||||
s.sawTree = true
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanParents, nil
|
||||
}
|
||||
|
||||
// scanParents consumes contiguous `parent HASH` lines. The first non-parent
|
||||
// line ends the parent block and is handed off to scanAuthor; any later
|
||||
// `parent` line is silently dropped (matches upstream's parse_commit_buffer
|
||||
// exiting its parent loop at the first non-parent line and
|
||||
// read_commit_extra_header_lines filtering `parent` out of extras).
|
||||
func scanParents(s *commitScanner) (commitState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if isBlankLine(line) {
|
||||
return scanMessage, nil
|
||||
}
|
||||
|
||||
key, data := splitHeader(line)
|
||||
if key == "parent" {
|
||||
h, herr := parseObjectIDHex(data, ErrMalformedCommit, "parent")
|
||||
if herr != nil {
|
||||
return nil, herr
|
||||
}
|
||||
s.c.ParentHashes = append(s.c.ParentHashes, h)
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanParents, nil
|
||||
}
|
||||
s.pushBack(line, err)
|
||||
return scanAuthor, nil
|
||||
}
|
||||
|
||||
// scanAuthor accepts an `author` line at its canonical position immediately
|
||||
// after the parent block. Any other header here is pushed back for
|
||||
// scanCommitter; an out-of-place author is therefore silently dropped.
|
||||
// Mirrors upstream's parse_commit_date func.
|
||||
func scanAuthor(s *commitScanner) (commitState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if isBlankLine(line) {
|
||||
return scanMessage, nil
|
||||
}
|
||||
|
||||
key, data := splitHeader(line)
|
||||
if key == "author" {
|
||||
s.c.Author.Decode(data)
|
||||
s.sawAuthor = true
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanCommitter, nil
|
||||
}
|
||||
s.pushBack(line, err)
|
||||
return scanCommitter, nil
|
||||
}
|
||||
|
||||
// scanCommitter accepts a `committer` line at its canonical position
|
||||
// immediately after the author. Any other header is pushed back for
|
||||
// scanHeaders. Same upstream rationale as scanAuthor.
|
||||
func scanCommitter(s *commitScanner) (commitState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if isBlankLine(line) {
|
||||
return scanMessage, nil
|
||||
}
|
||||
|
||||
key, data := splitHeader(line)
|
||||
if key == "committer" {
|
||||
s.c.Committer.Decode(data)
|
||||
s.sawCommitter = true
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanHeaders, nil
|
||||
}
|
||||
s.pushBack(line, err)
|
||||
return scanHeaders, nil
|
||||
}
|
||||
|
||||
// scanHeaders dispatches one header line. Continuation-bearing headers
|
||||
// (mergetag, gpgsig, gpgsig-sha256, and unknown extras whose value is
|
||||
// continued on subsequent lines) hand off to a dedicated continuation state
|
||||
// that handles the `<space>...` lines and then returns here.
|
||||
func scanHeaders(s *commitScanner) (commitState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if isBlankLine(line) {
|
||||
return scanMessage, nil
|
||||
}
|
||||
|
||||
originalLine := line
|
||||
key, data := splitHeader(line)
|
||||
|
||||
var next commitState = scanHeaders
|
||||
switch key {
|
||||
case "tree", "parent", "author", "committer":
|
||||
// Anything reaching scanHeaders with one of these keys is out of
|
||||
// canonical position: duplicate tree, parent past the contiguous
|
||||
// block, or author/committer not at their expected slot. Drop them
|
||||
// the same way upstream's standard_header_field filter excludes
|
||||
// them from the extras list (read_commit_extra_header_lines,
|
||||
// commit.c:1520-1522).
|
||||
case headerencoding:
|
||||
if !s.sawEncoding {
|
||||
s.c.Encoding = MessageEncoding(data)
|
||||
s.sawEncoding = true
|
||||
}
|
||||
case headermergetag:
|
||||
if s.sawMergetag {
|
||||
next = scanSkipCont
|
||||
} else {
|
||||
s.c.MergeTag += string(data) + "\n"
|
||||
s.sawMergetag = true
|
||||
next = scanMergetagCont
|
||||
}
|
||||
case headerpgp:
|
||||
s.c.PGPSignature += string(data) + "\n"
|
||||
next = scanPgpCont
|
||||
case headerpgp256:
|
||||
next = scanSkipCont
|
||||
default:
|
||||
h, multiline := parseExtraHeader(originalLine)
|
||||
if multiline {
|
||||
s.extra = &h
|
||||
next = scanExtraCont
|
||||
} else {
|
||||
s.c.ExtraHeaders = append(s.c.ExtraHeaders, h)
|
||||
}
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return next, nil
|
||||
}
|
||||
|
||||
// scanMergetagCont accumulates continuation lines for the first mergetag
|
||||
// header. Continuations strip exactly one leading space, mirroring upstream's
|
||||
// `line + 1` (commit.c:1509). The first non-continuation line is pushed back
|
||||
// so scanHeaders can dispatch it.
|
||||
func scanMergetagCont(s *commitScanner) (commitState, error) {
|
||||
return continuationCont(s, &s.c.MergeTag, scanMergetagCont)
|
||||
}
|
||||
|
||||
// scanPgpCont accumulates continuation lines for a signature header.
|
||||
// Continuations strip exactly one leading space, mirroring upstream's
|
||||
// `line + 1` (commit.c:1509). The first non-continuation line is pushed back
|
||||
// so scanHeaders can dispatch it. Repeat occurrences of the same signature
|
||||
// header land back here and concatenate, matching upstream's
|
||||
// parse_buffer_signed_by_header (commit.c:1186).
|
||||
func scanPgpCont(s *commitScanner) (commitState, error) {
|
||||
return continuationCont(s, &s.c.PGPSignature, scanPgpCont)
|
||||
}
|
||||
|
||||
func continuationCont(s *commitScanner, dst *string, self commitState) (commitState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) > 0 && line[0] == ' ' {
|
||||
*dst += string(line[1:])
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return self, nil
|
||||
}
|
||||
if len(line) > 0 {
|
||||
s.pushBack(line, err)
|
||||
}
|
||||
return scanHeaders, nil
|
||||
}
|
||||
|
||||
// scanSkipCont discards continuation lines that belong to a header scanHeaders
|
||||
// chose to drop.
|
||||
func scanSkipCont(s *commitScanner) (commitState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) > 0 && line[0] == ' ' {
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanSkipCont, nil
|
||||
}
|
||||
if len(line) > 0 {
|
||||
s.pushBack(line, err)
|
||||
}
|
||||
return scanHeaders, nil
|
||||
}
|
||||
|
||||
// scanExtraCont accumulates continuation lines for an unknown ExtraHeader
|
||||
// whose value spans multiple lines, then finalises the entry once the
|
||||
// continuation block ends.
|
||||
func scanExtraCont(s *commitScanner) (commitState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) > 0 && line[0] == ' ' {
|
||||
s.extra.Value += string(line[1:])
|
||||
if err == io.EOF {
|
||||
s.finaliseExtra()
|
||||
return nil, nil
|
||||
}
|
||||
return scanExtraCont, nil
|
||||
}
|
||||
s.finaliseExtra()
|
||||
if len(line) > 0 {
|
||||
s.pushBack(line, err)
|
||||
}
|
||||
return scanHeaders, nil
|
||||
}
|
||||
|
||||
func (s *commitScanner) finaliseExtra() {
|
||||
s.extra.Value = strings.TrimRight(s.extra.Value, "\n")
|
||||
s.c.ExtraHeaders = append(s.c.ExtraHeaders, *s.extra)
|
||||
s.extra = nil
|
||||
}
|
||||
|
||||
// scanMessage drains the remaining bytes into the message buffer.
|
||||
func scanMessage(s *commitScanner) (commitState, error) {
|
||||
for {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) > 0 {
|
||||
s.msgbuf.Write(line)
|
||||
}
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isBlankLine reports whether line is the canonical header/body separator:
|
||||
// a single newline. Mirrors upstream's `*line == '\n'` test in
|
||||
// read_commit_extra_header_lines (commit.c:1502).
|
||||
func isBlankLine(line []byte) bool {
|
||||
return len(line) == 1 && line[0] == '\n'
|
||||
}
|
||||
|
||||
// splitHeader returns the header keyword (everything before the first space)
|
||||
// and the value (everything after, with the trailing newline stripped). If
|
||||
// the header has no value the returned data is nil.
|
||||
func splitHeader(line []byte) (string, []byte) {
|
||||
trimmed := bytes.TrimRight(line, "\n")
|
||||
key, value, ok := bytes.Cut(trimmed, []byte{' '})
|
||||
if !ok {
|
||||
return string(trimmed), nil
|
||||
}
|
||||
return string(key), value
|
||||
}
|
||||
|
||||
func parseObjectIDHex(data []byte, malformedErr error, header string) (plumbing.Hash, error) {
|
||||
id := string(data)
|
||||
if !plumbing.IsHash(id) {
|
||||
return plumbing.ZeroHash, fmt.Errorf("%w: bad %s hash", malformedErr, header)
|
||||
}
|
||||
return plumbing.NewHash(id), nil
|
||||
}
|
||||
122
vendor/github.com/go-git/go-git/v5/plumbing/object/signature.go
generated
vendored
122
vendor/github.com/go-git/go-git/v5/plumbing/object/signature.go
generated
vendored
@ -1,6 +1,13 @@
|
||||
package object
|
||||
|
||||
import "bytes"
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/utils/ioutil"
|
||||
"github.com/go-git/go-git/v5/utils/sync"
|
||||
)
|
||||
|
||||
const (
|
||||
signatureTypeUnknown signatureType = iota
|
||||
@ -100,3 +107,116 @@ func parseSignedBytes(b []byte) (int, signatureType) {
|
||||
}
|
||||
return match, t
|
||||
}
|
||||
|
||||
// countSignatureBlocks reports how many distinct armored signature blocks
|
||||
// start at a line boundary in b. Used by verification paths to reject
|
||||
// multi-signature payloads, matching upstream's check in gpg-interface.c
|
||||
// where parse_gpg_output bails out the first time it sees a second
|
||||
// exclusive status line (a second GOODSIG/BADSIG/etc.).
|
||||
func countSignatureBlocks(b []byte) int {
|
||||
n, count := 0, 0
|
||||
for n < len(b) {
|
||||
i := b[n:]
|
||||
if typeForSignature(i) != signatureTypeUnknown {
|
||||
count++
|
||||
}
|
||||
if eol := bytes.IndexByte(i, '\n'); eol >= 0 {
|
||||
n += eol + 1
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// isSignatureHeader reports whether line is a canonical "gpgsig "/
|
||||
// "gpgsig-sha256 " header line. Other "gpgsig"-prefixed extra headers
|
||||
// are intentionally not matched.
|
||||
func isSignatureHeader(line []byte) bool {
|
||||
return bytes.HasPrefix(line, []byte(headerpgp+" ")) ||
|
||||
bytes.HasPrefix(line, []byte(headerpgp256+" "))
|
||||
}
|
||||
|
||||
// stripObjectSignatures streams src into dst, producing the byte sequence
|
||||
// over which a PGP/GPG signature is computed:
|
||||
//
|
||||
// - Canonical "gpgsig" and "gpgsig-sha256" headers (and their
|
||||
// continuation lines) are dropped, mirroring upstream's
|
||||
// remove_signature in commit.c.
|
||||
// - For tag objects, the inline trailing PGP signature is additionally
|
||||
// truncated, mirroring upstream's parse_signature in gpg-interface.c
|
||||
// used by gpg_verify_tag.
|
||||
//
|
||||
// The returned object's type is set to objType. Used by both
|
||||
// Commit.EncodeWithoutSignature and Tag.EncodeWithoutSignature to
|
||||
// reproduce the exact bytes the signature was computed over.
|
||||
func stripObjectSignatures(dst, src plumbing.EncodedObject, objType plumbing.ObjectType) (err error) {
|
||||
dst.SetType(objType)
|
||||
|
||||
r, err := src.Reader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ioutil.CheckClose(r, &err)
|
||||
|
||||
var input io.Reader = r
|
||||
if objType == plumbing.TagObject {
|
||||
raw, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sm, _ := parseSignedBytes(raw); sm >= 0 {
|
||||
raw = raw[:sm]
|
||||
}
|
||||
input = bytes.NewReader(raw)
|
||||
}
|
||||
|
||||
w, err := dst.Writer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ioutil.CheckClose(w, &err)
|
||||
|
||||
return stripHeaderSignatures(w, input)
|
||||
}
|
||||
|
||||
// stripHeaderSignatures copies r to w, dropping canonical signature header
|
||||
// lines (gpgsig and gpgsig-sha256) and their continuation lines. Lines
|
||||
// past the blank line that closes the header block are copied verbatim.
|
||||
func stripHeaderSignatures(w io.Writer, r io.Reader) error {
|
||||
br := sync.GetBufioReader(r)
|
||||
defer sync.PutBufioReader(br)
|
||||
|
||||
var inBody, skipping bool
|
||||
for {
|
||||
line, rerr := br.ReadBytes('\n')
|
||||
if rerr != nil && rerr != io.EOF {
|
||||
return rerr
|
||||
}
|
||||
|
||||
write := true
|
||||
if !inBody {
|
||||
switch {
|
||||
case skipping && len(line) > 0 && line[0] == ' ':
|
||||
write = false
|
||||
case isSignatureHeader(line):
|
||||
skipping = true
|
||||
write = false
|
||||
case len(line) == 1 && line[0] == '\n':
|
||||
skipping = false
|
||||
inBody = true
|
||||
default:
|
||||
skipping = false
|
||||
}
|
||||
}
|
||||
|
||||
if write && len(line) > 0 {
|
||||
if _, werr := w.Write(line); werr != nil {
|
||||
return werr
|
||||
}
|
||||
}
|
||||
if rerr == io.EOF {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
135
vendor/github.com/go-git/go-git/v5/plumbing/object/tag.go
generated
vendored
135
vendor/github.com/go-git/go-git/v5/plumbing/object/tag.go
generated
vendored
@ -1,9 +1,8 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
@ -13,6 +12,10 @@ import (
|
||||
"github.com/go-git/go-git/v5/utils/sync"
|
||||
)
|
||||
|
||||
// ErrMalformedTag is returned when a tag object cannot be decoded because
|
||||
// its required headers (object, type, tag) are missing or out of order.
|
||||
var ErrMalformedTag = errors.New("malformed tag")
|
||||
|
||||
// Tag represents an annotated tag object. It points to a single git object of
|
||||
// any type, but tags typically are applied to commit or blob objects. It
|
||||
// provides a reference that associates the target with a tag name. It also
|
||||
@ -39,6 +42,9 @@ type Tag struct {
|
||||
Target plumbing.Hash
|
||||
|
||||
s storer.EncodedObjectStorer
|
||||
// src holds the encoded object this Tag was decoded from, used by
|
||||
// EncodeWithoutSignature to recover the canonical signed bytes.
|
||||
src plumbing.EncodedObject
|
||||
}
|
||||
|
||||
// GetTag gets a tag from an object storer and decodes it.
|
||||
@ -77,13 +83,20 @@ func (t *Tag) Type() plumbing.ObjectType {
|
||||
return plumbing.TagObject
|
||||
}
|
||||
|
||||
func (t *Tag) reset() {
|
||||
storer := t.s
|
||||
*t = Tag{s: storer}
|
||||
}
|
||||
|
||||
// Decode transforms a plumbing.EncodedObject into a Tag struct.
|
||||
func (t *Tag) Decode(o plumbing.EncodedObject) (err error) {
|
||||
if o.Type() != plumbing.TagObject {
|
||||
return ErrUnsupportedObject
|
||||
}
|
||||
|
||||
t.reset()
|
||||
t.Hash = o.Hash()
|
||||
t.src = o
|
||||
|
||||
reader, err := o.Reader()
|
||||
if err != nil {
|
||||
@ -94,42 +107,15 @@ func (t *Tag) Decode(o plumbing.EncodedObject) (err error) {
|
||||
r := sync.GetBufioReader(reader)
|
||||
defer sync.PutBufioReader(r)
|
||||
|
||||
for {
|
||||
var line []byte
|
||||
line, err = r.ReadBytes('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
scanner := &tagScanner{r: r, t: t}
|
||||
for state := scanTagObject; state != nil; {
|
||||
state, err = state(scanner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
break // Start of message
|
||||
}
|
||||
|
||||
split := bytes.SplitN(line, []byte{' '}, 2)
|
||||
switch string(split[0]) {
|
||||
case "object":
|
||||
t.Target = plumbing.NewHash(string(split[1]))
|
||||
case "type":
|
||||
t.TargetType, err = plumbing.ParseObjectType(string(split[1]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "tag":
|
||||
t.Name = string(split[1])
|
||||
case "tagger":
|
||||
t.Tagger.Decode(split[1])
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := scanner.msgbuf.Bytes()
|
||||
if sm, _ := parseSignedBytes(data); sm >= 0 {
|
||||
t.PGPSignature = string(data[sm:])
|
||||
data = data[:sm]
|
||||
@ -144,11 +130,54 @@ func (t *Tag) Encode(o plumbing.EncodedObject) error {
|
||||
return t.encode(o, true)
|
||||
}
|
||||
|
||||
// EncodeWithoutSignature export a Tag into a plumbing.EncodedObject without the signature (correspond to the payload of the PGP signature).
|
||||
// EncodeWithoutSignature exports a Tag into a plumbing.EncodedObject without
|
||||
// any signature data, producing the payload that PGP/GPG signatures are
|
||||
// computed over.
|
||||
//
|
||||
// Behaviour mirrors Commit.EncodeWithoutSignature:
|
||||
//
|
||||
// - For Tags populated by Decode whose exported fields still match the
|
||||
// source object, the payload is streamed from the raw source bytes with
|
||||
// the inline trailing signature truncated and gpgsig/gpgsig-sha256
|
||||
// headers (and their continuation lines) stripped verbatim. This
|
||||
// preserves the exact bytes the signature was computed over, regardless
|
||||
// of any normalization performed by Decode.
|
||||
//
|
||||
// - For Tags constructed in memory, or for decoded Tags whose exported
|
||||
// fields have been mutated, the payload is derived from the current
|
||||
// struct fields. Mutation is detected by re-decoding the source object
|
||||
// and comparing exported fields; if any differ, the in-memory
|
||||
// representation prevails.
|
||||
func (t *Tag) EncodeWithoutSignature(o plumbing.EncodedObject) error {
|
||||
if t.matchesSource() {
|
||||
return stripObjectSignatures(o, t.src, plumbing.TagObject)
|
||||
}
|
||||
return t.encode(o, false)
|
||||
}
|
||||
|
||||
// matchesSource reports whether t.src is set and re-decoding it produces a
|
||||
// Tag whose payload-affecting exported fields are identical to those of t.
|
||||
//
|
||||
// PGPSignature is intentionally excluded from the comparison: neither path
|
||||
// emits it as part of the verification payload, so mutating it must not
|
||||
// trigger a switch to struct-encode (which would change the byte layout the
|
||||
// caller is trying to verify against).
|
||||
func (t *Tag) matchesSource() bool {
|
||||
if t.src == nil {
|
||||
return false
|
||||
}
|
||||
fresh := &Tag{}
|
||||
if err := fresh.Decode(t.src); err != nil {
|
||||
return false
|
||||
}
|
||||
return t.Hash == fresh.Hash &&
|
||||
t.Name == fresh.Name &&
|
||||
signatureEqual(t.Tagger, fresh.Tagger) &&
|
||||
t.Message == fresh.Message &&
|
||||
t.TargetType == fresh.TargetType &&
|
||||
t.Target == fresh.Target
|
||||
}
|
||||
|
||||
func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
||||
o.SetType(plumbing.TagObject)
|
||||
w, err := o.Writer()
|
||||
@ -158,16 +187,26 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
||||
defer ioutil.CheckClose(w, &err)
|
||||
|
||||
if _, err = fmt.Fprintf(w,
|
||||
"object %s\ntype %s\ntag %s\ntagger ",
|
||||
"object %s\ntype %s\ntag %s\n",
|
||||
t.Target.String(), t.TargetType.Bytes(), t.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = t.Tagger.Encode(w); err != nil {
|
||||
return err
|
||||
if !isZeroSignature(t.Tagger) {
|
||||
if _, err = fmt.Fprint(w, "tagger "); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = t.Tagger.Encode(w); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprint(w, "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprint(w, "\n\n"); err != nil {
|
||||
if _, err = fmt.Fprint(w, "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -175,11 +214,12 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Note that this is highly sensitive to what it sent along in the message.
|
||||
// Message *always* needs to end with a newline, or else the message and the
|
||||
// signature will be concatenated into a corrupt object. Since this is a
|
||||
// lower-level method, we assume you know what you are doing and have already
|
||||
// done the needful on the message in the caller.
|
||||
// Note that this is highly sensitive to what is sent along in the
|
||||
// message. Message *always* needs to end with a newline, or else the
|
||||
// message and the trailing signature will be concatenated into a
|
||||
// corrupt object. Since this is a lower-level method, we assume you
|
||||
// know what you are doing and have already done the needful on the
|
||||
// message in the caller.
|
||||
if includeSig {
|
||||
if _, err = fmt.Fprint(w, t.PGPSignature); err != nil {
|
||||
return err
|
||||
@ -189,6 +229,10 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func isZeroSignature(s Signature) bool {
|
||||
return s.Name == "" && s.Email == "" && s.When.IsZero()
|
||||
}
|
||||
|
||||
// Commit returns the commit pointed to by the tag. If the tag points to a
|
||||
// different type of object ErrUnsupportedObject will be returned.
|
||||
func (t *Tag) Commit() (*Commit, error) {
|
||||
@ -256,7 +300,8 @@ func (t *Tag) String() string {
|
||||
}
|
||||
|
||||
// Verify performs PGP verification of the tag with a provided armored
|
||||
// keyring and returns openpgp.Entity associated with verifying key on success.
|
||||
// keyring and returns openpgp.Entity associated with verifying key on
|
||||
// success.
|
||||
func (t *Tag) Verify(armoredKeyRing string) (*openpgp.Entity, error) {
|
||||
keyRingReader := strings.NewReader(armoredKeyRing)
|
||||
keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader)
|
||||
|
||||
237
vendor/github.com/go-git/go-git/v5/plumbing/object/tag_scanner.go
generated
vendored
Normal file
237
vendor/github.com/go-git/go-git/v5/plumbing/object/tag_scanner.go
generated
vendored
Normal file
@ -0,0 +1,237 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// tagScanner holds the working state of the tag decoder driven by the
|
||||
// stateFn loop in (*Tag).Decode. Each tagState reads one or more lines
|
||||
// from r, updates the in-progress *Tag and the scanner's bookkeeping,
|
||||
// and returns the state that should run next (or nil to stop).
|
||||
type tagScanner struct {
|
||||
r *bufio.Reader
|
||||
t *Tag
|
||||
msgbuf bytes.Buffer
|
||||
|
||||
// pending holds a line that was read but the current state decided to
|
||||
// hand back to the next state, paired with the io.EOF flag returned
|
||||
// when the line was originally read.
|
||||
pending []byte
|
||||
pendingErr error
|
||||
|
||||
// First-occurrence tracking: once the corresponding canonical
|
||||
// header has been decoded at its expected position, subsequent
|
||||
// occurrences (or out-of-position lines) are silently dropped,
|
||||
// matching the strict layout enforced by upstream's
|
||||
// parse_tag_buffer (tag.c:130).
|
||||
//
|
||||
// gpgsig-sha256 is recognized and skipped without exposing a new field
|
||||
// in v5.
|
||||
sawObject, sawType, sawName, sawTagger bool
|
||||
}
|
||||
|
||||
// tagState is one step of the decoder state machine. Each function reads
|
||||
// the lines it needs, mutates *Tag via s.t, and returns the next state
|
||||
// to run (or nil to terminate the loop).
|
||||
type tagState func(*tagScanner) (tagState, error)
|
||||
|
||||
// readLine returns the next line from the buffer, transparently
|
||||
// consuming any line that was previously pushed back by a state that
|
||||
// decided not to handle it.
|
||||
func (s *tagScanner) readLine() ([]byte, error) {
|
||||
if s.pending != nil {
|
||||
line, err := s.pending, s.pendingErr
|
||||
s.pending, s.pendingErr = nil, nil
|
||||
return line, err
|
||||
}
|
||||
return s.r.ReadBytes('\n')
|
||||
}
|
||||
|
||||
// pushBack stashes an unconsumed line so the next state's readLine call
|
||||
// sees it. Only one line can be pushed back at a time.
|
||||
func (s *tagScanner) pushBack(line []byte, err error) {
|
||||
s.pending = line
|
||||
s.pendingErr = err
|
||||
}
|
||||
|
||||
// scanTagObject requires the first line to be `object HASH`, mirroring
|
||||
// upstream's strict parse_tag_buffer (tag.c:151-156). Anything else
|
||||
// returns ErrMalformedTag.
|
||||
func scanTagObject(s *tagScanner) (tagState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 || isBlankLine(line) {
|
||||
return nil, fmt.Errorf("%w: missing object header", ErrMalformedTag)
|
||||
}
|
||||
|
||||
key, data := splitHeader(line)
|
||||
if key != "object" {
|
||||
return nil, fmt.Errorf("%w: object header must be first", ErrMalformedTag)
|
||||
}
|
||||
h, herr := parseObjectIDHex(data, ErrMalformedTag, "object")
|
||||
if herr != nil {
|
||||
return nil, herr
|
||||
}
|
||||
s.t.Target = h
|
||||
s.sawObject = true
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanTagType, nil
|
||||
}
|
||||
|
||||
// scanTagType requires a `type` line immediately after the object header,
|
||||
// mirroring upstream's parse_tag_buffer (tag.c:158-166).
|
||||
func scanTagType(s *tagScanner) (tagState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 || isBlankLine(line) {
|
||||
return nil, fmt.Errorf("%w: missing type header", ErrMalformedTag)
|
||||
}
|
||||
|
||||
key, data := splitHeader(line)
|
||||
if key != "type" {
|
||||
return nil, fmt.Errorf("%w: type header must follow object", ErrMalformedTag)
|
||||
}
|
||||
ot, perr := plumbing.ParseObjectType(string(data))
|
||||
if perr != nil {
|
||||
return nil, perr
|
||||
}
|
||||
s.t.TargetType = ot
|
||||
s.sawType = true
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanTagName, nil
|
||||
}
|
||||
|
||||
// scanTagName requires a `tag` line immediately after the type header,
|
||||
// mirroring upstream's parse_tag_buffer (tag.c:186-194).
|
||||
func scanTagName(s *tagScanner) (tagState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 || isBlankLine(line) {
|
||||
return nil, fmt.Errorf("%w: missing tag header", ErrMalformedTag)
|
||||
}
|
||||
|
||||
key, data := splitHeader(line)
|
||||
if key != "tag" {
|
||||
return nil, fmt.Errorf("%w: tag header must follow type", ErrMalformedTag)
|
||||
}
|
||||
s.t.Name = string(data)
|
||||
s.sawName = true
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanTagTagger, nil
|
||||
}
|
||||
|
||||
// scanTagTagger accepts a `tagger` line at its canonical position. Any
|
||||
// other header is pushed back for scanTagHeaders.
|
||||
func scanTagTagger(s *tagScanner) (tagState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if isBlankLine(line) {
|
||||
return scanTagMessage, nil
|
||||
}
|
||||
|
||||
key, data := splitHeader(line)
|
||||
if key == "tagger" {
|
||||
s.t.Tagger.Decode(data)
|
||||
s.sawTagger = true
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanTagHeaders, nil
|
||||
}
|
||||
s.pushBack(line, err)
|
||||
return scanTagHeaders, nil
|
||||
}
|
||||
|
||||
// scanTagHeaders dispatches one header line. gpgsig-sha256 hands off to
|
||||
// scanTagSkipCont so the continuation block can be consumed; out-of-position
|
||||
// canonical fields and unknown headers are silently dropped.
|
||||
func scanTagHeaders(s *tagScanner) (tagState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if isBlankLine(line) {
|
||||
return scanTagMessage, nil
|
||||
}
|
||||
|
||||
key, _ := splitHeader(line)
|
||||
next := scanTagHeaders
|
||||
switch key {
|
||||
case "object", "type", "tag", "tagger":
|
||||
// Out-of-canonical-position duplicates are dropped, mirroring the
|
||||
// strict ordering of upstream's parse_tag_buffer.
|
||||
case headerpgp256:
|
||||
next = scanTagSkipCont
|
||||
default:
|
||||
// Unknown header: silently dropped (the Tag struct does not
|
||||
// expose ExtraHeaders).
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return next, nil
|
||||
}
|
||||
|
||||
// scanTagSkipCont discards continuation lines for a header scanTagHeaders chose
|
||||
// to drop. The first non-continuation line is pushed back so scanTagHeaders can
|
||||
// dispatch it.
|
||||
func scanTagSkipCont(s *tagScanner) (tagState, error) {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) > 0 && line[0] == ' ' {
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
return scanTagSkipCont, nil
|
||||
}
|
||||
if len(line) > 0 {
|
||||
s.pushBack(line, err)
|
||||
}
|
||||
return scanTagHeaders, nil
|
||||
}
|
||||
|
||||
// scanTagMessage drains the remaining bytes into the message buffer.
|
||||
// (*Tag).Decode then runs parseSignedBytes over those bytes to peel off
|
||||
// the optional inline trailing PGP signature.
|
||||
func scanTagMessage(s *tagScanner) (tagState, error) {
|
||||
for {
|
||||
line, err := s.readLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
if len(line) > 0 {
|
||||
s.msgbuf.Write(line)
|
||||
}
|
||||
if err == io.EOF {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
151
vendor/github.com/go-git/go-git/v5/plumbing/object/tree.go
generated
vendored
151
vendor/github.com/go-git/go-git/v5/plumbing/object/tree.go
generated
vendored
@ -10,6 +10,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5/internal/pathutil"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||
"github.com/go-git/go-git/v5/plumbing/storer"
|
||||
@ -29,6 +30,7 @@ var (
|
||||
ErrDirectoryNotFound = errors.New("directory not found")
|
||||
ErrEntryNotFound = errors.New("entry not found")
|
||||
ErrEntriesNotSorted = errors.New("entries in tree are not sorted")
|
||||
ErrMalformedTree = errors.New("malformed tree")
|
||||
)
|
||||
|
||||
// Tree is basically like a directory - it references a bunch of other trees
|
||||
@ -37,9 +39,9 @@ type Tree struct {
|
||||
Entries []TreeEntry
|
||||
Hash plumbing.Hash
|
||||
|
||||
s storer.EncodedObjectStorer
|
||||
m map[string]*TreeEntry
|
||||
t map[string]*Tree // tree path cache
|
||||
s storer.EncodedObjectStorer
|
||||
t map[string]*Tree // tree path cache
|
||||
entriesSorted bool
|
||||
}
|
||||
|
||||
// GetTree gets a tree from an object storer and decodes it.
|
||||
@ -117,7 +119,16 @@ func (t *Tree) Tree(path string) (*Tree, error) {
|
||||
}
|
||||
|
||||
// TreeEntryFile returns the *File for a given *TreeEntry.
|
||||
//
|
||||
// The entry's name is validated against pathutil.ValidTreePath for
|
||||
// the same reason FindEntry validates: TreeEntryFile is a boundary
|
||||
// where attacker-controlled tree data leaves the trusted store as a
|
||||
// *File whose Name a caller can hand to filesystem ops.
|
||||
func (t *Tree) TreeEntryFile(e *TreeEntry) (*File, error) {
|
||||
if err := pathutil.ValidTreePath(e.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blob, err := GetBlob(t.s, e.Hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -127,7 +138,16 @@ func (t *Tree) TreeEntryFile(e *TreeEntry) (*File, error) {
|
||||
}
|
||||
|
||||
// FindEntry search a TreeEntry in this tree or any subtree.
|
||||
//
|
||||
// The lookup path is validated against pathutil.ValidTreePath to
|
||||
// prevent attacker-controlled tree contents from leaking past this
|
||||
// boundary as `.git`-shaped or path-traversal-shaped names. Callers
|
||||
// that legitimately need to look up unsafe paths should walk the
|
||||
// tree manually.
|
||||
func (t *Tree) FindEntry(path string) (*TreeEntry, error) {
|
||||
if err := pathutil.ValidTreePath(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t.t == nil {
|
||||
t.t = make(map[string]*Tree)
|
||||
}
|
||||
@ -182,16 +202,43 @@ func (t *Tree) dir(baseName string) (*Tree, error) {
|
||||
}
|
||||
|
||||
func (t *Tree) entry(baseName string) (*TreeEntry, error) {
|
||||
if t.m == nil {
|
||||
t.buildMap()
|
||||
}
|
||||
|
||||
entry, ok := t.m[baseName]
|
||||
if !ok {
|
||||
if t.entriesSorted {
|
||||
if entry := t.searchEntry(baseName); entry != nil {
|
||||
return entry, nil
|
||||
}
|
||||
return nil, ErrEntryNotFound
|
||||
}
|
||||
|
||||
return entry, nil
|
||||
pastName := baseName + "/"
|
||||
for i := range t.Entries {
|
||||
entry := &t.Entries[i]
|
||||
if entry.Name == baseName {
|
||||
return entry, nil
|
||||
}
|
||||
if treeEntrySortName(entry) > pastName {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrEntryNotFound
|
||||
}
|
||||
|
||||
func (t *Tree) searchEntry(baseName string) *TreeEntry {
|
||||
if i := t.searchEntryIndex(baseName); i < len(t.Entries) && t.Entries[i].Name == baseName {
|
||||
return &t.Entries[i]
|
||||
}
|
||||
|
||||
if i := t.searchEntryIndex(baseName + "/"); i < len(t.Entries) && t.Entries[i].Name == baseName {
|
||||
return &t.Entries[i]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tree) searchEntryIndex(name string) int {
|
||||
return sort.Search(len(t.Entries), func(i int) bool {
|
||||
return treeEntrySortName(&t.Entries[i]) >= name
|
||||
})
|
||||
}
|
||||
|
||||
// Files returns a FileIter allowing to iterate over the Tree
|
||||
@ -212,20 +259,25 @@ func (t *Tree) Type() plumbing.ObjectType {
|
||||
return plumbing.TreeObject
|
||||
}
|
||||
|
||||
func (t *Tree) reset() {
|
||||
storer := t.s
|
||||
*t = Tree{s: storer}
|
||||
}
|
||||
|
||||
// Decode transform an plumbing.EncodedObject into a Tree struct
|
||||
func (t *Tree) Decode(o plumbing.EncodedObject) (err error) {
|
||||
if o.Type() != plumbing.TreeObject {
|
||||
return ErrUnsupportedObject
|
||||
}
|
||||
|
||||
t.reset()
|
||||
t.Hash = o.Hash()
|
||||
// assume tree is sorted as a valid tree should always be sorted.
|
||||
t.entriesSorted = true
|
||||
if o.Size() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
t.Entries = nil
|
||||
t.m = nil
|
||||
|
||||
reader, err := o.Reader()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -235,10 +287,14 @@ func (t *Tree) Decode(o plumbing.EncodedObject) (err error) {
|
||||
r := sync.GetBufioReader(reader)
|
||||
defer sync.PutBufioReader(r)
|
||||
|
||||
var prevSortName string
|
||||
for {
|
||||
str, err := r.ReadString(' ')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
if len(str) != 0 {
|
||||
return fmt.Errorf("%w: missing mode terminator", ErrMalformedTree)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@ -248,25 +304,41 @@ func (t *Tree) Decode(o plumbing.EncodedObject) (err error) {
|
||||
|
||||
mode, err := filemode.New(str)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("%w: malformed mode", ErrMalformedTree)
|
||||
}
|
||||
mode = canonicalTreeMode(mode)
|
||||
|
||||
name, err := r.ReadString(0)
|
||||
if err != nil && err != io.EOF {
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return fmt.Errorf("%w: missing filename terminator", ErrMalformedTree)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if len(name) == 1 {
|
||||
return fmt.Errorf("%w: empty filename", ErrMalformedTree)
|
||||
}
|
||||
|
||||
var hash plumbing.Hash
|
||||
if _, err = io.ReadFull(r, hash[:]); err != nil {
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return fmt.Errorf("%w: truncated object id", ErrMalformedTree)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
baseName := name[:len(name)-1]
|
||||
t.Entries = append(t.Entries, TreeEntry{
|
||||
entry := TreeEntry{
|
||||
Hash: hash,
|
||||
Mode: mode,
|
||||
Name: baseName,
|
||||
})
|
||||
}
|
||||
sortName := treeEntrySortName(&entry)
|
||||
if len(t.Entries) != 0 && prevSortName > sortName {
|
||||
t.entriesSorted = false
|
||||
}
|
||||
prevSortName = sortName
|
||||
t.Entries = append(t.Entries, entry)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -279,21 +351,37 @@ func (s TreeEntrySorter) Len() int {
|
||||
}
|
||||
|
||||
func (s TreeEntrySorter) Less(i, j int) bool {
|
||||
name1 := s[i].Name
|
||||
name2 := s[j].Name
|
||||
if s[i].Mode == filemode.Dir {
|
||||
name1 += "/"
|
||||
}
|
||||
if s[j].Mode == filemode.Dir {
|
||||
name2 += "/"
|
||||
}
|
||||
return name1 < name2
|
||||
return treeEntrySortName(&s[i]) < treeEntrySortName(&s[j])
|
||||
}
|
||||
|
||||
func (s TreeEntrySorter) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
// Git compares tree entries as if directory names had a trailing slash.
|
||||
func treeEntrySortName(e *TreeEntry) string {
|
||||
if e.Mode == filemode.Dir {
|
||||
return e.Name + "/"
|
||||
}
|
||||
return e.Name
|
||||
}
|
||||
|
||||
func canonicalTreeMode(mode filemode.FileMode) filemode.FileMode {
|
||||
switch mode & 0o170000 {
|
||||
case 0o040000:
|
||||
return filemode.Dir
|
||||
case 0o100000:
|
||||
if mode&0o111 != 0 {
|
||||
return filemode.Executable
|
||||
}
|
||||
return filemode.Regular
|
||||
case 0o120000:
|
||||
return filemode.Symlink
|
||||
default:
|
||||
return filemode.Submodule
|
||||
}
|
||||
}
|
||||
|
||||
// Encode transforms a Tree into a plumbing.EncodedObject.
|
||||
// The tree entries must be sorted by name.
|
||||
func (t *Tree) Encode(o plumbing.EncodedObject) (err error) {
|
||||
@ -329,13 +417,6 @@ func (t *Tree) Encode(o plumbing.EncodedObject) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Tree) buildMap() {
|
||||
t.m = make(map[string]*TreeEntry)
|
||||
for i := 0; i < len(t.Entries); i++ {
|
||||
t.m[t.Entries[i].Name] = &t.Entries[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Diff returns a list of changes between this tree and the provided one
|
||||
func (t *Tree) Diff(to *Tree) (Changes, error) {
|
||||
return t.DiffContext(context.Background(), to)
|
||||
@ -455,6 +536,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 entry.Mode == filemode.Dir {
|
||||
obj, err = GetTree(w.s, entry.Hash)
|
||||
}
|
||||
|
||||
168
vendor/github.com/go-git/go-git/v5/plumbing/transport/http/common.go
generated
vendored
168
vendor/github.com/go-git/go-git/v5/plumbing/transport/http/common.go
generated
vendored
@ -7,7 +7,6 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
@ -24,6 +23,33 @@ import (
|
||||
"github.com/go-git/go-git/v5/utils/ioutil"
|
||||
)
|
||||
|
||||
type contextKey int
|
||||
|
||||
const initialRequestKey contextKey = iota
|
||||
|
||||
// RedirectPolicy controls how the HTTP transport follows redirects.
|
||||
//
|
||||
// The values mirror Git's http.followRedirects config:
|
||||
// "true" follows redirects for all requests, "false" treats redirects as
|
||||
// errors, and "initial" follows redirects only for the initial
|
||||
// /info/refs discovery request. The zero value defaults to "initial".
|
||||
type RedirectPolicy string
|
||||
|
||||
const (
|
||||
FollowInitialRedirects RedirectPolicy = "initial"
|
||||
FollowRedirects RedirectPolicy = "true"
|
||||
NoFollowRedirects RedirectPolicy = "false"
|
||||
)
|
||||
|
||||
func withInitialRequest(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, initialRequestKey, true)
|
||||
}
|
||||
|
||||
func isInitialRequest(req *http.Request) bool {
|
||||
v, _ := req.Context().Value(initialRequestKey).(bool)
|
||||
return v
|
||||
}
|
||||
|
||||
// it requires a bytes.Buffer, because we need to know the length
|
||||
func applyHeadersToRequest(req *http.Request, content *bytes.Buffer, host string, requestType string) {
|
||||
req.Header.Add("User-Agent", capability.DefaultAgent())
|
||||
@ -54,12 +80,15 @@ func advertisedReferences(ctx context.Context, s *session, serviceName string) (
|
||||
|
||||
s.ApplyAuthToRequest(req)
|
||||
applyHeadersToRequest(req, nil, s.endpoint.Host, serviceName)
|
||||
res, err := s.client.Do(req.WithContext(ctx))
|
||||
res, err := s.client.Do(req.WithContext(withInitialRequest(ctx)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.ModifyEndpointIfRedirect(res)
|
||||
if err := s.ModifyEndpointIfRedirect(res); err != nil {
|
||||
_ = res.Body.Close()
|
||||
return nil, err
|
||||
}
|
||||
defer ioutil.CheckClose(res.Body, &err)
|
||||
|
||||
if err = NewErr(res); err != nil {
|
||||
@ -96,6 +125,7 @@ type client struct {
|
||||
client *http.Client
|
||||
transports *lru.Cache
|
||||
mutex sync.RWMutex
|
||||
follow RedirectPolicy
|
||||
}
|
||||
|
||||
// ClientOptions holds user configurable options for the client.
|
||||
@ -106,6 +136,11 @@ type ClientOptions struct {
|
||||
// size, will result in the least recently used transport getting deleted
|
||||
// before the provided transport is added to the cache.
|
||||
CacheMaxEntries int
|
||||
|
||||
// RedirectPolicy controls redirect handling. Supported values are
|
||||
// "true", "false", and "initial". The zero value defaults to
|
||||
// "initial", matching Git's http.followRedirects default.
|
||||
RedirectPolicy RedirectPolicy
|
||||
}
|
||||
|
||||
var (
|
||||
@ -150,12 +185,16 @@ func NewClientWithOptions(c *http.Client, opts *ClientOptions) transport.Transpo
|
||||
}
|
||||
cl := &client{
|
||||
client: c,
|
||||
follow: FollowInitialRedirects,
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
if opts.CacheMaxEntries > 0 {
|
||||
cl.transports = lru.New(opts.CacheMaxEntries)
|
||||
}
|
||||
if opts.RedirectPolicy != "" {
|
||||
cl.follow = opts.RedirectPolicy
|
||||
}
|
||||
}
|
||||
return cl
|
||||
}
|
||||
@ -289,14 +328,9 @@ func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (*
|
||||
}
|
||||
}
|
||||
|
||||
httpClient = &http.Client{
|
||||
Transport: transport,
|
||||
CheckRedirect: c.client.CheckRedirect,
|
||||
Jar: c.client.Jar,
|
||||
Timeout: c.client.Timeout,
|
||||
}
|
||||
httpClient = c.cloneHTTPClient(transport)
|
||||
} else {
|
||||
httpClient = c.client
|
||||
httpClient = c.cloneHTTPClient(c.client.Transport)
|
||||
}
|
||||
|
||||
s := &session{
|
||||
@ -324,30 +358,122 @@ func (s *session) ApplyAuthToRequest(req *http.Request) {
|
||||
s.auth.SetAuth(req)
|
||||
}
|
||||
|
||||
func (s *session) ModifyEndpointIfRedirect(res *http.Response) {
|
||||
func (s *session) ModifyEndpointIfRedirect(res *http.Response) error {
|
||||
if res.Request == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
if s.endpoint == nil {
|
||||
return fmt.Errorf("http redirect: nil endpoint")
|
||||
}
|
||||
|
||||
r := res.Request
|
||||
if !strings.HasSuffix(r.URL.Path, infoRefsPath) {
|
||||
return
|
||||
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" {
|
||||
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") {
|
||||
return fmt.Errorf("http redirect: changes scheme from %q to %q", s.endpoint.Protocol, r.URL.Scheme)
|
||||
}
|
||||
|
||||
h, p, err := net.SplitHostPort(r.URL.Host)
|
||||
host := endpointHost(r.URL.Hostname())
|
||||
port, err := endpointPort(r.URL.Port())
|
||||
if err != nil {
|
||||
h = r.URL.Host
|
||||
return err
|
||||
}
|
||||
if p != "" {
|
||||
port, err := strconv.Atoi(p)
|
||||
if err == nil {
|
||||
s.endpoint.Port = port
|
||||
}
|
||||
|
||||
if host != s.endpoint.Host || effectivePort(r.URL.Scheme, port) != effectivePort(s.endpoint.Protocol, s.endpoint.Port) {
|
||||
s.endpoint.User = ""
|
||||
s.endpoint.Password = ""
|
||||
s.auth = nil
|
||||
}
|
||||
s.endpoint.Host = h
|
||||
|
||||
s.endpoint.Host = host
|
||||
s.endpoint.Port = port
|
||||
|
||||
s.endpoint.Protocol = r.URL.Scheme
|
||||
s.endpoint.Path = r.URL.Path[:len(r.URL.Path)-len(infoRefsPath)]
|
||||
return nil
|
||||
}
|
||||
|
||||
func endpointHost(host string) string {
|
||||
if strings.Contains(host, ":") {
|
||||
return "[" + host + "]"
|
||||
}
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
func endpointPort(port string) (int, error) {
|
||||
if port == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
parsed, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("http redirect: invalid port %q", port)
|
||||
}
|
||||
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func effectivePort(scheme string, port int) int {
|
||||
if port != 0 {
|
||||
return port
|
||||
}
|
||||
|
||||
switch strings.ToLower(scheme) {
|
||||
case "http":
|
||||
return 80
|
||||
case "https":
|
||||
return 443
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) cloneHTTPClient(transport http.RoundTripper) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
CheckRedirect: wrapCheckRedirect(c.follow, c.client.CheckRedirect),
|
||||
Jar: c.client.Jar,
|
||||
Timeout: c.client.Timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func wrapCheckRedirect(policy RedirectPolicy, next func(*http.Request, []*http.Request) error) func(*http.Request, []*http.Request) error {
|
||||
return func(req *http.Request, via []*http.Request) error {
|
||||
if err := checkRedirect(req, via, policy); err != nil {
|
||||
return err
|
||||
}
|
||||
if next != nil {
|
||||
return next(req, via)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func checkRedirect(req *http.Request, via []*http.Request, policy RedirectPolicy) error {
|
||||
switch policy {
|
||||
case FollowRedirects:
|
||||
case NoFollowRedirects:
|
||||
return fmt.Errorf("http redirect: redirects disabled to %s", req.URL)
|
||||
case "", FollowInitialRedirects:
|
||||
if !isInitialRequest(req) {
|
||||
return fmt.Errorf("http redirect: redirect on non-initial request to %s", req.URL)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("http redirect: invalid redirect policy %q", policy)
|
||||
}
|
||||
if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
|
||||
return fmt.Errorf("http redirect: unsupported scheme %q", req.URL.Scheme)
|
||||
}
|
||||
if len(via) >= 10 {
|
||||
return fmt.Errorf("http redirect: too many redirects")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*session) Close() error {
|
||||
|
||||
34
vendor/github.com/go-git/go-git/v5/plumbing/transport/ssh/common.go
generated
vendored
34
vendor/github.com/go-git/go-git/v5/plumbing/transport/ssh/common.go
generated
vendored
@ -252,7 +252,39 @@ func (c *command) setAuthFromEndpoint() error {
|
||||
}
|
||||
|
||||
func endpointToCommand(cmd string, ep *transport.Endpoint) string {
|
||||
return fmt.Sprintf("%s '%s'", cmd, ep.Path)
|
||||
var b strings.Builder
|
||||
b.WriteString(cmd)
|
||||
b.WriteByte(' ')
|
||||
writeShellQuote(&b, ep.Path)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// writeShellQuote writes s to b, wrapped in single quotes with
|
||||
// embedded single quotes and exclamation marks escaped using the
|
||||
// POSIX close-escape-reopen idiom:
|
||||
//
|
||||
// ' becomes '\''
|
||||
// ! becomes '\!'
|
||||
//
|
||||
// It is a direct port of canonical Git's sq_quote_buf (quote.c).
|
||||
// The bang escape keeps the result safe when re-evaluated under
|
||||
// csh-derived shells that perform history expansion. The output is
|
||||
// safe to pass as a single argument through any POSIX shell and
|
||||
// round-trips through git-shell's sq_dequote_to_argv.
|
||||
func writeShellQuote(b *strings.Builder, s string) {
|
||||
b.Grow(len(s) + 2)
|
||||
b.WriteByte('\'')
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if c == '\'' || c == '!' {
|
||||
b.WriteString(`'\`)
|
||||
b.WriteByte(c)
|
||||
b.WriteByte('\'')
|
||||
continue
|
||||
}
|
||||
b.WriteByte(c)
|
||||
}
|
||||
b.WriteByte('\'')
|
||||
}
|
||||
|
||||
func overrideConfig(overrides *ssh.ClientConfig, c *ssh.ClientConfig) {
|
||||
|
||||
13
vendor/github.com/go-git/go-git/v5/repository.go
generated
vendored
13
vendor/github.com/go-git/go-git/v5/repository.go
generated
vendored
@ -1530,7 +1530,18 @@ func (r *Repository) Worktree() (*Worktree, error) {
|
||||
return nil, ErrIsBareRepository
|
||||
}
|
||||
|
||||
return &Worktree{r: r, Filesystem: r.wt}, nil
|
||||
protectNTFS := defaultProtectNTFS()
|
||||
protectHFS := defaultProtectHFS()
|
||||
if cfg, err := r.Config(); err == nil {
|
||||
if cfg.Core.ProtectNTFS.IsSet() {
|
||||
protectNTFS = cfg.Core.ProtectNTFS.IsTrue()
|
||||
}
|
||||
if cfg.Core.ProtectHFS.IsSet() {
|
||||
protectHFS = cfg.Core.ProtectHFS.IsTrue()
|
||||
}
|
||||
}
|
||||
|
||||
return &Worktree{r: r, Filesystem: newWorktreeFilesystem(r.wt, protectNTFS, protectHFS)}, nil
|
||||
}
|
||||
|
||||
func expand_ref(s storer.ReferenceStorer, ref plumbing.ReferenceName) (*plumbing.Reference, error) {
|
||||
|
||||
19
vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/dotgit.go
generated
vendored
19
vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/dotgit.go
generated
vendored
@ -75,6 +75,10 @@ var (
|
||||
// ErrEmptyRefFile is returned when a reference file is attempted to be read,
|
||||
// but the file is empty
|
||||
ErrEmptyRefFile = errors.New("ref file is empty")
|
||||
// ErrModuleNameEscape is returned when a submodule name would
|
||||
// resolve outside the modules/ subtree, mirroring canonical Git's
|
||||
// "ignoring suspicious submodule name" defence.
|
||||
ErrModuleNameEscape = errors.New("submodule name escapes modules/ directory")
|
||||
)
|
||||
|
||||
// Options holds configuration for the storage.
|
||||
@ -1127,9 +1131,20 @@ func (d *DotGit) PackRefs() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Module return a billy.Filesystem pointing to the module folder
|
||||
// Module returns a billy.Filesystem pointing to the module folder.
|
||||
//
|
||||
// As a defence in depth against submodule name path traversal,
|
||||
// refuse names whose joined path leaves the modules/ subtree once
|
||||
// cleaned. The config-layer parser also validates submodule names,
|
||||
// but Module may be reached from any caller that constructs a
|
||||
// Submodule struct programmatically and so bypasses the parser.
|
||||
func (d *DotGit) Module(name string) (billy.Filesystem, error) {
|
||||
return d.fs.Chroot(d.fs.Join(modulePath, name))
|
||||
p := d.fs.Join(modulePath, name)
|
||||
cleaned := path.Clean(filepath.ToSlash(p))
|
||||
if cleaned != modulePath && !strings.HasPrefix(cleaned, modulePath+"/") {
|
||||
return nil, ErrModuleNameEscape
|
||||
}
|
||||
return d.fs.Chroot(p)
|
||||
}
|
||||
|
||||
func (d *DotGit) AddAlternate(remote string) error {
|
||||
|
||||
82
vendor/github.com/go-git/go-git/v5/submodule.go
generated
vendored
82
vendor/github.com/go-git/go-git/v5/submodule.go
generated
vendored
@ -6,9 +6,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-git/go-billy/v5"
|
||||
"github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/internal/pathutil"
|
||||
giturl "github.com/go-git/go-git/v5/internal/url"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/format/index"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||
@ -119,6 +122,16 @@ func (s *Submodule) Repository() (*Repository, error) {
|
||||
exists = true
|
||||
}
|
||||
|
||||
// s.c.Path is sourced from the worktree's .gitmodules and is
|
||||
// therefore tree-controlled. Apply the strict tree-path validator
|
||||
// before chroot — the wrapper's tolerant validPath would let a
|
||||
// final-position .git component through (e.g. "submodule/.git"),
|
||||
// which a malicious .gitmodules could use to chroot the submodule
|
||||
// worktree into the repository's actual .git directory.
|
||||
if err := pathutil.ValidTreePath(s.c.Path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var worktree billy.Filesystem
|
||||
if worktree, err = s.w.Filesystem.Chroot(s.c.Path); err != nil {
|
||||
return nil, err
|
||||
@ -138,18 +151,25 @@ func (s *Submodule) Repository() (*Repository, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !path.IsAbs(moduleEndpoint.Path) && moduleEndpoint.Protocol == "file" {
|
||||
remotes, err := s.w.r.Remotes()
|
||||
// A relative submodule URL such as "../X.git" must resolve against
|
||||
// the parent repository's remote URL, not against the process CWD.
|
||||
// Detect relativity from the raw configured URL because
|
||||
// transport.NewEndpoint normalizes local paths to absolute form via
|
||||
// filepath.Abs, which would otherwise mask the relative form here.
|
||||
if giturl.IsLocalEndpoint(s.c.URL) &&
|
||||
!path.IsAbs(s.c.URL) && !filepath.IsAbs(s.c.URL) {
|
||||
|
||||
base, err := defaultRemote(s.w.r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolving relative submodule URL: %w", err)
|
||||
}
|
||||
|
||||
rootEndpoint, err := transport.NewEndpoint(base.URLs[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rootEndpoint, err := transport.NewEndpoint(remotes[0].c.URLs[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rootEndpoint.Path = path.Join(rootEndpoint.Path, moduleEndpoint.Path)
|
||||
rootEndpoint.Path = path.Join(rootEndpoint.Path, s.c.URL)
|
||||
*moduleEndpoint = *rootEndpoint
|
||||
}
|
||||
|
||||
@ -161,6 +181,52 @@ func (s *Submodule) Repository() (*Repository, error) {
|
||||
return r, err
|
||||
}
|
||||
|
||||
// defaultRemote returns the remote that relative submodule URLs are
|
||||
// resolved against, mirroring canonical Git's repo_default_remote
|
||||
// (remote.c) and resolve_relative_url (builtin/submodule--helper.c):
|
||||
//
|
||||
// 1. if HEAD is on a branch with branch.<name>.remote configured,
|
||||
// use that remote;
|
||||
// 2. else if exactly one remote is configured, use it;
|
||||
// 3. otherwise fall back to DefaultRemoteName ("origin").
|
||||
//
|
||||
// Each rule falls through unconditionally: a branch lookup that
|
||||
// finds the branch but with an empty Remote does not short-circuit
|
||||
// rule (2). Returns an error when the chosen remote is not configured.
|
||||
func defaultRemote(r *Repository) (*config.RemoteConfig, error) {
|
||||
cfg, err := r.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ref, err := r.Reference(plumbing.HEAD, false); err == nil &&
|
||||
ref.Type() == plumbing.SymbolicReference &&
|
||||
ref.Target().IsBranch() {
|
||||
if b, ok := cfg.Branches[ref.Target().Short()]; ok && b.Remote != "" {
|
||||
return lookupRemote(cfg, b.Remote)
|
||||
}
|
||||
}
|
||||
|
||||
if len(cfg.Remotes) == 1 {
|
||||
for name := range cfg.Remotes {
|
||||
return lookupRemote(cfg, name)
|
||||
}
|
||||
}
|
||||
|
||||
return lookupRemote(cfg, DefaultRemoteName)
|
||||
}
|
||||
|
||||
func lookupRemote(cfg *config.Config, name string) (*config.RemoteConfig, error) {
|
||||
rc, ok := cfg.Remotes[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("remote %q not found", name)
|
||||
}
|
||||
if len(rc.URLs) == 0 {
|
||||
return nil, fmt.Errorf("remote %q has no configured URL", name)
|
||||
}
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// Update the registered submodule to match what the superproject expects, the
|
||||
// submodule should be initialized first calling the Init method or setting in
|
||||
// the options SubmoduleUpdateOptions.Init equals true
|
||||
|
||||
15
vendor/github.com/go-git/go-git/v5/utils/binary/read.go
generated
vendored
15
vendor/github.com/go-git/go-git/v5/utils/binary/read.go
generated
vendored
@ -5,11 +5,18 @@ package binary
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// ErrIntegerOverflow is returned when a Git-format variable-width integer
|
||||
// would not fit into an int64 because the input declares more continuation
|
||||
// bytes than the type can hold.
|
||||
var ErrIntegerOverflow = errors.New("variable-width integer overflow")
|
||||
|
||||
// Read reads structured binary data from r into data. Bytes are read and
|
||||
// decoded in BigEndian order
|
||||
// https://golang.org/pkg/encoding/binary/#Read
|
||||
@ -92,6 +99,14 @@ func ReadVariableWidthInt(r io.Reader) (int64, error) {
|
||||
|
||||
var v = int64(c & maskLength)
|
||||
for c&maskContinue > 0 {
|
||||
// Reject input that, after the v++ and shift below, would
|
||||
// not fit in an int64. With v < (MaxInt64-127)>>7, the
|
||||
// post-increment v is at most (MaxInt64-127)>>7 and the
|
||||
// final (v << 7) + (c & 0x7F) stays within int64.
|
||||
if v >= (math.MaxInt64-int64(maskLength))>>lengthBits {
|
||||
return 0, ErrIntegerOverflow
|
||||
}
|
||||
|
||||
v++
|
||||
if err := Read(r, &c); err != nil {
|
||||
return 0, err
|
||||
|
||||
115
vendor/github.com/go-git/go-git/v5/worktree.go
generated
vendored
115
vendor/github.com/go-git/go-git/v5/worktree.go
generated
vendored
@ -7,7 +7,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-billy/v5"
|
||||
@ -458,10 +457,6 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error {
|
||||
|
||||
filesMap := buildFilePathMap(files)
|
||||
for _, ch := range changes {
|
||||
if err := w.validChange(ch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(files) > 0 {
|
||||
file := ""
|
||||
if ch.From != nil {
|
||||
@ -489,108 +484,6 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error {
|
||||
return w.r.Storer.SetIndex(idx)
|
||||
}
|
||||
|
||||
// worktreeDeny is a list of paths that are not allowed
|
||||
// to be used when resetting the worktree.
|
||||
var worktreeDeny = map[string]struct{}{
|
||||
// .git
|
||||
GitDirName: {},
|
||||
|
||||
// For other historical reasons, file names that do not conform to the 8.3
|
||||
// format (up to eight characters for the basename, three for the file
|
||||
// extension, certain characters not allowed such as `+`, etc) are associated
|
||||
// with a so-called "short name", at least on the `C:` drive by default.
|
||||
// Which means that `git~1/` is a valid way to refer to `.git/`.
|
||||
"git~1": {},
|
||||
}
|
||||
|
||||
// validPath checks whether paths are valid.
|
||||
// The rules around invalid paths could differ from upstream based on how
|
||||
// filesystems are managed within go-git, but they are largely the same.
|
||||
//
|
||||
// For upstream rules:
|
||||
// https://github.com/git/git/blob/564d0252ca632e0264ed670534a51d18a689ef5d/read-cache.c#L946
|
||||
// https://github.com/git/git/blob/564d0252ca632e0264ed670534a51d18a689ef5d/path.c#L1383
|
||||
func validPath(paths ...string) error {
|
||||
for _, p := range paths {
|
||||
parts := strings.FieldsFunc(p, func(r rune) bool { return (r == '\\' || r == '/') })
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("invalid path: %q", p)
|
||||
}
|
||||
|
||||
if _, denied := worktreeDeny[strings.ToLower(parts[0])]; denied {
|
||||
return fmt.Errorf("invalid path prefix: %q", p)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Volume names are not supported, in both formats: \\ and <DRIVE_LETTER>:.
|
||||
if vol := filepath.VolumeName(p); vol != "" {
|
||||
return fmt.Errorf("invalid path: %q", p)
|
||||
}
|
||||
|
||||
if !windowsValidPath(parts[0]) {
|
||||
return fmt.Errorf("invalid path: %q", p)
|
||||
}
|
||||
}
|
||||
|
||||
for _, part := range parts {
|
||||
if part == ".." {
|
||||
return fmt.Errorf("invalid path %q: cannot use '..'", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// windowsPathReplacer defines the chars that need to be replaced
|
||||
// as part of windowsValidPath.
|
||||
var windowsPathReplacer *strings.Replacer
|
||||
|
||||
func init() {
|
||||
windowsPathReplacer = strings.NewReplacer(" ", "", ".", "")
|
||||
}
|
||||
|
||||
func windowsValidPath(part string) bool {
|
||||
if len(part) > 3 && strings.EqualFold(part[:4], GitDirName) {
|
||||
// For historical reasons, file names that end in spaces or periods are
|
||||
// automatically trimmed. Therefore, `.git . . ./` is a valid way to refer
|
||||
// to `.git/`.
|
||||
if windowsPathReplacer.Replace(part[4:]) == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// For yet other historical reasons, NTFS supports so-called "Alternate Data
|
||||
// Streams", i.e. metadata associated with a given file, referred to via
|
||||
// `<filename>:<stream-name>:<stream-type>`. There exists a default stream
|
||||
// type for directories, allowing `.git/` to be accessed via
|
||||
// `.git::$INDEX_ALLOCATION/`.
|
||||
//
|
||||
// For performance reasons, _all_ Alternate Data Streams of `.git/` are
|
||||
// forbidden, not just `::$INDEX_ALLOCATION`.
|
||||
if len(part) > 4 && part[4:5] == ":" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *Worktree) validChange(ch merkletrie.Change) error {
|
||||
action, err := ch.Action()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch action {
|
||||
case merkletrie.Delete:
|
||||
return validPath(ch.From.String())
|
||||
case merkletrie.Insert:
|
||||
return validPath(ch.To.String())
|
||||
case merkletrie.Modify:
|
||||
return validPath(ch.From.String(), ch.To.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Worktree) checkoutChange(ch merkletrie.Change, t *object.Tree, idx *indexBuilder) error {
|
||||
a, err := ch.Action()
|
||||
if err != nil {
|
||||
@ -763,10 +656,10 @@ func (w *Worktree) checkoutFile(f *object.File) (err error) {
|
||||
}
|
||||
|
||||
func (w *Worktree) checkoutFileSymlink(f *object.File) (err error) {
|
||||
// https://github.com/git/git/commit/10ecfa76491e4923988337b2e2243b05376b40de
|
||||
if strings.EqualFold(f.Name, gitmodulesFile) {
|
||||
return ErrGitModulesSymlink
|
||||
}
|
||||
// .gitmodules symlink rejection (and its NTFS / HFS variants) is
|
||||
// enforced by the worktreeFilesystem wrapper's Symlink method via
|
||||
// validSymlinkName. See https://github.com/git/git/commit/10ecfa7
|
||||
// for the upstream rationale.
|
||||
|
||||
from, err := f.Reader()
|
||||
if err != nil {
|
||||
|
||||
264
vendor/github.com/go-git/go-git/v5/worktree_fs.go
generated
vendored
Normal file
264
vendor/github.com/go-git/go-git/v5/worktree_fs.go
generated
vendored
Normal file
@ -0,0 +1,264 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-billy/v5"
|
||||
|
||||
"github.com/go-git/go-git/v5/internal/pathutil"
|
||||
)
|
||||
|
||||
// defaultProtectHFS returns the default value for core.protectHFS
|
||||
// when not explicitly configured. Matches upstream Git's
|
||||
// PROTECT_HFS_DEFAULT[1], which the Makefile sets to 1 on Darwin
|
||||
// and leaves at 0 on every other platform.
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/config.mak.uname#L146
|
||||
func defaultProtectHFS() bool {
|
||||
return runtime.GOOS == "darwin"
|
||||
}
|
||||
|
||||
// defaultProtectNTFS returns the default value for core.protectNTFS
|
||||
// when not explicitly configured. Matches upstream Git's
|
||||
// PROTECT_NTFS_DEFAULT, which has been 1 on every platform since
|
||||
// 9102f958ee5 (CVE-2019-1353)[1]: WSL allows Linux processes to
|
||||
// reach NTFS-mounted worktrees on Windows hosts, so the
|
||||
// is_ntfs_dotgit guard cannot safely be gated on the runtime OS.
|
||||
//
|
||||
// [1]: https://github.com/git/git/commit/9102f958ee5
|
||||
func defaultProtectNTFS() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// worktreeFilesystem wraps a billy.Filesystem and validates every path passed
|
||||
// to a mutating operation. This prevents writing to, or deleting from,
|
||||
// dangerous locations (e.g. .git/*, ../) regardless of which worktree
|
||||
// code path triggers the operation.
|
||||
type worktreeFilesystem struct {
|
||||
billy.Filesystem
|
||||
protectNTFS bool
|
||||
protectHFS bool
|
||||
}
|
||||
|
||||
func newWorktreeFilesystem(fs billy.Filesystem, protectNTFS, protectHFS bool) *worktreeFilesystem {
|
||||
return &worktreeFilesystem{Filesystem: fs, protectNTFS: protectNTFS, protectHFS: protectHFS}
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Create(filename string) (billy.File, error) {
|
||||
if err := sfs.validPath(filename); err != nil {
|
||||
return nil, fmt.Errorf("create: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Create(filename)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Open(filename string) (billy.File, error) {
|
||||
if err := sfs.validReadPath(filename); err != nil {
|
||||
return nil, fmt.Errorf("open: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Open(filename)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
|
||||
if err := sfs.validPath(filename); err != nil {
|
||||
return nil, fmt.Errorf("openfile: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.OpenFile(filename, flag, perm)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Stat(filename string) (os.FileInfo, error) {
|
||||
if err := sfs.validReadPath(filename); err != nil {
|
||||
return nil, fmt.Errorf("stat: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Stat(filename)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Remove(filename string) error {
|
||||
if err := sfs.validPath(filename); err != nil {
|
||||
return fmt.Errorf("remove: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Remove(filename)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Rename(from, to string) error {
|
||||
if err := sfs.validPath(from, to); err != nil {
|
||||
return fmt.Errorf("rename: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Rename(from, to)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) ReadDir(path string) ([]os.FileInfo, error) {
|
||||
if err := sfs.validReadPath(path); err != nil {
|
||||
return nil, fmt.Errorf("readdir: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.ReadDir(path)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Lstat(filename string) (os.FileInfo, error) {
|
||||
if err := sfs.validReadPath(filename); err != nil {
|
||||
return nil, fmt.Errorf("lstat: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Lstat(filename)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Symlink(target, link string) error {
|
||||
if err := sfs.validPath(link); err != nil {
|
||||
return fmt.Errorf("symlink: %w", err)
|
||||
}
|
||||
if err := sfs.validSymlinkName(link); err != nil {
|
||||
return fmt.Errorf("symlink: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Symlink(target, link)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Readlink(link string) (string, error) {
|
||||
if err := sfs.validReadPath(link); err != nil {
|
||||
return "", fmt.Errorf("readlink: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Readlink(link)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) MkdirAll(path string, perm os.FileMode) error {
|
||||
// MkdirAll on the worktree root is a no-op: the root always exists,
|
||||
// so there is nothing to materialise. Mirroring the tolerance that
|
||||
// validReadPath gives to read-side operations avoids breaking callers
|
||||
// that walk a directory tree and pass the relative-to-root prefix
|
||||
// ("") through to the worktree FS.
|
||||
if path == "" || path == "." || path == "/" {
|
||||
return nil
|
||||
}
|
||||
if err := sfs.validPath(path); err != nil {
|
||||
return fmt.Errorf("mkdirall: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.MkdirAll(path, perm)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) TempFile(_, _ string) (billy.File, error) {
|
||||
return nil, fmt.Errorf("tempfile: %w", errUnsupportedOperation)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Chroot(path string) (billy.Filesystem, error) {
|
||||
if err := sfs.validReadPath(path); err != nil {
|
||||
return nil, fmt.Errorf("chroot: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Chroot(path)
|
||||
}
|
||||
|
||||
// validReadPath is like validPath but treats the empty string and "." as
|
||||
// valid references to the worktree root. Read-side operations on the root
|
||||
// (e.g. ReadDir(""), Lstat(".")) are legitimate; mutating the root itself
|
||||
// is not, so write-side operations continue to use validPath directly.
|
||||
func (sfs *worktreeFilesystem) validReadPath(p string) error {
|
||||
if p == "" || p == "." || p == "/" {
|
||||
return nil
|
||||
}
|
||||
return sfs.validPath(p)
|
||||
}
|
||||
|
||||
var errUnsupportedOperation = errors.New("unsupported operation")
|
||||
|
||||
// isDotGitVariant reports whether part is .git, git~1, or an HFS+
|
||||
// equivalent of .git (when protectHFS is true). NTFS variants of .git
|
||||
// (e.g. ".git " with trailing space, ".git::$INDEX_ALLOCATION") are
|
||||
// detected separately by pathutil.WindowsValidPath, which applies
|
||||
// regardless of position in the path. Both validators reuse this
|
||||
// helper.
|
||||
func isDotGitVariant(part string, protectHFS bool) bool {
|
||||
if pathutil.IsDotGitName(part) {
|
||||
return true
|
||||
}
|
||||
if protectHFS && pathutil.IsHFSDotGit(part) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// validPath checks whether paths are valid for the worktree
|
||||
// filesystem abstraction. It is intentionally tolerant of .git as
|
||||
// the final path component of a multi-component path
|
||||
// (e.g. "submodule/.git"), so that legitimate gitlink pointer files
|
||||
// can still be Stat'd, Read, and Removed via the wrapper during
|
||||
// submodule cleanup. Attacker-controlled tree-entry paths are
|
||||
// validated separately by pathutil.ValidTreePath at the boundaries
|
||||
// where data leaves the trusted store (Tree.FindEntry, the explicit
|
||||
// callers in CherryPick and Submodule.Repository).
|
||||
//
|
||||
// For upstream rules:
|
||||
// https://github.com/git/git/blob/v2.54.0/read-cache.c#L987
|
||||
// https://github.com/git/git/blob/v2.54.0/path.c#L1419
|
||||
func (sfs *worktreeFilesystem) validPath(paths ...string) error {
|
||||
for _, p := range paths {
|
||||
for i := 0; i < len(p); i++ {
|
||||
if p[i] < 0x20 || p[i] == 0x7f {
|
||||
return fmt.Errorf("invalid path %q: contains control character", p)
|
||||
}
|
||||
}
|
||||
|
||||
parts := strings.FieldsFunc(p, func(r rune) bool { return (r == '\\' || r == '/') })
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("invalid path: %q", p)
|
||||
}
|
||||
|
||||
if sfs.protectNTFS {
|
||||
// Volume names are not supported, in both formats: \\ and <DRIVE_LETTER>:.
|
||||
if vol := filepath.VolumeName(p); vol != "" {
|
||||
return fmt.Errorf("invalid path: %q", p)
|
||||
}
|
||||
}
|
||||
|
||||
for i, part := range parts {
|
||||
if part == "." || part == ".." {
|
||||
return fmt.Errorf("invalid path %q: cannot use %q", p, part)
|
||||
}
|
||||
|
||||
// Reject .git (and equivalents) as a path component when it is
|
||||
// either the first component (root-level .git) or a non-final
|
||||
// component (traversal into a .git directory, e.g. "a/.git/config").
|
||||
// A final non-first .git component (e.g. "submodule/.git") is
|
||||
// allowed because submodule worktrees contain a .git pointer file.
|
||||
if isDotGitVariant(part, sfs.protectHFS) && (i == 0 || i < len(parts)-1) {
|
||||
return fmt.Errorf("invalid path component: %q", p)
|
||||
}
|
||||
|
||||
if sfs.protectNTFS && !pathutil.WindowsValidPath(part) {
|
||||
return fmt.Errorf("invalid path: %q", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validSymlinkName checks the per-component name of a symlink for
|
||||
// dotfile names that attackers can use to trick a checkout into
|
||||
// writing a dangerous symlink. Each path component is compared
|
||||
// against .gitmodules case-insensitively, against its NTFS variants
|
||||
// (e.g. ".gitmodules .", ".gitmodules::$INDEX_ALLOCATION", or 8.3
|
||||
// short-name forms) when protectNTFS is on, and against its HFS+
|
||||
// variants (Unicode ignored code points folded into ".gitmodules")
|
||||
// when protectHFS is on.
|
||||
//
|
||||
// Reference: upstream Git verify_path_internal at read-cache.c#L1004-L1024
|
||||
// in tag v2.54.0[1].
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/read-cache.c#L1004-L1024
|
||||
func (sfs *worktreeFilesystem) validSymlinkName(name string) error {
|
||||
parts := strings.FieldsFunc(name, func(r rune) bool {
|
||||
return r == '/' || r == '\\'
|
||||
})
|
||||
for _, part := range parts {
|
||||
if strings.EqualFold(part, gitmodulesFile) {
|
||||
return ErrGitModulesSymlink
|
||||
}
|
||||
if sfs.protectNTFS && pathutil.IsNTFSDotGitmodules(part) {
|
||||
return ErrGitModulesSymlink
|
||||
}
|
||||
if sfs.protectHFS && pathutil.IsHFSDotGitmodules(part) {
|
||||
return ErrGitModulesSymlink
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
9
vendor/github.com/go-git/go-git/v5/worktree_status.go
generated
vendored
9
vendor/github.com/go-git/go-git/v5/worktree_status.go
generated
vendored
@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-billy/v5/util"
|
||||
"github.com/go-git/go-git/v5/internal/pathutil"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
|
||||
@ -545,6 +546,14 @@ func (w *Worktree) addOrUpdateFileToIndex(idx *index.Index, filename string, h p
|
||||
}
|
||||
|
||||
func (w *Worktree) doAddFileToIndex(idx *index.Index, filename string, h plumbing.Hash) error {
|
||||
// Mirror upstream's Index.Add gate at the v5 caller boundary: the
|
||||
// index feeds future trees, so a name that the tree-side
|
||||
// pathutil.ValidTreePath gate would reject must not enter the
|
||||
// index in the first place. v5 keeps Index.Add's existing signature
|
||||
// for API compatibility, so the validation happens here.
|
||||
if err := pathutil.ValidTreePath(filename); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.doUpdateFileToIndex(idx.Add(filename), filename, h)
|
||||
}
|
||||
|
||||
|
||||
2
vendor/github.com/pjbgf/sha1cd/Dockerfile.arm
generated
vendored
2
vendor/github.com/pjbgf/sha1cd/Dockerfile.arm
generated
vendored
@ -1,4 +1,4 @@
|
||||
FROM golang:1.24@sha256:14fd8a55e59a560704e5fc44970b301d00d344e45d6b914dda228e09f359a088
|
||||
FROM golang:1.26@sha256:6df14f4a4bc9d979a3721f488981e0d1b318006377e473ed23d026796f5f4c0a
|
||||
|
||||
ENV GOOS=linux
|
||||
ENV GOARCH=arm
|
||||
|
||||
2
vendor/github.com/pjbgf/sha1cd/Dockerfile.arm64
generated
vendored
2
vendor/github.com/pjbgf/sha1cd/Dockerfile.arm64
generated
vendored
@ -1,4 +1,4 @@
|
||||
FROM golang:1.24@sha256:14fd8a55e59a560704e5fc44970b301d00d344e45d6b914dda228e09f359a088
|
||||
FROM golang:1.26@sha256:6df14f4a4bc9d979a3721f488981e0d1b318006377e473ed23d026796f5f4c0a
|
||||
|
||||
ENV GOOS=linux
|
||||
ENV GOARCH=arm64
|
||||
|
||||
5
vendor/github.com/pjbgf/sha1cd/sha1cd.go
generated
vendored
5
vendor/github.com/pjbgf/sha1cd/sha1cd.go
generated
vendored
@ -12,7 +12,6 @@ package sha1cd
|
||||
// Original: https://github.com/golang/go/blob/master/src/crypto/sha1/sha1.go
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"hash"
|
||||
@ -20,10 +19,6 @@ import (
|
||||
shared "github.com/pjbgf/sha1cd/internal"
|
||||
)
|
||||
|
||||
func init() {
|
||||
crypto.RegisterHash(crypto.SHA1, New)
|
||||
}
|
||||
|
||||
// The size of a SHA-1 checksum in bytes.
|
||||
const Size = shared.Size
|
||||
|
||||
|
||||
4
vendor/github.com/pjbgf/sha1cd/sha1cdblock_amd64.go
generated
vendored
4
vendor/github.com/pjbgf/sha1cd/sha1cdblock_amd64.go
generated
vendored
@ -37,9 +37,9 @@ func block(dig *digest, p []byte) {
|
||||
chunk := p[:shared.Chunk]
|
||||
|
||||
blockAMD64(dig.h[:], chunk, m1[:], cs[:])
|
||||
rectifyCompressionState(m1, &cs)
|
||||
rectifyCompressionState(&m1, &cs)
|
||||
|
||||
col := checkCollision(m1, cs, dig.h)
|
||||
col := checkCollision(&m1, &cs, &dig.h)
|
||||
if col {
|
||||
dig.col = true
|
||||
|
||||
|
||||
8
vendor/github.com/pjbgf/sha1cd/sha1cdblock_amd64.s
generated
vendored
8
vendor/github.com/pjbgf/sha1cd/sha1cdblock_amd64.s
generated
vendored
@ -11,11 +11,11 @@
|
||||
// Reference implementations:
|
||||
// - 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.
|
||||
#define LOADCS(abcd, e, index, target) \
|
||||
VPEXTRD $3, abcd, ((index*20)+0)(target); \
|
||||
VPEXTRD $2, abcd, ((index*20)+4)(target); \
|
||||
VPEXTRD $1, abcd, ((index*20)+8)(target); \
|
||||
VPEXTRD $0, abcd, ((index*20)+12)(target); \
|
||||
VPSHUFD $0x1B, abcd, X8; \
|
||||
VMOVDQU X8, ((index*20)+0)(target); \
|
||||
MOVL e, ((index*20)+16)(target);
|
||||
|
||||
#define LOADM1(m1, index, target) \
|
||||
|
||||
4
vendor/github.com/pjbgf/sha1cd/sha1cdblock_arm64.go
generated
vendored
4
vendor/github.com/pjbgf/sha1cd/sha1cdblock_arm64.go
generated
vendored
@ -34,8 +34,8 @@ func block(dig *digest, p []byte) {
|
||||
|
||||
blockARM64(dig.h[:], chunk, m1[:], cs[:])
|
||||
|
||||
rectifyCompressionState(m1, &cs)
|
||||
col := checkCollision(m1, cs, dig.h)
|
||||
rectifyCompressionState(&m1, &cs)
|
||||
col := checkCollision(&m1, &cs, &dig.h)
|
||||
if col {
|
||||
dig.col = true
|
||||
|
||||
|
||||
25
vendor/github.com/pjbgf/sha1cd/sha1cdblock_generic.go
generated
vendored
25
vendor/github.com/pjbgf/sha1cd/sha1cdblock_generic.go
generated
vendored
@ -127,7 +127,8 @@ func blockGeneric(dig *digest, p []byte) {
|
||||
}
|
||||
|
||||
if hi == 1 {
|
||||
col := checkCollision(m1, cs, [shared.WordBuffers]uint32{h0, h1, h2, h3, h4})
|
||||
h := [shared.WordBuffers]uint32{h0, h1, h2, h3, h4}
|
||||
col := checkCollision(&m1, &cs, &h)
|
||||
if col {
|
||||
dig.col = true
|
||||
hi++
|
||||
@ -143,23 +144,23 @@ func blockGeneric(dig *digest, p []byte) {
|
||||
|
||||
//go:noinline
|
||||
func checkCollision(
|
||||
m1 [shared.Rounds]uint32,
|
||||
cs [shared.PreStepState][shared.WordBuffers]uint32,
|
||||
h [shared.WordBuffers]uint32,
|
||||
m1 *[shared.Rounds]uint32,
|
||||
cs *[shared.PreStepState][shared.WordBuffers]uint32,
|
||||
h *[shared.WordBuffers]uint32,
|
||||
) bool {
|
||||
if mask := ubc.CalculateDvMask(m1); mask != 0 {
|
||||
dvs := ubc.SHA1_dvs()
|
||||
|
||||
for i := 0; dvs[i].DvType != 0; i++ {
|
||||
if (mask & ((uint32)(1) << uint32(dvs[i].MaskB))) != 0 {
|
||||
var csState [shared.WordBuffers]uint32
|
||||
var csState *[shared.WordBuffers]uint32
|
||||
switch dvs[i].TestT {
|
||||
case 58:
|
||||
csState = cs[1]
|
||||
csState = &cs[1]
|
||||
case 65:
|
||||
csState = cs[2]
|
||||
csState = &cs[2]
|
||||
case 0:
|
||||
csState = cs[0]
|
||||
csState = &cs[0]
|
||||
default:
|
||||
panic(fmt.Sprintf("dvs data is trying to use a testT that isn't available: %d", dvs[i].TestT))
|
||||
}
|
||||
@ -168,7 +169,7 @@ func checkCollision(
|
||||
dvs[i].TestT, // testT is the step number
|
||||
// m2 is a secondary message created XORing with
|
||||
// ubc's DM prior to the SHA recompression step.
|
||||
m1, dvs[i].Dm,
|
||||
m1, &dvs[i].Dm,
|
||||
csState,
|
||||
h)
|
||||
|
||||
@ -182,8 +183,8 @@ func checkCollision(
|
||||
}
|
||||
|
||||
//go:nosplit
|
||||
func hasCollided(step uint32, m1, dm [shared.Rounds]uint32,
|
||||
state [shared.WordBuffers]uint32, h [shared.WordBuffers]uint32) bool {
|
||||
func hasCollided(step uint32, m1, dm *[shared.Rounds]uint32,
|
||||
state *[shared.WordBuffers]uint32, h *[shared.WordBuffers]uint32) bool {
|
||||
// Intermediary Hash Value.
|
||||
ihv := [shared.WordBuffers]uint32{}
|
||||
|
||||
@ -282,7 +283,7 @@ func hasCollided(step uint32, m1, dm [shared.Rounds]uint32,
|
||||
//
|
||||
//go:nosplit
|
||||
func rectifyCompressionState(
|
||||
m1 [shared.Rounds]uint32,
|
||||
m1 *[shared.Rounds]uint32,
|
||||
cs *[shared.PreStepState][shared.WordBuffers]uint32,
|
||||
) {
|
||||
if cs == nil {
|
||||
|
||||
5
vendor/github.com/pjbgf/sha1cd/ubc/ubc.go
generated
vendored
5
vendor/github.com/pjbgf/sha1cd/ubc/ubc.go
generated
vendored
@ -29,7 +29,10 @@ type DvInfo struct {
|
||||
// bitconditions for that DV have been met.
|
||||
//
|
||||
//go:nosplit
|
||||
func CalculateDvMask(W [80]uint32) uint32 {
|
||||
func CalculateDvMask(W *[80]uint32) uint32 {
|
||||
if W == nil {
|
||||
return 0
|
||||
}
|
||||
mask := uint32(0xFFFFFFFF)
|
||||
mask &= (((((W[44] ^ W[45]) >> 29) & 1) - 1) | ^(DV_I_48_0_bit | DV_I_51_0_bit | DV_I_52_0_bit | DV_II_45_0_bit | DV_II_46_0_bit | DV_II_50_0_bit | DV_II_51_0_bit))
|
||||
mask &= (((((W[49] ^ W[50]) >> 29) & 1) - 1) | ^(DV_I_46_0_bit | DV_II_45_0_bit | DV_II_50_0_bit | DV_II_51_0_bit | DV_II_55_0_bit | DV_II_56_0_bit))
|
||||
|
||||
16
vendor/github.com/spf13/cobra/command.go
generated
vendored
16
vendor/github.com/spf13/cobra/command.go
generated
vendored
@ -925,15 +925,10 @@ func (c *Command) execute(a []string) (err error) {
|
||||
// Also say we need help if the command isn't runnable.
|
||||
helpVal, err := c.Flags().GetBool(helpFlagName)
|
||||
if err != nil {
|
||||
// NOTE(d1): temporarily hardcoding "ayuda" as a replacement for "help"
|
||||
// source of the pain: https://github.com/spf13/cobra/issues/2359
|
||||
helpVal, err = c.Flags().GetBool("ayuda")
|
||||
if err != nil {
|
||||
// should be impossible to get here as we always declare a help
|
||||
// flag in InitDefaultHelpFlag()
|
||||
c.Println("\"help\" flag declared as non-bool. Please correct your code")
|
||||
return err
|
||||
}
|
||||
// should be impossible to get here as we always declare a help
|
||||
// flag in InitDefaultHelpFlag()
|
||||
c.Println("\"help\" flag declared as non-bool. Please correct your code")
|
||||
return err
|
||||
}
|
||||
|
||||
if helpVal {
|
||||
@ -1231,8 +1226,7 @@ func (c *Command) InitDefaultHelpFlag() {
|
||||
} else {
|
||||
usage += name
|
||||
}
|
||||
// NOTE(d1): do not assume "help" exists in the context of translation
|
||||
// c.Flags().BoolP(helpFlagName, "h", false, usage)
|
||||
c.Flags().BoolP(helpFlagName, "h", false, usage)
|
||||
_ = c.Flags().SetAnnotation(helpFlagName, FlagSetByCobraAnnotation, []string{"true"})
|
||||
}
|
||||
}
|
||||
|
||||
2
vendor/golang.org/x/crypto/ssh/cipher.go
generated
vendored
2
vendor/golang.org/x/crypto/ssh/cipher.go
generated
vendored
@ -586,7 +586,7 @@ func (c *cbcCipher) writeCipherPacket(seqNum uint32, w io.Writer, rand io.Reader
|
||||
|
||||
// Length of encrypted portion of the packet (header, payload, padding).
|
||||
// Enforce minimum padding and packet size.
|
||||
encLength := maxUInt32(prefixLen+len(packet)+cbcMinPaddingSize, cbcMinPaddingSize)
|
||||
encLength := maxUInt32(prefixLen+len(packet)+cbcMinPaddingSize, cbcMinPacketSize)
|
||||
// Enforce block size.
|
||||
encLength = (encLength + effectiveBlockSize - 1) / effectiveBlockSize * effectiveBlockSize
|
||||
|
||||
|
||||
10
vendor/golang.org/x/crypto/ssh/client_auth.go
generated
vendored
10
vendor/golang.org/x/crypto/ssh/client_auth.go
generated
vendored
@ -274,10 +274,14 @@ func pickSignatureAlgorithm(signer Signer, extensions map[string][]byte) (MultiA
|
||||
}
|
||||
|
||||
// Filter algorithms based on those supported by MultiAlgorithmSigner.
|
||||
// Iterate over the signer's algorithms first to preserve its preference order.
|
||||
supportedKeyAlgos := algorithmsForKeyFormat(keyFormat)
|
||||
var keyAlgos []string
|
||||
for _, algo := range algorithmsForKeyFormat(keyFormat) {
|
||||
if slices.Contains(as.Algorithms(), underlyingAlgo(algo)) {
|
||||
keyAlgos = append(keyAlgos, algo)
|
||||
for _, signerAlgo := range as.Algorithms() {
|
||||
if idx := slices.IndexFunc(supportedKeyAlgos, func(algo string) bool {
|
||||
return underlyingAlgo(algo) == signerAlgo
|
||||
}); idx >= 0 {
|
||||
keyAlgos = append(keyAlgos, supportedKeyAlgos[idx])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
13
vendor/golang.org/x/net/http2/hpack/tables.go
generated
vendored
13
vendor/golang.org/x/net/http2/hpack/tables.go
generated
vendored
@ -6,6 +6,7 @@ package hpack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// headerFieldTable implements a list of HeaderFields.
|
||||
@ -54,10 +55,16 @@ func (t *headerFieldTable) len() int {
|
||||
|
||||
// addEntry adds a new entry.
|
||||
func (t *headerFieldTable) addEntry(f HeaderField) {
|
||||
// Prevent f from escaping to the heap.
|
||||
f2 := HeaderField{
|
||||
Name: strings.Clone(f.Name),
|
||||
Value: strings.Clone(f.Value),
|
||||
Sensitive: f.Sensitive,
|
||||
}
|
||||
id := uint64(t.len()) + t.evictCount + 1
|
||||
t.byName[f.Name] = id
|
||||
t.byNameValue[pairNameValue{f.Name, f.Value}] = id
|
||||
t.ents = append(t.ents, f)
|
||||
t.byName[f2.Name] = id
|
||||
t.byNameValue[pairNameValue{f2.Name, f2.Value}] = id
|
||||
t.ents = append(t.ents, f2)
|
||||
}
|
||||
|
||||
// evictOldest evicts the n oldest entries in the table.
|
||||
|
||||
9
vendor/golang.org/x/net/http2/transport.go
generated
vendored
9
vendor/golang.org/x/net/http2/transport.go
generated
vendored
@ -718,9 +718,6 @@ func canRetryError(err error) bool {
|
||||
}
|
||||
|
||||
func (t *Transport) dialClientConn(ctx context.Context, addr string, singleUse bool) (*ClientConn, error) {
|
||||
if t.transportTestHooks != nil {
|
||||
return t.newClientConn(nil, singleUse, nil)
|
||||
}
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -2861,6 +2858,9 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
|
||||
|
||||
var seenMaxConcurrentStreams bool
|
||||
err := f.ForeachSetting(func(s Setting) error {
|
||||
if err := s.Valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
switch s.ID {
|
||||
case SettingMaxFrameSize:
|
||||
cc.maxFrameSize = s.Val
|
||||
@ -2892,9 +2892,6 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error {
|
||||
cc.henc.SetMaxDynamicTableSize(s.Val)
|
||||
cc.peerMaxHeaderTableSize = s.Val
|
||||
case SettingEnableConnectProtocol:
|
||||
if err := s.Valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
// If the peer wants to send us SETTINGS_ENABLE_CONNECT_PROTOCOL,
|
||||
// we require that it do so in the first SETTINGS frame.
|
||||
//
|
||||
|
||||
19
vendor/golang.org/x/sys/cpu/cpu.go
generated
vendored
19
vendor/golang.org/x/sys/cpu/cpu.go
generated
vendored
@ -152,13 +152,17 @@ var ARM struct {
|
||||
// The booleans in Loong64 contain the correspondingly named cpu feature bit.
|
||||
// The struct is padded to avoid false sharing.
|
||||
var Loong64 struct {
|
||||
_ CacheLinePad
|
||||
HasLSX bool // support 128-bit vector extension
|
||||
HasLASX bool // support 256-bit vector extension
|
||||
HasCRC32 bool // support CRC instruction
|
||||
HasLAM_BH bool // support AM{SWAP/ADD}[_DB].{B/H} instruction
|
||||
HasLAMCAS bool // support AMCAS[_DB].{B/H/W/D} instruction
|
||||
_ CacheLinePad
|
||||
_ CacheLinePad
|
||||
HasLSX bool // support 128-bit vector extension
|
||||
HasLASX bool // support 256-bit vector extension
|
||||
HasCRC32 bool // support CRC instruction
|
||||
HasLAMCAS bool // support AMCAS[_DB].{B/H/W/D}
|
||||
HasLAM_BH bool // support AM{SWAP/ADD}[_DB].{B/H} instruction
|
||||
HasLLACQ_SCREL bool // support LLACQ.{W/D}, SCREL.{W/D} instruction
|
||||
HasSCQ bool // support SC.Q instruction
|
||||
HasDBAR_HINTS bool // supports finer-grained DBAR hints
|
||||
|
||||
_ CacheLinePad
|
||||
}
|
||||
|
||||
// MIPS64X contains the supported CPU features of the current mips64/mips64le
|
||||
@ -232,6 +236,7 @@ var RISCV64 struct {
|
||||
HasZba bool // Address generation instructions extension
|
||||
HasZbb bool // Basic bit-manipulation extension
|
||||
HasZbs bool // Single-bit instructions extension
|
||||
HasZbc bool // Carryless multiplication extension
|
||||
HasZvbb bool // Vector Basic Bit-manipulation
|
||||
HasZvbc bool // Vector Carryless Multiplication
|
||||
HasZvkb bool // Vector Cryptography Bit-manipulation
|
||||
|
||||
2
vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go
generated
vendored
2
vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go
generated
vendored
@ -6,6 +6,8 @@
|
||||
|
||||
package cpu
|
||||
|
||||
import "runtime"
|
||||
|
||||
func doinit() {
|
||||
setMinimalFeatures()
|
||||
|
||||
|
||||
2
vendor/golang.org/x/sys/cpu/cpu_linux_riscv64.go
generated
vendored
2
vendor/golang.org/x/sys/cpu/cpu_linux_riscv64.go
generated
vendored
@ -58,6 +58,7 @@ const (
|
||||
riscv_HWPROBE_EXT_ZBA = 0x8
|
||||
riscv_HWPROBE_EXT_ZBB = 0x10
|
||||
riscv_HWPROBE_EXT_ZBS = 0x20
|
||||
riscv_HWPROBE_EXT_ZBC = 0x80
|
||||
riscv_HWPROBE_EXT_ZVBB = 0x20000
|
||||
riscv_HWPROBE_EXT_ZVBC = 0x40000
|
||||
riscv_HWPROBE_EXT_ZVKB = 0x80000
|
||||
@ -108,6 +109,7 @@ func doinit() {
|
||||
RISCV64.HasZba = isSet(v, riscv_HWPROBE_EXT_ZBA)
|
||||
RISCV64.HasZbb = isSet(v, riscv_HWPROBE_EXT_ZBB)
|
||||
RISCV64.HasZbs = isSet(v, riscv_HWPROBE_EXT_ZBS)
|
||||
RISCV64.HasZbc = isSet(v, riscv_HWPROBE_EXT_ZBC)
|
||||
RISCV64.HasZvbb = isSet(v, riscv_HWPROBE_EXT_ZVBB)
|
||||
RISCV64.HasZvbc = isSet(v, riscv_HWPROBE_EXT_ZVBC)
|
||||
RISCV64.HasZvkb = isSet(v, riscv_HWPROBE_EXT_ZVKB)
|
||||
|
||||
16
vendor/golang.org/x/sys/cpu/cpu_loong64.go
generated
vendored
16
vendor/golang.org/x/sys/cpu/cpu_loong64.go
generated
vendored
@ -15,8 +15,13 @@ const (
|
||||
cpucfg1_CRC32 = 1 << 25
|
||||
|
||||
// CPUCFG2 bits
|
||||
cpucfg2_LAM_BH = 1 << 27
|
||||
cpucfg2_LAMCAS = 1 << 28
|
||||
cpucfg2_LAM_BH = 1 << 27
|
||||
cpucfg2_LAMCAS = 1 << 28
|
||||
cpucfg2_LLACQ_SCREL = 1 << 29
|
||||
cpucfg2_SCQ = 1 << 30
|
||||
|
||||
// CPUCFG3 bits
|
||||
cpucfg3_DBAR_HINTS = 1 << 17
|
||||
)
|
||||
|
||||
func initOptions() {
|
||||
@ -26,6 +31,9 @@ func initOptions() {
|
||||
{Name: "crc32", Feature: &Loong64.HasCRC32},
|
||||
{Name: "lam_bh", Feature: &Loong64.HasLAM_BH},
|
||||
{Name: "lamcas", Feature: &Loong64.HasLAMCAS},
|
||||
{Name: "llacq_screl", Feature: &Loong64.HasLLACQ_SCREL},
|
||||
{Name: "scq", Feature: &Loong64.HasSCQ},
|
||||
{Name: "dbar_hints", Feature: &Loong64.HasDBAR_HINTS},
|
||||
}
|
||||
|
||||
// The CPUCFG data on Loong64 only reflects the hardware capabilities,
|
||||
@ -37,10 +45,14 @@ func initOptions() {
|
||||
// through CPUCFG
|
||||
cfg1 := get_cpucfg(1)
|
||||
cfg2 := get_cpucfg(2)
|
||||
cfg3 := get_cpucfg(3)
|
||||
|
||||
Loong64.HasCRC32 = cfgIsSet(cfg1, cpucfg1_CRC32)
|
||||
Loong64.HasLAMCAS = cfgIsSet(cfg2, cpucfg2_LAMCAS)
|
||||
Loong64.HasLAM_BH = cfgIsSet(cfg2, cpucfg2_LAM_BH)
|
||||
Loong64.HasLLACQ_SCREL = cfgIsSet(cfg2, cpucfg2_LLACQ_SCREL)
|
||||
Loong64.HasSCQ = cfgIsSet(cfg2, cpucfg2_SCQ)
|
||||
Loong64.HasDBAR_HINTS = cfgIsSet(cfg3, cpucfg3_DBAR_HINTS)
|
||||
}
|
||||
|
||||
func get_cpucfg(reg uint32) uint32
|
||||
|
||||
1
vendor/golang.org/x/sys/cpu/cpu_riscv64.go
generated
vendored
1
vendor/golang.org/x/sys/cpu/cpu_riscv64.go
generated
vendored
@ -16,6 +16,7 @@ func initOptions() {
|
||||
{Name: "zba", Feature: &RISCV64.HasZba},
|
||||
{Name: "zbb", Feature: &RISCV64.HasZbb},
|
||||
{Name: "zbs", Feature: &RISCV64.HasZbs},
|
||||
{Name: "zbc", Feature: &RISCV64.HasZbc},
|
||||
// RISC-V Cryptography Extensions
|
||||
{Name: "zvbb", Feature: &RISCV64.HasZvbb},
|
||||
{Name: "zvbc", Feature: &RISCV64.HasZvbc},
|
||||
|
||||
26
vendor/golang.org/x/sys/cpu/cpu_windows.go
generated
vendored
Normal file
26
vendor/golang.org/x/sys/cpu/cpu_windows.go
generated
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright 2026 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cpu
|
||||
|
||||
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -systemdll=false -output zcpu_windows.go cpu_windows.go
|
||||
|
||||
//sys isProcessorFeaturePresent(ProcessorFeature uint32) (ret bool) = kernel32.IsProcessorFeaturePresent
|
||||
|
||||
// The processor features to be tested for IsProcessorFeaturePresent, see
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-isprocessorfeaturepresent
|
||||
const (
|
||||
_PF_ARM_V8_CRYPTO_INSTRUCTIONS_AVAILABLE = 30
|
||||
_PF_ARM_V8_CRC32_INSTRUCTIONS_AVAILABLE = 31
|
||||
_PF_ARM_V81_ATOMIC_INSTRUCTIONS_AVAILABLE = 34
|
||||
_PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE = 43
|
||||
|
||||
_PF_ARM_V83_JSCVT_INSTRUCTIONS_AVAILABLE = 44
|
||||
_PF_ARM_V83_LRCPC_INSTRUCTIONS_AVAILABLE = 45
|
||||
_PF_ARM_SVE_INSTRUCTIONS_AVAILABLE = 46
|
||||
_PF_ARM_SVE2_INSTRUCTIONS_AVAILABLE = 47
|
||||
|
||||
_PF_ARM_SHA3_INSTRUCTIONS_AVAILABLE = 64
|
||||
_PF_ARM_SHA512_INSTRUCTIONS_AVAILABLE = 65
|
||||
)
|
||||
24
vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go
generated
vendored
24
vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go
generated
vendored
@ -4,10 +4,6 @@
|
||||
|
||||
package cpu
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func doinit() {
|
||||
// set HasASIMD and HasFP to true as per
|
||||
// https://learn.microsoft.com/en-us/cpp/build/arm64-windows-abi-conventions?view=msvc-170#base-requirements
|
||||
@ -18,25 +14,25 @@ func doinit() {
|
||||
ARM64.HasASIMD = true
|
||||
ARM64.HasFP = true
|
||||
|
||||
if windows.IsProcessorFeaturePresent(windows.PF_ARM_V8_CRYPTO_INSTRUCTIONS_AVAILABLE) {
|
||||
if isProcessorFeaturePresent(_PF_ARM_V8_CRYPTO_INSTRUCTIONS_AVAILABLE) {
|
||||
ARM64.HasAES = true
|
||||
ARM64.HasPMULL = true
|
||||
ARM64.HasSHA1 = true
|
||||
ARM64.HasSHA2 = true
|
||||
}
|
||||
ARM64.HasSHA3 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SHA3_INSTRUCTIONS_AVAILABLE)
|
||||
ARM64.HasCRC32 = windows.IsProcessorFeaturePresent(windows.PF_ARM_V8_CRC32_INSTRUCTIONS_AVAILABLE)
|
||||
ARM64.HasSHA512 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SHA512_INSTRUCTIONS_AVAILABLE)
|
||||
ARM64.HasATOMICS = windows.IsProcessorFeaturePresent(windows.PF_ARM_V81_ATOMIC_INSTRUCTIONS_AVAILABLE)
|
||||
if windows.IsProcessorFeaturePresent(windows.PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE) {
|
||||
ARM64.HasSHA3 = isProcessorFeaturePresent(_PF_ARM_SHA3_INSTRUCTIONS_AVAILABLE)
|
||||
ARM64.HasCRC32 = isProcessorFeaturePresent(_PF_ARM_V8_CRC32_INSTRUCTIONS_AVAILABLE)
|
||||
ARM64.HasSHA512 = isProcessorFeaturePresent(_PF_ARM_SHA512_INSTRUCTIONS_AVAILABLE)
|
||||
ARM64.HasATOMICS = isProcessorFeaturePresent(_PF_ARM_V81_ATOMIC_INSTRUCTIONS_AVAILABLE)
|
||||
if isProcessorFeaturePresent(_PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE) {
|
||||
ARM64.HasASIMDDP = true
|
||||
ARM64.HasASIMDRDM = true
|
||||
}
|
||||
if windows.IsProcessorFeaturePresent(windows.PF_ARM_V83_LRCPC_INSTRUCTIONS_AVAILABLE) {
|
||||
if isProcessorFeaturePresent(_PF_ARM_V83_LRCPC_INSTRUCTIONS_AVAILABLE) {
|
||||
ARM64.HasLRCPC = true
|
||||
ARM64.HasSM3 = true
|
||||
}
|
||||
ARM64.HasSVE = windows.IsProcessorFeaturePresent(windows.PF_ARM_SVE_INSTRUCTIONS_AVAILABLE)
|
||||
ARM64.HasSVE2 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SVE2_INSTRUCTIONS_AVAILABLE)
|
||||
ARM64.HasJSCVT = windows.IsProcessorFeaturePresent(windows.PF_ARM_V83_JSCVT_INSTRUCTIONS_AVAILABLE)
|
||||
ARM64.HasSVE = isProcessorFeaturePresent(_PF_ARM_SVE_INSTRUCTIONS_AVAILABLE)
|
||||
ARM64.HasSVE2 = isProcessorFeaturePresent(_PF_ARM_SVE2_INSTRUCTIONS_AVAILABLE)
|
||||
ARM64.HasJSCVT = isProcessorFeaturePresent(_PF_ARM_V83_JSCVT_INSTRUCTIONS_AVAILABLE)
|
||||
}
|
||||
|
||||
48
vendor/golang.org/x/sys/cpu/zcpu_windows.go
generated
vendored
Normal file
48
vendor/golang.org/x/sys/cpu/zcpu_windows.go
generated
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
// Code generated by 'go generate'; DO NOT EDIT.
|
||||
|
||||
package cpu
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
errERROR_EINVAL error = syscall.EINVAL
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return errERROR_EINVAL
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
// TODO: add more here, after collecting data on the common
|
||||
// error values see on Windows. (perhaps when running
|
||||
// all.bat?)
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
|
||||
procIsProcessorFeaturePresent = modkernel32.NewProc("IsProcessorFeaturePresent")
|
||||
)
|
||||
|
||||
func isProcessorFeaturePresent(ProcessorFeature uint32) (ret bool) {
|
||||
r0, _, _ := syscall.SyscallN(procIsProcessorFeaturePresent.Addr(), uintptr(ProcessorFeature))
|
||||
ret = r0 != 0
|
||||
return
|
||||
}
|
||||
128
vendor/golang.org/x/sys/unix/affinity_linux.go
generated
vendored
128
vendor/golang.org/x/sys/unix/affinity_linux.go
generated
vendored
@ -13,11 +13,19 @@ import (
|
||||
|
||||
const cpuSetSize = _CPU_SETSIZE / _NCPUBITS
|
||||
|
||||
// CPUSet represents a CPU affinity mask.
|
||||
// CPUSet represents a bit mask of CPUs, to be used with [SchedGetaffinity], [SchedSetaffinity],
|
||||
// and [SetMemPolicy].
|
||||
//
|
||||
// Note this type can only represent CPU IDs 0 through 1023.
|
||||
// Use [CPUSetDynamic]/[NewCPUSet] instead to avoid this limit.
|
||||
type CPUSet [cpuSetSize]cpuMask
|
||||
|
||||
func schedAffinity(trap uintptr, pid int, set *CPUSet) error {
|
||||
_, _, e := RawSyscall(trap, uintptr(pid), uintptr(unsafe.Sizeof(*set)), uintptr(unsafe.Pointer(set)))
|
||||
// CPUSetDynamic represents a bit mask of CPUs, to be used with [SchedGetaffinityDynamic],
|
||||
// [SchedSetaffinityDynamic], and [SetMemPolicyDynamic]. Use [NewCPUSet] to allocate.
|
||||
type CPUSetDynamic []cpuMask
|
||||
|
||||
func schedAffinity(trap uintptr, pid int, size uintptr, ptr unsafe.Pointer) error {
|
||||
_, _, e := RawSyscall(trap, uintptr(pid), uintptr(size), uintptr(ptr))
|
||||
if e != 0 {
|
||||
return errnoErr(e)
|
||||
}
|
||||
@ -27,13 +35,13 @@ func schedAffinity(trap uintptr, pid int, set *CPUSet) error {
|
||||
// SchedGetaffinity gets the CPU affinity mask of the thread specified by pid.
|
||||
// If pid is 0 the calling thread is used.
|
||||
func SchedGetaffinity(pid int, set *CPUSet) error {
|
||||
return schedAffinity(SYS_SCHED_GETAFFINITY, pid, set)
|
||||
return schedAffinity(SYS_SCHED_GETAFFINITY, pid, unsafe.Sizeof(*set), unsafe.Pointer(set))
|
||||
}
|
||||
|
||||
// SchedSetaffinity sets the CPU affinity mask of the thread specified by pid.
|
||||
// If pid is 0 the calling thread is used.
|
||||
func SchedSetaffinity(pid int, set *CPUSet) error {
|
||||
return schedAffinity(SYS_SCHED_SETAFFINITY, pid, set)
|
||||
return schedAffinity(SYS_SCHED_SETAFFINITY, pid, unsafe.Sizeof(*set), unsafe.Pointer(set))
|
||||
}
|
||||
|
||||
// Zero clears the set s, so that it contains no CPUs.
|
||||
@ -45,9 +53,7 @@ func (s *CPUSet) Zero() {
|
||||
// will silently ignore any invalid CPU bits in [CPUSet] so this is an
|
||||
// efficient way of resetting the CPU affinity of a process.
|
||||
func (s *CPUSet) Fill() {
|
||||
for i := range s {
|
||||
s[i] = ^cpuMask(0)
|
||||
}
|
||||
cpuMaskFill(s[:])
|
||||
}
|
||||
|
||||
func cpuBitsIndex(cpu int) int {
|
||||
@ -58,24 +64,27 @@ func cpuBitsMask(cpu int) cpuMask {
|
||||
return cpuMask(1 << (uint(cpu) % _NCPUBITS))
|
||||
}
|
||||
|
||||
// Set adds cpu to the set s.
|
||||
func (s *CPUSet) Set(cpu int) {
|
||||
func cpuMaskFill(s []cpuMask) {
|
||||
for i := range s {
|
||||
s[i] = ^cpuMask(0)
|
||||
}
|
||||
}
|
||||
|
||||
func cpuMaskSet(s []cpuMask, cpu int) {
|
||||
i := cpuBitsIndex(cpu)
|
||||
if i < len(s) {
|
||||
s[i] |= cpuBitsMask(cpu)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear removes cpu from the set s.
|
||||
func (s *CPUSet) Clear(cpu int) {
|
||||
func cpuMaskClear(s []cpuMask, cpu int) {
|
||||
i := cpuBitsIndex(cpu)
|
||||
if i < len(s) {
|
||||
s[i] &^= cpuBitsMask(cpu)
|
||||
}
|
||||
}
|
||||
|
||||
// IsSet reports whether cpu is in the set s.
|
||||
func (s *CPUSet) IsSet(cpu int) bool {
|
||||
func cpuMaskIsSet(s []cpuMask, cpu int) bool {
|
||||
i := cpuBitsIndex(cpu)
|
||||
if i < len(s) {
|
||||
return s[i]&cpuBitsMask(cpu) != 0
|
||||
@ -83,11 +92,98 @@ func (s *CPUSet) IsSet(cpu int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Count returns the number of CPUs in the set s.
|
||||
func (s *CPUSet) Count() int {
|
||||
func cpuMaskCount(s []cpuMask) int {
|
||||
c := 0
|
||||
for _, b := range s {
|
||||
c += bits.OnesCount64(uint64(b))
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Set adds cpu to the set s. If cpu is out of bounds for s, no action is taken.
|
||||
func (s *CPUSet) Set(cpu int) {
|
||||
cpuMaskSet(s[:], cpu)
|
||||
}
|
||||
|
||||
// Clear removes cpu from the set s. If cpu is out of bounds for s, no action is taken.
|
||||
func (s *CPUSet) Clear(cpu int) {
|
||||
cpuMaskClear(s[:], cpu)
|
||||
}
|
||||
|
||||
// IsSet reports whether cpu is in the set s.
|
||||
func (s *CPUSet) IsSet(cpu int) bool {
|
||||
return cpuMaskIsSet(s[:], cpu)
|
||||
}
|
||||
|
||||
// Count returns the number of CPUs in the set s.
|
||||
func (s *CPUSet) Count() int {
|
||||
return cpuMaskCount(s[:])
|
||||
}
|
||||
|
||||
// NewCPUSet creates a CPU affinity mask capable of representing CPU IDs
|
||||
// up to maxCPU (exclusive).
|
||||
func NewCPUSet(maxCPU int) CPUSetDynamic {
|
||||
numMasks := (maxCPU + _NCPUBITS - 1) / _NCPUBITS
|
||||
if numMasks == 0 {
|
||||
numMasks = 1
|
||||
}
|
||||
return make(CPUSetDynamic, numMasks)
|
||||
}
|
||||
|
||||
// Zero clears the set s, so that it contains no CPUs.
|
||||
func (s CPUSetDynamic) Zero() {
|
||||
clear(s)
|
||||
}
|
||||
|
||||
// Fill adds all possible CPU bits to the set s. On Linux, [SchedSetaffinityDynamic]
|
||||
// will silently ignore any invalid CPU bits in [CPUSetDynamic] so this is an
|
||||
// efficient way of resetting the CPU affinity of a process.
|
||||
func (s CPUSetDynamic) Fill() {
|
||||
cpuMaskFill(s)
|
||||
}
|
||||
|
||||
// Set adds cpu to the set s. If cpu is out of bounds for s, no action is taken.
|
||||
func (s CPUSetDynamic) Set(cpu int) {
|
||||
cpuMaskSet(s, cpu)
|
||||
}
|
||||
|
||||
// Clear removes cpu from the set s. If cpu is out of bounds for s, no action is taken.
|
||||
func (s CPUSetDynamic) Clear(cpu int) {
|
||||
cpuMaskClear(s, cpu)
|
||||
}
|
||||
|
||||
// IsSet reports whether cpu is in the set s.
|
||||
func (s CPUSetDynamic) IsSet(cpu int) bool {
|
||||
return cpuMaskIsSet(s, cpu)
|
||||
}
|
||||
|
||||
// Count returns the number of CPUs in the set s.
|
||||
func (s CPUSetDynamic) Count() int {
|
||||
return cpuMaskCount(s)
|
||||
}
|
||||
|
||||
func (s CPUSetDynamic) size() uintptr {
|
||||
return uintptr(len(s)) * unsafe.Sizeof(cpuMask(0))
|
||||
}
|
||||
|
||||
func (s CPUSetDynamic) pointer() unsafe.Pointer {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
return unsafe.Pointer(&s[0])
|
||||
}
|
||||
|
||||
// SchedGetaffinityDynamic gets the CPU affinity mask of the thread specified by pid.
|
||||
// If pid is 0 the calling thread is used.
|
||||
//
|
||||
// If the set is smaller than the size of the affinity mask used by the kernel,
|
||||
// [EINVAL] is returned.
|
||||
func SchedGetaffinityDynamic(pid int, set CPUSetDynamic) error {
|
||||
return schedAffinity(SYS_SCHED_GETAFFINITY, pid, set.size(), set.pointer())
|
||||
}
|
||||
|
||||
// SchedSetaffinityDynamic sets the CPU affinity mask of the thread specified by pid.
|
||||
// If pid is 0 the calling thread is used.
|
||||
func SchedSetaffinityDynamic(pid int, set CPUSetDynamic) error {
|
||||
return schedAffinity(SYS_SCHED_SETAFFINITY, pid, set.size(), set.pointer())
|
||||
}
|
||||
|
||||
2
vendor/golang.org/x/sys/unix/mkall.sh
generated
vendored
2
vendor/golang.org/x/sys/unix/mkall.sh
generated
vendored
@ -51,7 +51,7 @@ if [[ "$GOOS" = "linux" ]]; then
|
||||
# Files generated through docker (use $cmd so you can Ctl-C the build or run)
|
||||
set -e
|
||||
$cmd docker build --tag generate:$GOOS $GOOS
|
||||
$cmd docker run --interactive --tty --volume $(cd -- "$(dirname -- "$0")/.." && pwd):/build generate:$GOOS
|
||||
$cmd docker run --rm --interactive --tty --volume $(cd -- "$(dirname -- "$0")/.." && pwd):/build generate:$GOOS
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
3
vendor/golang.org/x/sys/unix/mkerrors.sh
generated
vendored
3
vendor/golang.org/x/sys/unix/mkerrors.sh
generated
vendored
@ -354,6 +354,9 @@ struct ltchars {
|
||||
// Renamed in v6.16, commit c6d732c38f93 ("net: ethtool: remove duplicate defines for family info")
|
||||
#define ETHTOOL_FAMILY_NAME ETHTOOL_GENL_NAME
|
||||
#define ETHTOOL_FAMILY_VERSION ETHTOOL_GENL_VERSION
|
||||
|
||||
// Removed in v6.17, commit 760e6f7befba ("futex: Remove support for IMMUTABLE")
|
||||
#define PR_FUTEX_HASH_GET_IMMUTABLE 3
|
||||
'
|
||||
|
||||
includes_NetBSD='
|
||||
|
||||
103
vendor/golang.org/x/sys/unix/readv_unix.go
generated
vendored
Normal file
103
vendor/golang.org/x/sys/unix/readv_unix.go
generated
vendored
Normal file
@ -0,0 +1,103 @@
|
||||
// Copyright 2026 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build darwin || linux || openbsd
|
||||
|
||||
package unix
|
||||
|
||||
import "unsafe"
|
||||
|
||||
// minIovec is the size of the small initial allocation used by
|
||||
// Readv, Writev, etc.
|
||||
//
|
||||
// This small allocation gets stack allocated, which lets the
|
||||
// common use case of len(iovs) <= minIovec avoid more expensive
|
||||
// heap allocations.
|
||||
const minIovec = 8
|
||||
|
||||
// appendBytes converts bs to Iovecs and appends them to vecs.
|
||||
func appendBytes(vecs []Iovec, bs [][]byte) []Iovec {
|
||||
for _, b := range bs {
|
||||
var v Iovec
|
||||
v.SetLen(len(b))
|
||||
if len(b) > 0 {
|
||||
v.Base = &b[0]
|
||||
} else {
|
||||
v.Base = (*byte)(unsafe.Pointer(&_zero))
|
||||
}
|
||||
vecs = append(vecs, v)
|
||||
}
|
||||
return vecs
|
||||
}
|
||||
|
||||
// writevRaceDetect tells the race detector that the program
|
||||
// has read the first n bytes stored in iovecs.
|
||||
func writevRaceDetect(iovecs []Iovec, n int) {
|
||||
if !raceenabled {
|
||||
return
|
||||
}
|
||||
for i := 0; n > 0 && i < len(iovecs); i++ {
|
||||
m := min(int(iovecs[i].Len), n)
|
||||
n -= m
|
||||
if m > 0 {
|
||||
raceReadRange(unsafe.Pointer(iovecs[i].Base), m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readvRaceDetect tells the race detector that the program
|
||||
// has written to the first n bytes stored in iovecs.
|
||||
func readvRaceDetect(iovecs []Iovec, n int, err error) {
|
||||
if !raceenabled {
|
||||
return
|
||||
}
|
||||
for i := 0; n > 0 && i < len(iovecs); i++ {
|
||||
m := min(int(iovecs[i].Len), n)
|
||||
n -= m
|
||||
if m > 0 {
|
||||
raceWriteRange(unsafe.Pointer(iovecs[i].Base), m)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
raceAcquire(unsafe.Pointer(&ioSync))
|
||||
}
|
||||
}
|
||||
|
||||
func Readv(fd int, iovs [][]byte) (n int, err error) {
|
||||
iovecs := make([]Iovec, 0, minIovec)
|
||||
iovecs = appendBytes(iovecs, iovs)
|
||||
n, err = readv(fd, iovecs)
|
||||
readvRaceDetect(iovecs, n, err)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func Preadv(fd int, iovs [][]byte, offset int64) (n int, err error) {
|
||||
iovecs := make([]Iovec, 0, minIovec)
|
||||
iovecs = appendBytes(iovecs, iovs)
|
||||
n, err = preadv(fd, iovecs, offset)
|
||||
readvRaceDetect(iovecs, n, err)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func Writev(fd int, iovs [][]byte) (n int, err error) {
|
||||
iovecs := make([]Iovec, 0, minIovec)
|
||||
iovecs = appendBytes(iovecs, iovs)
|
||||
if raceenabled {
|
||||
raceReleaseMerge(unsafe.Pointer(&ioSync))
|
||||
}
|
||||
n, err = writev(fd, iovecs)
|
||||
writevRaceDetect(iovecs, n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func Pwritev(fd int, iovs [][]byte, offset int64) (n int, err error) {
|
||||
iovecs := make([]Iovec, 0, minIovec)
|
||||
iovecs = appendBytes(iovecs, iovs)
|
||||
if raceenabled {
|
||||
raceReleaseMerge(unsafe.Pointer(&ioSync))
|
||||
}
|
||||
n, err = pwritev(fd, iovecs, offset)
|
||||
writevRaceDetect(iovecs, n)
|
||||
return n, err
|
||||
}
|
||||
89
vendor/golang.org/x/sys/unix/syscall_darwin.go
generated
vendored
89
vendor/golang.org/x/sys/unix/syscall_darwin.go
generated
vendored
@ -602,95 +602,6 @@ func Connectx(fd int, srcIf uint32, srcAddr, dstAddr Sockaddr, associd SaeAssocI
|
||||
return
|
||||
}
|
||||
|
||||
const minIovec = 8
|
||||
|
||||
func Readv(fd int, iovs [][]byte) (n int, err error) {
|
||||
iovecs := make([]Iovec, 0, minIovec)
|
||||
iovecs = appendBytes(iovecs, iovs)
|
||||
n, err = readv(fd, iovecs)
|
||||
readvRacedetect(iovecs, n, err)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func Preadv(fd int, iovs [][]byte, offset int64) (n int, err error) {
|
||||
iovecs := make([]Iovec, 0, minIovec)
|
||||
iovecs = appendBytes(iovecs, iovs)
|
||||
n, err = preadv(fd, iovecs, offset)
|
||||
readvRacedetect(iovecs, n, err)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func Writev(fd int, iovs [][]byte) (n int, err error) {
|
||||
iovecs := make([]Iovec, 0, minIovec)
|
||||
iovecs = appendBytes(iovecs, iovs)
|
||||
if raceenabled {
|
||||
raceReleaseMerge(unsafe.Pointer(&ioSync))
|
||||
}
|
||||
n, err = writev(fd, iovecs)
|
||||
writevRacedetect(iovecs, n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func Pwritev(fd int, iovs [][]byte, offset int64) (n int, err error) {
|
||||
iovecs := make([]Iovec, 0, minIovec)
|
||||
iovecs = appendBytes(iovecs, iovs)
|
||||
if raceenabled {
|
||||
raceReleaseMerge(unsafe.Pointer(&ioSync))
|
||||
}
|
||||
n, err = pwritev(fd, iovecs, offset)
|
||||
writevRacedetect(iovecs, n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func appendBytes(vecs []Iovec, bs [][]byte) []Iovec {
|
||||
for _, b := range bs {
|
||||
var v Iovec
|
||||
v.SetLen(len(b))
|
||||
if len(b) > 0 {
|
||||
v.Base = &b[0]
|
||||
} else {
|
||||
v.Base = (*byte)(unsafe.Pointer(&_zero))
|
||||
}
|
||||
vecs = append(vecs, v)
|
||||
}
|
||||
return vecs
|
||||
}
|
||||
|
||||
func writevRacedetect(iovecs []Iovec, n int) {
|
||||
if !raceenabled {
|
||||
return
|
||||
}
|
||||
for i := 0; n > 0 && i < len(iovecs); i++ {
|
||||
m := int(iovecs[i].Len)
|
||||
if m > n {
|
||||
m = n
|
||||
}
|
||||
n -= m
|
||||
if m > 0 {
|
||||
raceReadRange(unsafe.Pointer(iovecs[i].Base), m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readvRacedetect(iovecs []Iovec, n int, err error) {
|
||||
if !raceenabled {
|
||||
return
|
||||
}
|
||||
for i := 0; n > 0 && i < len(iovecs); i++ {
|
||||
m := int(iovecs[i].Len)
|
||||
if m > n {
|
||||
m = n
|
||||
}
|
||||
n -= m
|
||||
if m > 0 {
|
||||
raceWriteRange(unsafe.Pointer(iovecs[i].Base), m)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
raceAcquire(unsafe.Pointer(&ioSync))
|
||||
}
|
||||
}
|
||||
|
||||
//sys connectx(fd int, endpoints *SaEndpoints, associd SaeAssocID, flags uint32, iov []Iovec, n *uintptr, connid *SaeConnID) (err error)
|
||||
//sys sendfile(infd int, outfd int, offset int64, len *int64, hdtr unsafe.Pointer, flags int) (err error)
|
||||
|
||||
|
||||
114
vendor/golang.org/x/sys/unix/syscall_linux.go
generated
vendored
114
vendor/golang.org/x/sys/unix/syscall_linux.go
generated
vendored
@ -2150,33 +2150,10 @@ func Signalfd(fd int, sigmask *Sigset_t, flags int) (newfd int, err error) {
|
||||
//sys exitThread(code int) (err error) = SYS_EXIT
|
||||
//sys readv(fd int, iovs []Iovec) (n int, err error) = SYS_READV
|
||||
//sys writev(fd int, iovs []Iovec) (n int, err error) = SYS_WRITEV
|
||||
//sys preadv(fd int, iovs []Iovec, offs_l uintptr, offs_h uintptr) (n int, err error) = SYS_PREADV
|
||||
//sys pwritev(fd int, iovs []Iovec, offs_l uintptr, offs_h uintptr) (n int, err error) = SYS_PWRITEV
|
||||
//sys preadv2(fd int, iovs []Iovec, offs_l uintptr, offs_h uintptr, flags int) (n int, err error) = SYS_PREADV2
|
||||
//sys pwritev2(fd int, iovs []Iovec, offs_l uintptr, offs_h uintptr, flags int) (n int, err error) = SYS_PWRITEV2
|
||||
|
||||
// minIovec is the size of the small initial allocation used by
|
||||
// Readv, Writev, etc.
|
||||
//
|
||||
// This small allocation gets stack allocated, which lets the
|
||||
// common use case of len(iovs) <= minIovs avoid more expensive
|
||||
// heap allocations.
|
||||
const minIovec = 8
|
||||
|
||||
// appendBytes converts bs to Iovecs and appends them to vecs.
|
||||
func appendBytes(vecs []Iovec, bs [][]byte) []Iovec {
|
||||
for _, b := range bs {
|
||||
var v Iovec
|
||||
v.SetLen(len(b))
|
||||
if len(b) > 0 {
|
||||
v.Base = &b[0]
|
||||
} else {
|
||||
v.Base = (*byte)(unsafe.Pointer(&_zero))
|
||||
}
|
||||
vecs = append(vecs, v)
|
||||
}
|
||||
return vecs
|
||||
}
|
||||
//sys preadvSyscall(fd int, iovs []Iovec, offs_l uintptr, offs_h uintptr) (n int, err error) = SYS_PREADV
|
||||
//sys pwritevSyscall(fd int, iovs []Iovec, offs_l uintptr, offs_h uintptr) (n int, err error) = SYS_PWRITEV
|
||||
//sys preadv2Syscall(fd int, iovs []Iovec, offs_l uintptr, offs_h uintptr, flags int) (n int, err error) = SYS_PREADV2
|
||||
//sys pwritev2Syscall(fd int, iovs []Iovec, offs_l uintptr, offs_h uintptr, flags int) (n int, err error) = SYS_PWRITEV2
|
||||
|
||||
// offs2lohi splits offs into its low and high order bits.
|
||||
func offs2lohi(offs int64) (lo, hi uintptr) {
|
||||
@ -2184,69 +2161,23 @@ func offs2lohi(offs int64) (lo, hi uintptr) {
|
||||
return uintptr(offs), uintptr(uint64(offs) >> (longBits - 1) >> 1) // two shifts to avoid false positive in vet
|
||||
}
|
||||
|
||||
func Readv(fd int, iovs [][]byte) (n int, err error) {
|
||||
iovecs := make([]Iovec, 0, minIovec)
|
||||
iovecs = appendBytes(iovecs, iovs)
|
||||
n, err = readv(fd, iovecs)
|
||||
readvRacedetect(iovecs, n, err)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func Preadv(fd int, iovs [][]byte, offset int64) (n int, err error) {
|
||||
iovecs := make([]Iovec, 0, minIovec)
|
||||
iovecs = appendBytes(iovecs, iovs)
|
||||
func preadv(fd int, iovecs []Iovec, offset int64) (n int, err error) {
|
||||
lo, hi := offs2lohi(offset)
|
||||
n, err = preadv(fd, iovecs, lo, hi)
|
||||
readvRacedetect(iovecs, n, err)
|
||||
return n, err
|
||||
return preadvSyscall(fd, iovecs, lo, hi)
|
||||
}
|
||||
|
||||
func Preadv2(fd int, iovs [][]byte, offset int64, flags int) (n int, err error) {
|
||||
iovecs := make([]Iovec, 0, minIovec)
|
||||
iovecs = appendBytes(iovecs, iovs)
|
||||
lo, hi := offs2lohi(offset)
|
||||
n, err = preadv2(fd, iovecs, lo, hi, flags)
|
||||
readvRacedetect(iovecs, n, err)
|
||||
n, err = preadv2Syscall(fd, iovecs, lo, hi, flags)
|
||||
readvRaceDetect(iovecs, n, err)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func readvRacedetect(iovecs []Iovec, n int, err error) {
|
||||
if !raceenabled {
|
||||
return
|
||||
}
|
||||
for i := 0; n > 0 && i < len(iovecs); i++ {
|
||||
m := min(int(iovecs[i].Len), n)
|
||||
n -= m
|
||||
if m > 0 {
|
||||
raceWriteRange(unsafe.Pointer(iovecs[i].Base), m)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
raceAcquire(unsafe.Pointer(&ioSync))
|
||||
}
|
||||
}
|
||||
|
||||
func Writev(fd int, iovs [][]byte) (n int, err error) {
|
||||
iovecs := make([]Iovec, 0, minIovec)
|
||||
iovecs = appendBytes(iovecs, iovs)
|
||||
if raceenabled {
|
||||
raceReleaseMerge(unsafe.Pointer(&ioSync))
|
||||
}
|
||||
n, err = writev(fd, iovecs)
|
||||
writevRacedetect(iovecs, n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func Pwritev(fd int, iovs [][]byte, offset int64) (n int, err error) {
|
||||
iovecs := make([]Iovec, 0, minIovec)
|
||||
iovecs = appendBytes(iovecs, iovs)
|
||||
if raceenabled {
|
||||
raceReleaseMerge(unsafe.Pointer(&ioSync))
|
||||
}
|
||||
func pwritev(fd int, iovecs []Iovec, offset int64) (n int, err error) {
|
||||
lo, hi := offs2lohi(offset)
|
||||
n, err = pwritev(fd, iovecs, lo, hi)
|
||||
writevRacedetect(iovecs, n)
|
||||
return n, err
|
||||
return pwritevSyscall(fd, iovecs, lo, hi)
|
||||
}
|
||||
|
||||
func Pwritev2(fd int, iovs [][]byte, offset int64, flags int) (n int, err error) {
|
||||
@ -2256,24 +2187,11 @@ func Pwritev2(fd int, iovs [][]byte, offset int64, flags int) (n int, err error)
|
||||
raceReleaseMerge(unsafe.Pointer(&ioSync))
|
||||
}
|
||||
lo, hi := offs2lohi(offset)
|
||||
n, err = pwritev2(fd, iovecs, lo, hi, flags)
|
||||
writevRacedetect(iovecs, n)
|
||||
n, err = pwritev2Syscall(fd, iovecs, lo, hi, flags)
|
||||
writevRaceDetect(iovecs, n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func writevRacedetect(iovecs []Iovec, n int) {
|
||||
if !raceenabled {
|
||||
return
|
||||
}
|
||||
for i := 0; n > 0 && i < len(iovecs); i++ {
|
||||
m := min(int(iovecs[i].Len), n)
|
||||
n -= m
|
||||
if m > 0 {
|
||||
raceReadRange(unsafe.Pointer(iovecs[i].Base), m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mmap varies by architecture; see syscall_linux_*.go.
|
||||
//sys munmap(addr uintptr, length uintptr) (err error)
|
||||
//sys mremap(oldaddr uintptr, oldlength uintptr, newlength uintptr, flags int, newaddr uintptr) (xaddr uintptr, err error)
|
||||
@ -2644,8 +2562,12 @@ func SchedGetAttr(pid int, flags uint) (*SchedAttr, error) {
|
||||
//sys Cachestat(fd uint, crange *CachestatRange, cstat *Cachestat_t, flags uint) (err error)
|
||||
//sys Mseal(b []byte, flags uint) (err error)
|
||||
|
||||
//sys setMemPolicy(mode int, mask *CPUSet, size int) (err error) = SYS_SET_MEMPOLICY
|
||||
//sys setMemPolicy(mode int, mask unsafe.Pointer, size uintptr) (err error) = SYS_SET_MEMPOLICY
|
||||
|
||||
func SetMemPolicy(mode int, mask *CPUSet) error {
|
||||
return setMemPolicy(mode, mask, _CPU_SETSIZE)
|
||||
return setMemPolicy(mode, unsafe.Pointer(mask), _CPU_SETSIZE)
|
||||
}
|
||||
|
||||
func SetMemPolicyDynamic(mode int, mask CPUSetDynamic) error {
|
||||
return setMemPolicy(mode, mask.pointer(), mask.size())
|
||||
}
|
||||
|
||||
3
vendor/golang.org/x/sys/unix/syscall_linux_arm.go
generated
vendored
3
vendor/golang.org/x/sys/unix/syscall_linux_arm.go
generated
vendored
@ -82,6 +82,9 @@ func Time(t *Time_t) (Time_t, error) {
|
||||
}
|
||||
|
||||
func Utime(path string, buf *Utimbuf) error {
|
||||
if buf == nil {
|
||||
return Utimes(path, nil)
|
||||
}
|
||||
tv := []Timeval{
|
||||
{Sec: buf.Actime},
|
||||
{Sec: buf.Modtime},
|
||||
|
||||
3
vendor/golang.org/x/sys/unix/syscall_linux_arm64.go
generated
vendored
3
vendor/golang.org/x/sys/unix/syscall_linux_arm64.go
generated
vendored
@ -113,6 +113,9 @@ func Time(t *Time_t) (Time_t, error) {
|
||||
}
|
||||
|
||||
func Utime(path string, buf *Utimbuf) error {
|
||||
if buf == nil {
|
||||
return Utimes(path, nil)
|
||||
}
|
||||
tv := []Timeval{
|
||||
{Sec: buf.Actime},
|
||||
{Sec: buf.Modtime},
|
||||
|
||||
3
vendor/golang.org/x/sys/unix/syscall_linux_loong64.go
generated
vendored
3
vendor/golang.org/x/sys/unix/syscall_linux_loong64.go
generated
vendored
@ -150,6 +150,9 @@ func Time(t *Time_t) (Time_t, error) {
|
||||
}
|
||||
|
||||
func Utime(path string, buf *Utimbuf) error {
|
||||
if buf == nil {
|
||||
return Utimes(path, nil)
|
||||
}
|
||||
tv := []Timeval{
|
||||
{Sec: buf.Actime},
|
||||
{Sec: buf.Modtime},
|
||||
|
||||
3
vendor/golang.org/x/sys/unix/syscall_linux_riscv64.go
generated
vendored
3
vendor/golang.org/x/sys/unix/syscall_linux_riscv64.go
generated
vendored
@ -112,6 +112,9 @@ func Time(t *Time_t) (Time_t, error) {
|
||||
}
|
||||
|
||||
func Utime(path string, buf *Utimbuf) error {
|
||||
if buf == nil {
|
||||
return Utimes(path, nil)
|
||||
}
|
||||
tv := []Timeval{
|
||||
{Sec: buf.Actime},
|
||||
{Sec: buf.Modtime},
|
||||
|
||||
4
vendor/golang.org/x/sys/unix/syscall_openbsd.go
generated
vendored
4
vendor/golang.org/x/sys/unix/syscall_openbsd.go
generated
vendored
@ -300,6 +300,10 @@ func Uname(uname *Utsname) error {
|
||||
//sys Pathconf(path string, name int) (val int, err error)
|
||||
//sys pread(fd int, p []byte, offset int64) (n int, err error)
|
||||
//sys pwrite(fd int, p []byte, offset int64) (n int, err error)
|
||||
//sys readv(fd int, iovecs []Iovec) (n int, err error)
|
||||
//sys writev(fd int, iovecs []Iovec) (n int, err error)
|
||||
//sys preadv(fd int, iovecs []Iovec, offset int64) (n int, err error)
|
||||
//sys pwritev(fd int, iovecs []Iovec, offset int64) (n int, err error)
|
||||
//sys read(fd int, p []byte) (n int, err error)
|
||||
//sys Readlink(path string, buf []byte) (n int, err error)
|
||||
//sys Readlinkat(dirfd int, path string, buf []byte) (n int, err error)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user