mirror of
https://github.com/cheat/cheat.git
synced 2026-05-27 19:48:44 +02:00
chore(deps): bump github.com/go-git/go-git/v5 from 5.16.5 to 5.19.0
Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.16.5 to 5.19.0. - [Release notes](https://github.com/go-git/go-git/releases) - [Changelog](https://github.com/go-git/go-git/blob/main/HISTORY.md) - [Commits](https://github.com/go-git/go-git/compare/v5.16.5...v5.19.0) --- updated-dependencies: - dependency-name: github.com/go-git/go-git/v5 dependency-version: 5.19.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
@@ -5,7 +5,7 @@ go 1.26
|
|||||||
require (
|
require (
|
||||||
github.com/alecthomas/chroma/v2 v2.23.1
|
github.com/alecthomas/chroma/v2 v2.23.1
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/go-git/go-git/v5 v5.16.5
|
github.com/go-git/go-git/v5 v5.19.0
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
@@ -21,19 +21,19 @@ require (
|
|||||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.7.0 // indirect
|
github.com/go-git/go-billy/v5 v5.9.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.5.0 // indirect
|
github.com/kevinburke/ssh_config v1.5.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||||
github.com/sergi/go-diff v1.4.0 // indirect
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||||
github.com/spf13/pflag v1.0.9 // indirect
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
golang.org/x/crypto v0.48.0 // indirect
|
golang.org/x/crypto v0.50.0 // indirect
|
||||||
golang.org/x/net v0.50.0 // indirect
|
golang.org/x/net v0.53.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
|||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
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 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
|
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
|
||||||
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
|
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 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-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
|
github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc=
|
||||||
github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
|
github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
@@ -66,8 +66,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
|
|||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
||||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
@@ -93,13 +93,13 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM
|
|||||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -107,14 +107,14 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
+4
@@ -5,6 +5,10 @@ Billy implements an interface based on the `os` standard library, allowing to de
|
|||||||
|
|
||||||
Billy was born as part of [go-git/go-git](https://github.com/go-git/go-git) project.
|
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
|
## Installation
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|||||||
+198
-10
@@ -3,19 +3,25 @@ package chroot
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/go-git/go-billy/v5"
|
"github.com/go-git/go-billy/v5"
|
||||||
"github.com/go-git/go-billy/v5/helper/polyfill"
|
"github.com/go-git/go-billy/v5/helper/polyfill"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ChrootHelper is a helper to implement billy.Chroot.
|
// 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 {
|
type ChrootHelper struct {
|
||||||
underlying billy.Filesystem
|
underlying billy.Filesystem
|
||||||
base string
|
base string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxFollowedSymlinks = 8 // Aligns with POSIX_SYMLOOP_MAX
|
||||||
|
|
||||||
// New creates a new filesystem wrapping up the given 'fs'.
|
// New creates a new filesystem wrapping up the given 'fs'.
|
||||||
// The created filesystem has its base in the given ChrootHelperectory of the
|
// The created filesystem has its base in the given ChrootHelperectory of the
|
||||||
// underlying filesystem.
|
// underlying filesystem.
|
||||||
@@ -34,15 +40,184 @@ func (fs *ChrootHelper) underlyingPath(filename string) (string, error) {
|
|||||||
return fs.Join(fs.Root(), filename), nil
|
return fs.Join(fs.Root(), filename), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isCrossBoundaries(path string) bool {
|
func (fs *ChrootHelper) followedPath(filename string, followFinal bool, op string) (string, error) {
|
||||||
path = filepath.ToSlash(path)
|
fullpath, err := fs.underlyingPath(filename)
|
||||||
path = filepath.Clean(path)
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
return strings.HasPrefix(path, ".."+string(filepath.Separator))
|
sl, ok := fs.underlying.(billy.Symlink)
|
||||||
|
if !ok {
|
||||||
|
return fullpath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rel, err := fs.relativeToRoot(fullpath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
fullpath, err = fs.resolveFollowedPath(rel, followFinal, op, sl)
|
||||||
|
if errors.Is(err, billy.ErrNotSupported) {
|
||||||
|
return fs.underlyingPath(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullpath, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *ChrootHelper) resolveFollowedPath(rel string, followFinal bool, op string, sl billy.Symlink) (string, error) {
|
||||||
|
if rel == "" {
|
||||||
|
return fs.resolveFollowedRoot(followFinal, op, sl)
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := splitRelativePath(rel)
|
||||||
|
resolved := ""
|
||||||
|
followed := 0
|
||||||
|
|
||||||
|
for len(parts) > 0 {
|
||||||
|
part := parts[0]
|
||||||
|
parts = parts[1:]
|
||||||
|
|
||||||
|
currentRel := joinRelativePath(resolved, part)
|
||||||
|
currentPath := fs.Join(fs.Root(), currentRel)
|
||||||
|
if len(parts) == 0 && !followFinal {
|
||||||
|
return currentPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fi, err := sl.Lstat(currentPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return fs.Join(fs.Root(), joinRelativePath(append([]string{currentRel}, parts...)...)), nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi.Mode()&os.ModeSymlink == 0 {
|
||||||
|
resolved = currentRel
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
followed++
|
||||||
|
if followed > maxFollowedSymlinks {
|
||||||
|
return "", symlinkLoopError(op, currentPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
target, err := sl.Readlink(currentPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetRel, err := fs.linkTargetRel(currentPath, target)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if targetRel == currentRel {
|
||||||
|
return "", symlinkLoopError(op, currentPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
parts = append(splitRelativePath(targetRel), parts...)
|
||||||
|
resolved = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.Join(fs.Root(), resolved), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func symlinkLoopError(op, path string) error {
|
||||||
|
return &os.PathError{Op: op, Path: path, Err: syscall.ELOOP}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *ChrootHelper) resolveFollowedRoot(followFinal bool, op string, sl billy.Symlink) (string, error) {
|
||||||
|
root := fs.Join(fs.Root(), "")
|
||||||
|
if !followFinal {
|
||||||
|
return root, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fi, err := sl.Lstat(root)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return root, nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi.Mode()&os.ModeSymlink == 0 {
|
||||||
|
return root, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
target, err := sl.Readlink(root)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetRel, err := fs.linkTargetRel(root, target)
|
||||||
|
if err != nil {
|
||||||
|
return root, err
|
||||||
|
}
|
||||||
|
if targetRel == "" {
|
||||||
|
return "", symlinkLoopError(op, root)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.resolveFollowedPath(targetRel, followFinal, op, sl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *ChrootHelper) relativeToRoot(filename string) (string, error) {
|
||||||
|
rel, err := filepath.Rel(filepath.Clean(fs.Root()), filepath.Clean(filename))
|
||||||
|
if err != nil || isCrossBoundaries(rel) {
|
||||||
|
return "", billy.ErrCrossedBoundary
|
||||||
|
}
|
||||||
|
|
||||||
|
if rel == "." {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return rel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *ChrootHelper) linkTargetRel(linkPath, target string) (string, error) {
|
||||||
|
target = filepath.FromSlash(target)
|
||||||
|
if filepath.IsAbs(target) || strings.HasPrefix(target, string(filepath.Separator)) {
|
||||||
|
return fs.relativeToRoot(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.relativeToRoot(fs.Join(filepath.Dir(linkPath), target))
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitRelativePath(filename string) []string {
|
||||||
|
filename = filepath.Clean(filename)
|
||||||
|
if filename == "" || filename == "." {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Split(filepath.ToSlash(filename), "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinRelativePath(elem ...string) string {
|
||||||
|
parts := make([]string, 0, len(elem))
|
||||||
|
for _, part := range elem {
|
||||||
|
if part == "" || part == "." {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts = append(parts, part)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return filepath.Join(parts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCreateExclusive(flag int) bool {
|
||||||
|
return flag&os.O_CREATE != 0 && flag&os.O_EXCL != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCrossBoundaries(name string) bool {
|
||||||
|
name = filepath.ToSlash(name)
|
||||||
|
name = strings.TrimLeft(name, "/")
|
||||||
|
name = path.Clean(name)
|
||||||
|
|
||||||
|
return name == ".." || strings.HasPrefix(name, "../")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *ChrootHelper) Create(filename string) (billy.File, error) {
|
func (fs *ChrootHelper) Create(filename string) (billy.File, error) {
|
||||||
fullpath, err := fs.underlyingPath(filename)
|
fullpath, err := fs.followedPath(filename, true, "create")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -56,7 +231,7 @@ func (fs *ChrootHelper) Create(filename string) (billy.File, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fs *ChrootHelper) Open(filename string) (billy.File, error) {
|
func (fs *ChrootHelper) Open(filename string) (billy.File, error) {
|
||||||
fullpath, err := fs.underlyingPath(filename)
|
fullpath, err := fs.followedPath(filename, true, "open")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -70,7 +245,7 @@ func (fs *ChrootHelper) Open(filename string) (billy.File, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fs *ChrootHelper) OpenFile(filename string, flag int, mode os.FileMode) (billy.File, error) {
|
func (fs *ChrootHelper) OpenFile(filename string, flag int, mode os.FileMode) (billy.File, error) {
|
||||||
fullpath, err := fs.underlyingPath(filename)
|
fullpath, err := fs.followedPath(filename, !isCreateExclusive(flag), "open")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -84,12 +259,16 @@ func (fs *ChrootHelper) OpenFile(filename string, flag int, mode os.FileMode) (b
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fs *ChrootHelper) Stat(filename string) (os.FileInfo, error) {
|
func (fs *ChrootHelper) Stat(filename string) (os.FileInfo, error) {
|
||||||
fullpath, err := fs.underlyingPath(filename)
|
fullpath, err := fs.followedPath(filename, true, "stat")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return fs.underlying.Stat(fullpath)
|
fi, err := fs.underlying.Stat(fullpath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fileInfo{FileInfo: fi, name: filepath.Base(filename)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *ChrootHelper) Rename(from, to string) error {
|
func (fs *ChrootHelper) Rename(from, to string) error {
|
||||||
@@ -135,7 +314,7 @@ func (fs *ChrootHelper) TempFile(dir, prefix string) (billy.File, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fs *ChrootHelper) ReadDir(path string) ([]os.FileInfo, error) {
|
func (fs *ChrootHelper) ReadDir(path string) ([]os.FileInfo, error) {
|
||||||
fullpath, err := fs.underlyingPath(path)
|
fullpath, err := fs.followedPath(path, true, "readdir")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -241,6 +420,11 @@ type file struct {
|
|||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fileInfo struct {
|
||||||
|
os.FileInfo
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
func newFile(fs billy.Filesystem, f billy.File, filename string) billy.File {
|
func newFile(fs billy.Filesystem, f billy.File, filename string) billy.File {
|
||||||
filename = fs.Join(fs.Root(), filename)
|
filename = fs.Join(fs.Root(), filename)
|
||||||
filename, _ = filepath.Rel(fs.Root(), filename)
|
filename, _ = filepath.Rel(fs.Root(), filename)
|
||||||
@@ -254,3 +438,7 @@ func newFile(fs billy.Filesystem, f billy.File, filename string) billy.File {
|
|||||||
func (f *file) Name() string {
|
func (f *file) Name() string {
|
||||||
return f.name
|
return f.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fi fileInfo) Name() string {
|
||||||
|
return fi.name
|
||||||
|
}
|
||||||
|
|||||||
+10
-1
@@ -13,7 +13,7 @@ type Polyfill struct {
|
|||||||
c capabilities
|
c capabilities
|
||||||
}
|
}
|
||||||
|
|
||||||
type capabilities struct{ tempfile, dir, symlink, chroot bool }
|
type capabilities struct{ tempfile, dir, symlink, chroot, chmod bool }
|
||||||
|
|
||||||
// New creates a new filesystem wrapping up 'fs' the intercepts all the calls
|
// New creates a new filesystem wrapping up 'fs' the intercepts all the calls
|
||||||
// made and errors if fs doesn't implement any of the billy interfaces.
|
// made and errors if fs doesn't implement any of the billy interfaces.
|
||||||
@@ -28,6 +28,7 @@ func New(fs billy.Basic) billy.Filesystem {
|
|||||||
_, h.c.dir = h.Basic.(billy.Dir)
|
_, h.c.dir = h.Basic.(billy.Dir)
|
||||||
_, h.c.symlink = h.Basic.(billy.Symlink)
|
_, h.c.symlink = h.Basic.(billy.Symlink)
|
||||||
_, h.c.chroot = h.Basic.(billy.Chroot)
|
_, h.c.chroot = h.Basic.(billy.Chroot)
|
||||||
|
_, h.c.chmod = h.Basic.(billy.Chmod)
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +88,14 @@ func (h *Polyfill) Chroot(path string) (billy.Filesystem, error) {
|
|||||||
return h.Basic.(billy.Chroot).Chroot(path)
|
return h.Basic.(billy.Chroot).Chroot(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Polyfill) Chmod(path string, mode os.FileMode) error {
|
||||||
|
if !h.c.chmod {
|
||||||
|
return billy.ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Basic.(billy.Chmod).Chmod(path, mode)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Polyfill) Root() string {
|
func (h *Polyfill) Root() string {
|
||||||
if !h.c.chroot {
|
if !h.c.chroot {
|
||||||
return string(filepath.Separator)
|
return string(filepath.Separator)
|
||||||
|
|||||||
+5
@@ -24,6 +24,9 @@ var Default = &ChrootOS{}
|
|||||||
// New returns a new OS filesystem.
|
// New returns a new OS filesystem.
|
||||||
// By default paths are deduplicated, but still enforced
|
// By default paths are deduplicated, but still enforced
|
||||||
// under baseDir. For more info refer to WithDeduplicatePath.
|
// 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 {
|
func New(baseDir string, opts ...Option) billy.Filesystem {
|
||||||
o := &options{
|
o := &options{
|
||||||
deduplicatePath: true,
|
deduplicatePath: true,
|
||||||
@@ -47,6 +50,8 @@ func WithBoundOS() Option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithChrootOS returns the option of using a Chroot filesystem OS.
|
// WithChrootOS returns the option of using a Chroot filesystem OS.
|
||||||
|
//
|
||||||
|
// Deprecated: use WithBoundOS instead.
|
||||||
func WithChrootOS() Option {
|
func WithChrootOS() Option {
|
||||||
return func(o *options) {
|
return func(o *options) {
|
||||||
o.Type = ChrootOSFS
|
o.Type = ChrootOSFS
|
||||||
|
|||||||
+93
-15
@@ -20,6 +20,7 @@
|
|||||||
package osfs
|
package osfs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -29,6 +30,31 @@ import (
|
|||||||
"github.com/go-git/go-billy/v5"
|
"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
|
// BoundOS is a fs implementation based on the OS filesystem which is bound to
|
||||||
// a base dir.
|
// a base dir.
|
||||||
// Prefer this fs implementation over ChrootOS.
|
// Prefer this fs implementation over ChrootOS.
|
||||||
@@ -54,6 +80,7 @@ func (fs *BoundOS) Create(filename string) (billy.File, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fs *BoundOS) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
|
func (fs *BoundOS) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
|
||||||
|
filename = fs.expandDot(filename)
|
||||||
fn, err := fs.abs(filename)
|
fn, err := fs.abs(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -62,6 +89,7 @@ func (fs *BoundOS) OpenFile(filename string, flag int, perm os.FileMode) (billy.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fs *BoundOS) ReadDir(path string) ([]os.FileInfo, error) {
|
func (fs *BoundOS) ReadDir(path string) ([]os.FileInfo, error) {
|
||||||
|
path = fs.expandDot(path)
|
||||||
dir, err := fs.abs(path)
|
dir, err := fs.abs(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -71,6 +99,12 @@ func (fs *BoundOS) ReadDir(path string) ([]os.FileInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fs *BoundOS) Rename(from, to string) 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)
|
f, err := fs.abs(from)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -89,6 +123,7 @@ func (fs *BoundOS) Rename(from, to string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fs *BoundOS) MkdirAll(path string, perm os.FileMode) error {
|
func (fs *BoundOS) MkdirAll(path string, perm os.FileMode) error {
|
||||||
|
path = fs.expandDot(path)
|
||||||
dir, err := fs.abs(path)
|
dir, err := fs.abs(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -101,6 +136,7 @@ func (fs *BoundOS) Open(filename string) (billy.File, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fs *BoundOS) Stat(filename string) (os.FileInfo, error) {
|
func (fs *BoundOS) Stat(filename string) (os.FileInfo, error) {
|
||||||
|
filename = fs.expandDot(filename)
|
||||||
filename, err := fs.abs(filename)
|
filename, err := fs.abs(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -109,6 +145,11 @@ func (fs *BoundOS) Stat(filename string) (os.FileInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fs *BoundOS) Remove(filename string) error {
|
func (fs *BoundOS) Remove(filename string) error {
|
||||||
|
if fs.isBaseDir(filename) {
|
||||||
|
return ErrBaseDirCannotBeRemoved
|
||||||
|
}
|
||||||
|
filename = fs.expandDot(filename)
|
||||||
|
|
||||||
fn, err := fs.abs(filename)
|
fn, err := fs.abs(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -122,10 +163,19 @@ func (fs *BoundOS) Remove(filename string) error {
|
|||||||
func (fs *BoundOS) TempFile(dir, prefix string) (billy.File, error) {
|
func (fs *BoundOS) TempFile(dir, prefix string) (billy.File, error) {
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
var err error
|
var err error
|
||||||
|
dir = fs.expandDot(dir)
|
||||||
dir, err = fs.abs(dir)
|
dir, err = fs.abs(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = os.Stat(dir)
|
||||||
|
if err != nil && os.IsNotExist(err) {
|
||||||
|
err = os.MkdirAll(dir, defaultDirectoryMode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tempFile(dir, prefix)
|
return tempFile(dir, prefix)
|
||||||
@@ -136,6 +186,11 @@ func (fs *BoundOS) Join(elem ...string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fs *BoundOS) RemoveAll(path string) error {
|
func (fs *BoundOS) RemoveAll(path string) error {
|
||||||
|
if fs.isBaseDir(path) {
|
||||||
|
return ErrBaseDirCannotBeRemoved
|
||||||
|
}
|
||||||
|
path = fs.expandDot(path)
|
||||||
|
|
||||||
dir, err := fs.abs(path)
|
dir, err := fs.abs(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -144,6 +199,7 @@ func (fs *BoundOS) RemoveAll(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fs *BoundOS) Symlink(target, link string) error {
|
func (fs *BoundOS) Symlink(target, link string) error {
|
||||||
|
link = fs.expandDot(link)
|
||||||
ln, err := fs.abs(link)
|
ln, err := fs.abs(link)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -156,6 +212,7 @@ func (fs *BoundOS) Symlink(target, link string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fs *BoundOS) Lstat(filename string) (os.FileInfo, error) {
|
func (fs *BoundOS) Lstat(filename string) (os.FileInfo, error) {
|
||||||
|
filename = fs.expandDot(filename)
|
||||||
filename = filepath.Clean(filename)
|
filename = filepath.Clean(filename)
|
||||||
if !filepath.IsAbs(filename) {
|
if !filepath.IsAbs(filename) {
|
||||||
filename = filepath.Join(fs.baseDir, filename)
|
filename = filepath.Join(fs.baseDir, filename)
|
||||||
@@ -167,6 +224,7 @@ func (fs *BoundOS) Lstat(filename string) (os.FileInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fs *BoundOS) Readlink(link string) (string, error) {
|
func (fs *BoundOS) Readlink(link string) (string, error) {
|
||||||
|
link = fs.expandDot(link)
|
||||||
if !filepath.IsAbs(link) {
|
if !filepath.IsAbs(link) {
|
||||||
link = filepath.Clean(filepath.Join(fs.baseDir, link))
|
link = filepath.Clean(filepath.Join(fs.baseDir, link))
|
||||||
}
|
}
|
||||||
@@ -177,6 +235,7 @@ func (fs *BoundOS) Readlink(link string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fs *BoundOS) Chmod(path string, mode os.FileMode) error {
|
func (fs *BoundOS) Chmod(path string, mode os.FileMode) error {
|
||||||
|
path = fs.expandDot(path)
|
||||||
abspath, err := fs.abs(path)
|
abspath, err := fs.abs(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -191,7 +250,7 @@ func (fs *BoundOS) Chroot(path string) (billy.Filesystem, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return New(joined), nil
|
return New(joined, WithBoundOS()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Root returns the current base dir of the billy.Filesystem.
|
// Root returns the current base dir of the billy.Filesystem.
|
||||||
@@ -212,6 +271,37 @@ func (fs *BoundOS) createDir(fullpath string) error {
|
|||||||
return nil
|
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.
|
// 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
|
// Relative paths won't be allowed to ascend the base dir, so `../file` will become
|
||||||
// `/working-dir/file`.
|
// `/working-dir/file`.
|
||||||
@@ -225,7 +315,7 @@ func (fs *BoundOS) abs(filename string) (string, error) {
|
|||||||
|
|
||||||
path, err := securejoin.SecureJoin(fs.baseDir, filename)
|
path, err := securejoin.SecureJoin(fs.baseDir, filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if fs.deduplicatePath {
|
if fs.deduplicatePath {
|
||||||
@@ -238,24 +328,12 @@ func (fs *BoundOS) abs(filename string) (string, error) {
|
|||||||
return path, nil
|
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
|
// insideBaseDirEval checks whether filename is contained within
|
||||||
// a dir that is within the fs.baseDir, by first evaluating any symlinks
|
// a dir that is within the fs.baseDir, by first evaluating any symlinks
|
||||||
// that either filename or fs.baseDir may contain.
|
// that either filename or fs.baseDir may contain.
|
||||||
func (fs *BoundOS) insideBaseDirEval(filename string) (bool, error) {
|
func (fs *BoundOS) insideBaseDirEval(filename string) (bool, error) {
|
||||||
// "/" contains all others.
|
// "/" contains all others.
|
||||||
if fs.baseDir == "/" {
|
if fs.baseDir == "/" || fs.baseDir == filename {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
dir, err := filepath.EvalSymlinks(filepath.Dir(filename))
|
dir, err := filepath.EvalSymlinks(filepath.Dir(filename))
|
||||||
|
|||||||
+10
@@ -14,6 +14,8 @@ import (
|
|||||||
// ChrootOS is a legacy filesystem based on a "soft chroot" of the os filesystem.
|
// 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.
|
// Although this is still the default os filesystem, consider using BoundOS instead.
|
||||||
//
|
//
|
||||||
|
// Deprecated: use New with WithBoundOS instead.
|
||||||
|
//
|
||||||
// Behaviours of note:
|
// Behaviours of note:
|
||||||
// 1. A "soft chroot" translates the base dir to "/" for the purposes of the
|
// 1. A "soft chroot" translates the base dir to "/" for the purposes of the
|
||||||
// fs abstraction.
|
// fs abstraction.
|
||||||
@@ -24,6 +26,14 @@ import (
|
|||||||
type ChrootOS struct{}
|
type ChrootOS struct{}
|
||||||
|
|
||||||
func newChrootOS(baseDir string) billy.Filesystem {
|
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)
|
return chroot.New(&ChrootOS{}, baseDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
-24
@@ -16,8 +16,6 @@ import (
|
|||||||
// can but returns the first error it encounters. If the path does not exist,
|
// can but returns the first error it encounters. If the path does not exist,
|
||||||
// RemoveAll returns nil (no error).
|
// RemoveAll returns nil (no error).
|
||||||
func RemoveAll(fs billy.Basic, path string) error {
|
func RemoveAll(fs billy.Basic, path string) error {
|
||||||
fs, path = getUnderlyingAndPath(fs, path)
|
|
||||||
|
|
||||||
if r, ok := fs.(removerAll); ok {
|
if r, ok := fs.(removerAll); ok {
|
||||||
return r.RemoveAll(path)
|
return r.RemoveAll(path)
|
||||||
}
|
}
|
||||||
@@ -39,7 +37,7 @@ func removeAll(fs billy.Basic, path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, is this a directory we need to recurse into?
|
// Otherwise, is this a directory we need to recurse into?
|
||||||
dir, serr := fs.Stat(path)
|
dir, serr := lstat(fs, path)
|
||||||
if serr != nil {
|
if serr != nil {
|
||||||
if errors.Is(serr, os.ErrNotExist) {
|
if errors.Is(serr, os.ErrNotExist) {
|
||||||
return nil
|
return nil
|
||||||
@@ -48,8 +46,8 @@ func removeAll(fs billy.Basic, path string) error {
|
|||||||
return serr
|
return serr
|
||||||
}
|
}
|
||||||
|
|
||||||
if !dir.IsDir() {
|
if dir.Mode()&os.ModeSymlink != 0 || !dir.IsDir() {
|
||||||
// Not a directory; return the error from Remove.
|
// Not a directory we should recurse into; return the error from Remove.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +60,7 @@ func removeAll(fs billy.Basic, path string) error {
|
|||||||
fis, err := dirfs.ReadDir(path)
|
fis, err := dirfs.ReadDir(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
// Race. It was deleted between the Lstat and Open.
|
// Race. It was deleted between the Lstat and ReadDir.
|
||||||
// Return nil per RemoveAll's docs.
|
// Return nil per RemoveAll's docs.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -91,7 +89,18 @@ func removeAll(fs billy.Basic, path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return err
|
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.
|
// WriteFile writes data to a file named by filename in the given filesystem.
|
||||||
@@ -123,8 +132,10 @@ func WriteFile(fs billy.Basic, filename string, data []byte, perm os.FileMode) (
|
|||||||
// We generate random temporary file names so that there's a good
|
// 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
|
// chance the file doesn't exist yet - keeps the number of tries in
|
||||||
// TempFile to a minimum.
|
// TempFile to a minimum.
|
||||||
var rand uint32
|
var (
|
||||||
var randmu sync.Mutex
|
rand uint32
|
||||||
|
randmu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
func reseed() uint32 {
|
func reseed() uint32 {
|
||||||
return uint32(time.Now().UnixNano() + int64(os.Getpid()))
|
return uint32(time.Now().UnixNano() + int64(os.Getpid()))
|
||||||
@@ -220,22 +231,6 @@ func getTempDir(fs billy.Basic) string {
|
|||||||
return ".tmp"
|
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.
|
// ReadFile reads the named file and returns the contents from the given filesystem.
|
||||||
// A successful call returns err == nil, not err == EOF.
|
// A successful call returns err == nil, not err == EOF.
|
||||||
// Because ReadFile reads the whole file, it does not treat an EOF from Read
|
// Because ReadFile reads the whole file, it does not treat an EOF from Read
|
||||||
|
|||||||
+7
-3
@@ -91,8 +91,8 @@ func readVersion(idx *MemoryIndex, r io.Reader) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if v > VersionSupported {
|
if v != VersionSupported {
|
||||||
return ErrUnsupportedVersion
|
return fmt.Errorf("%w: v%d", ErrUnsupportedVersion, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
idx.Version = v
|
idx.Version = v
|
||||||
@@ -106,6 +106,10 @@ func readFanout(idx *MemoryIndex, r io.Reader) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if k > 0 && n < idx.Fanout[k-1] {
|
||||||
|
return fmt.Errorf("%w: fanout table is not monotonically non-decreasing at entry %d", ErrMalformedIdxFile, k)
|
||||||
|
}
|
||||||
|
|
||||||
idx.Fanout[k] = n
|
idx.Fanout[k] = n
|
||||||
idx.FanoutMapping[k] = noMapping
|
idx.FanoutMapping[k] = noMapping
|
||||||
}
|
}
|
||||||
@@ -155,7 +159,7 @@ func readCRC32(idx *MemoryIndex, r io.Reader) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readOffsets(idx *MemoryIndex, r io.Reader) error {
|
func readOffsets(idx *MemoryIndex, r io.Reader) error {
|
||||||
var o64cnt int
|
var o64cnt int64
|
||||||
for k := 0; k < fanout; k++ {
|
for k := 0; k < fanout; k++ {
|
||||||
if pos := idx.FanoutMapping[k]; pos != noMapping {
|
if pos := idx.FanoutMapping[k]; pos != noMapping {
|
||||||
if _, err := io.ReadFull(r, idx.Offset32[pos]); err != nil {
|
if _, err := io.ReadFull(r, idx.Offset32[pos]); err != nil {
|
||||||
|
|||||||
+70
-38
@@ -4,8 +4,8 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -26,12 +26,14 @@ var (
|
|||||||
ErrInvalidChecksum = errors.New("invalid checksum")
|
ErrInvalidChecksum = errors.New("invalid checksum")
|
||||||
// ErrUnknownExtension is returned when an index extension is encountered that is considered mandatory
|
// ErrUnknownExtension is returned when an index extension is encountered that is considered mandatory
|
||||||
ErrUnknownExtension = errors.New("unknown extension")
|
ErrUnknownExtension = errors.New("unknown extension")
|
||||||
|
// ErrMalformedIndexFile is returned when the index file contents are
|
||||||
|
// structurally invalid.
|
||||||
|
ErrMalformedIndexFile = errors.New("index decoder: malformed index file")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
entryHeaderLength = 62
|
entryHeaderLength = 62
|
||||||
entryExtended = 0x4000
|
entryExtended = 0x4000
|
||||||
entryValid = 0x8000
|
|
||||||
nameMask = 0xfff
|
nameMask = 0xfff
|
||||||
intentToAddMask = 1 << 13
|
intentToAddMask = 1 << 13
|
||||||
skipWorkTreeMask = 1 << 14
|
skipWorkTreeMask = 1 << 14
|
||||||
@@ -140,33 +142,55 @@ func (d *Decoder) readEntry(idx *Index) (*Entry, error) {
|
|||||||
e.SkipWorktree = extended&skipWorkTreeMask != 0
|
e.SkipWorktree = extended&skipWorkTreeMask != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := d.readEntryName(idx, e, flags); err != nil {
|
nameConsumed, err := d.readEntryName(idx, e, flags)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return e, d.padEntry(idx, e, read)
|
return e, d.padEntry(idx, e, read, nameConsumed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) readEntryName(idx *Index, e *Entry, flags uint16) error {
|
// readEntryName reads the entry path and sets e.Name. It returns the
|
||||||
var name string
|
// number of bytes consumed from the stream for the name portion.
|
||||||
var err error
|
func (d *Decoder) readEntryName(idx *Index, e *Entry, flags uint16) (int, error) {
|
||||||
|
|
||||||
switch idx.Version {
|
switch idx.Version {
|
||||||
case 2, 3:
|
case 2, 3:
|
||||||
len := flags & nameMask
|
nameLen := flags & nameMask
|
||||||
name, err = d.doReadEntryName(len)
|
name, consumed, err := d.doReadEntryName(nameLen)
|
||||||
case 4:
|
|
||||||
name, err = d.doReadEntryNameV4()
|
|
||||||
default:
|
|
||||||
return ErrUnsupportedVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
|
}
|
||||||
|
e.Name = name
|
||||||
|
return consumed, nil
|
||||||
|
case 4:
|
||||||
|
name, err := d.doReadEntryNameV4()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
e.Name = name
|
||||||
|
return 0, nil // V4 has no padding; consumed count unused
|
||||||
|
default:
|
||||||
|
return 0, ErrUnsupportedVersion
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Name = name
|
// doReadEntryName reads the entry path for V2/V3 indexes. It returns the
|
||||||
return nil
|
// name, the number of bytes consumed from the stream, and any error.
|
||||||
|
// When nameLen equals nameMask (0xFFF), the name was too long to fit in
|
||||||
|
// the 12-bit field and the real length is found by scanning for the NUL
|
||||||
|
// terminator — matching C Git's strlen(name) fallback in create_from_disk.
|
||||||
|
func (d *Decoder) doReadEntryName(nameLen uint16) (string, int, error) {
|
||||||
|
if nameLen == nameMask {
|
||||||
|
name, err := binary.ReadUntil(d.r, '\x00')
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
return string(name), len(name) + 1, nil // +1 for the consumed NUL delimiter
|
||||||
|
}
|
||||||
|
|
||||||
|
name := make([]byte, nameLen)
|
||||||
|
_, err := io.ReadFull(d.r, name)
|
||||||
|
return string(name), int(nameLen), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) doReadEntryNameV4() (string, error) {
|
func (d *Decoder) doReadEntryNameV4() (string, error) {
|
||||||
@@ -177,7 +201,14 @@ func (d *Decoder) doReadEntryNameV4() (string, error) {
|
|||||||
|
|
||||||
var base string
|
var base string
|
||||||
if d.lastEntry != nil {
|
if d.lastEntry != nil {
|
||||||
|
if l < 0 || int(l) > len(d.lastEntry.Name) {
|
||||||
|
return "", fmt.Errorf("%w: invalid V4 entry name strip length %d (previous name length: %d)",
|
||||||
|
ErrMalformedIndexFile, l, len(d.lastEntry.Name))
|
||||||
|
}
|
||||||
base = d.lastEntry.Name[:len(d.lastEntry.Name)-int(l)]
|
base = d.lastEntry.Name[:len(d.lastEntry.Name)-int(l)]
|
||||||
|
} else if l > 0 {
|
||||||
|
return "", fmt.Errorf("%w: non-zero strip length %d on first V4 entry",
|
||||||
|
ErrMalformedIndexFile, l)
|
||||||
}
|
}
|
||||||
|
|
||||||
name, err := binary.ReadUntil(d.r, '\x00')
|
name, err := binary.ReadUntil(d.r, '\x00')
|
||||||
@@ -188,25 +219,24 @@ func (d *Decoder) doReadEntryNameV4() (string, error) {
|
|||||||
return base + string(name), nil
|
return base + string(name), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) doReadEntryName(len uint16) (string, error) {
|
// padEntry discards NUL padding bytes that follow each V2/V3 entry on
|
||||||
name := make([]byte, len)
|
// disk. nameConsumed is the number of stream bytes consumed while reading
|
||||||
_, err := io.ReadFull(d.r, name)
|
// the entry name (which may exceed len(e.Name) when a NUL terminator was
|
||||||
|
// consumed for long names where the 12-bit length field overflowed).
|
||||||
return string(name), err
|
func (d *Decoder) padEntry(idx *Index, e *Entry, read, nameConsumed int) error {
|
||||||
}
|
|
||||||
|
|
||||||
// Index entries are padded out to the next 8 byte alignment
|
|
||||||
// for historical reasons related to how C Git read the files.
|
|
||||||
func (d *Decoder) padEntry(idx *Index, e *Entry, read int) error {
|
|
||||||
if idx.Version == 4 {
|
if idx.Version == 4 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
entrySize := read + len(e.Name)
|
entrySize := read + len(e.Name)
|
||||||
padLen := 8 - entrySize%8
|
padLen := 8 - entrySize%8
|
||||||
|
padLen -= nameConsumed - len(e.Name)
|
||||||
|
if padLen > 0 {
|
||||||
_, err := io.CopyN(io.Discard, d.r, int64(padLen))
|
_, err := io.CopyN(io.Discard, d.r, int64(padLen))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Decoder) readExtensions(idx *Index) error {
|
func (d *Decoder) readExtensions(idx *Index) error {
|
||||||
// TODO: support 'Split index' and 'Untracked cache' extensions, take in
|
// TODO: support 'Split index' and 'Untracked cache' extensions, take in
|
||||||
@@ -312,7 +342,7 @@ func (d *Decoder) readChecksum(expected []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateHeader(r io.Reader) (version uint32, err error) {
|
func validateHeader(r io.Reader) (version uint32, err error) {
|
||||||
var s = make([]byte, 4)
|
s := make([]byte, 4)
|
||||||
if _, err := io.ReadFull(r, s); err != nil {
|
if _, err := io.ReadFull(r, s); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -376,24 +406,26 @@ func (d *treeExtensionDecoder) readEntry() (*TreeEntry, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// An entry can be in an invalidated state and is represented by having a
|
|
||||||
// negative number in the entry_count field.
|
|
||||||
if i == -1 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Entries = i
|
e.Entries = i
|
||||||
trees, err := binary.ReadUntil(d.r, '\n')
|
trees, err := binary.ReadUntil(d.r, '\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
i, err = strconv.Atoi(string(trees))
|
subtrees, err := strconv.Atoi(string(trees))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Trees = i
|
e.Trees = subtrees
|
||||||
|
|
||||||
|
// An entry can be in an invalidated state and is represented by having a
|
||||||
|
// negative number in the entry_count field. In this case, there is no
|
||||||
|
// object name and the next entry starts immediately after the newline.
|
||||||
|
if i < 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
_, err = io.ReadFull(d.r, e.Hash[:])
|
_, err = io.ReadFull(d.r, e.Hash[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
+24
-13
@@ -5,9 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"path"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing/hash"
|
"github.com/go-git/go-git/v5/plumbing/hash"
|
||||||
@@ -160,26 +158,39 @@ func (e *Encoder) encodeEntryName(entry *Entry) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *Encoder) encodeEntryNameV4(entry *Entry) error {
|
func (e *Encoder) encodeEntryNameV4(entry *Entry) error {
|
||||||
name := entry.Name
|
// V4 prefix compression: find the longest common prefix between the
|
||||||
l := 0
|
// previous entry's name and the current one. The strip length tells
|
||||||
|
// the decoder how many bytes to remove from the end of the previous
|
||||||
|
// name, and the suffix is the remainder of the current name.
|
||||||
|
prefix := 0
|
||||||
if e.lastEntry != nil {
|
if e.lastEntry != nil {
|
||||||
dir := path.Dir(e.lastEntry.Name) + "/"
|
prefix = commonPrefixLen(e.lastEntry.Name, entry.Name)
|
||||||
if strings.HasPrefix(entry.Name, dir) {
|
|
||||||
l = len(e.lastEntry.Name) - len(dir)
|
|
||||||
name = strings.TrimPrefix(entry.Name, dir)
|
|
||||||
} else {
|
|
||||||
l = len(e.lastEntry.Name)
|
|
||||||
}
|
}
|
||||||
|
stripLen := 0
|
||||||
|
if e.lastEntry != nil {
|
||||||
|
stripLen = len(e.lastEntry.Name) - prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
e.lastEntry = entry
|
e.lastEntry = entry
|
||||||
|
|
||||||
err := binary.WriteVariableWidthInt(e.w, int64(l))
|
if err := binary.WriteVariableWidthInt(e.w, int64(stripLen)); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return binary.Write(e.w, []byte(name+string('\x00')))
|
suffix := entry.Name[prefix:]
|
||||||
|
return binary.Write(e.w, append([]byte(suffix), '\x00'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// commonPrefixLen returns the length of the longest common byte prefix
|
||||||
|
// between a and b.
|
||||||
|
func commonPrefixLen(a, b string) int {
|
||||||
|
n := min(len(b), len(a))
|
||||||
|
for i := range n {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Encoder) encodeRawExtension(signature string, data []byte) error {
|
func (e *Encoder) encodeRawExtension(signature string, data []byte) error {
|
||||||
|
|||||||
+2
@@ -54,6 +54,8 @@ type Index struct {
|
|||||||
ResolveUndo *ResolveUndo
|
ResolveUndo *ResolveUndo
|
||||||
// EndOfIndexEntry represents the 'End of Index Entry' extension
|
// EndOfIndexEntry represents the 'End of Index Entry' extension
|
||||||
EndOfIndexEntry *EndOfIndexEntry
|
EndOfIndexEntry *EndOfIndexEntry
|
||||||
|
// ModTime is the modification time of the index file
|
||||||
|
ModTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add creates a new Entry and returns it. The caller should first check that
|
// Add creates a new Entry and returns it. The caller should first check that
|
||||||
|
|||||||
+110
-91
@@ -5,7 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp"
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
@@ -20,6 +20,7 @@ const (
|
|||||||
beginpgp string = "-----BEGIN PGP SIGNATURE-----"
|
beginpgp string = "-----BEGIN PGP SIGNATURE-----"
|
||||||
endpgp string = "-----END PGP SIGNATURE-----"
|
endpgp string = "-----END PGP SIGNATURE-----"
|
||||||
headerpgp string = "gpgsig"
|
headerpgp string = "gpgsig"
|
||||||
|
headerpgp256 string = "gpgsig-sha256"
|
||||||
headerencoding string = "encoding"
|
headerencoding string = "encoding"
|
||||||
|
|
||||||
// https://github.com/git/git/blob/bcb6cae2966cc407ca1afc77413b3ef11103c175/Documentation/gitformat-signature.txt#L153
|
// https://github.com/git/git/blob/bcb6cae2966cc407ca1afc77413b3ef11103c175/Documentation/gitformat-signature.txt#L153
|
||||||
@@ -41,6 +42,11 @@ type MessageEncoding string
|
|||||||
// in time, such as a timestamp, the author of the changes since the last
|
// in time, such as a timestamp, the author of the changes since the last
|
||||||
// commit, a pointer to the previous commit(s), etc.
|
// commit, a pointer to the previous commit(s), etc.
|
||||||
// http://shafiulazam.com/gitbook/1_the_git_object_model.html
|
// 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 {
|
type Commit struct {
|
||||||
// Hash of the commit object.
|
// Hash of the commit object.
|
||||||
Hash plumbing.Hash
|
Hash plumbing.Hash
|
||||||
@@ -66,6 +72,9 @@ type Commit struct {
|
|||||||
ExtraHeaders []ExtraHeader
|
ExtraHeaders []ExtraHeader
|
||||||
|
|
||||||
s storer.EncodedObjectStorer
|
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
|
// ExtraHeader holds any non-standard header
|
||||||
@@ -181,6 +190,11 @@ func (c *Commit) NumParents() int {
|
|||||||
|
|
||||||
var ErrParentNotFound = errors.New("commit parent not found")
|
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.
|
// Parent returns the ith parent of a commit.
|
||||||
func (c *Commit) Parent(i int) (*Commit, error) {
|
func (c *Commit) Parent(i int) (*Commit, error) {
|
||||||
if len(c.ParentHashes) == 0 || i > len(c.ParentHashes)-1 {
|
if len(c.ParentHashes) == 0 || i > len(c.ParentHashes)-1 {
|
||||||
@@ -227,14 +241,23 @@ func (c *Commit) Type() plumbing.ObjectType {
|
|||||||
return plumbing.CommitObject
|
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.
|
// Decode transforms a plumbing.EncodedObject into a Commit struct.
|
||||||
func (c *Commit) Decode(o plumbing.EncodedObject) (err error) {
|
func (c *Commit) Decode(o plumbing.EncodedObject) (err error) {
|
||||||
if o.Type() != plumbing.CommitObject {
|
if o.Type() != plumbing.CommitObject {
|
||||||
return ErrUnsupportedObject
|
return ErrUnsupportedObject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.reset()
|
||||||
c.Hash = o.Hash()
|
c.Hash = o.Hash()
|
||||||
c.Encoding = defaultUtf8CommitMessageEncoding
|
c.src = o
|
||||||
|
|
||||||
reader, err := o.Reader()
|
reader, err := o.Reader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -245,97 +268,17 @@ func (c *Commit) Decode(o plumbing.EncodedObject) (err error) {
|
|||||||
r := sync.GetBufioReader(reader)
|
r := sync.GetBufioReader(reader)
|
||||||
defer sync.PutBufioReader(r)
|
defer sync.PutBufioReader(r)
|
||||||
|
|
||||||
var message bool
|
s := &commitScanner{r: r, c: c}
|
||||||
var mergetag bool
|
for state := scanTree; state != nil; {
|
||||||
var pgpsig bool
|
state, err = state(s)
|
||||||
var msgbuf bytes.Buffer
|
if err != nil {
|
||||||
var extraheader *ExtraHeader = nil
|
|
||||||
for {
|
|
||||||
line, err := r.ReadBytes('\n')
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if mergetag {
|
|
||||||
if len(line) > 0 && line[0] == ' ' {
|
|
||||||
line = bytes.TrimLeft(line, " ")
|
|
||||||
c.MergeTag += string(line)
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
mergetag = false
|
|
||||||
}
|
}
|
||||||
|
if !s.sawTree {
|
||||||
|
return fmt.Errorf("%w: missing tree header", ErrMalformedCommit)
|
||||||
}
|
}
|
||||||
|
c.Message = s.msgbuf.String()
|
||||||
if pgpsig {
|
|
||||||
if len(line) > 0 && line[0] == ' ' {
|
|
||||||
line = bytes.TrimLeft(line, " ")
|
|
||||||
c.PGPSignature += string(line)
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
pgpsig = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if extraheader != nil {
|
|
||||||
if len(line) > 0 && line[0] == ' ' {
|
|
||||||
extraheader.Value += string(line[1:])
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
extraheader.Value = strings.TrimRight(extraheader.Value, "\n")
|
|
||||||
c.ExtraHeaders = append(c.ExtraHeaders, *extraheader)
|
|
||||||
extraheader = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !message {
|
|
||||||
original_line := line
|
|
||||||
line = bytes.TrimSpace(line)
|
|
||||||
if len(line) == 0 {
|
|
||||||
message = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
split := bytes.SplitN(line, []byte{' '}, 2)
|
|
||||||
|
|
||||||
var data []byte
|
|
||||||
if len(split) == 2 {
|
|
||||||
data = split[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
switch string(split[0]) {
|
|
||||||
case "tree":
|
|
||||||
c.TreeHash = plumbing.NewHash(string(data))
|
|
||||||
case "parent":
|
|
||||||
c.ParentHashes = append(c.ParentHashes, plumbing.NewHash(string(data)))
|
|
||||||
case "author":
|
|
||||||
c.Author.Decode(data)
|
|
||||||
case "committer":
|
|
||||||
c.Committer.Decode(data)
|
|
||||||
case headermergetag:
|
|
||||||
c.MergeTag += string(data) + "\n"
|
|
||||||
mergetag = true
|
|
||||||
case headerencoding:
|
|
||||||
c.Encoding = MessageEncoding(data)
|
|
||||||
case headerpgp:
|
|
||||||
c.PGPSignature += string(data) + "\n"
|
|
||||||
pgpsig = true
|
|
||||||
default:
|
|
||||||
h, maybecontinued := parseExtraHeader(original_line)
|
|
||||||
if maybecontinued {
|
|
||||||
extraheader = &h
|
|
||||||
} else {
|
|
||||||
c.ExtraHeaders = append(c.ExtraHeaders, h)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
msgbuf.Write(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.Message = msgbuf.String()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,11 +287,73 @@ func (c *Commit) Encode(o plumbing.EncodedObject) error {
|
|||||||
return c.encode(o, true)
|
return c.encode(o, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncodeWithoutSignature export a Commit into a plumbing.EncodedObject without the signature (correspond to the payload of the PGP signature).
|
// EncodeWithoutSignature exports a Commit into a plumbing.EncodedObject
|
||||||
|
// without any signature headers, producing the payload that PGP/GPG
|
||||||
|
// signatures are computed over.
|
||||||
|
//
|
||||||
|
// Behaviour depends on how the Commit was created:
|
||||||
|
//
|
||||||
|
// - For Commits populated by Decode whose exported fields still match the
|
||||||
|
// source object, the payload is streamed from the raw source bytes with
|
||||||
|
// gpgsig and gpgsig-sha256 headers (and their continuation lines)
|
||||||
|
// stripped verbatim. This preserves the exact bytes the signature was
|
||||||
|
// computed over, regardless of any normalization performed by Decode.
|
||||||
|
//
|
||||||
|
// - For Commits constructed in memory, or for decoded Commits whose
|
||||||
|
// exported fields have been mutated, the payload is derived from the
|
||||||
|
// current struct fields. Mutation is detected by re-decoding the source
|
||||||
|
// object and comparing exported fields; if any differ, the in-memory
|
||||||
|
// representation prevails.
|
||||||
func (c *Commit) EncodeWithoutSignature(o plumbing.EncodedObject) error {
|
func (c *Commit) EncodeWithoutSignature(o plumbing.EncodedObject) error {
|
||||||
|
if c.matchesSource() {
|
||||||
|
return stripObjectSignatures(o, c.src, plumbing.CommitObject)
|
||||||
|
}
|
||||||
return c.encode(o, false)
|
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) {
|
func (c *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
||||||
o.SetType(plumbing.CommitObject)
|
o.SetType(plumbing.CommitObject)
|
||||||
w, err := o.Writer()
|
w, err := o.Writer()
|
||||||
@@ -407,7 +412,9 @@ func (c *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, header := range c.ExtraHeaders {
|
for _, header := range c.ExtraHeaders {
|
||||||
|
if isStandardHeader(header.Key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if _, err = fmt.Fprintf(w, "\n%s", header); err != nil {
|
if _, err = fmt.Fprintf(w, "\n%s", header); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -478,9 +485,21 @@ func (c *Commit) String() string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrMultipleSignatures is returned by Verify when the commit carries more
|
||||||
|
// than one armored signature block. Mirrors upstream's parse_gpg_output
|
||||||
|
// rejection of GOODSIG/BADSIG status lines after the first
|
||||||
|
// (gpg-interface.c:257-269): multi-signature commits are intentionally
|
||||||
|
// unsupported because their provenance cannot be reduced to a single
|
||||||
|
// authoritative signer.
|
||||||
|
var ErrMultipleSignatures = errors.New("commit has multiple signatures")
|
||||||
|
|
||||||
// Verify performs PGP verification of the commit with a provided armored
|
// Verify performs PGP verification of the commit 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 (c *Commit) Verify(armoredKeyRing string) (*openpgp.Entity, error) {
|
func (c *Commit) Verify(armoredKeyRing string) (*openpgp.Entity, error) {
|
||||||
|
if countSignatureBlocks([]byte(c.PGPSignature)) > 1 {
|
||||||
|
return nil, ErrMultipleSignatures
|
||||||
|
}
|
||||||
|
|
||||||
keyRingReader := strings.NewReader(armoredKeyRing)
|
keyRingReader := strings.NewReader(armoredKeyRing)
|
||||||
keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader)
|
keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+377
@@ -0,0 +1,377 @@
|
|||||||
|
package object
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// commitScanner holds the working state of the commit decoder driven by the
|
||||||
|
// stateFn loop in (*Commit).Decode. Each commitState reads one or more lines
|
||||||
|
// from r, updates the in-progress *Commit and the scanner's bookkeeping, and
|
||||||
|
// returns the state that should run next (or nil to stop).
|
||||||
|
type commitScanner struct {
|
||||||
|
r *bufio.Reader
|
||||||
|
c *Commit
|
||||||
|
msgbuf bytes.Buffer
|
||||||
|
|
||||||
|
// pending holds a line that was read but the current state decided to
|
||||||
|
// hand back to the next state, paired with the io.EOF flag that was
|
||||||
|
// returned when the line was originally read.
|
||||||
|
pending []byte
|
||||||
|
pendingErr error
|
||||||
|
|
||||||
|
// First-occurrence tracking: once the corresponding field has been
|
||||||
|
// decoded, subsequent occurrences are silently dropped (matches
|
||||||
|
// upstream's find_commit_header / first-wins semantics).
|
||||||
|
//
|
||||||
|
// gpgsig is not tracked here: upstream's parse_buffer_signed_by_header
|
||||||
|
// (commit.c:1186) accumulates every occurrence into one signature buffer,
|
||||||
|
// so we do the same on the scanner side to keep verification payloads
|
||||||
|
// byte-aligned. gpgsig-sha256 is recognized and skipped without exposing a
|
||||||
|
// new field in v5.
|
||||||
|
sawTree, sawAuthor, sawCommitter bool
|
||||||
|
sawEncoding, sawMergetag bool
|
||||||
|
|
||||||
|
// extra is the multi-line ExtraHeader currently being assembled.
|
||||||
|
extra *ExtraHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
// commitState is one step of the decoder state machine. Each function reads
|
||||||
|
// the lines it needs, mutates *Commit via s.c, and returns the next state to
|
||||||
|
// run (or nil to terminate the loop).
|
||||||
|
type commitState func(*commitScanner) (commitState, error)
|
||||||
|
|
||||||
|
// readLine returns the next line from the buffer, transparently consuming any
|
||||||
|
// line that was previously pushed back by a state that decided not to handle
|
||||||
|
// it.
|
||||||
|
func (s *commitScanner) readLine() ([]byte, error) {
|
||||||
|
if s.pending != nil {
|
||||||
|
line, err := s.pending, s.pendingErr
|
||||||
|
s.pending, s.pendingErr = nil, nil
|
||||||
|
return line, err
|
||||||
|
}
|
||||||
|
line, err := s.r.ReadBytes('\n')
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return line, err
|
||||||
|
}
|
||||||
|
return line, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// pushBack stashes an unconsumed line so the next state's readLine call sees
|
||||||
|
// it. Only one line can be pushed back at a time.
|
||||||
|
func (s *commitScanner) pushBack(line []byte, err error) {
|
||||||
|
s.pending = line
|
||||||
|
s.pendingErr = err
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanTree expects the first non-empty header to be `tree HASH`. Anything
|
||||||
|
// else (or an empty buffer) is rejected with ErrMalformedCommit, matching
|
||||||
|
// upstream's `bogus commit object` check.
|
||||||
|
func scanTree(s *commitScanner) (commitState, error) {
|
||||||
|
line, err := s.readLine()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) == 0 || isBlankLine(line) {
|
||||||
|
return nil, fmt.Errorf("%w: missing tree header", ErrMalformedCommit)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, data := splitHeader(line)
|
||||||
|
if key != "tree" {
|
||||||
|
return nil, fmt.Errorf("%w: tree header must be first", ErrMalformedCommit)
|
||||||
|
}
|
||||||
|
h, herr := parseObjectIDHex(data, ErrMalformedCommit, "tree")
|
||||||
|
if herr != nil {
|
||||||
|
return nil, herr
|
||||||
|
}
|
||||||
|
s.c.TreeHash = h
|
||||||
|
s.sawTree = true
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return scanParents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanParents consumes contiguous `parent HASH` lines. The first non-parent
|
||||||
|
// line ends the parent block and is handed off to scanAuthor; any later
|
||||||
|
// `parent` line is silently dropped (matches upstream's parse_commit_buffer
|
||||||
|
// exiting its parent loop at the first non-parent line and
|
||||||
|
// read_commit_extra_header_lines filtering `parent` out of extras).
|
||||||
|
func scanParents(s *commitScanner) (commitState, error) {
|
||||||
|
line, err := s.readLine()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if isBlankLine(line) {
|
||||||
|
return scanMessage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
key, data := splitHeader(line)
|
||||||
|
if key == "parent" {
|
||||||
|
h, herr := parseObjectIDHex(data, ErrMalformedCommit, "parent")
|
||||||
|
if herr != nil {
|
||||||
|
return nil, herr
|
||||||
|
}
|
||||||
|
s.c.ParentHashes = append(s.c.ParentHashes, h)
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return scanParents, nil
|
||||||
|
}
|
||||||
|
s.pushBack(line, err)
|
||||||
|
return scanAuthor, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanAuthor accepts an `author` line at its canonical position immediately
|
||||||
|
// after the parent block. Any other header here is pushed back for
|
||||||
|
// scanCommitter; an out-of-place author is therefore silently dropped.
|
||||||
|
// Mirrors upstream's parse_commit_date func.
|
||||||
|
func scanAuthor(s *commitScanner) (commitState, error) {
|
||||||
|
line, err := s.readLine()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if isBlankLine(line) {
|
||||||
|
return scanMessage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
key, data := splitHeader(line)
|
||||||
|
if key == "author" {
|
||||||
|
s.c.Author.Decode(data)
|
||||||
|
s.sawAuthor = true
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return scanCommitter, nil
|
||||||
|
}
|
||||||
|
s.pushBack(line, err)
|
||||||
|
return scanCommitter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanCommitter accepts a `committer` line at its canonical position
|
||||||
|
// immediately after the author. Any other header is pushed back for
|
||||||
|
// scanHeaders. Same upstream rationale as scanAuthor.
|
||||||
|
func scanCommitter(s *commitScanner) (commitState, error) {
|
||||||
|
line, err := s.readLine()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if isBlankLine(line) {
|
||||||
|
return scanMessage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
key, data := splitHeader(line)
|
||||||
|
if key == "committer" {
|
||||||
|
s.c.Committer.Decode(data)
|
||||||
|
s.sawCommitter = true
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return scanHeaders, nil
|
||||||
|
}
|
||||||
|
s.pushBack(line, err)
|
||||||
|
return scanHeaders, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanHeaders dispatches one header line. Continuation-bearing headers
|
||||||
|
// (mergetag, gpgsig, gpgsig-sha256, and unknown extras whose value is
|
||||||
|
// continued on subsequent lines) hand off to a dedicated continuation state
|
||||||
|
// that handles the `<space>...` lines and then returns here.
|
||||||
|
func scanHeaders(s *commitScanner) (commitState, error) {
|
||||||
|
line, err := s.readLine()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if isBlankLine(line) {
|
||||||
|
return scanMessage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
originalLine := line
|
||||||
|
key, data := splitHeader(line)
|
||||||
|
|
||||||
|
var next commitState = scanHeaders
|
||||||
|
switch key {
|
||||||
|
case "tree", "parent", "author", "committer":
|
||||||
|
// Anything reaching scanHeaders with one of these keys is out of
|
||||||
|
// canonical position: duplicate tree, parent past the contiguous
|
||||||
|
// block, or author/committer not at their expected slot. Drop them
|
||||||
|
// the same way upstream's standard_header_field filter excludes
|
||||||
|
// them from the extras list (read_commit_extra_header_lines,
|
||||||
|
// commit.c:1520-1522).
|
||||||
|
case headerencoding:
|
||||||
|
if !s.sawEncoding {
|
||||||
|
s.c.Encoding = MessageEncoding(data)
|
||||||
|
s.sawEncoding = true
|
||||||
|
}
|
||||||
|
case headermergetag:
|
||||||
|
if s.sawMergetag {
|
||||||
|
next = scanSkipCont
|
||||||
|
} else {
|
||||||
|
s.c.MergeTag += string(data) + "\n"
|
||||||
|
s.sawMergetag = true
|
||||||
|
next = scanMergetagCont
|
||||||
|
}
|
||||||
|
case headerpgp:
|
||||||
|
s.c.PGPSignature += string(data) + "\n"
|
||||||
|
next = scanPgpCont
|
||||||
|
case headerpgp256:
|
||||||
|
next = scanSkipCont
|
||||||
|
default:
|
||||||
|
h, multiline := parseExtraHeader(originalLine)
|
||||||
|
if multiline {
|
||||||
|
s.extra = &h
|
||||||
|
next = scanExtraCont
|
||||||
|
} else {
|
||||||
|
s.c.ExtraHeaders = append(s.c.ExtraHeaders, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return next, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanMergetagCont accumulates continuation lines for the first mergetag
|
||||||
|
// header. Continuations strip exactly one leading space, mirroring upstream's
|
||||||
|
// `line + 1` (commit.c:1509). The first non-continuation line is pushed back
|
||||||
|
// so scanHeaders can dispatch it.
|
||||||
|
func scanMergetagCont(s *commitScanner) (commitState, error) {
|
||||||
|
return continuationCont(s, &s.c.MergeTag, scanMergetagCont)
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanPgpCont accumulates continuation lines for a signature header.
|
||||||
|
// Continuations strip exactly one leading space, mirroring upstream's
|
||||||
|
// `line + 1` (commit.c:1509). The first non-continuation line is pushed back
|
||||||
|
// so scanHeaders can dispatch it. Repeat occurrences of the same signature
|
||||||
|
// header land back here and concatenate, matching upstream's
|
||||||
|
// parse_buffer_signed_by_header (commit.c:1186).
|
||||||
|
func scanPgpCont(s *commitScanner) (commitState, error) {
|
||||||
|
return continuationCont(s, &s.c.PGPSignature, scanPgpCont)
|
||||||
|
}
|
||||||
|
|
||||||
|
func continuationCont(s *commitScanner, dst *string, self commitState) (commitState, error) {
|
||||||
|
line, err := s.readLine()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) > 0 && line[0] == ' ' {
|
||||||
|
*dst += string(line[1:])
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return self, nil
|
||||||
|
}
|
||||||
|
if len(line) > 0 {
|
||||||
|
s.pushBack(line, err)
|
||||||
|
}
|
||||||
|
return scanHeaders, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanSkipCont discards continuation lines that belong to a header scanHeaders
|
||||||
|
// chose to drop.
|
||||||
|
func scanSkipCont(s *commitScanner) (commitState, error) {
|
||||||
|
line, err := s.readLine()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) > 0 && line[0] == ' ' {
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return scanSkipCont, nil
|
||||||
|
}
|
||||||
|
if len(line) > 0 {
|
||||||
|
s.pushBack(line, err)
|
||||||
|
}
|
||||||
|
return scanHeaders, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanExtraCont accumulates continuation lines for an unknown ExtraHeader
|
||||||
|
// whose value spans multiple lines, then finalises the entry once the
|
||||||
|
// continuation block ends.
|
||||||
|
func scanExtraCont(s *commitScanner) (commitState, error) {
|
||||||
|
line, err := s.readLine()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) > 0 && line[0] == ' ' {
|
||||||
|
s.extra.Value += string(line[1:])
|
||||||
|
if err == io.EOF {
|
||||||
|
s.finaliseExtra()
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return scanExtraCont, nil
|
||||||
|
}
|
||||||
|
s.finaliseExtra()
|
||||||
|
if len(line) > 0 {
|
||||||
|
s.pushBack(line, err)
|
||||||
|
}
|
||||||
|
return scanHeaders, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *commitScanner) finaliseExtra() {
|
||||||
|
s.extra.Value = strings.TrimRight(s.extra.Value, "\n")
|
||||||
|
s.c.ExtraHeaders = append(s.c.ExtraHeaders, *s.extra)
|
||||||
|
s.extra = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanMessage drains the remaining bytes into the message buffer.
|
||||||
|
func scanMessage(s *commitScanner) (commitState, error) {
|
||||||
|
for {
|
||||||
|
line, err := s.readLine()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) > 0 {
|
||||||
|
s.msgbuf.Write(line)
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isBlankLine reports whether line is the canonical header/body separator:
|
||||||
|
// a single newline. Mirrors upstream's `*line == '\n'` test in
|
||||||
|
// read_commit_extra_header_lines (commit.c:1502).
|
||||||
|
func isBlankLine(line []byte) bool {
|
||||||
|
return len(line) == 1 && line[0] == '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitHeader returns the header keyword (everything before the first space)
|
||||||
|
// and the value (everything after, with the trailing newline stripped). If
|
||||||
|
// the header has no value the returned data is nil.
|
||||||
|
func splitHeader(line []byte) (string, []byte) {
|
||||||
|
trimmed := bytes.TrimRight(line, "\n")
|
||||||
|
key, value, ok := bytes.Cut(trimmed, []byte{' '})
|
||||||
|
if !ok {
|
||||||
|
return string(trimmed), nil
|
||||||
|
}
|
||||||
|
return string(key), value
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseObjectIDHex(data []byte, malformedErr error, header string) (plumbing.Hash, error) {
|
||||||
|
id := string(data)
|
||||||
|
if !plumbing.IsHash(id) {
|
||||||
|
return plumbing.ZeroHash, fmt.Errorf("%w: bad %s hash", malformedErr, header)
|
||||||
|
}
|
||||||
|
return plumbing.NewHash(id), nil
|
||||||
|
}
|
||||||
+121
-1
@@ -1,6 +1,13 @@
|
|||||||
package object
|
package object
|
||||||
|
|
||||||
import "bytes"
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
"github.com/go-git/go-git/v5/utils/ioutil"
|
||||||
|
"github.com/go-git/go-git/v5/utils/sync"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
signatureTypeUnknown signatureType = iota
|
signatureTypeUnknown signatureType = iota
|
||||||
@@ -100,3 +107,116 @@ func parseSignedBytes(b []byte) (int, signatureType) {
|
|||||||
}
|
}
|
||||||
return match, t
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+87
-42
@@ -1,9 +1,8 @@
|
|||||||
package object
|
package object
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp"
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
@@ -13,6 +12,10 @@ import (
|
|||||||
"github.com/go-git/go-git/v5/utils/sync"
|
"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
|
// 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
|
// 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
|
// provides a reference that associates the target with a tag name. It also
|
||||||
@@ -39,6 +42,9 @@ type Tag struct {
|
|||||||
Target plumbing.Hash
|
Target plumbing.Hash
|
||||||
|
|
||||||
s storer.EncodedObjectStorer
|
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.
|
// GetTag gets a tag from an object storer and decodes it.
|
||||||
@@ -77,13 +83,20 @@ func (t *Tag) Type() plumbing.ObjectType {
|
|||||||
return plumbing.TagObject
|
return plumbing.TagObject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Tag) reset() {
|
||||||
|
storer := t.s
|
||||||
|
*t = Tag{s: storer}
|
||||||
|
}
|
||||||
|
|
||||||
// Decode transforms a plumbing.EncodedObject into a Tag struct.
|
// Decode transforms a plumbing.EncodedObject into a Tag struct.
|
||||||
func (t *Tag) Decode(o plumbing.EncodedObject) (err error) {
|
func (t *Tag) Decode(o plumbing.EncodedObject) (err error) {
|
||||||
if o.Type() != plumbing.TagObject {
|
if o.Type() != plumbing.TagObject {
|
||||||
return ErrUnsupportedObject
|
return ErrUnsupportedObject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.reset()
|
||||||
t.Hash = o.Hash()
|
t.Hash = o.Hash()
|
||||||
|
t.src = o
|
||||||
|
|
||||||
reader, err := o.Reader()
|
reader, err := o.Reader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -94,42 +107,15 @@ func (t *Tag) Decode(o plumbing.EncodedObject) (err error) {
|
|||||||
r := sync.GetBufioReader(reader)
|
r := sync.GetBufioReader(reader)
|
||||||
defer sync.PutBufioReader(r)
|
defer sync.PutBufioReader(r)
|
||||||
|
|
||||||
for {
|
scanner := &tagScanner{r: r, t: t}
|
||||||
var line []byte
|
for state := scanTagObject; state != nil; {
|
||||||
line, err = r.ReadBytes('\n')
|
state, err = state(scanner)
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "tag":
|
|
||||||
t.Name = string(split[1])
|
|
||||||
case "tagger":
|
|
||||||
t.Tagger.Decode(split[1])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == io.EOF {
|
data := scanner.msgbuf.Bytes()
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if sm, _ := parseSignedBytes(data); sm >= 0 {
|
if sm, _ := parseSignedBytes(data); sm >= 0 {
|
||||||
t.PGPSignature = string(data[sm:])
|
t.PGPSignature = string(data[sm:])
|
||||||
data = data[:sm]
|
data = data[:sm]
|
||||||
@@ -144,11 +130,54 @@ func (t *Tag) Encode(o plumbing.EncodedObject) error {
|
|||||||
return t.encode(o, true)
|
return t.encode(o, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncodeWithoutSignature export a Tag into a plumbing.EncodedObject without the signature (correspond to the payload of the PGP signature).
|
// EncodeWithoutSignature exports a Tag into a plumbing.EncodedObject without
|
||||||
|
// any signature data, producing the payload that PGP/GPG signatures are
|
||||||
|
// computed over.
|
||||||
|
//
|
||||||
|
// Behaviour mirrors Commit.EncodeWithoutSignature:
|
||||||
|
//
|
||||||
|
// - For Tags populated by Decode whose exported fields still match the
|
||||||
|
// source object, the payload is streamed from the raw source bytes with
|
||||||
|
// the inline trailing signature truncated and gpgsig/gpgsig-sha256
|
||||||
|
// headers (and their continuation lines) stripped verbatim. This
|
||||||
|
// preserves the exact bytes the signature was computed over, regardless
|
||||||
|
// of any normalization performed by Decode.
|
||||||
|
//
|
||||||
|
// - For Tags constructed in memory, or for decoded Tags whose exported
|
||||||
|
// fields have been mutated, the payload is derived from the current
|
||||||
|
// struct fields. Mutation is detected by re-decoding the source object
|
||||||
|
// and comparing exported fields; if any differ, the in-memory
|
||||||
|
// representation prevails.
|
||||||
func (t *Tag) EncodeWithoutSignature(o plumbing.EncodedObject) error {
|
func (t *Tag) EncodeWithoutSignature(o plumbing.EncodedObject) error {
|
||||||
|
if t.matchesSource() {
|
||||||
|
return stripObjectSignatures(o, t.src, plumbing.TagObject)
|
||||||
|
}
|
||||||
return t.encode(o, false)
|
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) {
|
func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
||||||
o.SetType(plumbing.TagObject)
|
o.SetType(plumbing.TagObject)
|
||||||
w, err := o.Writer()
|
w, err := o.Writer()
|
||||||
@@ -158,16 +187,26 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
|||||||
defer ioutil.CheckClose(w, &err)
|
defer ioutil.CheckClose(w, &err)
|
||||||
|
|
||||||
if _, err = fmt.Fprintf(w,
|
if _, err = fmt.Fprintf(w,
|
||||||
"object %s\ntype %s\ntag %s\ntagger ",
|
"object %s\ntype %s\ntag %s\n",
|
||||||
t.Target.String(), t.TargetType.Bytes(), t.Name); err != nil {
|
t.Target.String(), t.TargetType.Bytes(), t.Name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isZeroSignature(t.Tagger) {
|
||||||
|
if _, err = fmt.Fprint(w, "tagger "); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err = t.Tagger.Encode(w); err != nil {
|
if err = t.Tagger.Encode(w); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = fmt.Fprint(w, "\n\n"); err != nil {
|
if _, err = fmt.Fprint(w, "\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = fmt.Fprint(w, "\n"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,11 +214,12 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that this is highly sensitive to what it sent along in the message.
|
// Note that this is highly sensitive to what is sent along in the
|
||||||
// Message *always* needs to end with a newline, or else the message and the
|
// message. Message *always* needs to end with a newline, or else the
|
||||||
// signature will be concatenated into a corrupt object. Since this is a
|
// message and the trailing signature will be concatenated into a
|
||||||
// lower-level method, we assume you know what you are doing and have already
|
// corrupt object. Since this is a lower-level method, we assume you
|
||||||
// done the needful on the message in the caller.
|
// know what you are doing and have already done the needful on the
|
||||||
|
// message in the caller.
|
||||||
if includeSig {
|
if includeSig {
|
||||||
if _, err = fmt.Fprint(w, t.PGPSignature); err != nil {
|
if _, err = fmt.Fprint(w, t.PGPSignature); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -189,6 +229,10 @@ func (t *Tag) encode(o plumbing.EncodedObject, includeSig bool) (err error) {
|
|||||||
return err
|
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
|
// Commit returns the commit pointed to by the tag. If the tag points to a
|
||||||
// different type of object ErrUnsupportedObject will be returned.
|
// different type of object ErrUnsupportedObject will be returned.
|
||||||
func (t *Tag) Commit() (*Commit, error) {
|
func (t *Tag) Commit() (*Commit, error) {
|
||||||
@@ -256,7 +300,8 @@ func (t *Tag) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify performs PGP verification of the tag with a provided armored
|
// 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) {
|
func (t *Tag) Verify(armoredKeyRing string) (*openpgp.Entity, error) {
|
||||||
keyRingReader := strings.NewReader(armoredKeyRing)
|
keyRingReader := strings.NewReader(armoredKeyRing)
|
||||||
keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader)
|
keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader)
|
||||||
|
|||||||
+237
@@ -0,0 +1,237 @@
|
|||||||
|
package object
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// tagScanner holds the working state of the tag decoder driven by the
|
||||||
|
// stateFn loop in (*Tag).Decode. Each tagState reads one or more lines
|
||||||
|
// from r, updates the in-progress *Tag and the scanner's bookkeeping,
|
||||||
|
// and returns the state that should run next (or nil to stop).
|
||||||
|
type tagScanner struct {
|
||||||
|
r *bufio.Reader
|
||||||
|
t *Tag
|
||||||
|
msgbuf bytes.Buffer
|
||||||
|
|
||||||
|
// pending holds a line that was read but the current state decided to
|
||||||
|
// hand back to the next state, paired with the io.EOF flag returned
|
||||||
|
// when the line was originally read.
|
||||||
|
pending []byte
|
||||||
|
pendingErr error
|
||||||
|
|
||||||
|
// First-occurrence tracking: once the corresponding canonical
|
||||||
|
// header has been decoded at its expected position, subsequent
|
||||||
|
// occurrences (or out-of-position lines) are silently dropped,
|
||||||
|
// matching the strict layout enforced by upstream's
|
||||||
|
// parse_tag_buffer (tag.c:130).
|
||||||
|
//
|
||||||
|
// gpgsig-sha256 is recognized and skipped without exposing a new field
|
||||||
|
// in v5.
|
||||||
|
sawObject, sawType, sawName, sawTagger bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// tagState is one step of the decoder state machine. Each function reads
|
||||||
|
// the lines it needs, mutates *Tag via s.t, and returns the next state
|
||||||
|
// to run (or nil to terminate the loop).
|
||||||
|
type tagState func(*tagScanner) (tagState, error)
|
||||||
|
|
||||||
|
// readLine returns the next line from the buffer, transparently
|
||||||
|
// consuming any line that was previously pushed back by a state that
|
||||||
|
// decided not to handle it.
|
||||||
|
func (s *tagScanner) readLine() ([]byte, error) {
|
||||||
|
if s.pending != nil {
|
||||||
|
line, err := s.pending, s.pendingErr
|
||||||
|
s.pending, s.pendingErr = nil, nil
|
||||||
|
return line, err
|
||||||
|
}
|
||||||
|
return s.r.ReadBytes('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// pushBack stashes an unconsumed line so the next state's readLine call
|
||||||
|
// sees it. Only one line can be pushed back at a time.
|
||||||
|
func (s *tagScanner) pushBack(line []byte, err error) {
|
||||||
|
s.pending = line
|
||||||
|
s.pendingErr = err
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanTagObject requires the first line to be `object HASH`, mirroring
|
||||||
|
// upstream's strict parse_tag_buffer (tag.c:151-156). Anything else
|
||||||
|
// returns ErrMalformedTag.
|
||||||
|
func scanTagObject(s *tagScanner) (tagState, error) {
|
||||||
|
line, err := s.readLine()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) == 0 || isBlankLine(line) {
|
||||||
|
return nil, fmt.Errorf("%w: missing object header", ErrMalformedTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, data := splitHeader(line)
|
||||||
|
if key != "object" {
|
||||||
|
return nil, fmt.Errorf("%w: object header must be first", ErrMalformedTag)
|
||||||
|
}
|
||||||
|
h, herr := parseObjectIDHex(data, ErrMalformedTag, "object")
|
||||||
|
if herr != nil {
|
||||||
|
return nil, herr
|
||||||
|
}
|
||||||
|
s.t.Target = h
|
||||||
|
s.sawObject = true
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return scanTagType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanTagType requires a `type` line immediately after the object header,
|
||||||
|
// mirroring upstream's parse_tag_buffer (tag.c:158-166).
|
||||||
|
func scanTagType(s *tagScanner) (tagState, error) {
|
||||||
|
line, err := s.readLine()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) == 0 || isBlankLine(line) {
|
||||||
|
return nil, fmt.Errorf("%w: missing type header", ErrMalformedTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, data := splitHeader(line)
|
||||||
|
if key != "type" {
|
||||||
|
return nil, fmt.Errorf("%w: type header must follow object", ErrMalformedTag)
|
||||||
|
}
|
||||||
|
ot, perr := plumbing.ParseObjectType(string(data))
|
||||||
|
if perr != nil {
|
||||||
|
return nil, perr
|
||||||
|
}
|
||||||
|
s.t.TargetType = ot
|
||||||
|
s.sawType = true
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return scanTagName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanTagName requires a `tag` line immediately after the type header,
|
||||||
|
// mirroring upstream's parse_tag_buffer (tag.c:186-194).
|
||||||
|
func scanTagName(s *tagScanner) (tagState, error) {
|
||||||
|
line, err := s.readLine()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) == 0 || isBlankLine(line) {
|
||||||
|
return nil, fmt.Errorf("%w: missing tag header", ErrMalformedTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, data := splitHeader(line)
|
||||||
|
if key != "tag" {
|
||||||
|
return nil, fmt.Errorf("%w: tag header must follow type", ErrMalformedTag)
|
||||||
|
}
|
||||||
|
s.t.Name = string(data)
|
||||||
|
s.sawName = true
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return scanTagTagger, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanTagTagger accepts a `tagger` line at its canonical position. Any
|
||||||
|
// other header is pushed back for scanTagHeaders.
|
||||||
|
func scanTagTagger(s *tagScanner) (tagState, error) {
|
||||||
|
line, err := s.readLine()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if isBlankLine(line) {
|
||||||
|
return scanTagMessage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
key, data := splitHeader(line)
|
||||||
|
if key == "tagger" {
|
||||||
|
s.t.Tagger.Decode(data)
|
||||||
|
s.sawTagger = true
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return scanTagHeaders, nil
|
||||||
|
}
|
||||||
|
s.pushBack(line, err)
|
||||||
|
return scanTagHeaders, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanTagHeaders dispatches one header line. gpgsig-sha256 hands off to
|
||||||
|
// scanTagSkipCont so the continuation block can be consumed; out-of-position
|
||||||
|
// canonical fields and unknown headers are silently dropped.
|
||||||
|
func scanTagHeaders(s *tagScanner) (tagState, error) {
|
||||||
|
line, err := s.readLine()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if isBlankLine(line) {
|
||||||
|
return scanTagMessage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
key, _ := splitHeader(line)
|
||||||
|
next := scanTagHeaders
|
||||||
|
switch key {
|
||||||
|
case "object", "type", "tag", "tagger":
|
||||||
|
// Out-of-canonical-position duplicates are dropped, mirroring the
|
||||||
|
// strict ordering of upstream's parse_tag_buffer.
|
||||||
|
case headerpgp256:
|
||||||
|
next = scanTagSkipCont
|
||||||
|
default:
|
||||||
|
// Unknown header: silently dropped (the Tag struct does not
|
||||||
|
// expose ExtraHeaders).
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return next, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanTagSkipCont discards continuation lines for a header scanTagHeaders chose
|
||||||
|
// to drop. The first non-continuation line is pushed back so scanTagHeaders can
|
||||||
|
// dispatch it.
|
||||||
|
func scanTagSkipCont(s *tagScanner) (tagState, error) {
|
||||||
|
line, err := s.readLine()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) > 0 && line[0] == ' ' {
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return scanTagSkipCont, nil
|
||||||
|
}
|
||||||
|
if len(line) > 0 {
|
||||||
|
s.pushBack(line, err)
|
||||||
|
}
|
||||||
|
return scanTagHeaders, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanTagMessage drains the remaining bytes into the message buffer.
|
||||||
|
// (*Tag).Decode then runs parseSignedBytes over those bytes to peel off
|
||||||
|
// the optional inline trailing PGP signature.
|
||||||
|
func scanTagMessage(s *tagScanner) (tagState, error) {
|
||||||
|
for {
|
||||||
|
line, err := s.readLine()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) > 0 {
|
||||||
|
s.msgbuf.Write(line)
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+91
-29
@@ -29,6 +29,7 @@ var (
|
|||||||
ErrDirectoryNotFound = errors.New("directory not found")
|
ErrDirectoryNotFound = errors.New("directory not found")
|
||||||
ErrEntryNotFound = errors.New("entry not found")
|
ErrEntryNotFound = errors.New("entry not found")
|
||||||
ErrEntriesNotSorted = errors.New("entries in tree are not sorted")
|
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
|
// Tree is basically like a directory - it references a bunch of other trees
|
||||||
@@ -38,8 +39,8 @@ type Tree struct {
|
|||||||
Hash plumbing.Hash
|
Hash plumbing.Hash
|
||||||
|
|
||||||
s storer.EncodedObjectStorer
|
s storer.EncodedObjectStorer
|
||||||
m map[string]*TreeEntry
|
|
||||||
t map[string]*Tree // tree path cache
|
t map[string]*Tree // tree path cache
|
||||||
|
entriesSorted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTree gets a tree from an object storer and decodes it.
|
// GetTree gets a tree from an object storer and decodes it.
|
||||||
@@ -182,17 +183,44 @@ func (t *Tree) dir(baseName string) (*Tree, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tree) entry(baseName string) (*TreeEntry, error) {
|
func (t *Tree) entry(baseName string) (*TreeEntry, error) {
|
||||||
if t.m == nil {
|
if t.entriesSorted {
|
||||||
t.buildMap()
|
if entry := t.searchEntry(baseName); entry != nil {
|
||||||
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, ok := t.m[baseName]
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrEntryNotFound
|
return nil, ErrEntryNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pastName := baseName + "/"
|
||||||
|
for i := range t.Entries {
|
||||||
|
entry := &t.Entries[i]
|
||||||
|
if entry.Name == baseName {
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
if treeEntrySortName(entry) > pastName {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrEntryNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tree) searchEntry(baseName string) *TreeEntry {
|
||||||
|
if i := t.searchEntryIndex(baseName); i < len(t.Entries) && t.Entries[i].Name == baseName {
|
||||||
|
return &t.Entries[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if i := t.searchEntryIndex(baseName + "/"); i < len(t.Entries) && t.Entries[i].Name == baseName {
|
||||||
|
return &t.Entries[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tree) searchEntryIndex(name string) int {
|
||||||
|
return sort.Search(len(t.Entries), func(i int) bool {
|
||||||
|
return treeEntrySortName(&t.Entries[i]) >= name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Files returns a FileIter allowing to iterate over the Tree
|
// Files returns a FileIter allowing to iterate over the Tree
|
||||||
func (t *Tree) Files() *FileIter {
|
func (t *Tree) Files() *FileIter {
|
||||||
@@ -212,20 +240,25 @@ func (t *Tree) Type() plumbing.ObjectType {
|
|||||||
return plumbing.TreeObject
|
return plumbing.TreeObject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Tree) reset() {
|
||||||
|
storer := t.s
|
||||||
|
*t = Tree{s: storer}
|
||||||
|
}
|
||||||
|
|
||||||
// Decode transform an plumbing.EncodedObject into a Tree struct
|
// Decode transform an plumbing.EncodedObject into a Tree struct
|
||||||
func (t *Tree) Decode(o plumbing.EncodedObject) (err error) {
|
func (t *Tree) Decode(o plumbing.EncodedObject) (err error) {
|
||||||
if o.Type() != plumbing.TreeObject {
|
if o.Type() != plumbing.TreeObject {
|
||||||
return ErrUnsupportedObject
|
return ErrUnsupportedObject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.reset()
|
||||||
t.Hash = o.Hash()
|
t.Hash = o.Hash()
|
||||||
|
// assume tree is sorted as a valid tree should always be sorted.
|
||||||
|
t.entriesSorted = true
|
||||||
if o.Size() == 0 {
|
if o.Size() == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Entries = nil
|
|
||||||
t.m = nil
|
|
||||||
|
|
||||||
reader, err := o.Reader()
|
reader, err := o.Reader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -235,10 +268,14 @@ func (t *Tree) Decode(o plumbing.EncodedObject) (err error) {
|
|||||||
r := sync.GetBufioReader(reader)
|
r := sync.GetBufioReader(reader)
|
||||||
defer sync.PutBufioReader(r)
|
defer sync.PutBufioReader(r)
|
||||||
|
|
||||||
|
var prevSortName string
|
||||||
for {
|
for {
|
||||||
str, err := r.ReadString(' ')
|
str, err := r.ReadString(' ')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
|
if len(str) != 0 {
|
||||||
|
return fmt.Errorf("%w: missing mode terminator", ErrMalformedTree)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,25 +285,41 @@ func (t *Tree) Decode(o plumbing.EncodedObject) (err error) {
|
|||||||
|
|
||||||
mode, err := filemode.New(str)
|
mode, err := filemode.New(str)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("%w: malformed mode", ErrMalformedTree)
|
||||||
}
|
}
|
||||||
|
mode = canonicalTreeMode(mode)
|
||||||
|
|
||||||
name, err := r.ReadString(0)
|
name, err := r.ReadString(0)
|
||||||
if err != nil && err != io.EOF {
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
return fmt.Errorf("%w: missing filename terminator", ErrMalformedTree)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(name) == 1 {
|
||||||
|
return fmt.Errorf("%w: empty filename", ErrMalformedTree)
|
||||||
|
}
|
||||||
|
|
||||||
var hash plumbing.Hash
|
var hash plumbing.Hash
|
||||||
if _, err = io.ReadFull(r, hash[:]); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
baseName := name[:len(name)-1]
|
baseName := name[:len(name)-1]
|
||||||
t.Entries = append(t.Entries, TreeEntry{
|
entry := TreeEntry{
|
||||||
Hash: hash,
|
Hash: hash,
|
||||||
Mode: mode,
|
Mode: mode,
|
||||||
Name: baseName,
|
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
|
return nil
|
||||||
@@ -279,21 +332,37 @@ func (s TreeEntrySorter) Len() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s TreeEntrySorter) Less(i, j int) bool {
|
func (s TreeEntrySorter) Less(i, j int) bool {
|
||||||
name1 := s[i].Name
|
return treeEntrySortName(&s[i]) < treeEntrySortName(&s[j])
|
||||||
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) {
|
func (s TreeEntrySorter) Swap(i, j int) {
|
||||||
s[i], s[j] = s[j], s[i]
|
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.
|
// Encode transforms a Tree into a plumbing.EncodedObject.
|
||||||
// The tree entries must be sorted by name.
|
// The tree entries must be sorted by name.
|
||||||
func (t *Tree) Encode(o plumbing.EncodedObject) (err error) {
|
func (t *Tree) Encode(o plumbing.EncodedObject) (err error) {
|
||||||
@@ -329,13 +398,6 @@ func (t *Tree) Encode(o plumbing.EncodedObject) (err error) {
|
|||||||
return err
|
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
|
// Diff returns a list of changes between this tree and the provided one
|
||||||
func (t *Tree) Diff(to *Tree) (Changes, error) {
|
func (t *Tree) Diff(to *Tree) (Changes, error) {
|
||||||
return t.DiffContext(context.Background(), to)
|
return t.DiffContext(context.Background(), to)
|
||||||
|
|||||||
+147
-21
@@ -7,7 +7,6 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -24,6 +23,33 @@ import (
|
|||||||
"github.com/go-git/go-git/v5/utils/ioutil"
|
"github.com/go-git/go-git/v5/utils/ioutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type contextKey int
|
||||||
|
|
||||||
|
const initialRequestKey contextKey = iota
|
||||||
|
|
||||||
|
// RedirectPolicy controls how the HTTP transport follows redirects.
|
||||||
|
//
|
||||||
|
// The values mirror Git's http.followRedirects config:
|
||||||
|
// "true" follows redirects for all requests, "false" treats redirects as
|
||||||
|
// errors, and "initial" follows redirects only for the initial
|
||||||
|
// /info/refs discovery request. The zero value defaults to "initial".
|
||||||
|
type RedirectPolicy string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FollowInitialRedirects RedirectPolicy = "initial"
|
||||||
|
FollowRedirects RedirectPolicy = "true"
|
||||||
|
NoFollowRedirects RedirectPolicy = "false"
|
||||||
|
)
|
||||||
|
|
||||||
|
func withInitialRequest(ctx context.Context) context.Context {
|
||||||
|
return context.WithValue(ctx, initialRequestKey, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isInitialRequest(req *http.Request) bool {
|
||||||
|
v, _ := req.Context().Value(initialRequestKey).(bool)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
// it requires a bytes.Buffer, because we need to know the length
|
// it requires a bytes.Buffer, because we need to know the length
|
||||||
func applyHeadersToRequest(req *http.Request, content *bytes.Buffer, host string, requestType string) {
|
func applyHeadersToRequest(req *http.Request, content *bytes.Buffer, host string, requestType string) {
|
||||||
req.Header.Add("User-Agent", capability.DefaultAgent())
|
req.Header.Add("User-Agent", capability.DefaultAgent())
|
||||||
@@ -54,12 +80,15 @@ func advertisedReferences(ctx context.Context, s *session, serviceName string) (
|
|||||||
|
|
||||||
s.ApplyAuthToRequest(req)
|
s.ApplyAuthToRequest(req)
|
||||||
applyHeadersToRequest(req, nil, s.endpoint.Host, serviceName)
|
applyHeadersToRequest(req, nil, s.endpoint.Host, serviceName)
|
||||||
res, err := s.client.Do(req.WithContext(ctx))
|
res, err := s.client.Do(req.WithContext(withInitialRequest(ctx)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.ModifyEndpointIfRedirect(res)
|
if err := s.ModifyEndpointIfRedirect(res); err != nil {
|
||||||
|
_ = res.Body.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
defer ioutil.CheckClose(res.Body, &err)
|
defer ioutil.CheckClose(res.Body, &err)
|
||||||
|
|
||||||
if err = NewErr(res); err != nil {
|
if err = NewErr(res); err != nil {
|
||||||
@@ -96,6 +125,7 @@ type client struct {
|
|||||||
client *http.Client
|
client *http.Client
|
||||||
transports *lru.Cache
|
transports *lru.Cache
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
|
follow RedirectPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientOptions holds user configurable options for the client.
|
// ClientOptions holds user configurable options for the client.
|
||||||
@@ -106,6 +136,11 @@ type ClientOptions struct {
|
|||||||
// size, will result in the least recently used transport getting deleted
|
// size, will result in the least recently used transport getting deleted
|
||||||
// before the provided transport is added to the cache.
|
// before the provided transport is added to the cache.
|
||||||
CacheMaxEntries int
|
CacheMaxEntries int
|
||||||
|
|
||||||
|
// RedirectPolicy controls redirect handling. Supported values are
|
||||||
|
// "true", "false", and "initial". The zero value defaults to
|
||||||
|
// "initial", matching Git's http.followRedirects default.
|
||||||
|
RedirectPolicy RedirectPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -150,12 +185,16 @@ func NewClientWithOptions(c *http.Client, opts *ClientOptions) transport.Transpo
|
|||||||
}
|
}
|
||||||
cl := &client{
|
cl := &client{
|
||||||
client: c,
|
client: c,
|
||||||
|
follow: FollowInitialRedirects,
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts != nil {
|
if opts != nil {
|
||||||
if opts.CacheMaxEntries > 0 {
|
if opts.CacheMaxEntries > 0 {
|
||||||
cl.transports = lru.New(opts.CacheMaxEntries)
|
cl.transports = lru.New(opts.CacheMaxEntries)
|
||||||
}
|
}
|
||||||
|
if opts.RedirectPolicy != "" {
|
||||||
|
cl.follow = opts.RedirectPolicy
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return cl
|
return cl
|
||||||
}
|
}
|
||||||
@@ -289,14 +328,9 @@ func newSession(c *client, ep *transport.Endpoint, auth transport.AuthMethod) (*
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
httpClient = &http.Client{
|
httpClient = c.cloneHTTPClient(transport)
|
||||||
Transport: transport,
|
|
||||||
CheckRedirect: c.client.CheckRedirect,
|
|
||||||
Jar: c.client.Jar,
|
|
||||||
Timeout: c.client.Timeout,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
httpClient = c.client
|
httpClient = c.cloneHTTPClient(c.client.Transport)
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &session{
|
s := &session{
|
||||||
@@ -324,30 +358,122 @@ func (s *session) ApplyAuthToRequest(req *http.Request) {
|
|||||||
s.auth.SetAuth(req)
|
s.auth.SetAuth(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *session) ModifyEndpointIfRedirect(res *http.Response) {
|
func (s *session) ModifyEndpointIfRedirect(res *http.Response) error {
|
||||||
if res.Request == nil {
|
if res.Request == nil {
|
||||||
return
|
return nil
|
||||||
|
}
|
||||||
|
if s.endpoint == nil {
|
||||||
|
return fmt.Errorf("http redirect: nil endpoint")
|
||||||
}
|
}
|
||||||
|
|
||||||
r := res.Request
|
r := res.Request
|
||||||
if !strings.HasSuffix(r.URL.Path, infoRefsPath) {
|
if !strings.HasSuffix(r.URL.Path, infoRefsPath) {
|
||||||
return
|
return fmt.Errorf("http redirect: target %q does not end with %s", r.URL.Path, infoRefsPath)
|
||||||
|
}
|
||||||
|
if r.URL.Scheme != "http" && r.URL.Scheme != "https" {
|
||||||
|
return fmt.Errorf("http redirect: unsupported scheme %q", r.URL.Scheme)
|
||||||
|
}
|
||||||
|
if r.URL.Scheme != s.endpoint.Protocol &&
|
||||||
|
!(s.endpoint.Protocol == "http" && r.URL.Scheme == "https") {
|
||||||
|
return fmt.Errorf("http redirect: changes scheme from %q to %q", s.endpoint.Protocol, r.URL.Scheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
h, p, err := net.SplitHostPort(r.URL.Host)
|
host := endpointHost(r.URL.Hostname())
|
||||||
|
port, err := endpointPort(r.URL.Port())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h = r.URL.Host
|
return err
|
||||||
}
|
}
|
||||||
if p != "" {
|
|
||||||
port, err := strconv.Atoi(p)
|
if host != s.endpoint.Host || effectivePort(r.URL.Scheme, port) != effectivePort(s.endpoint.Protocol, s.endpoint.Port) {
|
||||||
if err == nil {
|
s.endpoint.User = ""
|
||||||
|
s.endpoint.Password = ""
|
||||||
|
s.auth = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.endpoint.Host = host
|
||||||
s.endpoint.Port = port
|
s.endpoint.Port = port
|
||||||
}
|
|
||||||
}
|
|
||||||
s.endpoint.Host = h
|
|
||||||
|
|
||||||
s.endpoint.Protocol = r.URL.Scheme
|
s.endpoint.Protocol = r.URL.Scheme
|
||||||
s.endpoint.Path = r.URL.Path[:len(r.URL.Path)-len(infoRefsPath)]
|
s.endpoint.Path = r.URL.Path[:len(r.URL.Path)-len(infoRefsPath)]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func endpointHost(host string) string {
|
||||||
|
if strings.Contains(host, ":") {
|
||||||
|
return "[" + host + "]"
|
||||||
|
}
|
||||||
|
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
|
||||||
|
func endpointPort(port string) (int, error) {
|
||||||
|
if port == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := strconv.Atoi(port)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("http redirect: invalid port %q", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func effectivePort(scheme string, port int) int {
|
||||||
|
if port != 0 {
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(scheme) {
|
||||||
|
case "http":
|
||||||
|
return 80
|
||||||
|
case "https":
|
||||||
|
return 443
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) cloneHTTPClient(transport http.RoundTripper) *http.Client {
|
||||||
|
return &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
CheckRedirect: wrapCheckRedirect(c.follow, c.client.CheckRedirect),
|
||||||
|
Jar: c.client.Jar,
|
||||||
|
Timeout: c.client.Timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapCheckRedirect(policy RedirectPolicy, next func(*http.Request, []*http.Request) error) func(*http.Request, []*http.Request) error {
|
||||||
|
return func(req *http.Request, via []*http.Request) error {
|
||||||
|
if err := checkRedirect(req, via, policy); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if next != nil {
|
||||||
|
return next(req, via)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkRedirect(req *http.Request, via []*http.Request, policy RedirectPolicy) error {
|
||||||
|
switch policy {
|
||||||
|
case FollowRedirects:
|
||||||
|
case NoFollowRedirects:
|
||||||
|
return fmt.Errorf("http redirect: redirects disabled to %s", req.URL)
|
||||||
|
case "", FollowInitialRedirects:
|
||||||
|
if !isInitialRequest(req) {
|
||||||
|
return fmt.Errorf("http redirect: redirect on non-initial request to %s", req.URL)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("http redirect: invalid redirect policy %q", policy)
|
||||||
|
}
|
||||||
|
if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
|
||||||
|
return fmt.Errorf("http redirect: unsupported scheme %q", req.URL.Scheme)
|
||||||
|
}
|
||||||
|
if len(via) >= 10 {
|
||||||
|
return fmt.Errorf("http redirect: too many redirects")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*session) Close() error {
|
func (*session) Close() error {
|
||||||
|
|||||||
+6
@@ -208,6 +208,12 @@ func Open(s storage.Storer, worktree billy.Filesystem) (*Repository, error) {
|
|||||||
return nil, ErrRepositoryNotExists
|
return nil, ErrRepositoryNotExists
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg, err := s.Config()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = verifyExtensions(s, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/config"
|
||||||
|
cfgformat "github.com/go-git/go-git/v5/plumbing/format/config"
|
||||||
|
"github.com/go-git/go-git/v5/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrUnsupportedExtensionRepositoryFormatVersion represents when an
|
||||||
|
// extension being used is not compatible with the repository's
|
||||||
|
// core.repositoryFormatVersion.
|
||||||
|
ErrUnsupportedExtensionRepositoryFormatVersion = errors.New("core.repositoryformatversion does not support extension")
|
||||||
|
|
||||||
|
// ErrUnsupportedRepositoryFormatVersion represents when an repository
|
||||||
|
// is using a format version that is not supported.
|
||||||
|
ErrUnsupportedRepositoryFormatVersion = errors.New("core.repositoryformatversion not supported")
|
||||||
|
|
||||||
|
// ErrUnknownExtension represents when a repository has an extension
|
||||||
|
// which is unknown or unsupported by go-git.
|
||||||
|
ErrUnknownExtension = errors.New("unknown extension")
|
||||||
|
|
||||||
|
// builtinExtensions defines the Git extensions that are supported by
|
||||||
|
// the core go-git implementation.
|
||||||
|
//
|
||||||
|
// Some extensions are storage-specific, those are defined by the Storers
|
||||||
|
// themselves by implementing the ExtensionChecker interface.
|
||||||
|
builtinExtensions = map[string]struct{}{
|
||||||
|
// noop does not change git’s behavior at all.
|
||||||
|
// It is useful only for testing format-1 compatibility.
|
||||||
|
//
|
||||||
|
// This extension is respected regardless of the
|
||||||
|
// core.repositoryFormatVersion setting.
|
||||||
|
"noop": {},
|
||||||
|
|
||||||
|
// noop-v1 does not change git’s behavior at all.
|
||||||
|
// It is useful only for testing format-1 compatibility.
|
||||||
|
"noop-v1": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some Git extensions were supported upstream before the introduction
|
||||||
|
// of repositoryformatversion. These are the only extensions that can be
|
||||||
|
// enabled while core.repositoryformatversion is unset or set to 0.
|
||||||
|
extensionsValidForV0 = map[string]struct{}{
|
||||||
|
"noop": {},
|
||||||
|
"partialClone": {},
|
||||||
|
"preciousObjects": {},
|
||||||
|
"worktreeConfig": {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type extension struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func extensions(cfg *config.Config) []extension {
|
||||||
|
if cfg == nil || cfg.Raw == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.Raw.HasSection("extensions") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
section := cfg.Raw.Section("extensions")
|
||||||
|
out := make([]extension, 0, len(section.Options))
|
||||||
|
for _, opt := range section.Options {
|
||||||
|
out = append(out, extension{name: strings.ToLower(opt.Key), value: strings.ToLower(opt.Value)})
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyExtensions(st storage.Storer, cfg *config.Config) error {
|
||||||
|
needed := extensions(cfg)
|
||||||
|
|
||||||
|
switch cfg.Core.RepositoryFormatVersion {
|
||||||
|
case "", cfgformat.Version_0, cfgformat.Version_1:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w: %q",
|
||||||
|
ErrUnsupportedRepositoryFormatVersion,
|
||||||
|
cfg.Core.RepositoryFormatVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(needed) > 0 {
|
||||||
|
if cfg.Core.RepositoryFormatVersion == cfgformat.Version_0 ||
|
||||||
|
cfg.Core.RepositoryFormatVersion == "" {
|
||||||
|
var unsupported []string
|
||||||
|
for _, ext := range needed {
|
||||||
|
if _, ok := extensionsValidForV0[ext.name]; !ok {
|
||||||
|
unsupported = append(unsupported, ext.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(unsupported) > 0 {
|
||||||
|
return fmt.Errorf("%w: %s",
|
||||||
|
ErrUnsupportedExtensionRepositoryFormatVersion,
|
||||||
|
strings.Join(unsupported, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var missing []string
|
||||||
|
for _, ext := range needed {
|
||||||
|
if _, ok := builtinExtensions[ext.name]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
missing = append(missing, ext.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missing) > 0 {
|
||||||
|
return fmt.Errorf("%w: %s", ErrUnknownExtension, strings.Join(missing, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+58
-4
@@ -3,6 +3,7 @@ package dotgit
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
@@ -131,20 +132,62 @@ func (w *PackWriter) clean() error {
|
|||||||
|
|
||||||
func (w *PackWriter) save() error {
|
func (w *PackWriter) save() error {
|
||||||
base := w.fs.Join(objectsPath, packPath, fmt.Sprintf("pack-%s", w.checksum))
|
base := w.fs.Join(objectsPath, packPath, fmt.Sprintf("pack-%s", w.checksum))
|
||||||
idx, err := w.fs.Create(fmt.Sprintf("%s.idx", base))
|
|
||||||
|
// Pack files are content addressable. Each file is checked
|
||||||
|
// individually — if it already exists on disk, skip creating it.
|
||||||
|
idxPath := fmt.Sprintf("%s.idx", base)
|
||||||
|
exists, err := fileExists(w.fs, idxPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
idx, err := w.fs.Create(idxPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := w.encodeIdx(idx); err != nil {
|
if err := w.encodeIdx(idx); err != nil {
|
||||||
|
_ = idx.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := idx.Close(); err != nil {
|
if err := idx.Close(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
fixPermissions(w.fs, idxPath)
|
||||||
|
}
|
||||||
|
|
||||||
return w.fs.Rename(w.fw.Name(), fmt.Sprintf("%s.pack", base))
|
packPath := fmt.Sprintf("%s.pack", base)
|
||||||
|
exists, err = fileExists(w.fs, packPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
if err := w.fs.Rename(w.fw.Name(), packPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fixPermissions(w.fs, packPath)
|
||||||
|
} else {
|
||||||
|
// Pack already exists, clean up the temp file.
|
||||||
|
return w.clean()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileExists checks whether path already exists as a regular file.
|
||||||
|
// It returns (true, nil) for an existing regular file, (false, nil) when the
|
||||||
|
// path does not exist, and (false, err) if the path exists but is not a
|
||||||
|
// regular file (e.g. a directory or symlink).
|
||||||
|
func fileExists(fs billy.Filesystem, path string) (bool, error) {
|
||||||
|
fi, err := fs.Lstat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if !fi.Mode().IsRegular() {
|
||||||
|
return false, fmt.Errorf("unexpected file type for %q: %s", path, fi.Mode().Type())
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *PackWriter) encodeIdx(writer io.Writer) error {
|
func (w *PackWriter) encodeIdx(writer io.Writer) error {
|
||||||
@@ -226,7 +269,6 @@ func (s *syncedReader) sleep() {
|
|||||||
atomic.StoreUint32(&s.blocked, 1)
|
atomic.StoreUint32(&s.blocked, 1)
|
||||||
<-s.news
|
<-s.news
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *syncedReader) Seek(offset int64, whence int) (int64, error) {
|
func (s *syncedReader) Seek(offset int64, whence int) (int64, error) {
|
||||||
@@ -281,5 +323,17 @@ func (w *ObjectWriter) save() error {
|
|||||||
hex := w.Hash().String()
|
hex := w.Hash().String()
|
||||||
file := w.fs.Join(objectsPath, hex[0:2], hex[2:hash.HexSize])
|
file := w.fs.Join(objectsPath, hex[0:2], hex[2:hash.HexSize])
|
||||||
|
|
||||||
return w.fs.Rename(w.f.Name(), file)
|
// Loose objects are content addressable, if they already exist
|
||||||
|
// we can safely delete the temporary file and short-circuit the
|
||||||
|
// operation.
|
||||||
|
if _, err := w.fs.Lstat(file); err == nil || os.IsExist(err) {
|
||||||
|
return w.fs.Remove(w.f.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.fs.Rename(w.f.Name(), file); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fixPermissions(w.fs, file)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package dotgit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-git/go-billy/v5"
|
||||||
|
"github.com/go-git/go-git/v5/utils/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fixPermissions(fs billy.Filesystem, path string) {
|
||||||
|
if chmodFS, ok := fs.(billy.Chmod); ok {
|
||||||
|
if err := chmodFS.Chmod(path, 0o444); err != nil {
|
||||||
|
trace.General.Printf("failed to chmod %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isReadOnly(fs billy.Filesystem, path string) (bool, error) {
|
||||||
|
fi, err := fs.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi.Mode().Perm() == 0o444 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package dotgit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/go-git/go-billy/v5"
|
||||||
|
"github.com/go-git/go-git/v5/utils/trace"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fixPermissions(fs billy.Filesystem, path string) {
|
||||||
|
fullpath := filepath.Join(fs.Root(), path)
|
||||||
|
p, err := windows.UTF16PtrFromString(fullpath)
|
||||||
|
if err != nil {
|
||||||
|
trace.General.Printf("failed to chmod %s: %v", fullpath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs, err := windows.GetFileAttributes(p)
|
||||||
|
if err != nil {
|
||||||
|
trace.General.Printf("failed to chmod %s: %v", fullpath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if attrs&windows.FILE_ATTRIBUTE_READONLY != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = windows.SetFileAttributes(p,
|
||||||
|
attrs|windows.FILE_ATTRIBUTE_READONLY,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
trace.General.Printf("failed to chmod %s: %v", fullpath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isReadOnly(fs billy.Filesystem, path string) (bool, error) {
|
||||||
|
fullpath := filepath.Join(fs.Root(), path)
|
||||||
|
p, err := windows.UTF16PtrFromString(fullpath)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("%w: %q", err, fullpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs, err := windows.GetFileAttributes(p)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("%w: %q", err, fullpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if attrs&windows.FILE_ATTRIBUTE_READONLY != 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
+5
@@ -48,6 +48,11 @@ func (s *IndexStorage) Index() (i *index.Index, err error) {
|
|||||||
|
|
||||||
defer ioutil.CheckClose(f, &err)
|
defer ioutil.CheckClose(f, &err)
|
||||||
|
|
||||||
|
fi, statErr := s.dir.Fs().Stat(f.Name())
|
||||||
|
if statErr == nil {
|
||||||
|
idx.ModTime = fi.ModTime()
|
||||||
|
}
|
||||||
|
|
||||||
d := index.NewDecoder(f)
|
d := index.NewDecoder(f)
|
||||||
err = d.Decode(idx)
|
err = d.Decode(idx)
|
||||||
return idx, err
|
return idx, err
|
||||||
|
|||||||
+4
@@ -69,7 +69,11 @@ type IndexStorage struct {
|
|||||||
index *index.Index
|
index *index.Index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetIndex stores the given index.
|
||||||
|
// Note: this method sets idx.ModTime to simulate filesystem storage behavior.
|
||||||
func (c *IndexStorage) SetIndex(idx *index.Index) error {
|
func (c *IndexStorage) SetIndex(idx *index.Index) error {
|
||||||
|
// Set ModTime to enable racy git detection in the metadata optimization.
|
||||||
|
idx.ModTime = time.Now()
|
||||||
c.index = idx
|
c.index = idx
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+99
-1
@@ -4,9 +4,11 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
"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/filemode"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/format/index"
|
||||||
"github.com/go-git/go-git/v5/utils/merkletrie/noder"
|
"github.com/go-git/go-git/v5/utils/merkletrie/noder"
|
||||||
|
|
||||||
"github.com/go-git/go-billy/v5"
|
"github.com/go-git/go-billy/v5"
|
||||||
@@ -16,6 +18,14 @@ var ignore = map[string]bool{
|
|||||||
".git": true,
|
".git": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Options contains configuration for the filesystem node.
|
||||||
|
type Options struct {
|
||||||
|
// Index is used to enable the metadata-first comparison optimization while
|
||||||
|
// correctly handling the "racy git" condition. If no index is provided,
|
||||||
|
// the function works without the optimization.
|
||||||
|
Index *index.Index
|
||||||
|
}
|
||||||
|
|
||||||
// The node represents a file or a directory in a billy.Filesystem. It
|
// The node represents a file or a directory in a billy.Filesystem. It
|
||||||
// implements the interface noder.Noder of merkletrie package.
|
// implements the interface noder.Noder of merkletrie package.
|
||||||
//
|
//
|
||||||
@@ -24,6 +34,8 @@ var ignore = map[string]bool{
|
|||||||
type node struct {
|
type node struct {
|
||||||
fs billy.Filesystem
|
fs billy.Filesystem
|
||||||
submodules map[string]plumbing.Hash
|
submodules map[string]plumbing.Hash
|
||||||
|
idx *index.Index
|
||||||
|
idxMap map[string]*index.Entry
|
||||||
|
|
||||||
path string
|
path string
|
||||||
hash []byte
|
hash []byte
|
||||||
@@ -31,6 +43,7 @@ type node struct {
|
|||||||
isDir bool
|
isDir bool
|
||||||
mode os.FileMode
|
mode os.FileMode
|
||||||
size int64
|
size int64
|
||||||
|
modTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRootNode returns the root node based on a given billy.Filesystem.
|
// NewRootNode returns the root node based on a given billy.Filesystem.
|
||||||
@@ -42,7 +55,41 @@ func NewRootNode(
|
|||||||
fs billy.Filesystem,
|
fs billy.Filesystem,
|
||||||
submodules map[string]plumbing.Hash,
|
submodules map[string]plumbing.Hash,
|
||||||
) noder.Noder {
|
) noder.Noder {
|
||||||
return &node{fs: fs, submodules: submodules, isDir: true}
|
return NewRootNodeWithOptions(fs, submodules, Options{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRootNodeWithOptions returns the root node based on a given billy.Filesystem
|
||||||
|
// with options to set an index. Providing an index enables the metadata-first
|
||||||
|
// comparison optimization while correctly handling the "racy git" condition. If
|
||||||
|
// no index is provided, the function works without the optimization.
|
||||||
|
//
|
||||||
|
// The index's ModTime field is used to detect the racy git condition. When a file's
|
||||||
|
// mtime equals or is newer than the index ModTime, we must hash the file content
|
||||||
|
// even if other metadata matches, because the file may have been modified in the
|
||||||
|
// same second that the index was written.
|
||||||
|
//
|
||||||
|
// Reference: https://git-scm.com/docs/racy-git
|
||||||
|
func NewRootNodeWithOptions(
|
||||||
|
fs billy.Filesystem,
|
||||||
|
submodules map[string]plumbing.Hash,
|
||||||
|
options Options,
|
||||||
|
) noder.Noder {
|
||||||
|
var idxMap map[string]*index.Entry
|
||||||
|
|
||||||
|
if options.Index != nil {
|
||||||
|
idxMap = make(map[string]*index.Entry, len(options.Index.Entries))
|
||||||
|
for _, entry := range options.Index.Entries {
|
||||||
|
idxMap[entry.Name] = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &node{
|
||||||
|
fs: fs,
|
||||||
|
submodules: submodules,
|
||||||
|
idx: options.Index,
|
||||||
|
idxMap: idxMap,
|
||||||
|
isDir: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash the hash of a filesystem is the result of concatenating the computed
|
// Hash the hash of a filesystem is the result of concatenating the computed
|
||||||
@@ -133,11 +180,14 @@ func (n *node) newChildNode(file os.FileInfo) (*node, error) {
|
|||||||
node := &node{
|
node := &node{
|
||||||
fs: n.fs,
|
fs: n.fs,
|
||||||
submodules: n.submodules,
|
submodules: n.submodules,
|
||||||
|
idx: n.idx,
|
||||||
|
idxMap: n.idxMap,
|
||||||
|
|
||||||
path: path,
|
path: path,
|
||||||
isDir: file.IsDir(),
|
isDir: file.IsDir(),
|
||||||
size: file.Size(),
|
size: file.Size(),
|
||||||
mode: file.Mode(),
|
mode: file.Mode(),
|
||||||
|
modTime: file.ModTime(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, isSubmodule := n.submodules[path]; isSubmodule {
|
if _, isSubmodule := n.submodules[path]; isSubmodule {
|
||||||
@@ -161,6 +211,16 @@ func (n *node) calculateHash() {
|
|||||||
n.hash = append(submoduleHash[:], filemode.Submodule.Bytes()...)
|
n.hash = append(submoduleHash[:], filemode.Submodule.Bytes()...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if n.idxMap != nil {
|
||||||
|
if entry, ok := n.idxMap[n.path]; ok {
|
||||||
|
if n.metadataMatches(entry) {
|
||||||
|
n.hash = append(entry.Hash[:], mode.Bytes()...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var hash plumbing.Hash
|
var hash plumbing.Hash
|
||||||
if n.mode&os.ModeSymlink != 0 {
|
if n.mode&os.ModeSymlink != 0 {
|
||||||
hash = n.doCalculateHashForSymlink()
|
hash = n.doCalculateHashForSymlink()
|
||||||
@@ -170,6 +230,44 @@ func (n *node) calculateHash() {
|
|||||||
n.hash = append(hash[:], mode.Bytes()...)
|
n.hash = append(hash[:], mode.Bytes()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *node) metadataMatches(entry *index.Entry) bool {
|
||||||
|
if entry == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if uint32(n.size) != entry.Size {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !n.modTime.IsZero() && !n.modTime.Equal(entry.ModifiedAt) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
mode, err := filemode.NewFromOSFileMode(n.mode)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode != entry.Mode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.idx != nil && !n.idx.ModTime.IsZero() && !n.modTime.IsZero() {
|
||||||
|
if !n.modTime.Before(n.idx.ModTime) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we couldn't perform the racy git check (idx is nil or idx.ModTime is zero),
|
||||||
|
// we cannot safely rely on metadata alone — force content hashing.
|
||||||
|
// This can occur with in-memory storage where the index file timestamp is unavailable.
|
||||||
|
if n.idx == nil || n.idx.ModTime.IsZero() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (n *node) doCalculateHashForRegular() plumbing.Hash {
|
func (n *node) doCalculateHashForRegular() plumbing.Hash {
|
||||||
f, err := n.fs.Open(n.path)
|
f, err := n.fs.Open(n.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+22
-11
@@ -385,7 +385,8 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) ([]
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var removedFiles []string
|
removedFiles := make([]string, 0, len(changes))
|
||||||
|
filesMap := buildFilePathMap(files)
|
||||||
for _, ch := range changes {
|
for _, ch := range changes {
|
||||||
a, err := ch.Action()
|
a, err := ch.Action()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -407,7 +408,7 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) ([]
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(files) > 0 {
|
if len(files) > 0 {
|
||||||
contains := inFiles(files, name)
|
contains := inFiles(filesMap, name)
|
||||||
if !contains {
|
if !contains {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -436,15 +437,11 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) ([]
|
|||||||
return removedFiles, w.r.Storer.SetIndex(idx)
|
return removedFiles, w.r.Storer.SetIndex(idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func inFiles(files []string, v string) bool {
|
// inFiles checks if the given file is in the list of files. The incoming filepaths in files should be cleaned before calling this function.
|
||||||
|
func inFiles(files map[string]struct{}, v string) bool {
|
||||||
v = filepath.Clean(v)
|
v = filepath.Clean(v)
|
||||||
for _, s := range files {
|
_, exists := files[v]
|
||||||
if filepath.Clean(s) == v {
|
return exists
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Worktree) resetWorktree(t *object.Tree, files []string) error {
|
func (w *Worktree) resetWorktree(t *object.Tree, files []string) error {
|
||||||
@@ -459,6 +456,7 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error {
|
|||||||
}
|
}
|
||||||
b := newIndexBuilder(idx)
|
b := newIndexBuilder(idx)
|
||||||
|
|
||||||
|
filesMap := buildFilePathMap(files)
|
||||||
for _, ch := range changes {
|
for _, ch := range changes {
|
||||||
if err := w.validChange(ch); err != nil {
|
if err := w.validChange(ch); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -476,7 +474,7 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
contains := inFiles(files, file)
|
contains := inFiles(filesMap, file)
|
||||||
if !contains {
|
if !contains {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1206,3 +1204,16 @@ func (b *indexBuilder) Add(e *index.Entry) {
|
|||||||
func (b *indexBuilder) Remove(name string) {
|
func (b *indexBuilder) Remove(name string) {
|
||||||
delete(b.entries, filepath.ToSlash(name))
|
delete(b.entries, filepath.ToSlash(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildFilePathMap creates a map of cleaned file paths for efficient lookup.
|
||||||
|
// Returns nil if the input slice is empty.
|
||||||
|
func buildFilePathMap(files []string) map[string]struct{} {
|
||||||
|
if len(files) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
filesMap := make(map[string]struct{}, len(files))
|
||||||
|
for _, f := range files {
|
||||||
|
filesMap[filepath.Clean(f)] = struct{}{}
|
||||||
|
}
|
||||||
|
return filesMap
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -141,7 +141,7 @@ func (w *Worktree) diffStagingWithWorktree(reverse, excludeIgnoredChanges bool)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
to := filesystem.NewRootNode(w.Filesystem, submodules)
|
to := filesystem.NewRootNodeWithOptions(w.Filesystem, submodules, filesystem.Options{Index: idx})
|
||||||
|
|
||||||
var c merkletrie.Changes
|
var c merkletrie.Changes
|
||||||
if reverse {
|
if reverse {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.24@sha256:14fd8a55e59a560704e5fc44970b301d00d344e45d6b914dda228e09f359a088
|
FROM golang:1.25@sha256:31c1e53dfc1cc2d269deec9c83f58729fa3c53dc9a576f6426109d1e319e9e9a
|
||||||
|
|
||||||
ENV GOOS=linux
|
ENV GOOS=linux
|
||||||
ENV GOARCH=arm
|
ENV GOARCH=arm
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.24@sha256:14fd8a55e59a560704e5fc44970b301d00d344e45d6b914dda228e09f359a088
|
FROM golang:1.25@sha256:31c1e53dfc1cc2d269deec9c83f58729fa3c53dc9a576f6426109d1e319e9e9a
|
||||||
|
|
||||||
ENV GOOS=linux
|
ENV GOOS=linux
|
||||||
ENV GOARCH=arm64
|
ENV GOARCH=arm64
|
||||||
|
|||||||
-5
@@ -12,7 +12,6 @@ package sha1cd
|
|||||||
// Original: https://github.com/golang/go/blob/master/src/crypto/sha1/sha1.go
|
// Original: https://github.com/golang/go/blob/master/src/crypto/sha1/sha1.go
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"hash"
|
"hash"
|
||||||
@@ -20,10 +19,6 @@ import (
|
|||||||
shared "github.com/pjbgf/sha1cd/internal"
|
shared "github.com/pjbgf/sha1cd/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
crypto.RegisterHash(crypto.SHA1, New)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The size of a SHA-1 checksum in bytes.
|
// The size of a SHA-1 checksum in bytes.
|
||||||
const Size = shared.Size
|
const Size = shared.Size
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -37,9 +37,9 @@ func block(dig *digest, p []byte) {
|
|||||||
chunk := p[:shared.Chunk]
|
chunk := p[:shared.Chunk]
|
||||||
|
|
||||||
blockAMD64(dig.h[:], chunk, m1[:], cs[:])
|
blockAMD64(dig.h[:], chunk, m1[:], cs[:])
|
||||||
rectifyCompressionState(m1, &cs)
|
rectifyCompressionState(&m1, &cs)
|
||||||
|
|
||||||
col := checkCollision(m1, cs, dig.h)
|
col := checkCollision(&m1, &cs, &dig.h)
|
||||||
if col {
|
if col {
|
||||||
dig.col = true
|
dig.col = true
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -11,11 +11,11 @@
|
|||||||
// Reference implementations:
|
// Reference implementations:
|
||||||
// - https://github.com/golang/go/blob/master/src/crypto/sha1/sha1block_amd64.s
|
// - https://github.com/golang/go/blob/master/src/crypto/sha1/sha1block_amd64.s
|
||||||
|
|
||||||
|
// Reverse the dword order in abcd via PSHUFD then store the 16 bytes in one
|
||||||
|
// move, instead of issuing four VPEXTRD's that each go through the store port.
|
||||||
#define LOADCS(abcd, e, index, target) \
|
#define LOADCS(abcd, e, index, target) \
|
||||||
VPEXTRD $3, abcd, ((index*20)+0)(target); \
|
VPSHUFD $0x1B, abcd, X8; \
|
||||||
VPEXTRD $2, abcd, ((index*20)+4)(target); \
|
VMOVDQU X8, ((index*20)+0)(target); \
|
||||||
VPEXTRD $1, abcd, ((index*20)+8)(target); \
|
|
||||||
VPEXTRD $0, abcd, ((index*20)+12)(target); \
|
|
||||||
MOVL e, ((index*20)+16)(target);
|
MOVL e, ((index*20)+16)(target);
|
||||||
|
|
||||||
#define LOADM1(m1, index, target) \
|
#define LOADM1(m1, index, target) \
|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@ func block(dig *digest, p []byte) {
|
|||||||
|
|
||||||
blockARM64(dig.h[:], chunk, m1[:], cs[:])
|
blockARM64(dig.h[:], chunk, m1[:], cs[:])
|
||||||
|
|
||||||
rectifyCompressionState(m1, &cs)
|
rectifyCompressionState(&m1, &cs)
|
||||||
col := checkCollision(m1, cs, dig.h)
|
col := checkCollision(&m1, &cs, &dig.h)
|
||||||
if col {
|
if col {
|
||||||
dig.col = true
|
dig.col = true
|
||||||
|
|
||||||
|
|||||||
+13
-12
@@ -127,7 +127,8 @@ func blockGeneric(dig *digest, p []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if hi == 1 {
|
if hi == 1 {
|
||||||
col := checkCollision(m1, cs, [shared.WordBuffers]uint32{h0, h1, h2, h3, h4})
|
h := [shared.WordBuffers]uint32{h0, h1, h2, h3, h4}
|
||||||
|
col := checkCollision(&m1, &cs, &h)
|
||||||
if col {
|
if col {
|
||||||
dig.col = true
|
dig.col = true
|
||||||
hi++
|
hi++
|
||||||
@@ -143,23 +144,23 @@ func blockGeneric(dig *digest, p []byte) {
|
|||||||
|
|
||||||
//go:noinline
|
//go:noinline
|
||||||
func checkCollision(
|
func checkCollision(
|
||||||
m1 [shared.Rounds]uint32,
|
m1 *[shared.Rounds]uint32,
|
||||||
cs [shared.PreStepState][shared.WordBuffers]uint32,
|
cs *[shared.PreStepState][shared.WordBuffers]uint32,
|
||||||
h [shared.WordBuffers]uint32,
|
h *[shared.WordBuffers]uint32,
|
||||||
) bool {
|
) bool {
|
||||||
if mask := ubc.CalculateDvMask(m1); mask != 0 {
|
if mask := ubc.CalculateDvMask(m1); mask != 0 {
|
||||||
dvs := ubc.SHA1_dvs()
|
dvs := ubc.SHA1_dvs()
|
||||||
|
|
||||||
for i := 0; dvs[i].DvType != 0; i++ {
|
for i := 0; dvs[i].DvType != 0; i++ {
|
||||||
if (mask & ((uint32)(1) << uint32(dvs[i].MaskB))) != 0 {
|
if (mask & ((uint32)(1) << uint32(dvs[i].MaskB))) != 0 {
|
||||||
var csState [shared.WordBuffers]uint32
|
var csState *[shared.WordBuffers]uint32
|
||||||
switch dvs[i].TestT {
|
switch dvs[i].TestT {
|
||||||
case 58:
|
case 58:
|
||||||
csState = cs[1]
|
csState = &cs[1]
|
||||||
case 65:
|
case 65:
|
||||||
csState = cs[2]
|
csState = &cs[2]
|
||||||
case 0:
|
case 0:
|
||||||
csState = cs[0]
|
csState = &cs[0]
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("dvs data is trying to use a testT that isn't available: %d", dvs[i].TestT))
|
panic(fmt.Sprintf("dvs data is trying to use a testT that isn't available: %d", dvs[i].TestT))
|
||||||
}
|
}
|
||||||
@@ -168,7 +169,7 @@ func checkCollision(
|
|||||||
dvs[i].TestT, // testT is the step number
|
dvs[i].TestT, // testT is the step number
|
||||||
// m2 is a secondary message created XORing with
|
// m2 is a secondary message created XORing with
|
||||||
// ubc's DM prior to the SHA recompression step.
|
// ubc's DM prior to the SHA recompression step.
|
||||||
m1, dvs[i].Dm,
|
m1, &dvs[i].Dm,
|
||||||
csState,
|
csState,
|
||||||
h)
|
h)
|
||||||
|
|
||||||
@@ -182,8 +183,8 @@ func checkCollision(
|
|||||||
}
|
}
|
||||||
|
|
||||||
//go:nosplit
|
//go:nosplit
|
||||||
func hasCollided(step uint32, m1, dm [shared.Rounds]uint32,
|
func hasCollided(step uint32, m1, dm *[shared.Rounds]uint32,
|
||||||
state [shared.WordBuffers]uint32, h [shared.WordBuffers]uint32) bool {
|
state *[shared.WordBuffers]uint32, h *[shared.WordBuffers]uint32) bool {
|
||||||
// Intermediary Hash Value.
|
// Intermediary Hash Value.
|
||||||
ihv := [shared.WordBuffers]uint32{}
|
ihv := [shared.WordBuffers]uint32{}
|
||||||
|
|
||||||
@@ -282,7 +283,7 @@ func hasCollided(step uint32, m1, dm [shared.Rounds]uint32,
|
|||||||
//
|
//
|
||||||
//go:nosplit
|
//go:nosplit
|
||||||
func rectifyCompressionState(
|
func rectifyCompressionState(
|
||||||
m1 [shared.Rounds]uint32,
|
m1 *[shared.Rounds]uint32,
|
||||||
cs *[shared.PreStepState][shared.WordBuffers]uint32,
|
cs *[shared.PreStepState][shared.WordBuffers]uint32,
|
||||||
) {
|
) {
|
||||||
if cs == nil {
|
if cs == nil {
|
||||||
|
|||||||
+4
-1
@@ -29,7 +29,10 @@ type DvInfo struct {
|
|||||||
// bitconditions for that DV have been met.
|
// bitconditions for that DV have been met.
|
||||||
//
|
//
|
||||||
//go:nosplit
|
//go:nosplit
|
||||||
func CalculateDvMask(W [80]uint32) uint32 {
|
func CalculateDvMask(W *[80]uint32) uint32 {
|
||||||
|
if W == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
mask := uint32(0xFFFFFFFF)
|
mask := uint32(0xFFFFFFFF)
|
||||||
mask &= (((((W[44] ^ W[45]) >> 29) & 1) - 1) | ^(DV_I_48_0_bit | DV_I_51_0_bit | DV_I_52_0_bit | DV_II_45_0_bit | DV_II_46_0_bit | DV_II_50_0_bit | DV_II_51_0_bit))
|
mask &= (((((W[44] ^ W[45]) >> 29) & 1) - 1) | ^(DV_I_48_0_bit | DV_I_51_0_bit | DV_I_52_0_bit | DV_II_45_0_bit | DV_II_46_0_bit | DV_II_50_0_bit | DV_II_51_0_bit))
|
||||||
mask &= (((((W[49] ^ W[50]) >> 29) & 1) - 1) | ^(DV_I_46_0_bit | DV_II_45_0_bit | DV_II_50_0_bit | DV_II_51_0_bit | DV_II_55_0_bit | DV_II_56_0_bit))
|
mask &= (((((W[49] ^ W[50]) >> 29) & 1) - 1) | ^(DV_I_46_0_bit | DV_II_45_0_bit | DV_II_50_0_bit | DV_II_51_0_bit | DV_II_55_0_bit | DV_II_56_0_bit))
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ func (s *server) processRequestBytes(reqData []byte) []byte {
|
|||||||
return []byte{agentFailure}
|
return []byte{agentFailure}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil && rep == nil {
|
if rep == nil {
|
||||||
return []byte{agentSuccess}
|
return []byte{agentSuccess}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -586,7 +586,7 @@ func (c *cbcCipher) writeCipherPacket(seqNum uint32, w io.Writer, rand io.Reader
|
|||||||
|
|
||||||
// Length of encrypted portion of the packet (header, payload, padding).
|
// Length of encrypted portion of the packet (header, payload, padding).
|
||||||
// Enforce minimum padding and packet size.
|
// Enforce minimum padding and packet size.
|
||||||
encLength := maxUInt32(prefixLen+len(packet)+cbcMinPaddingSize, cbcMinPaddingSize)
|
encLength := maxUInt32(prefixLen+len(packet)+cbcMinPaddingSize, cbcMinPacketSize)
|
||||||
// Enforce block size.
|
// Enforce block size.
|
||||||
encLength = (encLength + effectiveBlockSize - 1) / effectiveBlockSize * effectiveBlockSize
|
encLength = (encLength + effectiveBlockSize - 1) / effectiveBlockSize * effectiveBlockSize
|
||||||
|
|
||||||
|
|||||||
+7
-3
@@ -274,10 +274,14 @@ func pickSignatureAlgorithm(signer Signer, extensions map[string][]byte) (MultiA
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter algorithms based on those supported by MultiAlgorithmSigner.
|
// Filter algorithms based on those supported by MultiAlgorithmSigner.
|
||||||
|
// Iterate over the signer's algorithms first to preserve its preference order.
|
||||||
|
supportedKeyAlgos := algorithmsForKeyFormat(keyFormat)
|
||||||
var keyAlgos []string
|
var keyAlgos []string
|
||||||
for _, algo := range algorithmsForKeyFormat(keyFormat) {
|
for _, signerAlgo := range as.Algorithms() {
|
||||||
if slices.Contains(as.Algorithms(), underlyingAlgo(algo)) {
|
if idx := slices.IndexFunc(supportedKeyAlgos, func(algo string) bool {
|
||||||
keyAlgos = append(keyAlgos, algo)
|
return underlyingAlgo(algo) == signerAlgo
|
||||||
|
}); idx >= 0 {
|
||||||
|
keyAlgos = append(keyAlgos, supportedKeyAlgos[idx])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright 2024 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build darwin && arm64 && gc
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
TEXT libc_sysctlbyname_trampoline<>(SB),NOSPLIT,$0-0
|
||||||
|
JMP libc_sysctlbyname(SB)
|
||||||
|
GLOBL ·libc_sysctlbyname_trampoline_addr(SB), RODATA, $8
|
||||||
|
DATA ·libc_sysctlbyname_trampoline_addr(SB)/8, $libc_sysctlbyname_trampoline<>(SB)
|
||||||
+3
-6
@@ -44,14 +44,11 @@ func initOptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func archInit() {
|
func archInit() {
|
||||||
switch runtime.GOOS {
|
if runtime.GOOS == "freebsd" {
|
||||||
case "freebsd":
|
|
||||||
readARM64Registers()
|
readARM64Registers()
|
||||||
case "linux", "netbsd", "openbsd", "windows":
|
} else {
|
||||||
|
// Most platforms don't seem to allow directly reading these registers.
|
||||||
doinit()
|
doinit()
|
||||||
default:
|
|
||||||
// Many platforms don't seem to allow reading these registers.
|
|
||||||
setMinimalFeatures()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
// Copyright 2026 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build darwin && arm64 && gc
|
||||||
|
|
||||||
|
package cpu
|
||||||
|
|
||||||
|
func doinit() {
|
||||||
|
setMinimalFeatures()
|
||||||
|
|
||||||
|
// The feature flags are explained in [Instruction Set Detection].
|
||||||
|
// There are some differences between MacOS versions:
|
||||||
|
//
|
||||||
|
// MacOS 11 and 12 do not have "hw.optional" sysctl values for some of the features.
|
||||||
|
//
|
||||||
|
// MacOS 13 changed some of the naming conventions to align with ARM Architecture Reference Manual.
|
||||||
|
// For example "hw.optional.armv8_2_sha512" became "hw.optional.arm.FEAT_SHA512".
|
||||||
|
// It currently checks both to stay compatible with MacOS 11 and 12.
|
||||||
|
// The old names also work with MacOS 13, however it's not clear whether
|
||||||
|
// they will continue working with future OS releases.
|
||||||
|
//
|
||||||
|
// Once MacOS 12 is no longer supported the old names can be removed.
|
||||||
|
//
|
||||||
|
// [Instruction Set Detection]: https://developer.apple.com/documentation/kernel/1387446-sysctlbyname/determining_instruction_set_characteristics
|
||||||
|
|
||||||
|
// Encryption, hashing and checksum capabilities
|
||||||
|
|
||||||
|
// For the following flags there are no MacOS 11 sysctl flags.
|
||||||
|
ARM64.HasAES = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_AES\x00"))
|
||||||
|
ARM64.HasPMULL = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_PMULL\x00"))
|
||||||
|
ARM64.HasSHA1 = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA1\x00"))
|
||||||
|
ARM64.HasSHA2 = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA256\x00"))
|
||||||
|
|
||||||
|
ARM64.HasSHA3 = darwinSysctlEnabled([]byte("hw.optional.armv8_2_sha3\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA3\x00"))
|
||||||
|
ARM64.HasSHA512 = darwinSysctlEnabled([]byte("hw.optional.armv8_2_sha512\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA512\x00"))
|
||||||
|
|
||||||
|
ARM64.HasCRC32 = darwinSysctlEnabled([]byte("hw.optional.armv8_crc32\x00"))
|
||||||
|
|
||||||
|
// Atomic and memory ordering
|
||||||
|
ARM64.HasATOMICS = darwinSysctlEnabled([]byte("hw.optional.armv8_1_atomics\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_LSE\x00"))
|
||||||
|
ARM64.HasLRCPC = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_LRCPC\x00"))
|
||||||
|
|
||||||
|
// SIMD and floating point capabilities
|
||||||
|
ARM64.HasFPHP = darwinSysctlEnabled([]byte("hw.optional.neon_fp16\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FP16\x00"))
|
||||||
|
ARM64.HasASIMDHP = darwinSysctlEnabled([]byte("hw.optional.neon_hpfp\x00")) || darwinSysctlEnabled([]byte("hw.optional.AdvSIMD_HPFPCvt\x00"))
|
||||||
|
ARM64.HasASIMDRDM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_RDM\x00"))
|
||||||
|
ARM64.HasASIMDDP = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DotProd\x00"))
|
||||||
|
ARM64.HasASIMDFHM = darwinSysctlEnabled([]byte("hw.optional.armv8_2_fhm\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FHM\x00"))
|
||||||
|
ARM64.HasI8MM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_I8MM\x00"))
|
||||||
|
|
||||||
|
ARM64.HasJSCVT = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_JSCVT\x00"))
|
||||||
|
ARM64.HasFCMA = darwinSysctlEnabled([]byte("hw.optional.armv8_3_compnum\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FCMA\x00"))
|
||||||
|
|
||||||
|
// Miscellaneous
|
||||||
|
ARM64.HasDCPOP = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DPB\x00"))
|
||||||
|
ARM64.HasEVTSTRM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_ECV\x00"))
|
||||||
|
ARM64.HasDIT = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DIT\x00"))
|
||||||
|
|
||||||
|
// Not supported, but added for completeness
|
||||||
|
ARM64.HasCPUID = false
|
||||||
|
|
||||||
|
ARM64.HasSM3 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SM3\x00"))
|
||||||
|
ARM64.HasSM4 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SM4\x00"))
|
||||||
|
ARM64.HasSVE = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SVE\x00"))
|
||||||
|
ARM64.HasSVE2 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SVE2\x00"))
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2026 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build darwin && arm64 && !gc
|
||||||
|
|
||||||
|
package cpu
|
||||||
|
|
||||||
|
import "runtime"
|
||||||
|
|
||||||
|
func doinit() {
|
||||||
|
setMinimalFeatures()
|
||||||
|
|
||||||
|
ARM64.HasASIMD = true
|
||||||
|
ARM64.HasFP = true
|
||||||
|
|
||||||
|
// Go already assumes these to be available because they were on the M1
|
||||||
|
// and these are supported on all Apple arm64 chips.
|
||||||
|
ARM64.HasAES = true
|
||||||
|
ARM64.HasPMULL = true
|
||||||
|
ARM64.HasSHA1 = true
|
||||||
|
ARM64.HasSHA2 = true
|
||||||
|
|
||||||
|
if runtime.GOOS != "ios" {
|
||||||
|
// Apple A7 processors do not support these, however
|
||||||
|
// M-series SoCs are at least armv8.4-a
|
||||||
|
ARM64.HasCRC32 = true // armv8.1
|
||||||
|
ARM64.HasATOMICS = true // armv8.2
|
||||||
|
ARM64.HasJSCVT = true // armv8.3, if HasFP
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -9,3 +9,4 @@ package cpu
|
|||||||
func getisar0() uint64 { return 0 }
|
func getisar0() uint64 { return 0 }
|
||||||
func getisar1() uint64 { return 0 }
|
func getisar1() uint64 { return 0 }
|
||||||
func getpfr0() uint64 { return 0 }
|
func getpfr0() uint64 { return 0 }
|
||||||
|
func getzfr0() uint64 { return 0 }
|
||||||
|
|||||||
+4
-2
@@ -2,8 +2,10 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build !linux && !netbsd && !openbsd && !windows && arm64
|
//go:build !darwin && !linux && !netbsd && !openbsd && arm64
|
||||||
|
|
||||||
package cpu
|
package cpu
|
||||||
|
|
||||||
func doinit() {}
|
func doinit() {
|
||||||
|
setMinimalFeatures()
|
||||||
|
}
|
||||||
|
|||||||
-42
@@ -1,42 +0,0 @@
|
|||||||
// Copyright 2026 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package cpu
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
func doinit() {
|
|
||||||
// set HasASIMD and HasFP to true as per
|
|
||||||
// https://learn.microsoft.com/en-us/cpp/build/arm64-windows-abi-conventions?view=msvc-170#base-requirements
|
|
||||||
//
|
|
||||||
// The ARM64 version of Windows always presupposes that it's running on an ARMv8 or later architecture.
|
|
||||||
// Both floating-point and NEON support are presumed to be present in hardware.
|
|
||||||
//
|
|
||||||
ARM64.HasASIMD = true
|
|
||||||
ARM64.HasFP = true
|
|
||||||
|
|
||||||
if windows.IsProcessorFeaturePresent(windows.PF_ARM_V8_CRYPTO_INSTRUCTIONS_AVAILABLE) {
|
|
||||||
ARM64.HasAES = true
|
|
||||||
ARM64.HasPMULL = true
|
|
||||||
ARM64.HasSHA1 = true
|
|
||||||
ARM64.HasSHA2 = true
|
|
||||||
}
|
|
||||||
ARM64.HasSHA3 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SHA3_INSTRUCTIONS_AVAILABLE)
|
|
||||||
ARM64.HasCRC32 = windows.IsProcessorFeaturePresent(windows.PF_ARM_V8_CRC32_INSTRUCTIONS_AVAILABLE)
|
|
||||||
ARM64.HasSHA512 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SHA512_INSTRUCTIONS_AVAILABLE)
|
|
||||||
ARM64.HasATOMICS = windows.IsProcessorFeaturePresent(windows.PF_ARM_V81_ATOMIC_INSTRUCTIONS_AVAILABLE)
|
|
||||||
if windows.IsProcessorFeaturePresent(windows.PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE) {
|
|
||||||
ARM64.HasASIMDDP = true
|
|
||||||
ARM64.HasASIMDRDM = true
|
|
||||||
}
|
|
||||||
if windows.IsProcessorFeaturePresent(windows.PF_ARM_V83_LRCPC_INSTRUCTIONS_AVAILABLE) {
|
|
||||||
ARM64.HasLRCPC = true
|
|
||||||
ARM64.HasSM3 = true
|
|
||||||
}
|
|
||||||
ARM64.HasSVE = windows.IsProcessorFeaturePresent(windows.PF_ARM_SVE_INSTRUCTIONS_AVAILABLE)
|
|
||||||
ARM64.HasSVE2 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SVE2_INSTRUCTIONS_AVAILABLE)
|
|
||||||
ARM64.HasJSCVT = windows.IsProcessorFeaturePresent(windows.PF_ARM_V83_JSCVT_INSTRUCTIONS_AVAILABLE)
|
|
||||||
}
|
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
// Copyright 2024 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Minimal copy from internal/cpu and runtime to make sysctl calls.
|
||||||
|
|
||||||
|
//go:build darwin && arm64 && gc
|
||||||
|
|
||||||
|
package cpu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Errno = syscall.Errno
|
||||||
|
|
||||||
|
// adapted from internal/cpu/cpu_arm64_darwin.go
|
||||||
|
func darwinSysctlEnabled(name []byte) bool {
|
||||||
|
out := int32(0)
|
||||||
|
nout := unsafe.Sizeof(out)
|
||||||
|
if ret := sysctlbyname(&name[0], (*byte)(unsafe.Pointer(&out)), &nout, nil, 0); ret != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return out > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:cgo_import_dynamic libc_sysctl sysctl "/usr/lib/libSystem.B.dylib"
|
||||||
|
|
||||||
|
var libc_sysctlbyname_trampoline_addr uintptr
|
||||||
|
|
||||||
|
// adapted from runtime/sys_darwin.go in the pattern of sysctl() above, as defined in x/sys/unix
|
||||||
|
func sysctlbyname(name *byte, old *byte, oldlen *uintptr, new *byte, newlen uintptr) error {
|
||||||
|
if _, _, err := syscall_syscall6(
|
||||||
|
libc_sysctlbyname_trampoline_addr,
|
||||||
|
uintptr(unsafe.Pointer(name)),
|
||||||
|
uintptr(unsafe.Pointer(old)),
|
||||||
|
uintptr(unsafe.Pointer(oldlen)),
|
||||||
|
uintptr(unsafe.Pointer(new)),
|
||||||
|
uintptr(newlen),
|
||||||
|
0,
|
||||||
|
); err != 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:cgo_import_dynamic libc_sysctlbyname sysctlbyname "/usr/lib/libSystem.B.dylib"
|
||||||
|
|
||||||
|
// Implemented in the runtime package (runtime/sys_darwin.go)
|
||||||
|
func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)
|
||||||
|
|
||||||
|
//go:linkname syscall_syscall6 syscall.syscall6
|
||||||
+21
@@ -684,12 +684,17 @@ const (
|
|||||||
RTN_THROW = 0x9
|
RTN_THROW = 0x9
|
||||||
RTN_NAT = 0xa
|
RTN_NAT = 0xa
|
||||||
RTN_XRESOLVE = 0xb
|
RTN_XRESOLVE = 0xb
|
||||||
|
PREFIX_UNSPEC = 0x0
|
||||||
|
PREFIX_ADDRESS = 0x1
|
||||||
|
PREFIX_CACHEINFO = 0x2
|
||||||
SizeofNlMsghdr = 0x10
|
SizeofNlMsghdr = 0x10
|
||||||
SizeofNlMsgerr = 0x14
|
SizeofNlMsgerr = 0x14
|
||||||
SizeofRtGenmsg = 0x1
|
SizeofRtGenmsg = 0x1
|
||||||
SizeofNlAttr = 0x4
|
SizeofNlAttr = 0x4
|
||||||
SizeofRtAttr = 0x4
|
SizeofRtAttr = 0x4
|
||||||
SizeofIfInfomsg = 0x10
|
SizeofIfInfomsg = 0x10
|
||||||
|
SizeofPrefixmsg = 0xc
|
||||||
|
SizeofPrefixCacheinfo = 0x8
|
||||||
SizeofIfAddrmsg = 0x8
|
SizeofIfAddrmsg = 0x8
|
||||||
SizeofIfAddrlblmsg = 0xc
|
SizeofIfAddrlblmsg = 0xc
|
||||||
SizeofIfaCacheinfo = 0x10
|
SizeofIfaCacheinfo = 0x10
|
||||||
@@ -735,6 +740,22 @@ type IfInfomsg struct {
|
|||||||
Change uint32
|
Change uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Prefixmsg struct {
|
||||||
|
Family uint8
|
||||||
|
Pad1 uint8
|
||||||
|
Pad2 uint16
|
||||||
|
Ifindex int32
|
||||||
|
Type uint8
|
||||||
|
Len uint8
|
||||||
|
Flags uint8
|
||||||
|
Pad3 uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrefixCacheinfo struct {
|
||||||
|
Preferred_time uint32
|
||||||
|
Valid_time uint32
|
||||||
|
}
|
||||||
|
|
||||||
type IfAddrmsg struct {
|
type IfAddrmsg struct {
|
||||||
Family uint8
|
Family uint8
|
||||||
Prefixlen uint8
|
Prefixlen uint8
|
||||||
|
|||||||
+1
@@ -8,5 +8,6 @@ package windows
|
|||||||
|
|
||||||
import "syscall"
|
import "syscall"
|
||||||
|
|
||||||
|
type Signal = syscall.Signal
|
||||||
type Errno = syscall.Errno
|
type Errno = syscall.Errno
|
||||||
type SysProcAttr = syscall.SysProcAttr
|
type SysProcAttr = syscall.SysProcAttr
|
||||||
|
|||||||
+1
-36
@@ -163,42 +163,7 @@ func (p *Proc) Addr() uintptr {
|
|||||||
// (according to the semantics of the specific function being called) before consulting
|
// (according to the semantics of the specific function being called) before consulting
|
||||||
// the error. The error will be guaranteed to contain windows.Errno.
|
// the error. The error will be guaranteed to contain windows.Errno.
|
||||||
func (p *Proc) Call(a ...uintptr) (r1, r2 uintptr, lastErr error) {
|
func (p *Proc) Call(a ...uintptr) (r1, r2 uintptr, lastErr error) {
|
||||||
switch len(a) {
|
return syscall.SyscallN(p.Addr(), a...)
|
||||||
case 0:
|
|
||||||
return syscall.Syscall(p.Addr(), uintptr(len(a)), 0, 0, 0)
|
|
||||||
case 1:
|
|
||||||
return syscall.Syscall(p.Addr(), uintptr(len(a)), a[0], 0, 0)
|
|
||||||
case 2:
|
|
||||||
return syscall.Syscall(p.Addr(), uintptr(len(a)), a[0], a[1], 0)
|
|
||||||
case 3:
|
|
||||||
return syscall.Syscall(p.Addr(), uintptr(len(a)), a[0], a[1], a[2])
|
|
||||||
case 4:
|
|
||||||
return syscall.Syscall6(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], 0, 0)
|
|
||||||
case 5:
|
|
||||||
return syscall.Syscall6(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], 0)
|
|
||||||
case 6:
|
|
||||||
return syscall.Syscall6(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5])
|
|
||||||
case 7:
|
|
||||||
return syscall.Syscall9(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], 0, 0)
|
|
||||||
case 8:
|
|
||||||
return syscall.Syscall9(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], 0)
|
|
||||||
case 9:
|
|
||||||
return syscall.Syscall9(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8])
|
|
||||||
case 10:
|
|
||||||
return syscall.Syscall12(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], 0, 0)
|
|
||||||
case 11:
|
|
||||||
return syscall.Syscall12(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], 0)
|
|
||||||
case 12:
|
|
||||||
return syscall.Syscall12(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11])
|
|
||||||
case 13:
|
|
||||||
return syscall.Syscall15(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], 0, 0)
|
|
||||||
case 14:
|
|
||||||
return syscall.Syscall15(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], a[13], 0)
|
|
||||||
case 15:
|
|
||||||
return syscall.Syscall15(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], a[13], a[14])
|
|
||||||
default:
|
|
||||||
panic("Call " + p.Name + " with too many arguments " + itoa(len(a)) + ".")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A LazyDLL implements access to a single DLL.
|
// A LazyDLL implements access to a single DLL.
|
||||||
|
|||||||
+5
-1
@@ -1438,13 +1438,17 @@ func GetSecurityInfo(handle Handle, objectType SE_OBJECT_TYPE, securityInformati
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetNamedSecurityInfo queries the security information for a given named object and returns the self-relative security
|
// GetNamedSecurityInfo queries the security information for a given named object and returns the self-relative security
|
||||||
// descriptor result on the Go heap.
|
// descriptor result on the Go heap. The security descriptor might be nil, even when err is nil, if the object exists
|
||||||
|
// but has no security descriptor.
|
||||||
func GetNamedSecurityInfo(objectName string, objectType SE_OBJECT_TYPE, securityInformation SECURITY_INFORMATION) (sd *SECURITY_DESCRIPTOR, err error) {
|
func GetNamedSecurityInfo(objectName string, objectType SE_OBJECT_TYPE, securityInformation SECURITY_INFORMATION) (sd *SECURITY_DESCRIPTOR, err error) {
|
||||||
var winHeapSD *SECURITY_DESCRIPTOR
|
var winHeapSD *SECURITY_DESCRIPTOR
|
||||||
err = getNamedSecurityInfo(objectName, objectType, securityInformation, nil, nil, nil, nil, &winHeapSD)
|
err = getNamedSecurityInfo(objectName, objectType, securityInformation, nil, nil, nil, nil, &winHeapSD)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if winHeapSD == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
defer LocalFree(Handle(unsafe.Pointer(winHeapSD)))
|
defer LocalFree(Handle(unsafe.Pointer(winHeapSD)))
|
||||||
return winHeapSD.copySelfRelativeSecurityDescriptor(), nil
|
return winHeapSD.copySelfRelativeSecurityDescriptor(), nil
|
||||||
}
|
}
|
||||||
|
|||||||
-14
@@ -1490,20 +1490,6 @@ func Getgid() (gid int) { return -1 }
|
|||||||
func Getegid() (egid int) { return -1 }
|
func Getegid() (egid int) { return -1 }
|
||||||
func Getgroups() (gids []int, err error) { return nil, syscall.EWINDOWS }
|
func Getgroups() (gids []int, err error) { return nil, syscall.EWINDOWS }
|
||||||
|
|
||||||
type Signal int
|
|
||||||
|
|
||||||
func (s Signal) Signal() {}
|
|
||||||
|
|
||||||
func (s Signal) String() string {
|
|
||||||
if 0 <= s && int(s) < len(signals) {
|
|
||||||
str := signals[s]
|
|
||||||
if str != "" {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "signal " + itoa(int(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadCreateSymbolicLink() error {
|
func LoadCreateSymbolicLink() error {
|
||||||
return procCreateSymbolicLinkW.Find()
|
return procCreateSymbolicLinkW.Find()
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+11
-11
@@ -80,16 +80,16 @@ github.com/go-git/gcfg
|
|||||||
github.com/go-git/gcfg/scanner
|
github.com/go-git/gcfg/scanner
|
||||||
github.com/go-git/gcfg/token
|
github.com/go-git/gcfg/token
|
||||||
github.com/go-git/gcfg/types
|
github.com/go-git/gcfg/types
|
||||||
# github.com/go-git/go-billy/v5 v5.7.0
|
# github.com/go-git/go-billy/v5 v5.9.0
|
||||||
## explicit; go 1.23.0
|
## explicit; go 1.25.0
|
||||||
github.com/go-git/go-billy/v5
|
github.com/go-git/go-billy/v5
|
||||||
github.com/go-git/go-billy/v5/helper/chroot
|
github.com/go-git/go-billy/v5/helper/chroot
|
||||||
github.com/go-git/go-billy/v5/helper/polyfill
|
github.com/go-git/go-billy/v5/helper/polyfill
|
||||||
github.com/go-git/go-billy/v5/memfs
|
github.com/go-git/go-billy/v5/memfs
|
||||||
github.com/go-git/go-billy/v5/osfs
|
github.com/go-git/go-billy/v5/osfs
|
||||||
github.com/go-git/go-billy/v5/util
|
github.com/go-git/go-billy/v5/util
|
||||||
# github.com/go-git/go-git/v5 v5.16.5
|
# github.com/go-git/go-git/v5 v5.19.0
|
||||||
## explicit; go 1.24.0
|
## explicit; go 1.25.0
|
||||||
github.com/go-git/go-git/v5
|
github.com/go-git/go-git/v5
|
||||||
github.com/go-git/go-git/v5/config
|
github.com/go-git/go-git/v5/config
|
||||||
github.com/go-git/go-git/v5/internal/path_util
|
github.com/go-git/go-git/v5/internal/path_util
|
||||||
@@ -157,7 +157,7 @@ github.com/mattn/go-isatty
|
|||||||
# github.com/mitchellh/go-homedir v1.1.0
|
# github.com/mitchellh/go-homedir v1.1.0
|
||||||
## explicit
|
## explicit
|
||||||
github.com/mitchellh/go-homedir
|
github.com/mitchellh/go-homedir
|
||||||
# github.com/pjbgf/sha1cd v0.5.0
|
# github.com/pjbgf/sha1cd v0.6.0
|
||||||
## explicit; go 1.22
|
## explicit; go 1.22
|
||||||
github.com/pjbgf/sha1cd
|
github.com/pjbgf/sha1cd
|
||||||
github.com/pjbgf/sha1cd/internal
|
github.com/pjbgf/sha1cd/internal
|
||||||
@@ -177,8 +177,8 @@ github.com/spf13/pflag
|
|||||||
# github.com/xanzy/ssh-agent v0.3.3
|
# github.com/xanzy/ssh-agent v0.3.3
|
||||||
## explicit; go 1.16
|
## explicit; go 1.16
|
||||||
github.com/xanzy/ssh-agent
|
github.com/xanzy/ssh-agent
|
||||||
# golang.org/x/crypto v0.48.0
|
# golang.org/x/crypto v0.50.0
|
||||||
## explicit; go 1.24.0
|
## explicit; go 1.25.0
|
||||||
golang.org/x/crypto/argon2
|
golang.org/x/crypto/argon2
|
||||||
golang.org/x/crypto/blake2b
|
golang.org/x/crypto/blake2b
|
||||||
golang.org/x/crypto/blowfish
|
golang.org/x/crypto/blowfish
|
||||||
@@ -195,13 +195,13 @@ golang.org/x/crypto/ssh
|
|||||||
golang.org/x/crypto/ssh/agent
|
golang.org/x/crypto/ssh/agent
|
||||||
golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
|
golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
|
||||||
golang.org/x/crypto/ssh/knownhosts
|
golang.org/x/crypto/ssh/knownhosts
|
||||||
# golang.org/x/net v0.50.0
|
# golang.org/x/net v0.53.0
|
||||||
## explicit; go 1.24.0
|
## explicit; go 1.25.0
|
||||||
golang.org/x/net/context
|
golang.org/x/net/context
|
||||||
golang.org/x/net/internal/socks
|
golang.org/x/net/internal/socks
|
||||||
golang.org/x/net/proxy
|
golang.org/x/net/proxy
|
||||||
# golang.org/x/sys v0.41.0
|
# golang.org/x/sys v0.43.0
|
||||||
## explicit; go 1.24.0
|
## explicit; go 1.25.0
|
||||||
golang.org/x/sys/cpu
|
golang.org/x/sys/cpu
|
||||||
golang.org/x/sys/execabs
|
golang.org/x/sys/execabs
|
||||||
golang.org/x/sys/unix
|
golang.org/x/sys/unix
|
||||||
|
|||||||
Reference in New Issue
Block a user