forked from toolshed/abra
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6d6887127
|
+8
-13
@@ -3,22 +3,17 @@ kind: pipeline
|
||||
name: coopcloud.tech/abra
|
||||
steps:
|
||||
- name: make check
|
||||
image: golang:1.27
|
||||
image: golang:1.26
|
||||
commands:
|
||||
- make check
|
||||
|
||||
- name: xgettext-go
|
||||
image: golang:1.27
|
||||
environment:
|
||||
GOPRIVATE: coopcloud.tech
|
||||
commands:
|
||||
- go run git.coopcloud.tech/toolshed/xgettext-go@latest
|
||||
-o pkg/i18n/locales/abra.pot
|
||||
--keyword=i18n.G
|
||||
--keyword-ctx=i18n.GC
|
||||
--sort-output
|
||||
--add-comments-tag="translators"
|
||||
$(find . -name "*.go" -not -path "*vendor*" | sort)
|
||||
image: git.coopcloud.tech/toolshed/drone-xgettext-go:latest
|
||||
settings:
|
||||
keyword: i18n.G
|
||||
keyword_ctx: i18n.GC
|
||||
out: pkg/i18n/locales/abra.pot
|
||||
comments_tag: translators
|
||||
depends_on:
|
||||
- make check
|
||||
when:
|
||||
@@ -43,7 +38,7 @@ steps:
|
||||
- tag
|
||||
|
||||
- name: make test
|
||||
image: golang:1.27
|
||||
image: golang:1.26
|
||||
environment:
|
||||
ABRA_DIR: /root/.abra_test
|
||||
commands:
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
vendor/** linguist-generated=true
|
||||
@@ -7,4 +7,3 @@
|
||||
/bin
|
||||
dist/
|
||||
tests/integration/.bats
|
||||
tests/resources/path_to_repo
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
# Build image
|
||||
FROM golang:1.27-alpine AS build
|
||||
FROM golang:1.26-alpine AS build
|
||||
|
||||
ENV GOPRIVATE=coopcloud.tech
|
||||
|
||||
@@ -15,7 +15,7 @@ WORKDIR /app
|
||||
|
||||
RUN CGO_ENABLED=0 make build
|
||||
|
||||
FROM alpine:3.24
|
||||
FROM alpine:3.22
|
||||
|
||||
RUN apk add --no-cache \
|
||||
ca-certificates \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
ABRA := ./cmd/abra
|
||||
XGETTEXT := go run git.coopcloud.tech/toolshed/xgettext-go@latest
|
||||
XGETTEXT := ./bin/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) \
|
||||
update-pot: $(XGETTEXT)
|
||||
@${XGETTEXT} \
|
||||
-o pkg/i18n/locales/$(DOMAIN).pot \
|
||||
--keyword=i18n.G \
|
||||
--keyword-ctx=i18n.GC \
|
||||
@@ -71,6 +71,11 @@ update-pot:
|
||||
--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: update-pot-po-metadata
|
||||
update-pot-po-metadata:
|
||||
@sed -i "s/charset=CHARSET/charset=UTF-8/g" pkg/i18n/locales/*.po pkg/i18n/locales/*.pot
|
||||
@@ -84,53 +89,3 @@ build-mo:
|
||||
|
||||
release:
|
||||
@goreleaser release --clean
|
||||
|
||||
CLIENT_VM := abra-client
|
||||
vm-client-status:
|
||||
sudo systemctl status microvm@$(CLIENT_VM).service
|
||||
vm-client-create:test-integration-hosts
|
||||
sudo microvm -f git+file://$$(pwd) -c $(CLIENT_VM)
|
||||
vm-client-start:
|
||||
sudo systemctl start microvm@$(CLIENT_VM).service
|
||||
vm-client-update:test-integration-hosts
|
||||
sudo microvm -R -f git+file://$$(pwd) -u $(CLIENT_VM)
|
||||
vm-client-run:
|
||||
sudo microvm -f git+file://$$(pwd) -r $(CLIENT_VM)
|
||||
vm-client-stop:
|
||||
sudo systemctl stop microvm@$(CLIENT_VM)
|
||||
vm-client-delete: vm-client-stop
|
||||
sudo rm -rf /var/lib/microvms/$(CLIENT_VM)
|
||||
vm-client-connect:
|
||||
ssh abra@10.0.0.2 -i ./tests/resources/local_integration_ssh
|
||||
|
||||
SERVER_VM := abra-server
|
||||
vm-server-status:
|
||||
sudo systemctl status microvm@$(SERVER_VM).service
|
||||
vm-server-create:
|
||||
sudo microvm -f git+file://$$(pwd) -c $(SERVER_VM)
|
||||
vm-server-start:
|
||||
sudo systemctl start microvm@$(SERVER_VM).service
|
||||
vm-server-update:
|
||||
sudo microvm -R -f git+file://$$(pwd) -u $(SERVER_VM)
|
||||
vm-server-run:
|
||||
sudo microvm -f git+file://$$(pwd) -r $(SERVER_VM)
|
||||
vm-server-stop:
|
||||
sudo systemctl stop microvm@$(SERVER_VM)
|
||||
vm-server-delete: vm-server-stop
|
||||
sudo rm -rf /var/lib/microvms/$(SERVER_VM)
|
||||
vm-server-connect:
|
||||
ssh abra@10.0.0.3 -i ./tests/resources/local_integration_ssh
|
||||
|
||||
test-integration-hosts:
|
||||
./scripts/tests/extra_hosts
|
||||
|
||||
# TEST_INTEGRATION_FILES overrides the default of running all test files
|
||||
ifdef TEST_INTEGRATION_FILES
|
||||
test_files := $(TEST_INTEGRATION_FILES);
|
||||
else
|
||||
test_files := *;
|
||||
endif
|
||||
test-integration: build
|
||||
chmod 600 ./tests/resources/local_integration_ssh
|
||||
ssh-add ./tests/resources/local_integration_ssh
|
||||
@bats -Tp --verbose-run tests/integration/$(test_files)
|
||||
|
||||
+5
-6
@@ -151,12 +151,11 @@ 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,
|
||||
SendRegistryAuth: true,
|
||||
Composefiles: composeFiles,
|
||||
Namespace: stackName,
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
Detach: false,
|
||||
}
|
||||
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||
if err != nil {
|
||||
|
||||
+1
-5
@@ -112,11 +112,7 @@ var AppNewCommand = &cobra.Command{
|
||||
}
|
||||
}
|
||||
|
||||
if recipeVersion != "" {
|
||||
if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else if len(recipeVersions) > 0 {
|
||||
if len(recipeVersions) > 0 {
|
||||
latest := recipeVersions[len(recipeVersions)-1]
|
||||
for tag := range latest {
|
||||
recipeVersion = tag
|
||||
|
||||
+5
-6
@@ -166,12 +166,11 @@ beforehand. See "abra app backup" for more.`),
|
||||
|
||||
stackName := app.StackName()
|
||||
deployOpts := stack.Deploy{
|
||||
Composefiles: composeFiles,
|
||||
Namespace: stackName,
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
Detach: false,
|
||||
SendRegistryAuth: true,
|
||||
Composefiles: composeFiles,
|
||||
Namespace: stackName,
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
Detach: false,
|
||||
}
|
||||
|
||||
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||
|
||||
+5
-6
@@ -178,12 +178,11 @@ beforehand. See "abra app backup" for more.`),
|
||||
|
||||
stackName := app.StackName()
|
||||
deployOpts := stack.Deploy{
|
||||
Composefiles: composeFiles,
|
||||
Namespace: stackName,
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
Detach: false,
|
||||
SendRegistryAuth: true,
|
||||
Composefiles: composeFiles,
|
||||
Namespace: stackName,
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
Detach: false,
|
||||
}
|
||||
|
||||
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
||||
|
||||
@@ -19,6 +19,5 @@ var (
|
||||
Minor bool
|
||||
NoDomainChecks bool
|
||||
Patch bool
|
||||
PinDigests bool
|
||||
ShowUnchanged bool
|
||||
)
|
||||
|
||||
@@ -92,7 +92,7 @@ your private key and enter your passphrase beforehand.
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
mainAppVersion := formatter.RemoveSha(imagesTmp[mainApp])
|
||||
mainAppVersion := imagesTmp[mainApp]
|
||||
if mainAppVersion == "" {
|
||||
log.Fatal(i18n.G("main app service version for %s is empty?", recipe.Name))
|
||||
}
|
||||
@@ -192,9 +192,6 @@ likely to change.
|
||||
serviceMeta := serviceVersions[serviceName]
|
||||
|
||||
existingImageTag := fmt.Sprintf("%s:%s", serviceMeta.Image, serviceMeta.Tag)
|
||||
if serviceMeta.Digest != "" {
|
||||
existingImageTag += "@" + serviceMeta.Digest
|
||||
}
|
||||
newImageTag := fmt.Sprintf("%s:%s", serviceMeta.Image, imagesTmp[serviceMeta.Image])
|
||||
|
||||
if existingImageTag == newImageTag {
|
||||
@@ -334,8 +331,10 @@ func GetImageVersions(recipe recipePkg.Recipe) (map[string]string, error) {
|
||||
|
||||
path = formatter.StripTagMeta(path)
|
||||
|
||||
var tag string
|
||||
switch img.(type) {
|
||||
case reference.NamedTagged:
|
||||
tag = img.(reference.NamedTagged).Tag()
|
||||
case reference.Named:
|
||||
if service.Name == "app" {
|
||||
missingTag = true
|
||||
@@ -343,7 +342,7 @@ func GetImageVersions(recipe recipePkg.Recipe) (map[string]string, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
services[path] = formatter.GetTagAndDigest(img)
|
||||
services[path] = tag
|
||||
}
|
||||
|
||||
if missingTag {
|
||||
|
||||
+8
-42
@@ -130,7 +130,7 @@ interface.`),
|
||||
}
|
||||
|
||||
for _, service := range config.Services {
|
||||
img, err := reference.ParseNormalizedNamed(formatter.RemoveSha(service.Image))
|
||||
img, err := reference.ParseNormalizedNamed(service.Image)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -178,14 +178,9 @@ interface.`),
|
||||
|
||||
sort.Sort(tagcmp.ByTagDesc(compatible))
|
||||
|
||||
var upgradeTag string
|
||||
if len(compatible) == 0 && !allTags {
|
||||
log.Info(i18n.G("no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag))
|
||||
|
||||
if !shouldPin(service.Image, internal.PinDigests) {
|
||||
continue
|
||||
}
|
||||
upgradeTag = tag.String()
|
||||
continue // skip on to the next tag and don't update any compose files
|
||||
}
|
||||
|
||||
catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name, internal.Offline)
|
||||
@@ -208,8 +203,9 @@ interface.`),
|
||||
|
||||
log.Debug(i18n.G("detected compatible upgradable tags %s for %s", compatibleStrings, service.Name))
|
||||
|
||||
var upgradeTag string
|
||||
_, ok := servicePins[service.Name]
|
||||
if upgradeTag == "" && versionsPresent && ok {
|
||||
if versionsPresent && ok {
|
||||
pinnedTag := servicePins[service.Name].version
|
||||
if tag.IsLessThan(pinnedTag) {
|
||||
pinnedTagString := pinnedTag.String()
|
||||
@@ -231,7 +227,7 @@ interface.`),
|
||||
log.Fatal(i18n.G("service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String()))
|
||||
continue
|
||||
}
|
||||
} else if upgradeTag == "" {
|
||||
} else {
|
||||
if bumpType != 0 {
|
||||
for _, upTag := range compatible {
|
||||
upElement, err := tag.UpgradeDelta(upTag)
|
||||
@@ -246,10 +242,7 @@ interface.`),
|
||||
}
|
||||
if upgradeTag == "" {
|
||||
log.Warn(i18n.G("not upgrading from %s to %s for %s, because the upgrade type is more serious than what user wants", tag.String(), compatible[0].String(), image))
|
||||
if !shouldPin(service.Image, internal.PinDigests) {
|
||||
continue
|
||||
}
|
||||
upgradeTag = tag.String()
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
msg := i18n.G("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag)
|
||||
@@ -300,25 +293,12 @@ interface.`),
|
||||
}
|
||||
}
|
||||
if upgradeTag != "skip" {
|
||||
var resolvedDigest string
|
||||
if shouldPin(service.Image, internal.PinDigests) {
|
||||
resolvedDigest, err = client.GetImageMultiplatformDigest(img, upgradeTag)
|
||||
if err != nil {
|
||||
log.Fatal(i18n.G("failed to resolve digest for %s:%s: %s", image, upgradeTag, err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
ok, err := recipe.UpdateTag(service.Image, upgradeTag, resolvedDigest)
|
||||
|
||||
ok, err := recipe.UpdateTag(image, upgradeTag)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if ok {
|
||||
if shouldPin(service.Image, internal.PinDigests) {
|
||||
log.Info(i18n.G("tag upgraded and pinned from %s to %s@%s for %s", tag.String(), upgradeTag, resolvedDigest, image))
|
||||
} else {
|
||||
log.Info(i18n.G("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image))
|
||||
}
|
||||
log.Info(i18n.G("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image))
|
||||
}
|
||||
} else {
|
||||
if !internal.NoInput {
|
||||
@@ -384,12 +364,6 @@ interface.`),
|
||||
},
|
||||
}
|
||||
|
||||
// shouldPin reports whether a service image should be pinned to a digest.
|
||||
// An image that is already pinned stays pinned, even without --pindigests.
|
||||
func shouldPin(image string, pinDigestsFlag bool) bool {
|
||||
return pinDigestsFlag || strings.Contains(image, "@sha256:")
|
||||
}
|
||||
|
||||
var (
|
||||
allTags bool
|
||||
createCommit bool
|
||||
@@ -443,12 +417,4 @@ func init() {
|
||||
false,
|
||||
i18n.G("commit changes"),
|
||||
)
|
||||
|
||||
RecipeUpgradeCommand.Flags().BoolVarP(
|
||||
&internal.PinDigests,
|
||||
i18n.G("pindigests"),
|
||||
i18n.GC("p", "pin container image by digest"),
|
||||
true,
|
||||
i18n.G("pin the container image version by manifest digest (image:tag@sha256:...)"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package recipe
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestShouldPin(t *testing.T) {
|
||||
tests := []struct {
|
||||
image string
|
||||
pinDigests bool
|
||||
want bool
|
||||
}{
|
||||
{"nginx:1.21.0", false, false},
|
||||
{"nginx:1.21.0", true, true},
|
||||
{"nginx:1.21.0@sha256:2bcabc23b45489fb0885d69a06ba1d648aeda973fae7bb981bafbb884165e514", false, true},
|
||||
{"nginx:1.21.0@sha256:2bcabc23b45489fb0885d69a06ba1d648aeda973fae7bb981bafbb884165e514", true, true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if got := shouldPin(test.image, test.pinDigests); got != test.want {
|
||||
t.Errorf("shouldPin(%q, %v) = %v, want %v", test.image, test.pinDigests, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
-99
@@ -1,99 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"microvm": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"spectrum": "spectrum"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1784666190,
|
||||
"narHash": "sha256-xgfS6slV7J3baMooNN1UuBi51RIgg9y0DbCxfSA0668=",
|
||||
"owner": "astro",
|
||||
"repo": "microvm.nix",
|
||||
"rev": "fa5340ac684cdce8a22b6d4a0bcebb0cc999275e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "astro",
|
||||
"repo": "microvm.nix",
|
||||
"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",
|
||||
"microvm": "microvm",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"spectrum": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1783694892,
|
||||
"narHash": "sha256-xO8f7Qng+18FK2UlB9vcrkxCaQMCt5WjCH24aW/11eg=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "24c4346e30fdea8d8e80f34aec3554a15a667d24",
|
||||
"revCount": 1410,
|
||||
"type": "git",
|
||||
"url": "https://spectrum-os.org/git/spectrum"
|
||||
},
|
||||
"original": {
|
||||
"type": "git",
|
||||
"url": "https://spectrum-os.org/git/spectrum"
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
{
|
||||
description = "The Co-op Cloud utility belt";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
microvm = {
|
||||
url = "github:astro/microvm.nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
microvm,
|
||||
flake-utils,
|
||||
}:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
publicKey = builtins.readFile ./tests/resources/local_integration_ssh.pub;
|
||||
privateKey = builtins.readFile ./tests/resources/local_integration_ssh;
|
||||
pathToRepo = builtins.readFile ./tests/resources/path_to_repo;
|
||||
# local DNS aliases for the server host
|
||||
extraHosts = builtins.readFile ./tests/resources/extra_hosts;
|
||||
username = "abra";
|
||||
password = "abra";
|
||||
defaultDNS = [
|
||||
# Quad9.net
|
||||
"9.9.9.9"
|
||||
"149.112.112.112"
|
||||
"2620:fe::fe"
|
||||
"2620:fe::9"
|
||||
];
|
||||
in
|
||||
{
|
||||
nixosConfigurations = {
|
||||
abra-client = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
modules = [
|
||||
microvm.nixosModules.microvm
|
||||
./nix/hosts/client/configuration.nix
|
||||
];
|
||||
specialArgs = {
|
||||
inherit
|
||||
publicKey
|
||||
privateKey
|
||||
username
|
||||
password
|
||||
defaultDNS
|
||||
extraHosts
|
||||
pathToRepo
|
||||
;
|
||||
};
|
||||
};
|
||||
abra-server = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
modules = [
|
||||
microvm.nixosModules.microvm
|
||||
./nix/hosts/server/configuration.nix
|
||||
];
|
||||
specialArgs = {
|
||||
inherit
|
||||
publicKey
|
||||
privateKey
|
||||
username
|
||||
password
|
||||
defaultDNS
|
||||
;
|
||||
};
|
||||
};
|
||||
};
|
||||
nixosModules = rec {
|
||||
host = {
|
||||
imports = [
|
||||
microvm.nixosModules.host
|
||||
./nix/modules/host.nix
|
||||
];
|
||||
};
|
||||
default = host;
|
||||
};
|
||||
packages = rec {
|
||||
abra = pkgs.callPackage ./nix/package.nix { };
|
||||
default = abra;
|
||||
};
|
||||
apps = rec {
|
||||
abra = flake-utils.lib.mkApp { drv = self.packages.${system}.abra; };
|
||||
default = abra;
|
||||
};
|
||||
devShells.${system}.default = pkgs.mkShell {
|
||||
# testing env variables
|
||||
BATS_LIB_PATH = "~/.local/share/bats/";
|
||||
TEST_SERVER = "abra.local";
|
||||
ABRA_DIR = "$HOME/.abra_test";
|
||||
|
||||
packages = with pkgs; [
|
||||
go_1_26
|
||||
gnumake
|
||||
gopls
|
||||
];
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,7 @@ module coopcloud.tech/abra
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
coopcloud.tech/tagcmp v0.0.0-20260515102403-c26951b55977
|
||||
coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca
|
||||
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,15 +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.19.3
|
||||
github.com/go-git/go-git/v5 v5.18.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/leonelquinteros/gotext v1.7.2
|
||||
github.com/moby/sys/signal v0.7.1
|
||||
github.com/moby/term v0.5.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/regclient/regclient v0.11.5
|
||||
github.com/schollz/progressbar/v3 v3.19.1
|
||||
golang.org/x/term v0.47.0
|
||||
github.com/schollz/progressbar/v3 v3.19.0
|
||||
golang.org/x/term v0.42.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gotest.tools/v3 v3.5.2
|
||||
)
|
||||
@@ -38,7 +37,7 @@ require (
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.7 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
@@ -51,8 +50,9 @@ require (
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
@@ -60,30 +60,31 @@ require (
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.9.2 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.8.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
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.21 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.1.0 // indirect
|
||||
github.com/moby/go-archive v0.2.0 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
@@ -95,7 +96,8 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/runc v1.1.13 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.1.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.7.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
@@ -104,32 +106,31 @@ require (
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
|
||||
go.opentelemetry.io/otel v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/crypto v0.56.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/net v0.57.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/net v0.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
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect
|
||||
google.golang.org/grpc v1.80.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
@@ -140,7 +141,7 @@ require (
|
||||
github.com/containers/image v3.0.2+incompatible
|
||||
github.com/containers/storage v1.38.2 // indirect
|
||||
github.com/decentral1se/passgen v1.0.1
|
||||
github.com/docker/docker-credential-helpers v0.9.5 // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.6 // indirect
|
||||
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
@@ -150,13 +151,13 @@ require (
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/spf13/cobra v1.10.1
|
||||
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.49.0
|
||||
golang.org/x/sys v0.43.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
|
||||
replace github.com/spf13/cobra => github.com/decentral1se/cobra v1.10.2-i18n
|
||||
|
||||
@@ -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-20260515102403-c26951b55977 h1:J7I0HFjwVAj/kkX6lwSTHmlXDRjQRsdIFNUUqu55ADY=
|
||||
coopcloud.tech/tagcmp v0.0.0-20260515102403-c26951b55977/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
|
||||
coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca h1:gSD53tBAsbIGq4SnFfq+mEep6foekQ2a5ea7b38qkm0=
|
||||
coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca/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=
|
||||
@@ -130,6 +130,7 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
@@ -144,6 +145,8 @@ github.com/charmbracelet/log v1.0.0 h1:HVVVMmfOorfj3BA9i8X8UL69Hoz9lI0PYwXfJvOdR
|
||||
github.com/charmbracelet/log v1.0.0/go.mod h1:uYgY3SmLpwJWxmlrPwXvzVYujxis1vAKRV/0VQB7yWA=
|
||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
||||
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||
@@ -306,9 +309,10 @@ github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW
|
||||
github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
|
||||
github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I=
|
||||
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 h1:MZ8Ifi/jRels9sZrpSccDbUlK++3b2HlBODfv0Bh6x0=
|
||||
github.com/decentral1se/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
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/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=
|
||||
@@ -330,11 +334,15 @@ github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bc
|
||||
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
|
||||
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
|
||||
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
||||
github.com/docker/docker-credential-helpers v0.9.6 h1:cT2PbRPSlnMmNTfT2TDMXRyQ1KMWHG7xoTLBcn1ZNv0=
|
||||
github.com/docker/docker-credential-helpers v0.9.6/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
|
||||
github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
|
||||
@@ -388,12 +396,14 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.9.2 h1:OXFSRyz4g20upsGDJgQG9Bak1l/ZEv8GHVYB52O71sE=
|
||||
github.com/go-git/go-billy/v5 v5.9.2/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
|
||||
github.com/go-git/go-billy/v5 v5.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-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.19.3 h1:qHttbZ+Am7wEjdTpR1XMQ4rl42FK9SIXzZ4o9x7s6M4=
|
||||
github.com/go-git/go-git/v5 v5.19.3/go.mod h1:Ye4C8dVigkqrv9UsB4NMdSLV4yAIkLiIhW61gcOyhl4=
|
||||
github.com/go-git/go-git/v5 v5.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.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM=
|
||||
github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@@ -426,8 +436,6 @@ github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
|
||||
@@ -526,9 +534,12 @@ github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:Fecb
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -585,8 +596,12 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@@ -622,6 +637,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
@@ -629,6 +646,8 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
@@ -653,6 +672,8 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
|
||||
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
|
||||
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
|
||||
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
|
||||
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
||||
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
@@ -698,8 +719,6 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+
|
||||
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/olareg/olareg v0.2.1 h1:RPHGIaqlVWPbKAsOYUj7e2WfEEW5M7F8I6hKMrwd4jU=
|
||||
github.com/olareg/olareg v0.2.1/go.mod h1:dhr8QetC7U7jJ2m93oxDhEEOKCRbPgOK1oGyKfB4QNo=
|
||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -754,13 +773,16 @@ github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuh
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/pjbgf/sha1cd v0.7.0 h1:ZRNPKHj+gfkLBf0KJv/p1Hmz+7bqCT5o311YX0nq+DA=
|
||||
github.com/pjbgf/sha1cd v0.7.0/go.mod h1:pKR5Li+qTCo+ebqITZfwPlJGoCFCvSO00qxjbNtTUFQ=
|
||||
github.com/pjbgf/sha1cd v0.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=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
@@ -796,8 +818,6 @@ github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/regclient/regclient v0.11.5 h1:OHRsXO0F3qHGfa4HEUv+EkMH9NXNcCTBKjNzyC/UhIA=
|
||||
github.com/regclient/regclient v0.11.5/go.mod h1:DZUOfIT14WFTK2Pj4vjd93avy9O4Fdpjrf9ir23TbRE=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
@@ -805,13 +825,15 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
|
||||
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/schollz/progressbar/v3 v3.19.1 h1:iv8BgwOvdML/S3p84uBpy/IMigv4U9594vPZYa2EdrU=
|
||||
github.com/schollz/progressbar/v3 v3.19.1/go.mod h1:LFL7jqimKxfhero4K1eCkUr/6R39AgQeiPCJtlTWIW8=
|
||||
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
|
||||
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
||||
github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
|
||||
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
|
||||
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
|
||||
@@ -859,10 +881,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/sudo-bmitch/oci-digest v0.1.2 h1:are0qzWTsFZGZ3Uvdi9OSztJszSWaab6iqquMEEB7rw=
|
||||
github.com/sudo-bmitch/oci-digest v0.1.2/go.mod h1:SH6l5OIe0islKBZBedjiPOeET/0QwGL+/oYfQt51uQo=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
|
||||
@@ -874,8 +894,6 @@ github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnH
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
@@ -923,24 +941,42 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
|
||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
@@ -952,9 +988,8 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -973,8 +1008,10 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
|
||||
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
|
||||
golang.org/x/crypto v0.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=
|
||||
@@ -985,6 +1022,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=
|
||||
@@ -1050,8 +1089,10 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/net v0.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=
|
||||
@@ -1146,13 +1187,18 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/sys v0.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.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
|
||||
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
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=
|
||||
@@ -1162,8 +1208,10 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/text v0.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=
|
||||
@@ -1269,8 +1317,12 @@ google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7Fc
|
||||
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4 h1:yOzSCGPx+cp5VO7IxvZ9SBFF7j1tZVcNtlHR2iYKtVo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:Q9HWtNeE7tM9npdIsEvqXj1QJIvVoeAV3rtXtS715Cw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@@ -1344,6 +1396,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
"github.com/containers/image/docker"
|
||||
"github.com/containers/image/types"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/regclient/regclient"
|
||||
"github.com/regclient/regclient/types/ref"
|
||||
)
|
||||
|
||||
// GetRegistryTags retrieves all tags of an image from a container registry.
|
||||
@@ -30,33 +28,3 @@ func GetRegistryTags(img reference.Named) ([]string, error) {
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// GetImageDigest resolves the content digest (sha256:...) for a specific
|
||||
// image:tag reference by querying the registry manifest. This gives us a
|
||||
// digest to pin to while keeping support for multi-arch deployments
|
||||
// See: https://docs.docker.com/build/building/multi-platform/#why-multi-platform-builds
|
||||
// and: https://docs.docker.com/dhi/explore/security-concepts/digests/#multi-platform-images-and-manifests
|
||||
func GetImageMultiplatformDigest(img reference.Named, tag string) (string, error) {
|
||||
tagged, err := reference.WithTag(reference.TrimNamed(img), tag)
|
||||
if err != nil {
|
||||
return "", errors.New(i18n.G("failed to parse image %s:%s, saw: %s", img.Name(), tag, err.Error()))
|
||||
}
|
||||
r, err := ref.New(tagged.String())
|
||||
if err != nil {
|
||||
return "", errors.New(i18n.G("failed to parse image %s:%s, saw: %s", img.Name(), tag, err.Error()))
|
||||
}
|
||||
|
||||
rc := regclient.New(regclient.WithDockerCreds())
|
||||
|
||||
m, err := rc.ManifestHead(context.Background(), r)
|
||||
if err != nil {
|
||||
return "", errors.New(i18n.G("failed to fetch manifest for %s:%s, saw: %s", img.Name(), tag, err.Error()))
|
||||
}
|
||||
|
||||
digest := m.GetDescriptor().Digest.String()
|
||||
if digest == "" {
|
||||
return "", errors.New(i18n.G("registry did not return a digest for %s:%s", img.Name(), tag))
|
||||
}
|
||||
|
||||
return digest, nil
|
||||
}
|
||||
|
||||
+2
-6
@@ -171,7 +171,7 @@ func GetImagesForStack(cl *dockerClient.Client, app appPkg.App) (map[string]stri
|
||||
images[imageBaseName] = ""
|
||||
continue
|
||||
}
|
||||
imageTag := formatter.GetTagAndDigest(namedTag)
|
||||
imageTag := namedTag.Tag()
|
||||
|
||||
existingImageVersion, ok := images[imageBaseName]
|
||||
if !ok {
|
||||
@@ -294,7 +294,7 @@ func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *com
|
||||
newImages[imageBaseName] = ""
|
||||
continue
|
||||
}
|
||||
imageTag := formatter.GetTagAndDigest(namedTag)
|
||||
imageTag := namedTag.Tag()
|
||||
|
||||
existingImageVersion, ok := newImages[imageBaseName]
|
||||
if !ok {
|
||||
@@ -312,10 +312,6 @@ func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *com
|
||||
var imageInfo []string
|
||||
for newImageName, newImageVersion := range newImages {
|
||||
if currentVersion, exists := currentImages[newImageName]; exists {
|
||||
// Swarm adds a resolved digest even for unpinned compose images.
|
||||
if !strings.Contains(newImageVersion, "@") {
|
||||
currentVersion = formatter.RemoveSha(currentVersion)
|
||||
}
|
||||
if currentVersion == newImageVersion {
|
||||
if showUnchanged {
|
||||
imageInfo = append(imageInfo, i18n.G("%s: %s (unchanged)", formatter.StripTagMeta(newImageName), newImageVersion))
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/lipgloss/table"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/go-units"
|
||||
"golang.org/x/term"
|
||||
|
||||
@@ -35,24 +34,11 @@ func SmallSHA(hash string) string {
|
||||
return hash[:8]
|
||||
}
|
||||
|
||||
// RemoveSha remove image sha (digest suffix) from a string that are added in some docker outputs
|
||||
// e.g., "image:tag@sha256:..." -> "image:tag"
|
||||
// RemoveSha remove image sha from a string that are added in some docker outputs
|
||||
func RemoveSha(str string) string {
|
||||
return strings.Split(str, "@")[0]
|
||||
}
|
||||
|
||||
// GetTagAndDigest returns the tag and any digest for comparison and display.
|
||||
func GetTagAndDigest(image reference.Named) string {
|
||||
var version string
|
||||
if tagged, ok := image.(reference.NamedTagged); ok {
|
||||
version = tagged.Tag()
|
||||
}
|
||||
if pinned, ok := image.(reference.Digested); ok {
|
||||
version += "@" + pinned.Digest().String()
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
// HumanDuration from docker/cli RunningFor() to be accessible outside of the class
|
||||
func HumanDuration(timestamp int64) string {
|
||||
date := time.Unix(timestamp, 0)
|
||||
@@ -236,8 +222,7 @@ func CreateProgressbar(length int, title string) *progressbar.ProgressBar {
|
||||
)
|
||||
}
|
||||
|
||||
// StripTagMeta strips front-matter image tag data that we don't need for parsing,
|
||||
// like registry host and library prefix.
|
||||
// StripTagMeta strips front-matter image tag data that we don't need for parsing.
|
||||
func StripTagMeta(image string) string {
|
||||
originalImage := image
|
||||
|
||||
|
||||
@@ -3,35 +3,9 @@ package formatter
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRemoveSha(t *testing.T) {
|
||||
assert.Equal(t, "nginx:latest", RemoveSha("nginx:latest@sha256:1234567890abcdef"))
|
||||
assert.Equal(t, "nginx:latest", RemoveSha("nginx:latest"))
|
||||
assert.Equal(t, "my-repo/my-image:v1", RemoveSha("my-repo/my-image:v1@sha256:abcdef1234567890"))
|
||||
assert.Equal(t, "my-repo/my-image", RemoveSha("my-repo/my-image@sha256:abcdef1234567890"))
|
||||
}
|
||||
|
||||
func TestBoldDirtyDefault(t *testing.T) {
|
||||
assert.Equal(t, "foo", BoldDirtyDefault("foo"))
|
||||
}
|
||||
|
||||
func TestGetTagAndDigest(t *testing.T) {
|
||||
digest := "sha256:908dc23e643a1447dbfb2e189ed268bfde6a51a5bf9a34d3dd3440a24f58ccf7"
|
||||
for _, tc := range []struct{ image, want string }{
|
||||
{"nginx:1.21.6", "1.21.6"},
|
||||
{"ghcr.io/foo/bar:1.0@" + digest, "1.0@" + digest},
|
||||
{"nginx@" + digest, "@" + digest},
|
||||
{"nginx", ""},
|
||||
} {
|
||||
img, err := reference.ParseNormalizedNamed(tc.image)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := GetTagAndDigest(img); got != tc.want {
|
||||
t.Errorf("%s: got %s, want %s", tc.image, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ func Clone(dir, url string) error {
|
||||
URL: url,
|
||||
Tags: git.AllTags,
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/main"),
|
||||
SingleBranch: true,
|
||||
})
|
||||
|
||||
if err != nil && gitCloneIgnoreErr(err) {
|
||||
@@ -70,6 +71,7 @@ func Clone(dir, url string) error {
|
||||
URL: url,
|
||||
Tags: git.AllTags,
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
|
||||
SingleBranch: true,
|
||||
})
|
||||
|
||||
if err != nil && gitCloneIgnoreErr(err) {
|
||||
|
||||
+212
-278
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+478
-901
File diff suppressed because it is too large
Load Diff
@@ -125,13 +125,6 @@ var LintRules = map[string][]LintRule{
|
||||
HowToResolve: i18n.G("reduce length of secret names to 12 chars"),
|
||||
Function: LintSecretLengths,
|
||||
},
|
||||
{
|
||||
Ref: "R016",
|
||||
Level: i18n.G("warn"),
|
||||
Description: i18n.G("all images use a digest"),
|
||||
HowToResolve: i18n.G("use a digest for all images"),
|
||||
Function: LintAllImagesTaggedWithDigest,
|
||||
},
|
||||
},
|
||||
"error": {
|
||||
{
|
||||
@@ -340,28 +333,6 @@ func LintAllImagesTagged(recipe recipe.Recipe) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func LintAllImagesTaggedWithDigest(recipe recipe.Recipe) (bool, error) {
|
||||
config, err := recipe.GetComposeConfig(nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, service := range config.Services {
|
||||
img, err := reference.ParseNormalizedNamed(service.Image)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if reference.IsNameOnly(img) {
|
||||
return false, nil
|
||||
}
|
||||
_, ok := img.(reference.Digested)
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func LintNoUnstableTags(recipe recipe.Recipe) (bool, error) {
|
||||
config, err := recipe.GetComposeConfig(nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// Use the test recipe at the given commit, in a temporary recipes directory.
|
||||
func setupTestRecipe(t *testing.T, commit string) Recipe {
|
||||
t.Helper()
|
||||
// Other tests in this package use the real RECIPES_DIR.
|
||||
recipesDir := config.RECIPES_DIR
|
||||
config.RECIPES_DIR = t.TempDir()
|
||||
t.Cleanup(func() { config.RECIPES_DIR = recipesDir })
|
||||
|
||||
r := Get("abra-test-recipe")
|
||||
if err := gitPkg.Clone(r.Dir, "https://git.coopcloud.tech/toolshed/abra-test-recipe"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := r.EnsureVersion(commit); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func TestGetRecipeVersionsIncludesDigest(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping e2e test in short mode")
|
||||
}
|
||||
// https://git.coopcloud.tech/toolshed/abra-test-recipe/commit/4f0642719d2e2a568f4350cf7eea182438d58be3
|
||||
r := setupTestRecipe(t, "4f0642719d2e2a568f4350cf7eea182438d58be3")
|
||||
repo, err := git.PlainOpen(r.Dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for version, commit := range map[string]string{
|
||||
"9.0.0+1.21.5": "4f0642719d2e2a568f4350cf7eea182438d58be3",
|
||||
"9.0.1+1.21.6": "77d752b3c61a71bfb2c45f40ec07c9d626a0e580",
|
||||
} {
|
||||
if _, err := repo.CreateTag(version, plumbing.NewHash(commit), nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
versions, _, err := r.GetRecipeVersions()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := map[string]ServiceMeta{}
|
||||
for _, version := range versions {
|
||||
for tag, services := range version {
|
||||
if strings.HasPrefix(tag, "9.0.") {
|
||||
found[tag] = services["app"]
|
||||
}
|
||||
}
|
||||
}
|
||||
pinned := found["9.0.0+1.21.5"]
|
||||
if pinned.Tag != "1.21.5" || pinned.Digest != "sha256:0d17b565c37bcbd895e9d92315a05c1c3c9a29f762b011a10c54a66cd53c9b31" {
|
||||
t.Fatalf("got pinned metadata %+v", pinned)
|
||||
}
|
||||
unpinned := found["9.0.1+1.21.6"]
|
||||
if unpinned.Tag != "1.21.6" || unpinned.Digest != "" {
|
||||
t.Fatalf("got unpinned metadata %+v", unpinned)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
meta ServiceMeta
|
||||
wantDigest bool
|
||||
}{{pinned, true}, {unpinned, false}} {
|
||||
encoded, err := json.Marshal(tc.meta)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(encoded), `"digest"`) != tc.wantDigest {
|
||||
t.Fatalf("unexpected JSON %s", encoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-42
@@ -6,7 +6,6 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
@@ -111,23 +110,10 @@ func (r Recipe) GetVersionLabelLocal() (string, error) {
|
||||
return label, nil
|
||||
}
|
||||
|
||||
var sha256Digest = regexp.MustCompile(`^sha256:[a-f0-9]{64}$`)
|
||||
|
||||
// UpdateTag updates an image tag in-place on file system local compose files.
|
||||
// A non-empty digest pins the tag (image:tag@sha256:...).
|
||||
func (r Recipe) UpdateTag(image, tag, digest string) (bool, error) {
|
||||
// Optional parameter, but must be a valid sha256 digest if provided.
|
||||
if digest != "" && !sha256Digest.MatchString(digest) {
|
||||
return false, errors.New(i18n.G("invalid digest %s, want sha256:<64 hex digits>", digest))
|
||||
}
|
||||
|
||||
func (r Recipe) UpdateTag(image, tag string) (bool, error) {
|
||||
fullPattern := fmt.Sprintf("%s/compose**yml", r.Dir)
|
||||
source, parseErr := reference.ParseNormalizedNamed(image)
|
||||
_, exactImage := source.(reference.NamedTagged)
|
||||
if parseErr != nil {
|
||||
return false, parseErr
|
||||
}
|
||||
imageName := formatter.StripTagMeta(reference.Path(source))
|
||||
image = formatter.StripTagMeta(image)
|
||||
|
||||
composeFiles, err := filepath.Glob(fullPattern)
|
||||
if err != nil {
|
||||
@@ -136,7 +122,6 @@ func (r Recipe) UpdateTag(image, tag, digest string) (bool, error) {
|
||||
|
||||
log.Debug(i18n.G("considering %s config(s) for tag update", strings.Join(composeFiles, ", ")))
|
||||
|
||||
updated := false
|
||||
for _, composeFile := range composeFiles {
|
||||
opts := stack.Deploy{Composefiles: []string{composeFile}}
|
||||
|
||||
@@ -155,58 +140,44 @@ func (r Recipe) UpdateTag(image, tag, digest string) (bool, error) {
|
||||
continue // may be a compose.$optional.yml file
|
||||
}
|
||||
|
||||
img, err := reference.ParseNormalizedNamed(formatter.RemoveSha(service.Image))
|
||||
img, _ := reference.ParseNormalizedNamed(service.Image)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var composeTag string
|
||||
switch img.(type) {
|
||||
case reference.NamedTagged:
|
||||
composeTag = img.(reference.NamedTagged).Tag()
|
||||
default:
|
||||
log.Debug(i18n.G("unable to parse %s, skipping", img))
|
||||
continue
|
||||
}
|
||||
|
||||
// Found image that needs replacing
|
||||
composeImage := formatter.StripTagMeta(reference.Path(img))
|
||||
if imageName == composeImage && (!exactImage || service.Image == image) {
|
||||
|
||||
log.Debug(i18n.G("parsed %s from %s", composeTag, service.Image))
|
||||
|
||||
if image == composeImage {
|
||||
bytes, err := ioutil.ReadFile(composeFile)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
old := service.Image
|
||||
tagged, err := reference.WithTag(reference.TrimNamed(img), tag)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
new := reference.FamiliarString(tagged)
|
||||
if digest != "" {
|
||||
new += "@" + digest
|
||||
}
|
||||
if old == new {
|
||||
continue
|
||||
}
|
||||
|
||||
// Don't confuse digests if the image isn't an exact match. E.g. if old is postgres:13, don't
|
||||
// match postgres:13-alpine
|
||||
pattern := regexp.MustCompile(`(?m)(^\s*image:\s*["']?)` + regexp.QuoteMeta(old) + `(["']?\s*(?:#.*)?$)`)
|
||||
replacedBytes := pattern.ReplaceAllString(string(bytes), "${1}"+new+"${2}")
|
||||
if replacedBytes == string(bytes) {
|
||||
continue
|
||||
}
|
||||
old := fmt.Sprintf("%s:%s", composeImage, composeTag)
|
||||
new := fmt.Sprintf("%s:%s", composeImage, tag)
|
||||
replacedBytes := strings.Replace(string(bytes), old, new, -1)
|
||||
|
||||
log.Debug(i18n.G("updating %s to %s in %s", old, new, compose.Filename))
|
||||
|
||||
if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
|
||||
return false, err
|
||||
}
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// UpdateLabel updates a label in-place on file system local compose files.
|
||||
|
||||
+7
-14
@@ -16,7 +16,6 @@ import (
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/go-git/go-git/v5"
|
||||
gitCfg "github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
@@ -164,7 +163,7 @@ func (r Recipe) EnsureVersion(version string) (bool, error) {
|
||||
|
||||
hash, err := repo.ResolveRevision(plumbing.Revision(version))
|
||||
if err != nil {
|
||||
return isChaosCommit, errors.New(i18n.G("unable to resolve '%s': %s", version, err))
|
||||
log.Fatal(i18n.G("unable to resolve '%s': %s", version, err))
|
||||
}
|
||||
|
||||
opts = &git.CheckoutOptions{Hash: *hash, Create: false, Force: true}
|
||||
@@ -175,11 +174,11 @@ func (r Recipe) EnsureVersion(version string) (bool, error) {
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return isChaosCommit, err
|
||||
return isChaosCommit, nil
|
||||
}
|
||||
|
||||
if err := worktree.Checkout(opts); err != nil {
|
||||
return isChaosCommit, err
|
||||
return isChaosCommit, nil
|
||||
}
|
||||
|
||||
log.Debug(i18n.G("successfully checked %s out to %s in %s", r.Name, tagRef.Short(), r.Dir))
|
||||
@@ -263,12 +262,7 @@ func (r Recipe) EnsureUpToDate() error {
|
||||
return errors.New(i18n.G("unable to check out default branch in %s: %s", r.Dir, err))
|
||||
}
|
||||
|
||||
// the refspec is passed explicitly, because a repository cloned by an older abra has a
|
||||
// single-branch refspec stored in its config and would otherwise never see other branches
|
||||
fetchOpts := &git.FetchOptions{
|
||||
Tags: git.AllTags,
|
||||
RefSpecs: []gitCfg.RefSpec{"+refs/heads/*:refs/remotes/origin/*"},
|
||||
}
|
||||
fetchOpts := &git.FetchOptions{Tags: git.AllTags}
|
||||
if err := repo.Fetch(fetchOpts); err != nil {
|
||||
if !strings.Contains(err.Error(), "already up-to-date") {
|
||||
return errors.New(i18n.G("unable to fetch tags in %s: %s", r.Dir, err))
|
||||
@@ -452,11 +446,10 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
meta := ServiceMeta{Image: path, Tag: tag}
|
||||
if pinned, ok := img.(reference.Digested); ok {
|
||||
meta.Digest = pinned.Digest().String()
|
||||
versionMeta[service.Name] = ServiceMeta{
|
||||
Image: path,
|
||||
Tag: tag,
|
||||
}
|
||||
versionMeta[service.Name] = meta
|
||||
}
|
||||
|
||||
versions = append(versions, map[string]map[string]ServiceMeta{tag: versionMeta})
|
||||
|
||||
@@ -39,9 +39,8 @@ type service = string
|
||||
|
||||
// ServiceMeta represents meta info associated with a service.
|
||||
type ServiceMeta struct {
|
||||
Image string `json:"image"`
|
||||
Tag string `json:"tag"`
|
||||
Digest string `json:"digest,omitempty"`
|
||||
Image string `json:"image"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
// RecipeVersions are the versions associated with a recipe.
|
||||
|
||||
@@ -3,7 +3,7 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: nginx:1.31.6
|
||||
image: nginx:1.21.0
|
||||
secrets:
|
||||
- test_pass_one
|
||||
- test_pass_two
|
||||
|
||||
+1
-2
@@ -7,5 +7,4 @@
|
||||
|
||||
* Integration tests are in `./tests/integration`. Please see [these
|
||||
docs](https://docs.coopcloud.tech/abra/hack/#integration-tests) for
|
||||
instructions and tips on how to run them. If you want to run it locally via nix,
|
||||
read the [nix testing docs](../nix/hosts/README.md).
|
||||
instructions and tips on how to run them.
|
||||
|
||||
@@ -598,7 +598,8 @@ teardown(){
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
|
||||
assert_success
|
||||
|
||||
_inspect_env_test_app
|
||||
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
|
||||
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
|
||||
assert_success
|
||||
assert_output --partial "WITH_COMMENT=foo"
|
||||
|
||||
@@ -609,7 +610,8 @@ teardown(){
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --force
|
||||
assert_success
|
||||
|
||||
_inspect_env_test_app
|
||||
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
|
||||
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
|
||||
assert_success
|
||||
refute_output --partial "WITH_COMMENT=foo"
|
||||
assert_output --partial "WITH_COMMENT=bar"
|
||||
@@ -627,14 +629,8 @@ teardown(){
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
|
||||
assert_success
|
||||
|
||||
if _is_local;
|
||||
then
|
||||
run docker service inspect --format "{{ range .Endpoint.Ports }}{{ .Protocol }}={{ .PublishedPort }}{{ end }}" \
|
||||
run docker service inspect --format '{{ range .Endpoint.Ports }}{{ .Protocol }}={{ .PublishedPort }}{{ end }}' \
|
||||
"${TEST_APP_DOMAIN//./_}_app"
|
||||
else
|
||||
run ssh "$TEST_SERVER" "docker service inspect --format '{{ range .Endpoint.Ports }}{{ .Protocol }}={{ .PublishedPort }}{{ end }}' ${TEST_APP_DOMAIN//./_}_app"
|
||||
fi
|
||||
|
||||
assert_success
|
||||
assert_output --partial "tcp=1312"
|
||||
assert_output --partial "udp=1312"
|
||||
@@ -706,67 +702,3 @@ teardown(){
|
||||
assert_failure
|
||||
assert_output --partial "unexpected character"
|
||||
}
|
||||
|
||||
# Deploy pinned 1.21.5, upgrade to pinned 1.21.6, then roll back to 1.21.5.
|
||||
# bats test_tags=slow
|
||||
@test "deploy, upgrade and rollback digest-pinned images" {
|
||||
recipeDir="$ABRA_DIR/recipes/$TEST_RECIPE"
|
||||
service="${TEST_APP_DOMAIN//./_}_app"
|
||||
|
||||
# Manually upgrade the unpinned 0.3.5 recipe to 1.21.5 and pin it.
|
||||
run git -C "$recipeDir" checkout "0.3.5+1.21.0"
|
||||
assert_success
|
||||
run docker pull nginx:1.21.5
|
||||
assert_success
|
||||
run docker image inspect --format '{{index .RepoDigests 0}}' nginx:1.21.5
|
||||
assert_success
|
||||
oldImage="nginx:1.21.5@${output#*@}"
|
||||
run sed -i "s|image: nginx:1.21.0|image: $oldImage|" "$recipeDir/compose.yml"
|
||||
assert_success
|
||||
|
||||
# Label and tag the local recipe release so app commands can track it.
|
||||
run sed -i 's/version=0.3.5+1.21.0/version=0.4.0+1.21.5/' "$recipeDir/compose.yml"
|
||||
assert_success
|
||||
_git_commit
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||
-a "0.4.0+1.21.5" -m "fake: 0.4.0+1.21.5"
|
||||
assert_success
|
||||
|
||||
# Manually upgrade and pin the next release to 1.21.6.
|
||||
run docker pull nginx:1.21.6
|
||||
assert_success
|
||||
run docker image inspect --format '{{index .RepoDigests 0}}' nginx:1.21.6
|
||||
assert_success
|
||||
newImage="nginx:1.21.6@${output#*@}"
|
||||
run sed -i "s|image: $oldImage|image: $newImage|" "$recipeDir/compose.yml"
|
||||
assert_success
|
||||
|
||||
# Update the release label to match the next recipe tag.
|
||||
run sed -i 's/version=0.4.0+1.21.5/version=0.4.1+1.21.6/' "$recipeDir/compose.yml"
|
||||
assert_success
|
||||
_git_commit
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||
-a "0.4.1+1.21.6" -m "fake: 0.4.1+1.21.6"
|
||||
assert_success
|
||||
|
||||
# Deploy recipe 0.4.0 with the exact pinned 1.21.5 image.
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.4.0+1.21.5" --offline --no-input --no-converge-checks
|
||||
assert_success
|
||||
run docker service inspect --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}' "$service"
|
||||
assert_success
|
||||
assert_output "$oldImage"
|
||||
|
||||
# Upgrade to recipe 0.4.1 and the exact pinned 1.21.6 image.
|
||||
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.4.1+1.21.6" --offline --no-input --no-converge-checks
|
||||
assert_success
|
||||
run docker service inspect --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}' "$service"
|
||||
assert_success
|
||||
assert_output "$newImage"
|
||||
|
||||
# Roll back to recipe 0.4.0 and its original pinned image.
|
||||
run $ABRA app rollback "$TEST_APP_DOMAIN" "0.4.0+1.21.5" --offline --no-input --no-converge-checks
|
||||
assert_success
|
||||
run docker service inspect --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}' "$service"
|
||||
assert_success
|
||||
assert_output "$oldImage"
|
||||
}
|
||||
|
||||
@@ -133,7 +133,8 @@ teardown(){
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input
|
||||
assert_success
|
||||
|
||||
_inspect_env_test_app
|
||||
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
|
||||
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
|
||||
assert_success
|
||||
assert_output --partial "$TEST_RECIIPE:0.1.0+1.20.0"
|
||||
}
|
||||
|
||||
@@ -61,16 +61,6 @@ teardown(){
|
||||
run grep -q "TYPE=$TEST_RECIPE:0.3.0+1.21.0" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
# 0.3.0-only
|
||||
run grep -q "NETWORK_WITH_COMMENT=BAZ" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
# latest-only
|
||||
run grep -q "TIMEOUT=120" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "ensure recipe is up-to-date" {
|
||||
@@ -110,49 +100,6 @@ teardown(){
|
||||
run grep -q "TYPE=$TEST_RECIPE:${tagHash}" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
# 0.3.0-only
|
||||
run grep -q "NETWORK_WITH_COMMENT=BAZ" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
# latest-only
|
||||
run grep -q "TIMEOUT=120" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "create new app with commit from another branch" {
|
||||
branchHash=$(_get_other_branch_hash)
|
||||
if [[ -z "$branchHash" ]]; then
|
||||
skip "$TEST_RECIPE has no branch besides main"
|
||||
fi
|
||||
|
||||
# re-clone the recipe the way an older abra did, with a single-branch refspec. --no-local
|
||||
# forces a real transfer, a local clone would hardlink the whole object database and leave
|
||||
# the commit reachable
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE"
|
||||
assert_success
|
||||
|
||||
run git clone -q --no-local --single-branch --branch main \
|
||||
"$ABRA_DIR/origin-recipes/$TEST_RECIPE.git" "$ABRA_DIR/recipes/$TEST_RECIPE"
|
||||
assert_success
|
||||
|
||||
# the commit has to be genuinely missing, otherwise this test passes for the wrong reason
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --verify "$branchHash^{commit}"
|
||||
assert_failure
|
||||
|
||||
run $ABRA app new "$TEST_RECIPE" "$branchHash" \
|
||||
--no-input \
|
||||
--server "$TEST_SERVER" \
|
||||
--domain "$TEST_APP_DOMAIN"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
|
||||
# the recipe names itself in TYPE, which differs per branch, so only the version is checked
|
||||
run grep -q "TYPE=.*:${branchHash}$" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "does not overwrite existing env files" {
|
||||
|
||||
@@ -140,22 +140,15 @@ teardown(){
|
||||
_undeploy_app
|
||||
|
||||
sanitisedDomainName="${TEST_APP_DOMAIN//./_}"
|
||||
remote_ssh_command=""
|
||||
if _is_local;
|
||||
then
|
||||
run docker config create "${sanitisedDomainName}_test_conf_v99" "$ABRA_DIR/recipes/abra-test-recipe/abra.sh"
|
||||
else
|
||||
remote_ssh_command="ssh '$TEST_SERVER'"
|
||||
run cat "$ABRA_DIR/recipes/abra-test-recipe/abra.sh" | ssh "$TEST_SERVER" docker config create "${sanitisedDomainName}_test_conf_v99 -"
|
||||
assert_success
|
||||
fi
|
||||
run docker config create "${sanitisedDomainName}_test_conf_v99" "$ABRA_DIR/recipes/abra-test-recipe/abra.sh"
|
||||
assert_success
|
||||
|
||||
assert bash -c "$remote_ssh_command docker config ls | grep -q test_conf_v99"
|
||||
assert bash -c "docker config ls | grep -q test_conf_v99"
|
||||
|
||||
run $ABRA app rm "$TEST_APP_DOMAIN" --no-input
|
||||
assert_success
|
||||
|
||||
refute bash -c "$remote_ssh_command docker config ls | grep -q test_conf_v99"
|
||||
refute bash -c "docker config ls | grep -q test_conf_v99"
|
||||
}
|
||||
|
||||
@test "remove .env file" {
|
||||
|
||||
@@ -54,7 +54,8 @@ teardown(){
|
||||
--no-input
|
||||
assert_success
|
||||
|
||||
_inspect_env_test_app
|
||||
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
|
||||
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
|
||||
assert_success
|
||||
assert_output --partial "$TEST_RECIIPE:0.1.0+1.20.0"
|
||||
}
|
||||
|
||||
@@ -53,7 +53,8 @@ teardown(){
|
||||
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input
|
||||
assert_success
|
||||
|
||||
_inspect_env_test_app
|
||||
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
|
||||
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
|
||||
assert_success
|
||||
assert_output --partial "$TEST_RECIPE:0.2.0+1.21.0"
|
||||
assert_output --partial "$TEST_RECIIPE:0.2.0+1.21.0"
|
||||
}
|
||||
|
||||
@@ -31,18 +31,9 @@ _ensure_ssh_agent() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -v SSH_AUTH_SOCK ]]; then
|
||||
export SSH_AUTH_SOCK="$HOME/.ssh/ssh_auth_sock"
|
||||
export SSH_AUTH_SOCK="$HOME/.ssh/ssh_auth_sock"
|
||||
if [ ! -S ~/.ssh/ssh_auth_sock ]; then
|
||||
eval `ssh-agent`
|
||||
ln -sf "$SSH_AUTH_SOCK" ~/.ssh/ssh_auth_sock
|
||||
fi
|
||||
}
|
||||
|
||||
_is_local() {
|
||||
if [[ "$TEST_SERVER" == "default" ]];
|
||||
then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1,37 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
_ensure_swarm() {
|
||||
if [ "$(docker info | grep Swarm | sed 's/Swarm: //g' | tr -d ' ')" == "inactive" ]; then
|
||||
run docker swarm init --advertise-addr 127.0.0.1:2377
|
||||
assert_success
|
||||
if [ "$(docker info | grep Swarm | sed 's/Swarm: //g' | tr -d ' ')" == "inactive" ]; then
|
||||
run docker swarm init --advertise-addr 127.0.0.1:2377
|
||||
assert_success
|
||||
fi
|
||||
|
||||
if ! docker network ls | grep -q 'proxy'; then
|
||||
run docker network create -d overlay proxy
|
||||
assert_success
|
||||
if ! $(docker network ls | grep -q 'proxy'); then
|
||||
run docker network create -d overlay proxy
|
||||
assert_success
|
||||
fi
|
||||
|
||||
if ! _is_local;
|
||||
then
|
||||
if [ "$(ssh "$TEST_SERVER" docker info | grep Swarm | sed 's/Swarm: //g' | tr -d ' ')" == "inactive" ]; then
|
||||
run ssh "$TEST_SERVER" docker swarm init --advertise-addr 127.0.0.1:2377
|
||||
assert_success
|
||||
fi
|
||||
|
||||
if ! ssh "$TEST_SERVER" docker network ls | grep -q 'proxy'; then
|
||||
run ssh "$TEST_SERVER" docker network create -d overlay proxy
|
||||
assert_success
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
_inspect_env_test_app() {
|
||||
if _is_local;
|
||||
then
|
||||
containerId="$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)"
|
||||
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' "$containerId"
|
||||
else
|
||||
containerId="$(ssh "$TEST_SERVER" docker ps -f name="$TEST_APP_DOMAIN" -q)"
|
||||
run ssh "$TEST_SERVER" "docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' '$containerId'"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -56,13 +56,6 @@ _get_tag_hash() {
|
||||
echo $(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-list -n 1 "$1")
|
||||
}
|
||||
|
||||
_get_other_branch_hash() {
|
||||
# asked from the origin mirror, not from the recipe checkout, so that the result does not
|
||||
# depend on what has been fetched. empty when the recipe only has a default branch
|
||||
echo $(git ls-remote "$ABRA_DIR/origin-recipes/$TEST_RECIPE.git" \
|
||||
| grep 'refs/heads/' | grep -v 'refs/heads/main$' | head -1 | cut -f1)
|
||||
}
|
||||
|
||||
_get_head_hash() {
|
||||
echo $(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" show -s --format="%H" HEAD)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ teardown(){
|
||||
|
||||
run $ABRA recipe upgrade "custom-html" --no-input
|
||||
assert_success
|
||||
assert_output --partial 'can upgrade service: app'
|
||||
|
||||
assert_exists "$ABRA_DIR/recipes/custom-html"
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ teardown(){
|
||||
assert bash -c "docker context ls | grep -q $TEST_SERVER"
|
||||
|
||||
server_dir_perms=$(stat -c "%a" "$ABRA_DIR/servers/$TEST_SERVER")
|
||||
assert_equal "$server_dir_perms" "700"
|
||||
assert_equal $server_dir_perms "700"
|
||||
}
|
||||
|
||||
@test "error if using name and --local together" {
|
||||
@@ -43,8 +43,8 @@ teardown(){
|
||||
assert bash -c "docker context ls | grep -q default"
|
||||
assert_output --partial 'local server successfully added'
|
||||
|
||||
server_dir_perms=$(stat -c "%a" "$ABRA_DIR/servers/default")
|
||||
assert_equal "$server_dir_perms" "700"
|
||||
server_dir_perms=$(stat -c "%a" "$ABRA_DIR/servers/$TEST_SERVER")
|
||||
assert_equal $server_dir_perms "700"
|
||||
}
|
||||
|
||||
@test "create local server fails when no docker swarm" {
|
||||
|
||||
@@ -3,7 +3,7 @@ version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: nginx:1.31.6
|
||||
image: nginx:1.29.0
|
||||
networks:
|
||||
- proxy
|
||||
deploy:
|
||||
|
||||
+3
-3
@@ -3,16 +3,16 @@ kind: pipeline
|
||||
name: coopcloud.tech/tagcmp
|
||||
steps:
|
||||
- name: gofmt
|
||||
image: golang:1.27
|
||||
image: golang:1.21
|
||||
commands:
|
||||
- test -z "$(gofmt -l .)"
|
||||
|
||||
- name: go build
|
||||
image: golang:1.27
|
||||
image: golang:1.21
|
||||
commands:
|
||||
- go build -v .
|
||||
|
||||
- name: go test
|
||||
image: golang:1.27
|
||||
image: golang:1.21
|
||||
commands:
|
||||
- go test . -cover
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -87,7 +87,7 @@ func stringWidth(m Method, s string) int {
|
||||
|
||||
for i := 0; i < len(s); i++ {
|
||||
state, action := parser.Table.Transition(pstate, s[i])
|
||||
if state == parser.Utf8State {
|
||||
if action == parser.PrintAction || state == parser.Utf8State {
|
||||
cluster, w := FirstGraphemeCluster(s[i:], m)
|
||||
width += w
|
||||
|
||||
|
||||
Generated
Vendored
-1
@@ -18,7 +18,6 @@
|
||||
// tag is deprecated and thus should not be used.
|
||||
// Go versions prior to 1.4 are disabled because they use a different layout
|
||||
// for interfaces which make the implementation of unsafeReflectValue more complex.
|
||||
//go:build !js && !appengine && !safe && !disableunsafe && go1.4
|
||||
// +build !js,!appengine,!safe,!disableunsafe,go1.4
|
||||
|
||||
package spew
|
||||
Generated
Vendored
-1
@@ -16,7 +16,6 @@
|
||||
// when the code is running on Google App Engine, compiled by GopherJS, or
|
||||
// "-tags safe" is added to the go build command line. The "disableunsafe"
|
||||
// tag is deprecated and thus should not be used.
|
||||
//go:build js || appengine || safe || disableunsafe || !go1.4
|
||||
// +build js appengine safe disableunsafe !go1.4
|
||||
|
||||
package spew
|
||||
Generated
Vendored
Generated
Vendored
+15
-15
@@ -254,15 +254,15 @@ pointer addresses used to indirect to the final value. It provides the
|
||||
following features over the built-in printing facilities provided by the fmt
|
||||
package:
|
||||
|
||||
- Pointers are dereferenced and followed
|
||||
- Circular data structures are detected and handled properly
|
||||
- Custom Stringer/error interfaces are optionally invoked, including
|
||||
on unexported types
|
||||
- Custom types which only implement the Stringer/error interfaces via
|
||||
a pointer receiver are optionally invoked when passing non-pointer
|
||||
variables
|
||||
- Byte arrays and slices are dumped like the hexdump -C command which
|
||||
includes offsets, byte values in hex, and ASCII output
|
||||
* Pointers are dereferenced and followed
|
||||
* Circular data structures are detected and handled properly
|
||||
* Custom Stringer/error interfaces are optionally invoked, including
|
||||
on unexported types
|
||||
* Custom types which only implement the Stringer/error interfaces via
|
||||
a pointer receiver are optionally invoked when passing non-pointer
|
||||
variables
|
||||
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||
includes offsets, byte values in hex, and ASCII output
|
||||
|
||||
The configuration options are controlled by modifying the public members
|
||||
of c. See ConfigState for options documentation.
|
||||
@@ -295,12 +295,12 @@ func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{})
|
||||
|
||||
// NewDefaultConfig returns a ConfigState with the following default settings.
|
||||
//
|
||||
// Indent: " "
|
||||
// MaxDepth: 0
|
||||
// DisableMethods: false
|
||||
// DisablePointerMethods: false
|
||||
// ContinueOnMethod: false
|
||||
// SortKeys: false
|
||||
// Indent: " "
|
||||
// MaxDepth: 0
|
||||
// DisableMethods: false
|
||||
// DisablePointerMethods: false
|
||||
// ContinueOnMethod: false
|
||||
// SortKeys: false
|
||||
func NewDefaultConfig() *ConfigState {
|
||||
return &ConfigState{Indent: " "}
|
||||
}
|
||||
Generated
Vendored
+61
-67
@@ -21,36 +21,35 @@ debugging.
|
||||
A quick overview of the additional features spew provides over the built-in
|
||||
printing facilities for Go data types are as follows:
|
||||
|
||||
- Pointers are dereferenced and followed
|
||||
- Circular data structures are detected and handled properly
|
||||
- Custom Stringer/error interfaces are optionally invoked, including
|
||||
on unexported types
|
||||
- Custom types which only implement the Stringer/error interfaces via
|
||||
a pointer receiver are optionally invoked when passing non-pointer
|
||||
variables
|
||||
- Byte arrays and slices are dumped like the hexdump -C command which
|
||||
includes offsets, byte values in hex, and ASCII output (only when using
|
||||
Dump style)
|
||||
* Pointers are dereferenced and followed
|
||||
* Circular data structures are detected and handled properly
|
||||
* Custom Stringer/error interfaces are optionally invoked, including
|
||||
on unexported types
|
||||
* Custom types which only implement the Stringer/error interfaces via
|
||||
a pointer receiver are optionally invoked when passing non-pointer
|
||||
variables
|
||||
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||
includes offsets, byte values in hex, and ASCII output (only when using
|
||||
Dump style)
|
||||
|
||||
There are two different approaches spew allows for dumping Go data structures:
|
||||
|
||||
- Dump style which prints with newlines, customizable indentation,
|
||||
and additional debug information such as types and all pointer addresses
|
||||
used to indirect to the final value
|
||||
- A custom Formatter interface that integrates cleanly with the standard fmt
|
||||
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
|
||||
similar to the default %v while providing the additional functionality
|
||||
outlined above and passing unsupported format verbs such as %x and %q
|
||||
along to fmt
|
||||
* Dump style which prints with newlines, customizable indentation,
|
||||
and additional debug information such as types and all pointer addresses
|
||||
used to indirect to the final value
|
||||
* A custom Formatter interface that integrates cleanly with the standard fmt
|
||||
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
|
||||
similar to the default %v while providing the additional functionality
|
||||
outlined above and passing unsupported format verbs such as %x and %q
|
||||
along to fmt
|
||||
|
||||
# Quick Start
|
||||
Quick Start
|
||||
|
||||
This section demonstrates how to quickly get started with spew. See the
|
||||
sections below for further details on formatting and configuration options.
|
||||
|
||||
To dump a variable with full newlines, indentation, type, and pointer
|
||||
information use Dump, Fdump, or Sdump:
|
||||
|
||||
spew.Dump(myVar1, myVar2, ...)
|
||||
spew.Fdump(someWriter, myVar1, myVar2, ...)
|
||||
str := spew.Sdump(myVar1, myVar2, ...)
|
||||
@@ -59,13 +58,12 @@ Alternatively, if you would prefer to use format strings with a compacted inline
|
||||
printing style, use the convenience wrappers Printf, Fprintf, etc with
|
||||
%v (most compact), %+v (adds pointer addresses), %#v (adds types), or
|
||||
%#+v (adds types and pointer addresses):
|
||||
|
||||
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||
spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
|
||||
spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
|
||||
|
||||
# Configuration Options
|
||||
Configuration Options
|
||||
|
||||
Configuration of spew is handled by fields in the ConfigState type. For
|
||||
convenience, all of the top-level functions use a global state available
|
||||
@@ -76,52 +74,51 @@ equivalent to the top-level functions. This allows concurrent configuration
|
||||
options. See the ConfigState documentation for more details.
|
||||
|
||||
The following configuration options are available:
|
||||
* Indent
|
||||
String to use for each indentation level for Dump functions.
|
||||
It is a single space by default. A popular alternative is "\t".
|
||||
|
||||
- Indent
|
||||
String to use for each indentation level for Dump functions.
|
||||
It is a single space by default. A popular alternative is "\t".
|
||||
* MaxDepth
|
||||
Maximum number of levels to descend into nested data structures.
|
||||
There is no limit by default.
|
||||
|
||||
- MaxDepth
|
||||
Maximum number of levels to descend into nested data structures.
|
||||
There is no limit by default.
|
||||
* DisableMethods
|
||||
Disables invocation of error and Stringer interface methods.
|
||||
Method invocation is enabled by default.
|
||||
|
||||
- DisableMethods
|
||||
Disables invocation of error and Stringer interface methods.
|
||||
Method invocation is enabled by default.
|
||||
* DisablePointerMethods
|
||||
Disables invocation of error and Stringer interface methods on types
|
||||
which only accept pointer receivers from non-pointer variables.
|
||||
Pointer method invocation is enabled by default.
|
||||
|
||||
- DisablePointerMethods
|
||||
Disables invocation of error and Stringer interface methods on types
|
||||
which only accept pointer receivers from non-pointer variables.
|
||||
Pointer method invocation is enabled by default.
|
||||
* DisablePointerAddresses
|
||||
DisablePointerAddresses specifies whether to disable the printing of
|
||||
pointer addresses. This is useful when diffing data structures in tests.
|
||||
|
||||
- DisablePointerAddresses
|
||||
DisablePointerAddresses specifies whether to disable the printing of
|
||||
pointer addresses. This is useful when diffing data structures in tests.
|
||||
* DisableCapacities
|
||||
DisableCapacities specifies whether to disable the printing of
|
||||
capacities for arrays, slices, maps and channels. This is useful when
|
||||
diffing data structures in tests.
|
||||
|
||||
- DisableCapacities
|
||||
DisableCapacities specifies whether to disable the printing of
|
||||
capacities for arrays, slices, maps and channels. This is useful when
|
||||
diffing data structures in tests.
|
||||
* ContinueOnMethod
|
||||
Enables recursion into types after invoking error and Stringer interface
|
||||
methods. Recursion after method invocation is disabled by default.
|
||||
|
||||
- ContinueOnMethod
|
||||
Enables recursion into types after invoking error and Stringer interface
|
||||
methods. Recursion after method invocation is disabled by default.
|
||||
* SortKeys
|
||||
Specifies map keys should be sorted before being printed. Use
|
||||
this to have a more deterministic, diffable output. Note that
|
||||
only native types (bool, int, uint, floats, uintptr and string)
|
||||
and types which implement error or Stringer interfaces are
|
||||
supported with other types sorted according to the
|
||||
reflect.Value.String() output which guarantees display
|
||||
stability. Natural map order is used by default.
|
||||
|
||||
- SortKeys
|
||||
Specifies map keys should be sorted before being printed. Use
|
||||
this to have a more deterministic, diffable output. Note that
|
||||
only native types (bool, int, uint, floats, uintptr and string)
|
||||
and types which implement error or Stringer interfaces are
|
||||
supported with other types sorted according to the
|
||||
reflect.Value.String() output which guarantees display
|
||||
stability. Natural map order is used by default.
|
||||
* SpewKeys
|
||||
Specifies that, as a last resort attempt, map keys should be
|
||||
spewed to strings and sorted by those strings. This is only
|
||||
considered if SortKeys is true.
|
||||
|
||||
- SpewKeys
|
||||
Specifies that, as a last resort attempt, map keys should be
|
||||
spewed to strings and sorted by those strings. This is only
|
||||
considered if SortKeys is true.
|
||||
|
||||
# Dump Usage
|
||||
Dump Usage
|
||||
|
||||
Simply call spew.Dump with a list of variables you want to dump:
|
||||
|
||||
@@ -136,7 +133,7 @@ A third option is to call spew.Sdump to get the formatted output as a string:
|
||||
|
||||
str := spew.Sdump(myVar1, myVar2, ...)
|
||||
|
||||
# Sample Dump Output
|
||||
Sample Dump Output
|
||||
|
||||
See the Dump example for details on the setup of the types and variables being
|
||||
shown here.
|
||||
@@ -153,14 +150,13 @@ shown here.
|
||||
|
||||
Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C
|
||||
command as shown.
|
||||
|
||||
([]uint8) (len=32 cap=32) {
|
||||
00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
|
||||
00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
|
||||
00000020 31 32 |12|
|
||||
}
|
||||
|
||||
# Custom Formatter
|
||||
Custom Formatter
|
||||
|
||||
Spew provides a custom formatter that implements the fmt.Formatter interface
|
||||
so that it integrates cleanly with standard fmt package printing functions. The
|
||||
@@ -174,7 +170,7 @@ standard fmt package for formatting. In addition, the custom formatter ignores
|
||||
the width and precision arguments (however they will still work on the format
|
||||
specifiers not handled by the custom formatter).
|
||||
|
||||
# Custom Formatter Usage
|
||||
Custom Formatter Usage
|
||||
|
||||
The simplest way to make use of the spew custom formatter is to call one of the
|
||||
convenience functions such as spew.Printf, spew.Println, or spew.Printf. The
|
||||
@@ -188,17 +184,15 @@ functions have syntax you are most likely already familiar with:
|
||||
|
||||
See the Index for the full list convenience functions.
|
||||
|
||||
# Sample Formatter Output
|
||||
Sample Formatter Output
|
||||
|
||||
Double pointer to a uint8:
|
||||
|
||||
%v: <**>5
|
||||
%+v: <**>(0xf8400420d0->0xf8400420c8)5
|
||||
%#v: (**uint8)5
|
||||
%#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5
|
||||
|
||||
Pointer to circular struct with a uint8 field and a pointer to itself:
|
||||
|
||||
%v: <*>{1 <*><shown>}
|
||||
%+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<shown>}
|
||||
%#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)<shown>}
|
||||
@@ -207,7 +201,7 @@ Pointer to circular struct with a uint8 field and a pointer to itself:
|
||||
See the Printf example for details on the setup of variables being shown
|
||||
here.
|
||||
|
||||
# Errors
|
||||
Errors
|
||||
|
||||
Since it is possible for custom Stringer/error interfaces to panic, spew
|
||||
detects them and handles them internally by printing the panic information
|
||||
Generated
Vendored
+9
-9
@@ -488,15 +488,15 @@ pointer addresses used to indirect to the final value. It provides the
|
||||
following features over the built-in printing facilities provided by the fmt
|
||||
package:
|
||||
|
||||
- Pointers are dereferenced and followed
|
||||
- Circular data structures are detected and handled properly
|
||||
- Custom Stringer/error interfaces are optionally invoked, including
|
||||
on unexported types
|
||||
- Custom types which only implement the Stringer/error interfaces via
|
||||
a pointer receiver are optionally invoked when passing non-pointer
|
||||
variables
|
||||
- Byte arrays and slices are dumped like the hexdump -C command which
|
||||
includes offsets, byte values in hex, and ASCII output
|
||||
* Pointers are dereferenced and followed
|
||||
* Circular data structures are detected and handled properly
|
||||
* Custom Stringer/error interfaces are optionally invoked, including
|
||||
on unexported types
|
||||
* Custom types which only implement the Stringer/error interfaces via
|
||||
a pointer receiver are optionally invoked when passing non-pointer
|
||||
variables
|
||||
* Byte arrays and slices are dumped like the hexdump -C command which
|
||||
includes offsets, byte values in hex, and ASCII output
|
||||
|
||||
The configuration options are controlled by an exported package global,
|
||||
spew.Config. See ConfigState for options documentation.
|
||||
Generated
Vendored
Generated
Vendored
+36
-36
@@ -27,19 +27,15 @@ type PortSet map[Port]struct{}
|
||||
type Port string
|
||||
|
||||
// NewPort creates a new instance of a Port given a protocol and port number or port range
|
||||
func NewPort(proto, port string) (Port, error) {
|
||||
// Check for parsing issues on "port" now so we can avoid having
|
||||
// to check it later on.
|
||||
|
||||
portStartInt, portEndInt, err := ParsePortRangeToInt(port)
|
||||
func NewPort(proto, portOrRange string) (Port, error) {
|
||||
start, end, err := parsePortRange(portOrRange)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if portStartInt == portEndInt {
|
||||
return Port(fmt.Sprintf("%d/%s", portStartInt, proto)), nil
|
||||
if start == end {
|
||||
return Port(fmt.Sprintf("%d/%s", start, proto)), nil
|
||||
}
|
||||
return Port(fmt.Sprintf("%d-%d/%s", portStartInt, portEndInt, proto)), nil
|
||||
return Port(fmt.Sprintf("%d-%d/%s", start, end, proto)), nil
|
||||
}
|
||||
|
||||
// ParsePort parses the port number string and returns an int
|
||||
@@ -47,49 +43,53 @@ func ParsePort(rawPort string) (int, error) {
|
||||
if rawPort == "" {
|
||||
return 0, nil
|
||||
}
|
||||
port, err := strconv.ParseUint(rawPort, 10, 16)
|
||||
port, err := parsePortNumber(rawPort)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid port '%s': %w", rawPort, errors.Unwrap(err))
|
||||
return 0, fmt.Errorf("invalid port '%s': %w", rawPort, err)
|
||||
}
|
||||
return int(port), nil
|
||||
return port, nil
|
||||
}
|
||||
|
||||
// ParsePortRangeToInt parses the port range string and returns start/end ints
|
||||
func ParsePortRangeToInt(rawPort string) (int, int, error) {
|
||||
func ParsePortRangeToInt(rawPort string) (startPort, endPort int, _ error) {
|
||||
if rawPort == "" {
|
||||
// TODO(thaJeztah): consider making this an error; this was kept to keep existing behavior.
|
||||
return 0, 0, nil
|
||||
}
|
||||
start, end, err := ParsePortRange(rawPort)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return int(start), int(end), nil
|
||||
return parsePortRange(rawPort)
|
||||
}
|
||||
|
||||
// Proto returns the protocol of a Port
|
||||
func (p Port) Proto() string {
|
||||
proto, _ := SplitProtoPort(string(p))
|
||||
_, proto, _ := strings.Cut(string(p), "/")
|
||||
if proto == "" {
|
||||
proto = "tcp"
|
||||
}
|
||||
return proto
|
||||
}
|
||||
|
||||
// Port returns the port number of a Port
|
||||
func (p Port) Port() string {
|
||||
_, port := SplitProtoPort(string(p))
|
||||
port, _, _ := strings.Cut(string(p), "/")
|
||||
return port
|
||||
}
|
||||
|
||||
// Int returns the port number of a Port as an int
|
||||
// Int returns the port number of a Port as an int. It assumes [Port]
|
||||
// is valid, and returns 0 otherwise.
|
||||
func (p Port) Int() int {
|
||||
portStr := p.Port()
|
||||
// We don't need to check for an error because we're going to
|
||||
// assume that any error would have been found, and reported, in NewPort()
|
||||
port, _ := ParsePort(portStr)
|
||||
// assume that any error would have been found, and reported, in [NewPort]
|
||||
port, _ := parsePortNumber(p.Port())
|
||||
return port
|
||||
}
|
||||
|
||||
// Range returns the start/end port numbers of a Port range as ints
|
||||
func (p Port) Range() (int, int, error) {
|
||||
return ParsePortRangeToInt(p.Port())
|
||||
portRange := p.Port()
|
||||
if portRange == "" {
|
||||
return 0, 0, nil
|
||||
}
|
||||
return parsePortRange(portRange)
|
||||
}
|
||||
|
||||
// SplitProtoPort splits a port(range) and protocol, formatted as "<portnum>/[<proto>]"
|
||||
@@ -173,6 +173,10 @@ func splitParts(rawport string) (hostIP, hostPort, containerPort string) {
|
||||
func ParsePortSpec(rawPort string) ([]PortMapping, error) {
|
||||
ip, hostPort, containerPort := splitParts(rawPort)
|
||||
proto, containerPort := SplitProtoPort(containerPort)
|
||||
if containerPort == "" {
|
||||
return nil, fmt.Errorf("no port specified: %s<empty>", rawPort)
|
||||
}
|
||||
|
||||
proto = strings.ToLower(proto)
|
||||
if err := validateProto(proto); err != nil {
|
||||
return nil, err
|
||||
@@ -189,18 +193,15 @@ func ParsePortSpec(rawPort string) ([]PortMapping, error) {
|
||||
if ip != "" && net.ParseIP(ip) == nil {
|
||||
return nil, errors.New("invalid IP address: " + ip)
|
||||
}
|
||||
if containerPort == "" {
|
||||
return nil, fmt.Errorf("no port specified: %s<empty>", rawPort)
|
||||
}
|
||||
|
||||
startPort, endPort, err := ParsePortRange(containerPort)
|
||||
startPort, endPort, err := parsePortRange(containerPort)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid containerPort: " + containerPort)
|
||||
}
|
||||
|
||||
var startHostPort, endHostPort uint64
|
||||
var startHostPort, endHostPort int
|
||||
if hostPort != "" {
|
||||
startHostPort, endHostPort, err = ParsePortRange(hostPort)
|
||||
startHostPort, endHostPort, err = parsePortRange(hostPort)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid hostPort: " + hostPort)
|
||||
}
|
||||
@@ -217,19 +218,18 @@ func ParsePortSpec(rawPort string) ([]PortMapping, error) {
|
||||
count := endPort - startPort + 1
|
||||
ports := make([]PortMapping, 0, count)
|
||||
|
||||
for i := uint64(0); i < count; i++ {
|
||||
cPort := Port(strconv.FormatUint(startPort+i, 10) + "/" + proto)
|
||||
for i := range count {
|
||||
hPort := ""
|
||||
if hostPort != "" {
|
||||
hPort = strconv.FormatUint(startHostPort+i, 10)
|
||||
hPort = strconv.Itoa(startHostPort + i)
|
||||
// Set hostPort to a range only if there is a single container port
|
||||
// and a dynamic host port.
|
||||
if count == 1 && startHostPort != endHostPort {
|
||||
hPort += "-" + strconv.FormatUint(endHostPort, 10)
|
||||
hPort += "-" + strconv.Itoa(endHostPort)
|
||||
}
|
||||
}
|
||||
ports = append(ports, PortMapping{
|
||||
Port: cPort,
|
||||
Port: Port(strconv.Itoa(startPort+i) + "/" + proto),
|
||||
Binding: PortBinding{HostIP: ip, HostPort: hPort},
|
||||
})
|
||||
}
|
||||
|
||||
+42
-15
@@ -2,32 +2,59 @@ package nat
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParsePortRange parses and validates the specified string as a port-range (8000-9000)
|
||||
func ParsePortRange(ports string) (uint64, uint64, error) {
|
||||
// ParsePortRange parses and validates the specified string as a port range (e.g., "8000-9000").
|
||||
func ParsePortRange(ports string) (startPort, endPort uint64, _ error) {
|
||||
start, end, err := parsePortRange(ports)
|
||||
return uint64(start), uint64(end), err
|
||||
}
|
||||
|
||||
// parsePortRange parses and validates the specified string as a port range (e.g., "8000-9000").
|
||||
func parsePortRange(ports string) (startPort, endPort int, _ error) {
|
||||
if ports == "" {
|
||||
return 0, 0, errors.New("empty string specified for ports")
|
||||
}
|
||||
if !strings.Contains(ports, "-") {
|
||||
start, err := strconv.ParseUint(ports, 10, 16)
|
||||
end := start
|
||||
return start, end, err
|
||||
start, end, ok := strings.Cut(ports, "-")
|
||||
|
||||
startPort, err := parsePortNumber(start)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("invalid start port '%s': %w", start, err)
|
||||
}
|
||||
if !ok || start == end {
|
||||
return startPort, startPort, nil
|
||||
}
|
||||
|
||||
parts := strings.Split(ports, "-")
|
||||
start, err := strconv.ParseUint(parts[0], 10, 16)
|
||||
endPort, err = parsePortNumber(end)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
return 0, 0, fmt.Errorf("invalid end port '%s': %w", end, err)
|
||||
}
|
||||
end, err := strconv.ParseUint(parts[1], 10, 16)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
if endPort < startPort {
|
||||
return 0, 0, errors.New("invalid port range: " + ports)
|
||||
}
|
||||
if end < start {
|
||||
return 0, 0, errors.New("invalid range specified for port: " + ports)
|
||||
return startPort, endPort, nil
|
||||
}
|
||||
|
||||
// parsePortNumber parses rawPort into an int, unwrapping strconv errors
|
||||
// and returning a single "out of range" error for any value outside 0–65535.
|
||||
func parsePortNumber(rawPort string) (int, error) {
|
||||
if rawPort == "" {
|
||||
return 0, errors.New("value is empty")
|
||||
}
|
||||
return start, end, nil
|
||||
port, err := strconv.ParseInt(rawPort, 10, 0)
|
||||
if err != nil {
|
||||
var numErr *strconv.NumError
|
||||
if errors.As(err, &numErr) {
|
||||
err = numErr.Err
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
if port < 0 || port > 65535 {
|
||||
return 0, errors.New("value out of range (0–65535)")
|
||||
}
|
||||
|
||||
return int(port), nil
|
||||
}
|
||||
|
||||
+27
-15
@@ -34,8 +34,10 @@ func Sort(ports []Port, predicate func(i, j Port) bool) {
|
||||
}
|
||||
|
||||
type portMapEntry struct {
|
||||
port Port
|
||||
binding PortBinding
|
||||
port Port
|
||||
binding *PortBinding
|
||||
portInt int
|
||||
portProto string
|
||||
}
|
||||
|
||||
type portMapSorter []portMapEntry
|
||||
@@ -48,23 +50,36 @@ func (s portMapSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
// 2. larger port
|
||||
// 3. port with tcp protocol
|
||||
func (s portMapSorter) Less(i, j int) bool {
|
||||
pi, pj := s[i].port, s[j].port
|
||||
hpi, hpj := toInt(s[i].binding.HostPort), toInt(s[j].binding.HostPort)
|
||||
return hpi > hpj || pi.Int() > pj.Int() || (pi.Int() == pj.Int() && strings.ToLower(pi.Proto()) == "tcp")
|
||||
pi, pj := s[i].portInt, s[j].portInt
|
||||
var hpi, hpj int
|
||||
if s[i].binding != nil {
|
||||
hpi = toInt(s[i].binding.HostPort)
|
||||
}
|
||||
if s[j].binding != nil {
|
||||
hpj = toInt(s[j].binding.HostPort)
|
||||
}
|
||||
return hpi > hpj || pi > pj || (pi == pj && strings.EqualFold(s[i].portProto, "tcp"))
|
||||
}
|
||||
|
||||
// SortPortMap sorts the list of ports and their respected mapping. The ports
|
||||
// will explicit HostPort will be placed first.
|
||||
func SortPortMap(ports []Port, bindings PortMap) {
|
||||
func SortPortMap(ports []Port, bindings map[Port][]PortBinding) {
|
||||
s := portMapSorter{}
|
||||
for _, p := range ports {
|
||||
portInt, portProto := p.Int(), p.Proto()
|
||||
if binding, ok := bindings[p]; ok && len(binding) > 0 {
|
||||
for _, b := range binding {
|
||||
s = append(s, portMapEntry{port: p, binding: b})
|
||||
s = append(s, portMapEntry{
|
||||
port: p, binding: &b,
|
||||
portInt: portInt, portProto: portProto,
|
||||
})
|
||||
}
|
||||
bindings[p] = []PortBinding{}
|
||||
} else {
|
||||
s = append(s, portMapEntry{port: p})
|
||||
s = append(s, portMapEntry{
|
||||
port: p,
|
||||
portInt: portInt, portProto: portProto,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,16 +96,13 @@ func SortPortMap(ports []Port, bindings PortMap) {
|
||||
i++
|
||||
}
|
||||
// reorder bindings for this port
|
||||
if _, ok := bindings[entry.port]; ok {
|
||||
bindings[entry.port] = append(bindings[entry.port], entry.binding)
|
||||
if entry.binding != nil {
|
||||
bindings[entry.port] = append(bindings[entry.port], *entry.binding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toInt(s string) uint64 {
|
||||
i, _, err := ParsePortRange(s)
|
||||
if err != nil {
|
||||
i = 0
|
||||
}
|
||||
func toInt(s string) int {
|
||||
i, _, _ := parsePortRange(s)
|
||||
return i
|
||||
}
|
||||
|
||||
+30
-30
@@ -1,48 +1,57 @@
|
||||
package sockets
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var errClosed = errors.New("use of closed network connection")
|
||||
|
||||
// InmemSocket implements net.Listener using in-memory only connections.
|
||||
type InmemSocket struct {
|
||||
chConn chan net.Conn
|
||||
chClose chan struct{}
|
||||
addr string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// dummyAddr is used to satisfy net.Addr for the in-mem socket
|
||||
// it is just stored as a string and returns the string for all calls
|
||||
type dummyAddr string
|
||||
|
||||
// NewInmemSocket creates an in-memory only net.Listener
|
||||
// The addr argument can be any string, but is used to satisfy the `Addr()` part
|
||||
// of the net.Listener interface
|
||||
// Network returns the addr string, satisfies net.Addr
|
||||
func (a dummyAddr) Network() string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
// String returns the string form
|
||||
func (a dummyAddr) String() string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
// InmemSocket implements [net.Listener] using in-memory only connections.
|
||||
type InmemSocket struct {
|
||||
chConn chan net.Conn
|
||||
chClose chan struct{}
|
||||
addr dummyAddr
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewInmemSocket creates an in-memory only [net.Listener]. The addr argument
|
||||
// can be any string, but is used to satisfy the [net.Listener.Addr] part
|
||||
// of the [net.Listener] interface
|
||||
func NewInmemSocket(addr string, bufSize int) *InmemSocket {
|
||||
return &InmemSocket{
|
||||
chConn: make(chan net.Conn, bufSize),
|
||||
chClose: make(chan struct{}),
|
||||
addr: addr,
|
||||
addr: dummyAddr(addr),
|
||||
}
|
||||
}
|
||||
|
||||
// Addr returns the socket's addr string to satisfy net.Listener
|
||||
func (s *InmemSocket) Addr() net.Addr {
|
||||
return dummyAddr(s.addr)
|
||||
return s.addr
|
||||
}
|
||||
|
||||
// Accept implements the Accept method in the Listener interface; it waits for the next call and returns a generic Conn.
|
||||
// Accept implements the Accept method in the Listener interface; it waits
|
||||
// for the next call and returns a generic Conn. It returns a [net.ErrClosed]
|
||||
// if the connection is already closed.
|
||||
func (s *InmemSocket) Accept() (net.Conn, error) {
|
||||
select {
|
||||
case conn := <-s.chConn:
|
||||
return conn, nil
|
||||
case <-s.chClose:
|
||||
return nil, errClosed
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,24 +67,15 @@ func (s *InmemSocket) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Dial is used to establish a connection with the in-mem server
|
||||
// Dial is used to establish a connection with the in-mem server.
|
||||
// It returns a [net.ErrClosed] if the connection is already closed.
|
||||
func (s *InmemSocket) Dial(network, addr string) (net.Conn, error) {
|
||||
srvConn, clientConn := net.Pipe()
|
||||
select {
|
||||
case s.chConn <- srvConn:
|
||||
case <-s.chClose:
|
||||
return nil, errClosed
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
|
||||
return clientConn, nil
|
||||
}
|
||||
|
||||
// Network returns the addr string, satisfies net.Addr
|
||||
func (a dummyAddr) Network() string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
// String returns the string form
|
||||
func (a dummyAddr) String() string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
package sockets
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetProxyEnv allows access to the uppercase and the lowercase forms of
|
||||
// proxy-related variables. See the Go specification for details on these
|
||||
// variables. https://golang.org/pkg/net/http/
|
||||
//
|
||||
// Deprecated: this function was used as helper for [DialerFromEnvironment] and is no longer used. It will be removed in the next release.
|
||||
func GetProxyEnv(key string) string {
|
||||
proxyValue := os.Getenv(strings.ToUpper(key))
|
||||
if proxyValue == "" {
|
||||
return os.Getenv(strings.ToLower(key))
|
||||
}
|
||||
return proxyValue
|
||||
}
|
||||
|
||||
// DialerFromEnvironment was previously used to configure a net.Dialer to route
|
||||
// connections through a SOCKS proxy.
|
||||
//
|
||||
// Deprecated: SOCKS proxies are now supported by configuring only
|
||||
// http.Transport.Proxy, and no longer require changing http.Transport.Dial.
|
||||
// Therefore, only [sockets.ConfigureTransport] needs to be called, and any
|
||||
// [sockets.DialerFromEnvironment] calls can be dropped.
|
||||
func DialerFromEnvironment(direct *net.Dialer) (*net.Dialer, error) {
|
||||
return direct, nil
|
||||
}
|
||||
+12
-12
@@ -27,11 +27,19 @@ var ErrProtocolNotAvailable = errors.New("protocol not available")
|
||||
// make sure you do it _after_ any subsequent calls to ConfigureTransport is made against the same
|
||||
// [http.Transport].
|
||||
func ConfigureTransport(tr *http.Transport, proto, addr string) error {
|
||||
if tr.MaxIdleConns == 0 {
|
||||
// prevent long-lived processes from leaking connections
|
||||
// due to idle connections not being released.
|
||||
//
|
||||
// TODO: see if we can also address this from the server side; see: https://github.com/moby/moby/issues/45539
|
||||
tr.MaxIdleConns = 6
|
||||
tr.IdleConnTimeout = 30 * time.Second
|
||||
}
|
||||
switch proto {
|
||||
case "unix":
|
||||
return configureUnixTransport(tr, proto, addr)
|
||||
return configureUnixTransport(tr, addr)
|
||||
case "npipe":
|
||||
return configureNpipeTransport(tr, proto, addr)
|
||||
return configureNpipeTransport(tr, addr)
|
||||
default:
|
||||
tr.Proxy = http.ProxyFromEnvironment
|
||||
tr.DisableCompression = false
|
||||
@@ -42,15 +50,7 @@ func ConfigureTransport(tr *http.Transport, proto, addr string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DialPipe connects to a Windows named pipe. It is not supported on
|
||||
// non-Windows platforms.
|
||||
//
|
||||
// Deprecated: use [github.com/Microsoft/go-winio.DialPipe] or [github.com/Microsoft/go-winio.DialPipeContext].
|
||||
func DialPipe(addr string, timeout time.Duration) (net.Conn, error) {
|
||||
return dialPipe(addr, timeout)
|
||||
}
|
||||
|
||||
func configureUnixTransport(tr *http.Transport, proto, addr string) error {
|
||||
func configureUnixTransport(tr *http.Transport, addr string) error {
|
||||
if len(addr) > maxUnixSocketPathSize {
|
||||
return fmt.Errorf("unix socket path %q is too long", addr)
|
||||
}
|
||||
@@ -60,7 +60,7 @@ func configureUnixTransport(tr *http.Transport, proto, addr string) error {
|
||||
Timeout: defaultTimeout,
|
||||
}
|
||||
tr.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, proto, addr)
|
||||
return dialer.DialContext(ctx, "unix", addr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+1
-12
@@ -2,17 +2,6 @@
|
||||
|
||||
package sockets
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func configureNpipeTransport(tr *http.Transport, proto, addr string) error {
|
||||
func configureNpipeTransport(any, string) error {
|
||||
return ErrProtocolNotAvailable
|
||||
}
|
||||
|
||||
func dialPipe(_ string, _ time.Duration) (net.Conn, error) {
|
||||
return nil, syscall.EAFNOSUPPORT
|
||||
}
|
||||
|
||||
+1
-6
@@ -4,12 +4,11 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/Microsoft/go-winio"
|
||||
)
|
||||
|
||||
func configureNpipeTransport(tr *http.Transport, proto, addr string) error {
|
||||
func configureNpipeTransport(tr *http.Transport, addr string) error {
|
||||
// No need for compression in local communications.
|
||||
tr.DisableCompression = true
|
||||
tr.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
@@ -17,7 +16,3 @@ func configureNpipeTransport(tr *http.Transport, proto, addr string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dialPipe(addr string, timeout time.Duration) (net.Conn, error) {
|
||||
return winio.DialPipe(addr, &timeout)
|
||||
}
|
||||
|
||||
+123
-1
@@ -1,6 +1,128 @@
|
||||
package sockets
|
||||
|
||||
import "net"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/Microsoft/go-winio"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// BasePermissions defines the default DACL, which allows Administrators
|
||||
// and LocalSystem full access (similar to defaults used in [moby]);
|
||||
//
|
||||
// - D:P: DACL without inheritance (protected, (P)).
|
||||
// - (A;;GA;;;BA): Allow full access (GA) for built-in Administrators (BA).
|
||||
// - (A;;GA;;;SY); Allow full access (GA) for LocalSystem (SY).
|
||||
// - Any other user is denied access.
|
||||
//
|
||||
// [moby]: https://github.com/moby/moby/blob/6b45c76a233b1b8b56465f76c21c09fd7920e82d/daemon/listeners/listeners_windows.go#L53-L59
|
||||
const BasePermissions = "D:P(A;;GA;;;BA)(A;;GA;;;SY)"
|
||||
|
||||
// WithBasePermissions sets a default DACL, which allows Administrators
|
||||
// and LocalSystem full access (similar to defaults used in [moby]);
|
||||
//
|
||||
// - D:P: DACL without inheritance (protected, (P)).
|
||||
// - (A;;GA;;;BA): Allow full access (GA) for built-in Administrators (BA).
|
||||
// - (A;;GA;;;SY); Allow full access (GA) for LocalSystem (SY).
|
||||
// - Any other user is denied access.
|
||||
//
|
||||
// [moby]: https://github.com/moby/moby/blob/6b45c76a233b1b8b56465f76c21c09fd7920e82d/daemon/listeners/listeners_windows.go#L53-L59
|
||||
func WithBasePermissions() SockOption {
|
||||
return withSDDL(BasePermissions)
|
||||
}
|
||||
|
||||
// WithAdditionalUsersAndGroups modifies the socket file's DACL to grant
|
||||
// access to additional users and groups.
|
||||
//
|
||||
// It sets [BasePermissions] on the socket path and grants the given additional
|
||||
// users and groups to generic read (GR) and write (GW) access. It returns
|
||||
// an error if no groups were given, when failing to resolve any of the
|
||||
// additional users and groups, or when failing to apply the ACL.
|
||||
func WithAdditionalUsersAndGroups(additionalUsersAndGroups []string) SockOption {
|
||||
return func(path string) error {
|
||||
if len(additionalUsersAndGroups) == 0 {
|
||||
return errors.New("no additional users specified")
|
||||
}
|
||||
sd, err := getSecurityDescriptor(additionalUsersAndGroups...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("looking up SID: %w", err)
|
||||
}
|
||||
return withSDDL(sd)(path)
|
||||
}
|
||||
}
|
||||
|
||||
// withSDDL applies the given SDDL to the socket. It returns an error
|
||||
// when failing parse the SDDL, or if the DACL was defaulted.
|
||||
//
|
||||
// TODO(thaJeztah); this is not exported yet, as some of the checks may need review if they're not too opinionated.
|
||||
func withSDDL(sddl string) SockOption {
|
||||
return func(path string) error {
|
||||
sd, err := windows.SecurityDescriptorFromString(sddl)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing SDDL: %w", err)
|
||||
}
|
||||
dacl, defaulted, err := sd.DACL()
|
||||
if err != nil {
|
||||
return fmt.Errorf("extracting DACL: %w", err)
|
||||
}
|
||||
if dacl == nil || defaulted {
|
||||
// should never be hit with our [DefaultPermissions],
|
||||
// as it contains "D:" and "P" (protected, don't inherit).
|
||||
return errors.New("no DACL found in security descriptor or defaulted")
|
||||
}
|
||||
return windows.SetNamedSecurityInfo(
|
||||
path,
|
||||
windows.SE_FILE_OBJECT,
|
||||
windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION,
|
||||
nil, // do not change the owner
|
||||
nil, // do not change the owner
|
||||
dacl,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// NewUnixSocket creates a new unix socket.
|
||||
//
|
||||
// It sets [BasePermissions] on the socket path and grants the given additional
|
||||
// users and groups to generic read (GR) and write (GW) access. It returns
|
||||
// an error when failing to resolve any of the additional users and groups,
|
||||
// or when failing to apply the ACL.
|
||||
func NewUnixSocket(path string, additionalUsersAndGroups []string) (net.Listener, error) {
|
||||
var opts []SockOption
|
||||
if len(additionalUsersAndGroups) > 0 {
|
||||
opts = append(opts, WithAdditionalUsersAndGroups(additionalUsersAndGroups))
|
||||
} else {
|
||||
opts = append(opts, WithBasePermissions())
|
||||
}
|
||||
return NewUnixSocketWithOpts(path, opts...)
|
||||
}
|
||||
|
||||
// getSecurityDescriptor returns the DACL for the Unix socket.
|
||||
//
|
||||
// By default, it grants [BasePermissions], but allows for additional
|
||||
// users and groups to get generic read (GR) and write (GW) access. It
|
||||
// returns an error when failing to resolve any of the additional users
|
||||
// and groups.
|
||||
func getSecurityDescriptor(additionalUsersAndGroups ...string) (string, error) {
|
||||
sddl := BasePermissions
|
||||
|
||||
// Grant generic read (GR) and write (GW) access to whatever
|
||||
// additional users or groups were specified.
|
||||
//
|
||||
// TODO(thaJeztah): should we fail on, or remove duplicates?
|
||||
for _, g := range additionalUsersAndGroups {
|
||||
sid, err := winio.LookupSidByName(strings.TrimSpace(g))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("looking up SID: %w", err)
|
||||
}
|
||||
sddl += fmt.Sprintf("(A;;GRGW;;;%s)", sid)
|
||||
}
|
||||
return sddl, nil
|
||||
}
|
||||
|
||||
func listenUnix(path string) (net.Listener, error) {
|
||||
return net.Listen("unix", path)
|
||||
|
||||
+7
-11
@@ -1,16 +1,12 @@
|
||||
package tlsconfig
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"runtime"
|
||||
)
|
||||
import "crypto/x509"
|
||||
|
||||
// SystemCertPool returns a copy of the system cert pool,
|
||||
// returns an error if failed to load or empty pool on windows.
|
||||
// SystemCertPool returns a copy of the system cert pool.
|
||||
//
|
||||
// Deprecated: use [x509.SystemCertPool] instead.
|
||||
//
|
||||
//go:fix inline
|
||||
func SystemCertPool() (*x509.CertPool, error) {
|
||||
certpool, err := x509.SystemCertPool()
|
||||
if err != nil && runtime.GOOS == "windows" {
|
||||
return x509.NewCertPool(), nil
|
||||
}
|
||||
return certpool, err
|
||||
return x509.SystemCertPool()
|
||||
}
|
||||
|
||||
+20
-8
@@ -34,6 +34,9 @@ type Options struct {
|
||||
// the system pool will be used.
|
||||
ExclusiveRootPools bool
|
||||
MinVersion uint16
|
||||
|
||||
// systemCertPool allows mocking the system cert-pool for testing.
|
||||
systemCertPool func() (*x509.CertPool, error)
|
||||
}
|
||||
|
||||
// DefaultServerAcceptedCiphers should be uses by code which already has a crypto/tls
|
||||
@@ -47,6 +50,8 @@ var defaultCipherSuites = []uint16{
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
}
|
||||
|
||||
// ServerDefault returns a secure-enough TLS configuration for the server TLS configuration.
|
||||
@@ -75,26 +80,33 @@ func defaultConfig(ops ...func(*tls.Config)) *tls.Config {
|
||||
}
|
||||
|
||||
// certPool returns an X.509 certificate pool from `caFile`, the certificate file.
|
||||
func certPool(caFile string, exclusivePool bool) (*x509.CertPool, error) {
|
||||
func certPool(opts Options) (*x509.CertPool, error) {
|
||||
// If we should verify the server, we need to load a trusted ca
|
||||
var (
|
||||
pool *x509.CertPool
|
||||
err error
|
||||
)
|
||||
if exclusivePool {
|
||||
if opts.ExclusiveRootPools {
|
||||
pool = x509.NewCertPool()
|
||||
} else {
|
||||
pool, err = SystemCertPool()
|
||||
if opts.systemCertPool != nil {
|
||||
pool, err = opts.systemCertPool()
|
||||
} else {
|
||||
pool, err = x509.SystemCertPool()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read system certificates: %v", err)
|
||||
}
|
||||
}
|
||||
pemData, err := os.ReadFile(caFile)
|
||||
if opts.CAFile == "" {
|
||||
return pool, nil
|
||||
}
|
||||
pemData, err := os.ReadFile(opts.CAFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read CA certificate %q: %v", caFile, err)
|
||||
return nil, fmt.Errorf("could not read CA certificate %q: %v", opts.CAFile, err)
|
||||
}
|
||||
if !pool.AppendCertsFromPEM(pemData) {
|
||||
return nil, fmt.Errorf("failed to append certificates from PEM file: %q", caFile)
|
||||
return nil, fmt.Errorf("failed to append certificates from PEM file: %q", opts.CAFile)
|
||||
}
|
||||
return pool, nil
|
||||
}
|
||||
@@ -197,7 +209,7 @@ func Client(options Options) (*tls.Config, error) {
|
||||
tlsConfig := defaultConfig()
|
||||
tlsConfig.InsecureSkipVerify = options.InsecureSkipVerify
|
||||
if !options.InsecureSkipVerify && options.CAFile != "" {
|
||||
CAs, err := certPool(options.CAFile, options.ExclusiveRootPools)
|
||||
CAs, err := certPool(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -230,7 +242,7 @@ func Server(options Options) (*tls.Config, error) {
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{tlsCert}
|
||||
if options.ClientAuth >= tls.VerifyClientCertIfGiven && options.CAFile != "" {
|
||||
CAs, err := certPool(options.CAFile, options.ExclusiveRootPools)
|
||||
CAs, err := certPool(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
-4
@@ -5,10 +5,6 @@ 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
|
||||
|
||||
+10
-198
@@ -3,25 +3,19 @@ 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.
|
||||
@@ -40,184 +34,15 @@ func (fs *ChrootHelper) underlyingPath(filename string) (string, error) {
|
||||
return fs.Join(fs.Root(), filename), nil
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) followedPath(filename string, followFinal bool, op string) (string, error) {
|
||||
fullpath, err := fs.underlyingPath(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
func isCrossBoundaries(path string) bool {
|
||||
path = filepath.ToSlash(path)
|
||||
path = filepath.Clean(path)
|
||||
|
||||
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, "../")
|
||||
return strings.HasPrefix(path, ".."+string(filepath.Separator))
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) Create(filename string) (billy.File, error) {
|
||||
fullpath, err := fs.followedPath(filename, true, "create")
|
||||
fullpath, err := fs.underlyingPath(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -231,7 +56,7 @@ func (fs *ChrootHelper) Create(filename string) (billy.File, error) {
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) Open(filename string) (billy.File, error) {
|
||||
fullpath, err := fs.followedPath(filename, true, "open")
|
||||
fullpath, err := fs.underlyingPath(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -245,7 +70,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.followedPath(filename, !isCreateExclusive(flag), "open")
|
||||
fullpath, err := fs.underlyingPath(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -259,16 +84,12 @@ func (fs *ChrootHelper) OpenFile(filename string, flag int, mode os.FileMode) (b
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) Stat(filename string) (os.FileInfo, error) {
|
||||
fullpath, err := fs.followedPath(filename, true, "stat")
|
||||
fullpath, err := fs.underlyingPath(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := fs.underlying.Stat(fullpath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileInfo{FileInfo: fi, name: filepath.Base(filename)}, nil
|
||||
return fs.underlying.Stat(fullpath)
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) Rename(from, to string) error {
|
||||
@@ -314,7 +135,7 @@ func (fs *ChrootHelper) TempFile(dir, prefix string) (billy.File, error) {
|
||||
}
|
||||
|
||||
func (fs *ChrootHelper) ReadDir(path string) ([]os.FileInfo, error) {
|
||||
fullpath, err := fs.followedPath(path, true, "readdir")
|
||||
fullpath, err := fs.underlyingPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -420,11 +241,6 @@ 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)
|
||||
@@ -438,7 +254,3 @@ 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
@@ -24,9 +24,6 @@ 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,
|
||||
@@ -50,8 +47,6 @@ 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
|
||||
|
||||
+15
-85
@@ -20,7 +20,6 @@
|
||||
package osfs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -30,31 +29,6 @@ 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.
|
||||
@@ -80,7 +54,6 @@ 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
|
||||
@@ -89,7 +62,6 @@ 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
|
||||
@@ -99,12 +71,6 @@ 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
|
||||
@@ -123,7 +89,6 @@ 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
|
||||
@@ -136,7 +101,6 @@ 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
|
||||
@@ -145,11 +109,6 @@ 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
|
||||
@@ -163,7 +122,6 @@ 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
|
||||
@@ -186,11 +144,6 @@ 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
|
||||
@@ -199,7 +152,6 @@ 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
|
||||
@@ -212,7 +164,6 @@ 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)
|
||||
@@ -224,7 +175,6 @@ 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))
|
||||
}
|
||||
@@ -235,7 +185,6 @@ 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
|
||||
@@ -250,7 +199,7 @@ func (fs *BoundOS) Chroot(path string) (billy.Filesystem, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return New(joined, WithBoundOS()), nil
|
||||
return New(joined), nil
|
||||
}
|
||||
|
||||
// Root returns the current base dir of the billy.Filesystem.
|
||||
@@ -271,37 +220,6 @@ 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`.
|
||||
@@ -315,7 +233,7 @@ func (fs *BoundOS) abs(filename string) (string, error) {
|
||||
|
||||
path, err := securejoin.SecureJoin(fs.baseDir, filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if fs.deduplicatePath {
|
||||
@@ -328,12 +246,24 @@ 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 == "/" || fs.baseDir == filename {
|
||||
if fs.baseDir == "/" {
|
||||
return true, nil
|
||||
}
|
||||
dir, err := filepath.EvalSymlinks(filepath.Dir(filename))
|
||||
|
||||
-10
@@ -14,8 +14,6 @@ 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.
|
||||
@@ -26,14 +24,6 @@ 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)
|
||||
}
|
||||
|
||||
|
||||
+24
-19
@@ -16,6 +16,8 @@ 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)
|
||||
}
|
||||
@@ -37,7 +39,7 @@ func removeAll(fs billy.Basic, path string) error {
|
||||
}
|
||||
|
||||
// Otherwise, is this a directory we need to recurse into?
|
||||
dir, serr := lstat(fs, path)
|
||||
dir, serr := fs.Stat(path)
|
||||
if serr != nil {
|
||||
if errors.Is(serr, os.ErrNotExist) {
|
||||
return nil
|
||||
@@ -46,8 +48,8 @@ func removeAll(fs billy.Basic, path string) error {
|
||||
return serr
|
||||
}
|
||||
|
||||
if dir.Mode()&os.ModeSymlink != 0 || !dir.IsDir() {
|
||||
// Not a directory we should recurse into; return the error from Remove.
|
||||
if !dir.IsDir() {
|
||||
// Not a directory; return the error from Remove.
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -60,7 +62,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 ReadDir.
|
||||
// Race. It was deleted between the Lstat and Open.
|
||||
// Return nil per RemoveAll's docs.
|
||||
return nil
|
||||
}
|
||||
@@ -89,18 +91,7 @@ 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.
|
||||
@@ -132,10 +123,8 @@ 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
|
||||
randmu sync.Mutex
|
||||
)
|
||||
var rand uint32
|
||||
var randmu sync.Mutex
|
||||
|
||||
func reseed() uint32 {
|
||||
return uint32(time.Now().UnixNano() + int64(os.Getpid()))
|
||||
@@ -231,6 +220,22 @@ 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
|
||||
|
||||
-1
@@ -5,4 +5,3 @@ profile.out
|
||||
.tmp/
|
||||
.git-dist/
|
||||
.vscode
|
||||
build/tools/
|
||||
|
||||
+1
-34
@@ -61,16 +61,6 @@ 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 {
|
||||
@@ -276,8 +266,6 @@ 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.
|
||||
@@ -321,18 +309,6 @@ 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
|
||||
}
|
||||
|
||||
if s.Options.Get(repositoryFormatVersionKey) == string(format.Version_1) {
|
||||
c.Core.RepositoryFormatVersion = format.Version_1
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) unmarshalUser() {
|
||||
@@ -403,8 +379,7 @@ func unmarshalSubmodules(fc *format.Config, submodules map[string]*Submodule) {
|
||||
m := &Submodule{}
|
||||
m.unmarshal(sub)
|
||||
|
||||
if err := m.Validate(); errors.Is(err, ErrModuleBadPath) ||
|
||||
errors.Is(err, ErrModuleBadName) {
|
||||
if m.Validate() == ErrModuleBadPath {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -461,14 +436,6 @@ 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
@@ -3,11 +3,8 @@ 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"
|
||||
)
|
||||
|
||||
@@ -15,7 +12,6 @@ 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 (
|
||||
@@ -98,10 +94,6 @@ 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
|
||||
}
|
||||
@@ -117,50 +109,6 @@ 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
@@ -1,82 +0,0 @@
|
||||
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
@@ -1,21 +0,0 @@
|
||||
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
@@ -1,99 +0,0 @@
|
||||
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
@@ -1,187 +0,0 @@
|
||||
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
@@ -1,66 +0,0 @@
|
||||
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
|
||||
}
|
||||
+2
-35
@@ -2,14 +2,12 @@ package url
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
isSchemeRegExp = regexp.MustCompile(`^[^:]+://`)
|
||||
|
||||
// Ref: https://github.com/git/git/blob/v2.54.0/Documentation/urls.adoc#L41-L48
|
||||
// Ref: https://github.com/git/git/blob/master/Documentation/urls.txt#L37
|
||||
scpLikeUrlRegExp = regexp.MustCompile(`^(?:(?P<user>[^@]+)@)?(?P<host>[^:\s]+):(?:(?P<port>[0-9]{1,5}):)?(?P<path>[^\\].*)$`)
|
||||
)
|
||||
|
||||
@@ -22,38 +20,7 @@ func MatchesScheme(url string) bool {
|
||||
// MatchesScpLike returns true if the given string matches an SCP-like
|
||||
// format scheme.
|
||||
func MatchesScpLike(url string) bool {
|
||||
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')
|
||||
return scpLikeUrlRegExp.MatchString(url)
|
||||
}
|
||||
|
||||
// FindScpLikeComponents returns the user, host, port and path of the
|
||||
|
||||
+5
-159
@@ -7,7 +7,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/hash"
|
||||
"github.com/go-git/go-git/v5/utils/binary"
|
||||
@@ -26,88 +25,35 @@ 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
|
||||
src io.Reader
|
||||
h hash.Hash
|
||||
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, r, h}
|
||||
return &Decoder{tr, 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
|
||||
}
|
||||
|
||||
headerFlow := []func(*MemoryIndex, io.Reader) error{
|
||||
flow := []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 bodyFlow {
|
||||
|
||||
for _, f := range flow {
|
||||
if err := f(idx, d); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -253,103 +199,3 @@ 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
|
||||
}
|
||||
|
||||
+8
-21
@@ -2,7 +2,6 @@ package idxfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"sync"
|
||||
@@ -127,10 +126,7 @@ func (idx *MemoryIndex) FindOffset(h plumbing.Hash) (int64, error) {
|
||||
return 0, plumbing.ErrObjectNotFound
|
||||
}
|
||||
|
||||
offset, err := idx.getOffset(k, i)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
offset := idx.getOffset(k, i)
|
||||
|
||||
// Save the offset for reverse lookup
|
||||
idx.mu.Lock()
|
||||
@@ -145,19 +141,17 @@ func (idx *MemoryIndex) FindOffset(h plumbing.Hash) (int64, error) {
|
||||
|
||||
const isO64Mask = uint64(1) << 31
|
||||
|
||||
func (idx *MemoryIndex) getOffset(firstLevel, secondLevel int) (uint64, error) {
|
||||
func (idx *MemoryIndex) getOffset(firstLevel, secondLevel int) uint64 {
|
||||
offset := secondLevel << 2
|
||||
ofs := encbin.BigEndian.Uint32(idx.Offset32[firstLevel][offset : offset+4])
|
||||
|
||||
if (uint64(ofs) & isO64Mask) != 0 {
|
||||
offset := 8 * (uint64(ofs) & ^isO64Mask)
|
||||
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
|
||||
n := encbin.BigEndian.Uint64(idx.Offset64[offset : offset+8])
|
||||
return n
|
||||
}
|
||||
|
||||
return uint64(ofs), nil
|
||||
return uint64(ofs)
|
||||
}
|
||||
|
||||
// FindCRC32 implements the Index interface.
|
||||
@@ -215,11 +209,8 @@ func (idx *MemoryIndex) genOffsetHash() error {
|
||||
mappedFirstLevel := idx.FanoutMapping[firstLevel]
|
||||
for secondLevel := uint32(0); i < fanoutValue; i++ {
|
||||
copy(hash[:], idx.Names[mappedFirstLevel][secondLevel*objectIDLength:])
|
||||
off, err := idx.getOffset(mappedFirstLevel, int(secondLevel))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
offsetHash[int64(off)] = hash
|
||||
offset := int64(idx.getOffset(mappedFirstLevel, int(secondLevel)))
|
||||
offsetHash[offset] = hash
|
||||
secondLevel++
|
||||
}
|
||||
}
|
||||
@@ -300,11 +291,7 @@ 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:])
|
||||
var err error
|
||||
entry.Offset, err = i.idx.getOffset(mappedFirstLevel, i.secondLevel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry.Offset = i.idx.getOffset(mappedFirstLevel, i.secondLevel)
|
||||
entry.CRC32 = i.idx.getCRC32(mappedFirstLevel, i.secondLevel)
|
||||
|
||||
i.secondLevel++
|
||||
|
||||
+3
-15
@@ -11,10 +11,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
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")
|
||||
ErrClosed = errors.New("objfile: already closed")
|
||||
ErrHeader = errors.New("objfile: invalid header")
|
||||
ErrNegativeSize = errors.New("objfile: negative object size")
|
||||
)
|
||||
|
||||
// Reader reads and decodes compressed objfile data from a provided io.Reader.
|
||||
@@ -101,23 +100,12 @@ 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
-25
@@ -139,12 +139,8 @@ func (dw *deltaSelector) fixAndBreakChains(objectsToPack []*ObjectToPack) error
|
||||
m[otp.Hash()] = otp
|
||||
}
|
||||
|
||||
// visiting holds the objects on the current resolution path, so that a
|
||||
// delta chain looping back on itself can be detected and broken.
|
||||
visiting := make(map[plumbing.Hash]bool)
|
||||
|
||||
for _, otp := range objectsToPack {
|
||||
if err := dw.fixAndBreakChainsOne(m, otp, visiting); err != nil {
|
||||
if err := dw.fixAndBreakChainsOne(m, otp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -152,11 +148,7 @@ func (dw *deltaSelector) fixAndBreakChains(objectsToPack []*ObjectToPack) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dw *deltaSelector) fixAndBreakChainsOne(
|
||||
objectsToPack map[plumbing.Hash]*ObjectToPack,
|
||||
otp *ObjectToPack,
|
||||
visiting map[plumbing.Hash]bool,
|
||||
) error {
|
||||
func (dw *deltaSelector) fixAndBreakChainsOne(objectsToPack map[plumbing.Hash]*ObjectToPack, otp *ObjectToPack) error {
|
||||
if !otp.Object.Type().IsDelta() {
|
||||
return nil
|
||||
}
|
||||
@@ -182,21 +174,7 @@ func (dw *deltaSelector) fixAndBreakChainsOne(
|
||||
return dw.undeltify(otp)
|
||||
}
|
||||
|
||||
// Mark this object as being resolved before looking at its base, so that
|
||||
// a delta based on itself is caught by the check below.
|
||||
h := otp.Hash()
|
||||
visiting[h] = true
|
||||
defer delete(visiting, h)
|
||||
|
||||
// A delta chain that loops back onto an object we are already resolving
|
||||
// cannot be written: every delta needs its base written first. Break the
|
||||
// chain here instead of following the cycle, which would recurse until
|
||||
// the goroutine stack is exhausted.
|
||||
if visiting[do.BaseHash()] {
|
||||
return dw.undeltify(otp)
|
||||
}
|
||||
|
||||
if err := dw.fixAndBreakChainsOne(objectsToPack, base, visiting); err != nil {
|
||||
if err := dw.fixAndBreakChainsOne(objectsToPack, base); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -19,6 +19,9 @@ 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,
|
||||
|
||||
+1
-7
@@ -78,13 +78,7 @@ func (o *FSObject) Reader() (io.ReadCloser, error) {
|
||||
_ = f.Close()
|
||||
return nil, err
|
||||
}
|
||||
// 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
|
||||
return ioutil.NewReadCloserWithCloser(r, f.Close), nil
|
||||
}
|
||||
r, err := p.getObjectContent(o.offset)
|
||||
if err != nil {
|
||||
|
||||
+6
-15
@@ -126,17 +126,11 @@ func (p *Packfile) nextObjectHeader() (*ObjectHeader, error) {
|
||||
return h, err
|
||||
}
|
||||
|
||||
func (p *Packfile) getDeltaObjectSize(buf *bytes.Buffer) (int64, error) {
|
||||
func (p *Packfile) getDeltaObjectSize(buf *bytes.Buffer) int64 {
|
||||
delta := buf.Bytes()
|
||||
_, 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
|
||||
_, delta = decodeLEB128(delta) // skip src size
|
||||
sz, _ := decodeLEB128(delta)
|
||||
return int64(sz)
|
||||
}
|
||||
|
||||
func (p *Packfile) getObjectSize(h *ObjectHeader) (int64, error) {
|
||||
@@ -151,7 +145,7 @@ func (p *Packfile) getObjectSize(h *ObjectHeader) (int64, error) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return p.getDeltaObjectSize(buf)
|
||||
return p.getDeltaObjectSize(buf), nil
|
||||
default:
|
||||
return 0, ErrInvalidObject.AddDetails("type %q", h.Type)
|
||||
}
|
||||
@@ -239,10 +233,7 @@ func (p *Packfile) getNextObject(h *ObjectHeader, hash plumbing.Hash) (plumbing.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
size, err = p.getDeltaObjectSize(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
size = p.getDeltaObjectSize(buf)
|
||||
if size <= smallObjectThreshold {
|
||||
var obj = new(plumbing.MemoryObject)
|
||||
obj.SetSize(size)
|
||||
|
||||
+7
-65
@@ -26,45 +26,6 @@ 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.
|
||||
@@ -205,10 +166,9 @@ func (p *Parser) init() error {
|
||||
}
|
||||
|
||||
p.count = c
|
||||
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)
|
||||
p.oiByHash = make(map[plumbing.Hash]*objectInfo, p.count)
|
||||
p.oiByOffset = make(map[int64]*objectInfo, p.count)
|
||||
p.oi = make([]*objectInfo, p.count)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -301,7 +261,7 @@ func (p *Parser) indexObjects() error {
|
||||
}
|
||||
if delta && !p.scanner.IsSeekable {
|
||||
buf.Reset()
|
||||
buf.Grow(growHint(oh.Length))
|
||||
buf.Grow(int(oh.Length))
|
||||
writers = append(writers, buf)
|
||||
}
|
||||
|
||||
@@ -346,7 +306,7 @@ func (p *Parser) indexObjects() error {
|
||||
}
|
||||
|
||||
p.oiByOffset[oh.Offset] = ota
|
||||
p.oi = append(p.oi, ota)
|
||||
p.oi[i] = ota
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -357,12 +317,8 @@ 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(growHint(obj.Length))
|
||||
buf.Grow(int(obj.Length))
|
||||
err := p.get(obj, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -381,9 +337,6 @@ 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.
|
||||
@@ -403,17 +356,6 @@ 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
|
||||
@@ -463,7 +405,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(growHint(o.Length))
|
||||
buf.Grow(int(o.Length))
|
||||
err := p.get(o.Parent, b)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+40
-71
@@ -31,15 +31,10 @@ const (
|
||||
// premptively made available for a patch operation.
|
||||
maxPatchPreemptionSize uint = 65536
|
||||
|
||||
// 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
|
||||
// minDeltaSize defines the smallest size for a delta.
|
||||
minDeltaSize = 4
|
||||
)
|
||||
|
||||
// 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
|
||||
@@ -147,7 +142,7 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo
|
||||
baseBuf := bufio.NewReader(baseRd)
|
||||
basePos := uint(0)
|
||||
|
||||
for remainingTargetSz > 0 {
|
||||
for {
|
||||
cmd, err := deltaBuf.ReadByte()
|
||||
if err == io.EOF {
|
||||
_ = dstWr.CloseWithError(ErrInvalidDelta)
|
||||
@@ -171,9 +166,9 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo
|
||||
return
|
||||
}
|
||||
|
||||
if invalidSize(sz, remainingTargetSz) ||
|
||||
if invalidSize(sz, targetSz) ||
|
||||
invalidOffsetSize(offset, sz, srcSz) {
|
||||
_ = dstWr.CloseWithError(ErrInvalidDelta)
|
||||
_ = dstWr.Close()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -215,7 +210,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, remainingTargetSz) {
|
||||
if invalidSize(sz, targetSz) {
|
||||
_ = dstWr.CloseWithError(ErrInvalidDelta)
|
||||
return
|
||||
}
|
||||
@@ -230,48 +225,40 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo
|
||||
_ = dstWr.CloseWithError(ErrDeltaCmd)
|
||||
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
|
||||
if remainingTargetSz <= 0 {
|
||||
_ = dstWr.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_ = dstWr.Close()
|
||||
}()
|
||||
|
||||
return dstRd, nil
|
||||
}
|
||||
|
||||
func patchDelta(dst *bytes.Buffer, src, delta []byte) error {
|
||||
srcSz, delta, err := decodeLEB128(delta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrInvalidDelta, err)
|
||||
if len(delta) < minCopySize {
|
||||
return ErrInvalidDelta
|
||||
}
|
||||
|
||||
srcSz, delta := decodeLEB128(delta)
|
||||
if srcSz != uint(len(src)) {
|
||||
return ErrInvalidDelta
|
||||
}
|
||||
|
||||
targetSz, delta, err := decodeLEB128(delta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrInvalidDelta, err)
|
||||
}
|
||||
targetSz, delta := decodeLEB128(delta)
|
||||
remainingTargetSz := targetSz
|
||||
|
||||
var cmd byte
|
||||
|
||||
growSz := min(targetSz, maxPatchPreemptionSize)
|
||||
dst.Grow(int(growSz))
|
||||
|
||||
for remainingTargetSz > 0 {
|
||||
for {
|
||||
if len(delta) == 0 {
|
||||
return ErrInvalidDelta
|
||||
}
|
||||
|
||||
cmd := delta[0]
|
||||
cmd = delta[0]
|
||||
delta = delta[1:]
|
||||
|
||||
switch {
|
||||
@@ -288,16 +275,16 @@ func patchDelta(dst *bytes.Buffer, src, delta []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if invalidSize(sz, remainingTargetSz) ||
|
||||
if invalidSize(sz, targetSz) ||
|
||||
invalidOffsetSize(offset, sz, srcSz) {
|
||||
return ErrInvalidDelta
|
||||
break
|
||||
}
|
||||
dst.Write(src[offset : offset+sz])
|
||||
remainingTargetSz -= sz
|
||||
|
||||
case isCopyFromDelta(cmd):
|
||||
sz := uint(cmd) // cmd is the size itself
|
||||
if invalidSize(sz, remainingTargetSz) {
|
||||
if invalidSize(sz, targetSz) {
|
||||
return ErrInvalidDelta
|
||||
}
|
||||
|
||||
@@ -312,12 +299,10 @@ func patchDelta(dst *bytes.Buffer, src, delta []byte) error {
|
||||
default:
|
||||
return ErrDeltaCmd
|
||||
}
|
||||
}
|
||||
|
||||
// Mirror upstream's `data != top` post-loop check: every byte of
|
||||
// the delta payload must be consumed.
|
||||
if len(delta) != 0 {
|
||||
return ErrInvalidDelta
|
||||
if remainingTargetSz <= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -369,7 +354,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 remainingTargetSz > 0 {
|
||||
for {
|
||||
buf := *bufp
|
||||
cmd, err := deltaBuf.ReadByte()
|
||||
if err == io.EOF {
|
||||
@@ -389,9 +374,9 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader,
|
||||
return 0, plumbing.ZeroHash, err
|
||||
}
|
||||
|
||||
if invalidSize(sz, remainingTargetSz) ||
|
||||
if invalidSize(sz, targetSz) ||
|
||||
invalidOffsetSize(offset, sz, srcSz) {
|
||||
return 0, plumbing.ZeroHash, ErrInvalidDelta
|
||||
return 0, plumbing.ZeroHash, err
|
||||
}
|
||||
|
||||
if _, err := sr.Seek(int64(offset), io.SeekStart); err != nil {
|
||||
@@ -404,7 +389,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, remainingTargetSz) {
|
||||
if invalidSize(sz, targetSz) {
|
||||
return 0, plumbing.ZeroHash, ErrInvalidDelta
|
||||
}
|
||||
deltalr.N = int64(sz)
|
||||
@@ -414,41 +399,30 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader,
|
||||
|
||||
remainingTargetSz -= sz
|
||||
} else {
|
||||
return 0, plumbing.ZeroHash, ErrDeltaCmd
|
||||
return 0, plumbing.ZeroHash, err
|
||||
}
|
||||
if remainingTargetSz <= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 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, the rest of the stream,
|
||||
// and an error if the encoded value does not fit in a uint.
|
||||
// binary data and returns the decoded number and the rest of the
|
||||
// stream.
|
||||
//
|
||||
// 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, error) {
|
||||
func decodeLEB128(input []byte) (uint, []byte) {
|
||||
if len(input) == 0 {
|
||||
return 0, input, nil
|
||||
return 0, input
|
||||
}
|
||||
|
||||
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++
|
||||
@@ -458,16 +432,12 @@ func decodeLEB128(input []byte) (uint, []byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return num, input[sz:], nil
|
||||
return num, input[sz:]
|
||||
}
|
||||
|
||||
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
|
||||
@@ -559,9 +529,8 @@ func decodeSize(cmd byte, delta []byte) (uint, []byte, error) {
|
||||
return sz, delta, nil
|
||||
}
|
||||
|
||||
// invalidSize reports whether sz exceeds the remaining target size.
|
||||
func invalidSize(sz, remaining uint) bool {
|
||||
return sz > remaining
|
||||
func invalidSize(sz, targetSz uint) bool {
|
||||
return sz > targetSz
|
||||
}
|
||||
|
||||
func invalidOffsetSize(offset, sz, srcSz uint) bool {
|
||||
|
||||
+9
-145
@@ -29,100 +29,8 @@ 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 {
|
||||
@@ -312,13 +220,6 @@ 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
|
||||
@@ -402,13 +303,6 @@ 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
|
||||
}
|
||||
@@ -421,18 +315,10 @@ 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.
|
||||
//
|
||||
// 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.
|
||||
// the number of bytes written, the CRC32 of the content and an error, if any
|
||||
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, declaredSize)
|
||||
written, err = s.copyObject(w)
|
||||
|
||||
s.r.Flush()
|
||||
crc32 = s.crc.Sum32()
|
||||
@@ -441,39 +327,23 @@ 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.
|
||||
//
|
||||
// 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.
|
||||
// ReadObject returns a reader for the object content and an error
|
||||
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)
|
||||
}
|
||||
|
||||
rc := ioutil.NewReadCloserWithCloser(zr.Reader, func() error {
|
||||
return ioutil.NewReadCloserWithCloser(zr.Reader, func() error {
|
||||
sync.PutZlibReader(zr)
|
||||
return nil
|
||||
})
|
||||
if declaredSize >= 0 {
|
||||
return newBoundedReadCloser(rc, declaredSize), nil
|
||||
}
|
||||
return rc, nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 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) {
|
||||
zr, err := sync.GetZlibReader(s.r)
|
||||
defer sync.PutZlibReader(zr)
|
||||
|
||||
@@ -482,14 +352,8 @@ func (s *Scanner) copyObject(w io.Writer, declaredSize int64) (n int64, err erro
|
||||
}
|
||||
|
||||
defer ioutil.CheckClose(zr.Reader, &err)
|
||||
|
||||
sink := w
|
||||
if declaredSize >= 0 {
|
||||
sink = &boundedWriter{w: w, limit: declaredSize}
|
||||
}
|
||||
|
||||
buf := sync.GetByteSlice()
|
||||
n, err = io.CopyBuffer(sink, zr.Reader, *buf)
|
||||
n, err = io.CopyBuffer(w, zr.Reader, *buf)
|
||||
sync.PutByteSlice(buf)
|
||||
return
|
||||
}
|
||||
|
||||
+94
-113
@@ -5,7 +5,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
@@ -20,7 +20,6 @@ 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
|
||||
@@ -42,11 +41,6 @@ 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
|
||||
@@ -72,9 +66,6 @@ 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
|
||||
@@ -107,8 +98,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: "",
|
||||
}
|
||||
|
||||
@@ -190,11 +181,6 @@ 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 {
|
||||
@@ -241,23 +227,14 @@ 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.src = o
|
||||
c.Encoding = defaultUtf8CommitMessageEncoding
|
||||
|
||||
reader, err := o.Reader()
|
||||
if err != nil {
|
||||
@@ -268,17 +245,97 @@ func (c *Commit) Decode(o plumbing.EncodedObject) (err error) {
|
||||
r := sync.GetBufioReader(reader)
|
||||
defer sync.PutBufioReader(r)
|
||||
|
||||
s := &commitScanner{r: r, c: c}
|
||||
for state := scanTree; state != nil; {
|
||||
state, err = state(s)
|
||||
if err != nil {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
if !s.sawTree {
|
||||
return fmt.Errorf("%w: missing tree header", ErrMalformedCommit)
|
||||
}
|
||||
c.Message = s.msgbuf.String()
|
||||
c.Message = msgbuf.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -287,73 +344,11 @@ func (c *Commit) Encode(o plumbing.EncodedObject) error {
|
||||
return c.encode(o, true)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// EncodeWithoutSignature export a Commit into a plumbing.EncodedObject without the signature (correspond to the payload of the PGP signature).
|
||||
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()
|
||||
@@ -412,9 +407,7 @@ 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
|
||||
}
|
||||
@@ -485,21 +478,9 @@ 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
@@ -1,377 +0,0 @@
|
||||
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
|
||||
}
|
||||
+1
-121
@@ -1,13 +1,6 @@
|
||||
package object
|
||||
|
||||
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"
|
||||
)
|
||||
import "bytes"
|
||||
|
||||
const (
|
||||
signatureTypeUnknown signatureType = iota
|
||||
@@ -107,116 +100,3 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
-90
@@ -1,8 +1,9 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
@@ -12,10 +13,6 @@ 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
|
||||
@@ -42,9 +39,6 @@ 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.
|
||||
@@ -83,20 +77,13 @@ 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 {
|
||||
@@ -107,15 +94,42 @@ func (t *Tag) Decode(o plumbing.EncodedObject) (err error) {
|
||||
r := sync.GetBufioReader(reader)
|
||||
defer sync.PutBufioReader(r)
|
||||
|
||||
scanner := &tagScanner{r: r, t: t}
|
||||
for state := scanTagObject; state != nil; {
|
||||
state, err = state(scanner)
|
||||
if err != nil {
|
||||
for {
|
||||
var line []byte
|
||||
line, err = r.ReadBytes('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
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 := scanner.msgbuf.Bytes()
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sm, _ := parseSignedBytes(data); sm >= 0 {
|
||||
t.PGPSignature = string(data[sm:])
|
||||
data = data[:sm]
|
||||
@@ -130,54 +144,11 @@ func (t *Tag) Encode(o plumbing.EncodedObject) error {
|
||||
return t.encode(o, true)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// EncodeWithoutSignature export a Tag into a plumbing.EncodedObject without the signature (correspond to the payload of the PGP signature).
|
||||
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()
|
||||
@@ -187,26 +158,16 @@ 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\n",
|
||||
"object %s\ntype %s\ntag %s\ntagger ",
|
||||
t.Target.String(), t.TargetType.Bytes(), t.Name); 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 = t.Tagger.Encode(w); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprint(w, "\n"); err != nil {
|
||||
if _, err = fmt.Fprint(w, "\n\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -214,12 +175,11 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
if includeSig {
|
||||
if _, err = fmt.Fprint(w, t.PGPSignature); err != nil {
|
||||
return err
|
||||
@@ -229,10 +189,6 @@ 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) {
|
||||
@@ -300,8 +256,7 @@ 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
@@ -1,237 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-152
@@ -10,7 +10,6 @@ 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"
|
||||
@@ -30,7 +29,6 @@ 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
|
||||
@@ -39,9 +37,9 @@ type Tree struct {
|
||||
Entries []TreeEntry
|
||||
Hash plumbing.Hash
|
||||
|
||||
s storer.EncodedObjectStorer
|
||||
t map[string]*Tree // tree path cache
|
||||
entriesSorted bool
|
||||
s storer.EncodedObjectStorer
|
||||
m map[string]*TreeEntry
|
||||
t map[string]*Tree // tree path cache
|
||||
}
|
||||
|
||||
// GetTree gets a tree from an object storer and decodes it.
|
||||
@@ -119,16 +117,7 @@ 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
|
||||
@@ -138,16 +127,7 @@ 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)
|
||||
}
|
||||
@@ -202,43 +182,16 @@ func (t *Tree) dir(baseName string) (*Tree, error) {
|
||||
}
|
||||
|
||||
func (t *Tree) entry(baseName string) (*TreeEntry, error) {
|
||||
if t.entriesSorted {
|
||||
if entry := t.searchEntry(baseName); entry != nil {
|
||||
return entry, nil
|
||||
}
|
||||
if t.m == nil {
|
||||
t.buildMap()
|
||||
}
|
||||
|
||||
entry, ok := t.m[baseName]
|
||||
if !ok {
|
||||
return nil, ErrEntryNotFound
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// Files returns a FileIter allowing to iterate over the Tree
|
||||
@@ -259,25 +212,20 @@ 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
|
||||
@@ -287,14 +235,10 @@ 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
|
||||
}
|
||||
|
||||
@@ -304,41 +248,25 @@ func (t *Tree) Decode(o plumbing.EncodedObject) (err error) {
|
||||
|
||||
mode, err := filemode.New(str)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: malformed mode", ErrMalformedTree)
|
||||
}
|
||||
mode = canonicalTreeMode(mode)
|
||||
|
||||
name, err := r.ReadString(0)
|
||||
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)
|
||||
|
||||
name, err := r.ReadString(0)
|
||||
if err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
|
||||
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]
|
||||
entry := TreeEntry{
|
||||
t.Entries = append(t.Entries, 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
|
||||
@@ -351,37 +279,21 @@ func (s TreeEntrySorter) Len() int {
|
||||
}
|
||||
|
||||
func (s TreeEntrySorter) Less(i, j int) bool {
|
||||
return treeEntrySortName(&s[i]) < treeEntrySortName(&s[j])
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -417,6 +329,13 @@ 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)
|
||||
@@ -474,14 +393,6 @@ type TreeWalker struct {
|
||||
recursive bool
|
||||
seen map[plumbing.Hash]bool
|
||||
|
||||
// skipPathValidation disables the pathutil.ValidTreePath check in Next.
|
||||
// It is set by inspection-only callers (e.g. the revlist object walk)
|
||||
// that never funnel entry names into the filesystem and must enumerate
|
||||
// trees faithfully, including entries with names upstream Git accepts
|
||||
// but that are unsafe to materialise (control characters, `.git`-shaped
|
||||
// names).
|
||||
skipPathValidation bool
|
||||
|
||||
s storer.EncodedObjectStorer
|
||||
t *Tree
|
||||
}
|
||||
@@ -504,32 +415,10 @@ func NewTreeWalker(t *Tree, recursive bool, seen map[plumbing.Hash]bool) *TreeWa
|
||||
}
|
||||
}
|
||||
|
||||
// SkipPathValidation disables the pathutil.ValidTreePath check performed by
|
||||
// Next, and must be called before the first Next.
|
||||
//
|
||||
// It is for inspection-only walks that never funnel an entry name into the
|
||||
// filesystem — enumerating which objects exist, rather than materialising
|
||||
// them — and that must therefore see the tree faithfully, including entries
|
||||
// whose names upstream Git accepts but that are unsafe to check out. Callers
|
||||
// that hand the returned name to filesystem or archive output must not use
|
||||
// it; path safety for those is enforced at the materialisation boundaries
|
||||
// (FindEntry, TreeEntryFile, archive, FileIter).
|
||||
func (w *TreeWalker) SkipPathValidation() {
|
||||
w.skipPathValidation = true
|
||||
}
|
||||
|
||||
// Next returns the next object from the tree. Objects are returned in order
|
||||
// and subtrees are included. After the last object has been returned further
|
||||
// calls to Next() will return io.EOF.
|
||||
//
|
||||
// Each entry's name is validated against pathutil.ValidTreePath as it
|
||||
// surfaces, so callers that funnel the returned name into filesystem
|
||||
// or archive output can trust it is free of `.git`-shaped components,
|
||||
// HFS+/NTFS variants, Windows reserved names, and traversal sequences.
|
||||
// A malformed entry stops the walk with the validator's error;
|
||||
// inspection-only callers that need to enumerate raw, unvalidated
|
||||
// names can read Tree.Entries directly or call SkipPathValidation.
|
||||
//
|
||||
// In the current implementation any objects which cannot be found in the
|
||||
// underlying repository will be skipped automatically. It is possible that this
|
||||
// may change in future versions.
|
||||
@@ -566,12 +455,6 @@ func (w *TreeWalker) Next() (name string, entry TreeEntry, err error) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !w.skipPathValidation {
|
||||
if err := pathutil.ValidTreePath(entry.Name); err != nil {
|
||||
return name, entry, err
|
||||
}
|
||||
}
|
||||
|
||||
if entry.Mode == filemode.Dir {
|
||||
obj, err = GetTree(w.s, entry.Hash)
|
||||
}
|
||||
|
||||
-6
@@ -106,12 +106,6 @@ func transformChildren(t *Tree) ([]noder.Noder, error) {
|
||||
ret := make([]noder.Noder, 0, len(t.Entries))
|
||||
|
||||
walker := NewTreeWalker(t, false, nil) // don't recurse
|
||||
// The diff walk is read-only and never materialises entry names into the
|
||||
// filesystem, so it must enumerate the tree faithfully — including entries
|
||||
// with names that are unsafe to check out but valid per upstream Git (e.g.
|
||||
// control characters). Path safety is enforced at materialisation
|
||||
// boundaries (FindEntry, TreeEntryFile, archive, FileIter), not here.
|
||||
walker.SkipPathValidation()
|
||||
// don't defer walker.Close() for efficiency reasons.
|
||||
for {
|
||||
_, e, err = walker.Next()
|
||||
|
||||
-42
@@ -110,48 +110,6 @@ func (r ReferenceName) IsTag() bool {
|
||||
return strings.HasPrefix(string(r), refTagPrefix)
|
||||
}
|
||||
|
||||
// IsSafe reports whether the reference name can be safely turned into a path
|
||||
// under the .git directory, mirroring Git's refname_is_safe (refs.c). A name
|
||||
// is safe when it is either:
|
||||
//
|
||||
// - under "refs/", non-empty after the prefix, containing no backslash and
|
||||
// no empty, "." or ".." path component (so it cannot escape the refs/
|
||||
// sub-tree, or alias another name, once turned into a path); or
|
||||
// - a one-level pseudo-ref whose spelling is restricted to [A-Z_]
|
||||
// (e.g. HEAD, ORIG_HEAD, FETCH_HEAD).
|
||||
//
|
||||
// Everything else — a lowercase or mixed one-level name such as "config" or
|
||||
// "index", an absolute or drive-prefixed name, or a refs/ name that escapes —
|
||||
// is unsafe, because it could resolve onto unrelated repository metadata.
|
||||
func (r ReferenceName) IsSafe() bool {
|
||||
s := string(r)
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if rest, ok := strings.CutPrefix(s, refPrefix); ok {
|
||||
// '\' is a path separator on Windows, so a refs/ name containing one
|
||||
// could escape the sub-tree or alias another name once turned into a
|
||||
// path; reject it outright (check_refname_format forbids '\' too).
|
||||
if rest == "" || strings.Contains(rest, "\\") {
|
||||
return false
|
||||
}
|
||||
for part := range strings.SplitSeq(rest, "/") {
|
||||
if part == "" || part == "." || part == ".." {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
for i := 0; i < len(s); i++ {
|
||||
if (s[i] < 'A' || s[i] > 'Z') && s[i] != '_' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (r ReferenceName) String() string {
|
||||
return string(r)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user