mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-05-24 22:19:09 -05:00
[full-ci] chore: bump reva to v2.46.0 (#2809)
This commit is contained in:
@@ -64,7 +64,7 @@ require (
|
||||
github.com/open-policy-agent/opa v1.15.2
|
||||
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d
|
||||
github.com/opencloud-eu/reva/v2 v2.45.0
|
||||
github.com/opencloud-eu/reva/v2 v2.46.0
|
||||
github.com/opensearch-project/opensearch-go/v4 v4.6.0
|
||||
github.com/orcaman/concurrent-map v1.0.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
@@ -205,7 +205,7 @@ require (
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.9.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.19.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.19.1 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-kit/log v0.2.1 // indirect
|
||||
@@ -346,7 +346,7 @@ require (
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/sethvargo/go-diceware v0.5.0 // indirect
|
||||
github.com/sethvargo/go-password v0.3.1 // indirect
|
||||
github.com/shamaton/msgpack/v2 v2.4.0 // indirect
|
||||
github.com/shamaton/msgpack/v2 v2.4.1 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
|
||||
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
|
||||
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect
|
||||
@@ -377,9 +377,9 @@ require (
|
||||
github.com/yashtewari/glob-intersection v0.2.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.6.10 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.10 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.6.10 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.6.11 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.11 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.6.11 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
||||
|
||||
@@ -386,8 +386,8 @@ github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmm
|
||||
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc=
|
||||
github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
||||
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
|
||||
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@@ -952,8 +952,8 @@ github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9 h1:dIft
|
||||
github.com/opencloud-eu/inotifywaitgo v0.0.0-20251111171128-a390bae3c5e9/go.mod h1:JWyDC6H+5oZRdUJUgKuaye+8Ph5hEs6HVzVoPKzWSGI=
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d h1:JcqGDiyrcaQwVyV861TUyQgO7uEmsjkhfm7aQd84dOw=
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q=
|
||||
github.com/opencloud-eu/reva/v2 v2.45.0 h1:62XctwzrGKjmWng9QI+tACEDQb6R9fG7oHlMPfQkjNs=
|
||||
github.com/opencloud-eu/reva/v2 v2.45.0/go.mod h1:tUL2X47YxLHrnBDArHrIP73UJliMI0PaY/3tPs31dTM=
|
||||
github.com/opencloud-eu/reva/v2 v2.46.0 h1:wYUHIHOsLP9vad/K19/52RRfoooeHUGY4BSeowNrbpY=
|
||||
github.com/opencloud-eu/reva/v2 v2.46.0/go.mod h1:fWAzVpZlJQEY/qeIrd9d3U+LpqY9JGsjJ2dc0a1jCEs=
|
||||
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 h1:l2oB/RctH+t8r7QBj5p8thfEHCM/jF35aAY3WQ3hADI=
|
||||
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
@@ -1110,8 +1110,8 @@ github.com/sethvargo/go-diceware v0.5.0 h1:exrQ7GpaBo00GqRVM1N8ChXSsi3oS7tjQiIeh
|
||||
github.com/sethvargo/go-diceware v0.5.0/go.mod h1:Lg1SyPS7yQO6BBgTN5r4f2MUDkqGfLWsOjHPY0kA8iw=
|
||||
github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU=
|
||||
github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs=
|
||||
github.com/shamaton/msgpack/v2 v2.4.0 h1:O5Z08MRmbo0lA9o2xnQ4TXx6teJbPqEurqcCOQ8Oi/4=
|
||||
github.com/shamaton/msgpack/v2 v2.4.0/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
|
||||
github.com/shamaton/msgpack/v2 v2.4.1 h1:JtJ141QoQ3NqgPDsjq2v9VXlaON8SiQOwEaoNLEK/MQ=
|
||||
github.com/shamaton/msgpack/v2 v2.4.1/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
|
||||
@@ -1282,12 +1282,12 @@ github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.etcd.io/etcd/api/v3 v3.6.10 h1:jlwjtELjA8yi2VWpOFH+0w0lGr3K6mVDyn0RDB9aaAY=
|
||||
go.etcd.io/etcd/api/v3 v3.6.10/go.mod h1:pdV4VeFmvhdNjB4LWRkC8ReLyRBAxUOze3GarMhE2sk=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.10 h1:tBT7podcPhuVbCVkAEzx8bC5I+aqxfLwBN8/As1arrA=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.10/go.mod h1:WEy3PpwbbEBVRdh1NVJYsuUe/8eyI21PNJRazeD8z/Y=
|
||||
go.etcd.io/etcd/client/v3 v3.6.10 h1:J598zJ+C/ZPvImypmq5waj84+bovePrlZERHklf34y0=
|
||||
go.etcd.io/etcd/client/v3 v3.6.10/go.mod h1:iHhUDUcEwaKs1YFq3MgmI9U4zhTVasp/vgdVbFf1RS8=
|
||||
go.etcd.io/etcd/api/v3 v3.6.11 h1:XFGTgrJ8nak3kB4NgMG8t7NT+lEeuuvKQAqUHKVgkWQ=
|
||||
go.etcd.io/etcd/api/v3 v3.6.11/go.mod h1:HYfTh0jyh+uFgp6gMbxJteIDYY97yMuYz85Rnw6Gy9o=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.11 h1:e41mp315Yn3QMGPmEzCyLsMINgJXTY/dX8kM++1csxU=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.11/go.mod h1:DysuMe/inqRyC/1tjRR6hReH/VV9Lufs27YKSKBWWJg=
|
||||
go.etcd.io/etcd/client/v3 v3.6.11 h1:LAByD96VmmeuairkvdAcE0RZnrmGz/q3ceeWePo9bwc=
|
||||
go.etcd.io/etcd/client/v3 v3.6.11/go.mod h1:vOTDMCo+fGPEClJqcFEFSqZ+8e7WKV7AyqJjX//HR2w=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
|
||||
+30
-1
@@ -61,6 +61,16 @@ type Config struct {
|
||||
CommentChar string
|
||||
// RepositoryFormatVersion identifies the repository format and layout version.
|
||||
RepositoryFormatVersion format.RepositoryFormatVersion
|
||||
// ProtectNTFS controls whether NTFS-specific path protections are
|
||||
// applied (e.g. rejecting .git trailing spaces/periods, alternate
|
||||
// data streams, 8.3 short names). When unset, defaults to true on
|
||||
// Windows.
|
||||
ProtectNTFS OptBool
|
||||
// ProtectHFS controls whether HFS+-specific path protections are
|
||||
// applied (e.g. rejecting .git with Unicode zero-width or
|
||||
// directional characters that HFS+ would normalize away).
|
||||
// When unset, defaults to true on macOS.
|
||||
ProtectHFS OptBool
|
||||
}
|
||||
|
||||
User struct {
|
||||
@@ -266,6 +276,8 @@ const (
|
||||
repositoryFormatVersionKey = "repositoryformatversion"
|
||||
objectFormat = "objectformat"
|
||||
mirrorKey = "mirror"
|
||||
protectNTFSKey = "protectNTFS"
|
||||
protectHFSKey = "protectHFS"
|
||||
|
||||
// DefaultPackWindow holds the number of previous objects used to
|
||||
// generate deltas. The value 10 is the same used by git command.
|
||||
@@ -309,6 +321,14 @@ func (c *Config) unmarshalCore() {
|
||||
|
||||
c.Core.Worktree = s.Options.Get(worktreeKey)
|
||||
c.Core.CommentChar = s.Options.Get(commentCharKey)
|
||||
|
||||
if parsed := parseConfigBool(s.Options.Get(protectNTFSKey)); parsed.IsSet() {
|
||||
c.Core.ProtectNTFS = parsed
|
||||
}
|
||||
|
||||
if parsed := parseConfigBool(s.Options.Get(protectHFSKey)); parsed.IsSet() {
|
||||
c.Core.ProtectHFS = parsed
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) unmarshalUser() {
|
||||
@@ -379,7 +399,8 @@ func unmarshalSubmodules(fc *format.Config, submodules map[string]*Submodule) {
|
||||
m := &Submodule{}
|
||||
m.unmarshal(sub)
|
||||
|
||||
if m.Validate() == ErrModuleBadPath {
|
||||
if err := m.Validate(); errors.Is(err, ErrModuleBadPath) ||
|
||||
errors.Is(err, ErrModuleBadName) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -436,6 +457,14 @@ func (c *Config) marshalCore() {
|
||||
if c.Core.Worktree != "" {
|
||||
s.SetOption(worktreeKey, c.Core.Worktree)
|
||||
}
|
||||
|
||||
if c.Core.ProtectNTFS.IsSet() {
|
||||
s.SetOption(protectNTFSKey, c.Core.ProtectNTFS.FormatBool())
|
||||
}
|
||||
|
||||
if c.Core.ProtectHFS.IsSet() {
|
||||
s.SetOption(protectHFSKey, c.Core.ProtectHFS.FormatBool())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) marshalExtensions() {
|
||||
|
||||
+52
@@ -3,8 +3,11 @@ package config
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5/internal/pathutil"
|
||||
format "github.com/go-git/go-git/v5/plumbing/format/config"
|
||||
)
|
||||
|
||||
@@ -12,6 +15,7 @@ var (
|
||||
ErrModuleEmptyURL = errors.New("module config: empty URL")
|
||||
ErrModuleEmptyPath = errors.New("module config: empty path")
|
||||
ErrModuleBadPath = errors.New("submodule has an invalid path")
|
||||
ErrModuleBadName = errors.New("ignoring suspicious submodule name")
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -94,6 +98,10 @@ type Submodule struct {
|
||||
|
||||
// Validate validates the fields and sets the default values.
|
||||
func (m *Submodule) Validate() error {
|
||||
if err := validSubmoduleName(m.Name); err != nil {
|
||||
return fmt.Errorf("%w: %q", ErrModuleBadName, m.Name)
|
||||
}
|
||||
|
||||
if m.Path == "" {
|
||||
return ErrModuleEmptyPath
|
||||
}
|
||||
@@ -109,6 +117,50 @@ func (m *Submodule) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validSubmoduleName mirrors canonical Git's check_submodule_name in
|
||||
// submodule-config.c [1]: reject empty names and any name with a ".."
|
||||
// path component, using both '/' and '\\' as separators so the rule
|
||||
// is consistent across platforms. The component check is delegated to
|
||||
// `pathutil.IsHFSDot` and `pathutil.IsNTFSDot` with `.` as the needle,
|
||||
// which both cover the bare ".." case and reject components that
|
||||
// resolve to ".." after HFS+ Unicode normalisation (ignored code
|
||||
// points, e.g. `.<U+200C>.`) or NTFS trailing-space/dot/ADS
|
||||
// canonicalisation (e.g. `.. `, `..::$INDEX_ALLOCATION`).
|
||||
// `.gitmodules` is attacker-controlled by definition, so both checks
|
||||
// run unconditionally regardless of host OS.
|
||||
//
|
||||
// The additional checks (bare ".", NUL byte, leading or trailing
|
||||
// separator, drive-letter prefix) close go-git-specific edge cases
|
||||
// the canonical loop does not exercise: canonical Git treats names
|
||||
// as opaque C strings, while Go strings carry NULs through and the
|
||||
// billy filesystem layer is path-aware in ways Git's working storage
|
||||
// is not.
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/submodule-config.c#L214-L237
|
||||
func validSubmoduleName(name string) error {
|
||||
if name == "" || name == "." {
|
||||
return ErrModuleBadName
|
||||
}
|
||||
for _, seg := range strings.FieldsFunc(name, isPathSep) {
|
||||
if pathutil.IsHFSDot(seg, ".") || pathutil.IsNTFSDot(seg, ".", "") {
|
||||
return ErrModuleBadName
|
||||
}
|
||||
}
|
||||
// go-git-specific defensive checks beyond canonical Git.
|
||||
if strings.ContainsRune(name, 0) {
|
||||
return ErrModuleBadName
|
||||
}
|
||||
if isPathSep(rune(name[0])) || isPathSep(rune(name[len(name)-1])) {
|
||||
return ErrModuleBadName
|
||||
}
|
||||
if len(name) >= 2 && name[1] == ':' {
|
||||
return ErrModuleBadName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isPathSep(r rune) bool { return r == '/' || r == '\\' }
|
||||
|
||||
func (m *Submodule) unmarshal(s *format.Subsection) {
|
||||
m.raw = s
|
||||
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// OptBool is a tri-state boolean: unset, explicitly false, or explicitly true.
|
||||
// Its zero value (OptBoolUnset) means the setting was not specified, which
|
||||
// allows merge logic based on reflect.Value.IsZero to skip unset fields while
|
||||
// still letting an explicit "false" override a previously set "true".
|
||||
type OptBool byte
|
||||
|
||||
const (
|
||||
// OptBoolUnset indicates the setting was not specified.
|
||||
OptBoolUnset OptBool = iota
|
||||
// OptBoolFalse indicates the setting was explicitly set to false.
|
||||
OptBoolFalse
|
||||
// OptBoolTrue indicates the setting was explicitly set to true.
|
||||
OptBoolTrue
|
||||
)
|
||||
|
||||
// NewOptBool converts a plain bool into an OptBool.
|
||||
func NewOptBool(v bool) OptBool {
|
||||
if v {
|
||||
return OptBoolTrue
|
||||
}
|
||||
return OptBoolFalse
|
||||
}
|
||||
|
||||
// IsTrue returns whether the value is explicitly true.
|
||||
func (o OptBool) IsTrue() bool { return o == OptBoolTrue }
|
||||
|
||||
// IsSet returns whether the value was explicitly specified (true or false).
|
||||
func (o OptBool) IsSet() bool { return o != OptBoolUnset }
|
||||
|
||||
func (o OptBool) String() string {
|
||||
switch o {
|
||||
case OptBoolTrue:
|
||||
return "true"
|
||||
case OptBoolFalse:
|
||||
return "false"
|
||||
default:
|
||||
return "unset"
|
||||
}
|
||||
}
|
||||
|
||||
// FormatBool returns the strconv-formatted value. Only meaningful when IsSet.
|
||||
func (o OptBool) FormatBool() string {
|
||||
return strconv.FormatBool(o.IsTrue())
|
||||
}
|
||||
|
||||
// parseConfigBool mirrors upstream Git's git_parse_maybe_bool: it
|
||||
// accepts true/yes/on (→ OptBoolTrue) and false/no/off (→
|
||||
// OptBoolFalse) case-insensitively, plus any decimal integer (zero
|
||||
// → OptBoolFalse, non-zero → OptBoolTrue). Empty or otherwise
|
||||
// unrecognised values return OptBoolUnset, leaving the caller's
|
||||
// platform default in place. The empty-string handling is the only
|
||||
// intentional divergence from upstream, which returns false for
|
||||
// empty: in our unmarshalCore caller, an empty value means the key
|
||||
// is unset and the platform default should apply.
|
||||
//
|
||||
// Reference: upstream Git git_parse_maybe_bool_text at parse.c
|
||||
// L157-L173 and git_parse_maybe_bool at parse.c L174-L182 in tag
|
||||
// v2.54.0[1].
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/parse.c#L157-L182
|
||||
func parseConfigBool(v string) OptBool {
|
||||
switch strings.ToLower(v) {
|
||||
case "true", "yes", "on":
|
||||
return OptBoolTrue
|
||||
case "false", "no", "off":
|
||||
return OptBoolFalse
|
||||
}
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
if i != 0 {
|
||||
return OptBoolTrue
|
||||
}
|
||||
return OptBoolFalse
|
||||
}
|
||||
return OptBoolUnset
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package pathutil
|
||||
|
||||
import "strings"
|
||||
|
||||
// IsDotGitName reports whether name is `.git` or its 8.3 NTFS short
|
||||
// alias `git~1`, case-insensitively. Both are forbidden as path
|
||||
// components (and as submodule names) because they refer to the
|
||||
// repository's own metadata directory.
|
||||
//
|
||||
// File names that do not conform to the 8.3 format (up to eight
|
||||
// characters for the basename, three for the file extension) are
|
||||
// associated with a so-called "short name" on NTFS — at least on
|
||||
// the `C:` drive by default — which means that `git~1/` is a valid
|
||||
// way to refer to `.git/`.
|
||||
func IsDotGitName(name string) bool {
|
||||
switch strings.ToLower(name) {
|
||||
case ".git", "git~1":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package pathutil
|
||||
|
||||
import "unicode"
|
||||
|
||||
// hfsIgnoredCodepoints contains Unicode code points that HFS+ ignores
|
||||
// during path normalization. A path component containing these
|
||||
// characters between the bytes of ".git" (or ".gitmodules", etc.)
|
||||
// will be treated as that name by HFS+, so they have to be filtered
|
||||
// out before comparison.
|
||||
//
|
||||
// See upstream Git utf8.c next_hfs_char in tag v2.54.0[1].
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/utf8.c#L703-L740
|
||||
var hfsIgnoredCodepoints = map[rune]struct{}{
|
||||
0x200c: {}, // ZERO WIDTH NON-JOINER
|
||||
0x200d: {}, // ZERO WIDTH JOINER
|
||||
0x200e: {}, // LEFT-TO-RIGHT MARK
|
||||
0x200f: {}, // RIGHT-TO-LEFT MARK
|
||||
0x202a: {}, // LEFT-TO-RIGHT EMBEDDING
|
||||
0x202b: {}, // RIGHT-TO-LEFT EMBEDDING
|
||||
0x202c: {}, // POP DIRECTIONAL FORMATTING
|
||||
0x202d: {}, // LEFT-TO-RIGHT OVERRIDE
|
||||
0x202e: {}, // RIGHT-TO-LEFT OVERRIDE
|
||||
0x206a: {}, // INHIBIT SYMMETRIC SWAPPING
|
||||
0x206b: {}, // ACTIVATE SYMMETRIC SWAPPING
|
||||
0x206c: {}, // INHIBIT ARABIC FORM SHAPING
|
||||
0x206d: {}, // ACTIVATE ARABIC FORM SHAPING
|
||||
0x206e: {}, // NATIONAL DIGIT SHAPES
|
||||
0x206f: {}, // NOMINAL DIGIT SHAPES
|
||||
0xfeff: {}, // ZERO WIDTH NO-BREAK SPACE
|
||||
}
|
||||
|
||||
// IsHFSDot reports whether part would be treated as ".<needle>" on an
|
||||
// HFS+ filesystem after stripping ignored Unicode code points and
|
||||
// folding ASCII to lower case. The needle is the lowercase ASCII
|
||||
// suffix without the leading dot (e.g. "git", "gitmodules"). It
|
||||
// mirrors upstream Git's is_hfs_dot_generic and is the building
|
||||
// block of IsHFSDotGit / IsHFSDotGitmodules.
|
||||
//
|
||||
// Reference: upstream Git utf8.c is_hfs_dot_generic at L741-L774 and
|
||||
// the dotgit family at L784-L809 in tag v2.54.0[1].
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/utf8.c#L741-L809
|
||||
func IsHFSDot(part, needle string) bool {
|
||||
runes := []rune(part)
|
||||
i := 0
|
||||
|
||||
// skip ignored code points, then expect '.'
|
||||
for i < len(runes) {
|
||||
if _, ok := hfsIgnoredCodepoints[runes[i]]; !ok {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i >= len(runes) || runes[i] != '.' {
|
||||
return false
|
||||
}
|
||||
i++
|
||||
|
||||
// match needle case-insensitively, skipping ignored code points
|
||||
for _, expected := range needle {
|
||||
for i < len(runes) {
|
||||
if _, ok := hfsIgnoredCodepoints[runes[i]]; !ok {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i >= len(runes) {
|
||||
return false
|
||||
}
|
||||
r := runes[i]
|
||||
if r > 127 {
|
||||
return false
|
||||
}
|
||||
if unicode.ToLower(r) != expected {
|
||||
return false
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
// skip trailing ignored code points
|
||||
for i < len(runes) {
|
||||
if _, ok := hfsIgnoredCodepoints[runes[i]]; !ok {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
// must be at end of component
|
||||
return i == len(runes)
|
||||
}
|
||||
|
||||
// IsHFSDotGit reports whether part is an HFS+ equivalent of ".git".
|
||||
func IsHFSDotGit(part string) bool { return IsHFSDot(part, "git") }
|
||||
|
||||
// IsHFSDotGitmodules reports whether part is an HFS+ equivalent of
|
||||
// ".gitmodules", catching attempts to plant the file via Unicode
|
||||
// code points that HFS+ would strip during normalisation.
|
||||
func IsHFSDotGitmodules(part string) bool { return IsHFSDot(part, "gitmodules") }
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package pathutil
|
||||
|
||||
import "strings"
|
||||
|
||||
// IsNTFSDotGit ports upstream Git's is_ntfs_dotgit. It detects path
|
||||
// components that NTFS would resolve to ".git": the canonical name
|
||||
// itself and its 8.3 short-name alias "git~1", each followed by any
|
||||
// number of trailing spaces or periods (which NTFS silently trims)
|
||||
// and an optional Alternate Data Stream suffix (":<stream>"). The
|
||||
// bare strings ".git" and "git~1" also match, mirroring upstream.
|
||||
//
|
||||
// Reference: upstream Git path.c is_ntfs_dotgit at L1415-L1449
|
||||
// in tag v2.54.0[1].
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/path.c#L1415-L1449
|
||||
func IsNTFSDotGit(part string) bool {
|
||||
var i int
|
||||
switch {
|
||||
case len(part) >= 4 && part[0] == '.' &&
|
||||
asciiToLower(part[1]) == 'g' &&
|
||||
asciiToLower(part[2]) == 'i' &&
|
||||
asciiToLower(part[3]) == 't':
|
||||
i = 4
|
||||
case len(part) >= 5 &&
|
||||
asciiToLower(part[0]) == 'g' &&
|
||||
asciiToLower(part[1]) == 'i' &&
|
||||
asciiToLower(part[2]) == 't' &&
|
||||
part[3] == '~' && part[4] == '1':
|
||||
i = 5
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
for ; i < len(part); i++ {
|
||||
c := part[i]
|
||||
if c == ':' {
|
||||
return true
|
||||
}
|
||||
if c != '.' && c != ' ' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// WindowsValidPath reports whether part is a valid Windows / NTFS
|
||||
// path component for the worktree filesystem abstraction. It rejects
|
||||
// NTFS-disguised variants of `.git` and `git~1` (trailing spaces,
|
||||
// periods, Alternate Data Streams) and Windows reserved device
|
||||
// names. Bare `.git` and `git~1` are allowed at this layer; the
|
||||
// caller decides whether they are permissible at the current path
|
||||
// position.
|
||||
func WindowsValidPath(part string) bool {
|
||||
if IsNTFSDotGit(part) && !IsDotGitName(part) {
|
||||
return false
|
||||
}
|
||||
return !isWindowsReservedName(part)
|
||||
}
|
||||
|
||||
// windowsReservedNames lists the Windows reserved device names.
|
||||
// A path component is reserved if its base name (ignoring trailing
|
||||
// spaces, extensions, and NTFS Alternate Data Streams) matches one of
|
||||
// these case-insensitively.
|
||||
//
|
||||
// See upstream Git compat/mingw.c is_valid_win32_path().
|
||||
var windowsReservedNames = []string{
|
||||
"CON", "PRN", "AUX", "NUL",
|
||||
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||
"CONIN$", "CONOUT$",
|
||||
}
|
||||
|
||||
func isWindowsReservedName(part string) bool {
|
||||
for _, name := range windowsReservedNames {
|
||||
if len(part) < len(name) {
|
||||
continue
|
||||
}
|
||||
if !strings.EqualFold(part[:len(name)], name) {
|
||||
continue
|
||||
}
|
||||
// Exact match or followed by space, dot, colon (ADS), or separator.
|
||||
if len(part) == len(name) {
|
||||
return true
|
||||
}
|
||||
switch part[len(name)] {
|
||||
case ' ', '.', ':':
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsNTFSDot ports upstream Git's is_ntfs_dot_generic. It detects NTFS
|
||||
// path-component variants of a dotfile name that attackers can use to
|
||||
// bypass case-insensitive comparisons against the canonical name on
|
||||
// Windows. The dotgit parameter is the lowercase name without the
|
||||
// leading dot (e.g. "gitmodules"); shortnamePrefix is the canonical
|
||||
// 6-character NTFS short-name prefix used as a fall-back match
|
||||
// (e.g. "gi7eba" for ".gitmodules").
|
||||
//
|
||||
// Reference: upstream Git path.c is_ntfs_dot_generic at L1451-L1507
|
||||
// in tag v2.54.0[1].
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/path.c#L1451-L1507
|
||||
func IsNTFSDot(name, dotgit, shortnamePrefix string) bool {
|
||||
// onlySpacesAndPeriods returns true when the suffix from start
|
||||
// onwards consists only of trailing spaces and periods, possibly
|
||||
// terminated by a NTFS Alternate Data Stream colon. Mirrors the
|
||||
// only_spaces_and_periods label in upstream's is_ntfs_dot_generic.
|
||||
onlySpacesAndPeriods := func(start int) bool {
|
||||
for i := start; i < len(name); i++ {
|
||||
c := name[i]
|
||||
if c == ':' {
|
||||
return true
|
||||
}
|
||||
if c != ' ' && c != '.' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Pattern 1: ".<dotgit>" prefix + trailing spaces / periods / ADS.
|
||||
if len(name) >= len(dotgit)+1 && name[0] == '.' &&
|
||||
strings.EqualFold(name[1:1+len(dotgit)], dotgit) {
|
||||
if onlySpacesAndPeriods(len(dotgit) + 1) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 2: standard NTFS short name <dotgit[:6]>~[1-4].
|
||||
if len(dotgit) >= 6 && len(name) >= 8 &&
|
||||
strings.EqualFold(name[:6], dotgit[:6]) &&
|
||||
name[6] == '~' && name[7] >= '1' && name[7] <= '4' {
|
||||
if onlySpacesAndPeriods(8) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 3: fall-back NTFS short name keyed by shortnamePrefix.
|
||||
if len(shortnamePrefix) < 6 || len(name) < 8 {
|
||||
return false
|
||||
}
|
||||
sawTilde := false
|
||||
i := 0
|
||||
for i < 8 {
|
||||
c := name[i]
|
||||
switch {
|
||||
case sawTilde:
|
||||
if c < '0' || c > '9' {
|
||||
return false
|
||||
}
|
||||
case c == '~':
|
||||
i++
|
||||
if i >= len(name) || name[i] < '1' || name[i] > '9' {
|
||||
return false
|
||||
}
|
||||
sawTilde = true
|
||||
case i >= 6:
|
||||
return false
|
||||
case c&0x80 != 0:
|
||||
return false
|
||||
default:
|
||||
if asciiToLower(c) != shortnamePrefix[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
return onlySpacesAndPeriods(8)
|
||||
}
|
||||
|
||||
// IsNTFSDotGitmodules reports whether part is an NTFS-equivalent of
|
||||
// ".gitmodules" — the file name (or any of its variants that NTFS
|
||||
// would resolve to it) that attackers can use to plant submodule
|
||||
// configuration disguised as a symlink. The 6-character canonical
|
||||
// short-name prefix "gi7eba" mirrors upstream Git's is_ntfs_dotgitmodules.
|
||||
func IsNTFSDotGitmodules(part string) bool {
|
||||
return IsNTFSDot(part, "gitmodules", "gi7eba")
|
||||
}
|
||||
|
||||
func asciiToLower(c byte) byte {
|
||||
if c >= 'A' && c <= 'Z' {
|
||||
return c + ('a' - 'A')
|
||||
}
|
||||
return c
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package pathutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrInvalidPath is returned by ValidTreePath when its argument is
|
||||
// not a safe path to materialise into the worktree.
|
||||
var ErrInvalidPath = fmt.Errorf("invalid path")
|
||||
|
||||
// ValidTreePath rejects path strings that, if materialised into a
|
||||
// worktree, would let an attacker-controlled tree entry escape the
|
||||
// worktree or rewrite repository metadata. It rejects:
|
||||
//
|
||||
// - control characters (< 0x20, 0x7f);
|
||||
// - empty paths and "." / ".." components;
|
||||
// - Windows volume name prefixes (e.g. C:);
|
||||
// - .git, its 8.3 NTFS short-name git~1, plus their HFS+ and NTFS
|
||||
// variants — at every position, not just the root.
|
||||
//
|
||||
// HFS+/NTFS variants of `.git` are always rejected at this layer
|
||||
// regardless of runtime config: tree paths are canonical UTF-8 with
|
||||
// no zero-width characters or NTFS short-name forms, so an entry
|
||||
// that looks like a disguised `.git` is suspicious anywhere. Windows
|
||||
// reserved device names (CON, NUL, etc.) are not policed here — they
|
||||
// are legitimate filenames on non-Windows filesystems and upstream
|
||||
// Git accepts them. The wrapper layer (validPath in package git)
|
||||
// rejects them at materialisation time when core.protectNTFS is on.
|
||||
//
|
||||
// Mirrors upstream Git's verify_path_internal at read-cache.c#L987
|
||||
// in tag v2.54.0[1] with protect_hfs / protect_ntfs treated as
|
||||
// always-on for `.git`-disguise detection (tree paths are not
|
||||
// application-supplied) and is_valid_win32_path left to the wrapper.
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/read-cache.c#L987
|
||||
func ValidTreePath(p string) error {
|
||||
for i := 0; i < len(p); i++ {
|
||||
if p[i] < 0x20 || p[i] == 0x7f {
|
||||
return fmt.Errorf("%w %q: contains control character", ErrInvalidPath, p)
|
||||
}
|
||||
}
|
||||
|
||||
parts := strings.FieldsFunc(p, func(r rune) bool { return r == '\\' || r == '/' })
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("%w: %q", ErrInvalidPath, p)
|
||||
}
|
||||
|
||||
// Volume names are not supported, in both formats: \\ and <DRIVE_LETTER>:.
|
||||
if vol := filepath.VolumeName(p); vol != "" {
|
||||
return fmt.Errorf("%w: %q", ErrInvalidPath, p)
|
||||
}
|
||||
|
||||
for _, part := range parts {
|
||||
if part == "." || part == ".." {
|
||||
return fmt.Errorf("%w %q: cannot use %q", ErrInvalidPath, p, part)
|
||||
}
|
||||
|
||||
if IsDotGitName(part) || IsHFSDotGit(part) || IsNTFSDotGit(part) {
|
||||
return fmt.Errorf("%w component: %q", ErrInvalidPath, p)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+35
-2
@@ -2,12 +2,14 @@ package url
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
isSchemeRegExp = regexp.MustCompile(`^[^:]+://`)
|
||||
|
||||
// Ref: https://github.com/git/git/blob/master/Documentation/urls.txt#L37
|
||||
// Ref: https://github.com/git/git/blob/v2.54.0/Documentation/urls.adoc#L41-L48
|
||||
scpLikeUrlRegExp = regexp.MustCompile(`^(?:(?P<user>[^@]+)@)?(?P<host>[^:\s]+):(?:(?P<port>[0-9]{1,5}):)?(?P<path>[^\\].*)$`)
|
||||
)
|
||||
|
||||
@@ -20,7 +22,38 @@ func MatchesScheme(url string) bool {
|
||||
// MatchesScpLike returns true if the given string matches an SCP-like
|
||||
// format scheme.
|
||||
func MatchesScpLike(url string) bool {
|
||||
return scpLikeUrlRegExp.MatchString(url)
|
||||
if !scpLikeUrlRegExp.MatchString(url) {
|
||||
return false
|
||||
}
|
||||
// Mirror canonical Git's url_is_local_not_ssh in connect.c[1] for
|
||||
// the cases the regex above cannot disambiguate by itself: a URL
|
||||
// is treated as a local path (not SCP-style SSH) when a `/`
|
||||
// precedes the first `:` (e.g. `./relative:path`,
|
||||
// `/abs/with:colon/file`), or — on Windows only — when it has a
|
||||
// DOS drive prefix like `C:foo` where the host is a single
|
||||
// ASCII letter.
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/connect.c#L710-L716
|
||||
if before, _, _ := strings.Cut(url, ":"); strings.Contains(before, "/") {
|
||||
return false
|
||||
}
|
||||
if runtime.GOOS == "windows" && hasDosDrivePrefix(url) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// hasDosDrivePrefix reports whether s begins with `<letter>:` (a
|
||||
// Windows drive prefix such as `C:` or `c:`). Mirrors canonical Git's
|
||||
// win32_has_dos_drive_prefix[1].
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/compat/win32/path-utils.c#L20-L29
|
||||
func hasDosDrivePrefix(s string) bool {
|
||||
if len(s) < 2 || s[1] != ':' {
|
||||
return false
|
||||
}
|
||||
c := s[0]
|
||||
return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')
|
||||
}
|
||||
|
||||
// FindScpLikeComponents returns the user, host, port and path of the
|
||||
|
||||
+159
-5
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/hash"
|
||||
"github.com/go-git/go-git/v5/utils/binary"
|
||||
@@ -25,35 +26,88 @@ const (
|
||||
objectIDLength = hash.Size
|
||||
)
|
||||
|
||||
// Byte sizes of the idx v2 layout elements, used by the size formula
|
||||
// in [validateIdxV2Size]. See [gitformat-pack] for the canonical
|
||||
// layout.
|
||||
//
|
||||
// [gitformat-pack]: https://git-scm.com/docs/gitformat-pack
|
||||
const (
|
||||
headerLen = 8 // magic + version
|
||||
fanoutLen = fanout * 4 // uint32 per bucket
|
||||
crc32Len = 4 // CRC32 per object
|
||||
offset32Len = 4 // 32-bit offset per object
|
||||
offset64Len = 8 // 64-bit overflow offset
|
||||
trailerHashes = 2 // pack checksum + idx checksum, each hashsz
|
||||
)
|
||||
|
||||
// statInput is the optional shape the [Decoder] probes for at the
|
||||
// start of [Decoder.Decode] to learn the on-disk length of the idx
|
||||
// blob, which it uses to validate the canonical-Git size formula
|
||||
// before any allocations driven by the fanout table. Callers that
|
||||
// pass an [*os.File] or a `billy.File` backed by an `*os.File`
|
||||
// (the production call sites in `storage/filesystem`) satisfy it
|
||||
// directly; arbitrary [io.Reader]s do not, and decode for them
|
||||
// retains the pre-existing behaviour of erroring out at the
|
||||
// truncated-payload boundary instead.
|
||||
//
|
||||
// The interface is intentionally unexported so the public
|
||||
// [NewDecoder] signature stays compatible with v5.
|
||||
type statInput interface {
|
||||
Stat() (fs.FileInfo, error)
|
||||
}
|
||||
|
||||
// Decoder reads and decodes idx files from an input stream.
|
||||
type Decoder struct {
|
||||
io.Reader
|
||||
h hash.Hash
|
||||
src io.Reader
|
||||
h hash.Hash
|
||||
}
|
||||
|
||||
// NewDecoder builds a new idx stream decoder, that reads from r.
|
||||
func NewDecoder(r io.Reader) *Decoder {
|
||||
h := hash.New(crypto.SHA1)
|
||||
tr := io.TeeReader(r, h)
|
||||
return &Decoder{tr, h}
|
||||
return &Decoder{tr, r, h}
|
||||
}
|
||||
|
||||
// Decode reads from the stream and decode the content into the MemoryIndex struct.
|
||||
func (d *Decoder) Decode(idx *MemoryIndex) error {
|
||||
idxSize := int64(-1)
|
||||
if in, ok := d.src.(statInput); ok {
|
||||
fi, err := in.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: stat input: %w", ErrMalformedIdxFile, err)
|
||||
}
|
||||
idxSize = fi.Size()
|
||||
}
|
||||
|
||||
if err := validateHeader(d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
flow := []func(*MemoryIndex, io.Reader) error{
|
||||
headerFlow := []func(*MemoryIndex, io.Reader) error{
|
||||
readVersion,
|
||||
readFanout,
|
||||
}
|
||||
for _, f := range headerFlow {
|
||||
if err := f(idx, d); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if idxSize >= 0 {
|
||||
if err := validateIdxV2Size(idx, idxSize); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
bodyFlow := []func(*MemoryIndex, io.Reader) error{
|
||||
readObjectNames,
|
||||
readCRC32,
|
||||
readOffsets,
|
||||
readPackChecksum,
|
||||
}
|
||||
|
||||
for _, f := range flow {
|
||||
for _, f := range bodyFlow {
|
||||
if err := f(idx, d); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -199,3 +253,103 @@ func readIdxChecksum(idx *MemoryIndex, r io.Reader) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateIdxV2Size enforces the size formula used by canonical Git
|
||||
// load_idx for idx v2 files: the on-disk length must lie within
|
||||
// [minSize, maxSize] where
|
||||
//
|
||||
// perObject = hashsz + crc32Len + offset32Len
|
||||
// minSize = headerLen + fanoutLen + trailerHashes*hashsz + nr*perObject
|
||||
// maxSize = minSize + (nr-1)*offset64Len when nr > 0
|
||||
//
|
||||
// with nr taken from the last fanout entry and hashsz fixed at
|
||||
// [objectIDLength] (SHA-1 in v5). Multiplications use a self-checking
|
||||
// overflow guard so inputs whose claimed object count overflows the
|
||||
// formula are rejected rather than wrapping into a smaller value.
|
||||
func validateIdxV2Size(idx *MemoryIndex, idxSize int64) error {
|
||||
nr := int64(idx.Fanout[fanout-1])
|
||||
hashsz := int64(objectIDLength)
|
||||
|
||||
minSize := minIdxV2Size(nr, hashsz)
|
||||
maxSize := maxIdxV2Size(nr, hashsz)
|
||||
if minSize < 0 || maxSize < 0 {
|
||||
return fmt.Errorf("%w: object count %d is inconsistent with file size", ErrMalformedIdxFile, nr)
|
||||
}
|
||||
|
||||
if idxSize < minSize || idxSize > maxSize {
|
||||
return fmt.Errorf("%w: file size %d is inconsistent with object count %d", ErrMalformedIdxFile, idxSize, nr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// minIdxV2Size returns the minimum on-disk size of an idx v2 file
|
||||
// holding nr objects with the given hash size, mirroring the
|
||||
// computation in canonical Git load_idx. Returns -1 when any
|
||||
// intermediate multiplication or addition would overflow int64.
|
||||
func minIdxV2Size(nr, hashsz int64) int64 {
|
||||
perObject := hashsz + crc32Len + offset32Len
|
||||
fixed := int64(headerLen+fanoutLen) + trailerHashes*hashsz
|
||||
|
||||
objects, ok := mulInt64(nr, perObject)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
sum, ok := addInt64(fixed, objects)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
// maxIdxV2Size returns the maximum on-disk size of an idx v2 file
|
||||
// holding nr objects with the given hash size, mirroring the
|
||||
// computation in canonical Git load_idx. Returns -1 on overflow.
|
||||
func maxIdxV2Size(nr, hashsz int64) int64 {
|
||||
minSize := minIdxV2Size(nr, hashsz)
|
||||
if minSize < 0 {
|
||||
return -1
|
||||
}
|
||||
if nr == 0 {
|
||||
return minSize
|
||||
}
|
||||
overflow, ok := mulInt64(nr-1, offset64Len)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
sum, ok := addInt64(minSize, overflow)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
// mulInt64 returns a*b and whether the result fits in an int64 without
|
||||
// overflow. Negative operands or overflow yield ok=false. The overflow
|
||||
// check uses the standard self-inverse identity: a*b/b == a only when
|
||||
// the multiplication did not wrap.
|
||||
func mulInt64(a, b int64) (int64, bool) {
|
||||
if a < 0 || b < 0 {
|
||||
return 0, false
|
||||
}
|
||||
if a == 0 || b == 0 {
|
||||
return 0, true
|
||||
}
|
||||
c := a * b
|
||||
if c/b != a {
|
||||
return 0, false
|
||||
}
|
||||
return c, true
|
||||
}
|
||||
|
||||
// addInt64 returns a+b and whether the result fits in an int64 without
|
||||
// overflow. Negative operands or overflow yield ok=false.
|
||||
func addInt64(a, b int64) (int64, bool) {
|
||||
if a < 0 || b < 0 {
|
||||
return 0, false
|
||||
}
|
||||
c := a + b
|
||||
if c < a {
|
||||
return 0, false
|
||||
}
|
||||
return c, true
|
||||
}
|
||||
|
||||
+21
-8
@@ -2,6 +2,7 @@ package idxfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"sync"
|
||||
@@ -126,7 +127,10 @@ func (idx *MemoryIndex) FindOffset(h plumbing.Hash) (int64, error) {
|
||||
return 0, plumbing.ErrObjectNotFound
|
||||
}
|
||||
|
||||
offset := idx.getOffset(k, i)
|
||||
offset, err := idx.getOffset(k, i)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Save the offset for reverse lookup
|
||||
idx.mu.Lock()
|
||||
@@ -141,17 +145,19 @@ func (idx *MemoryIndex) FindOffset(h plumbing.Hash) (int64, error) {
|
||||
|
||||
const isO64Mask = uint64(1) << 31
|
||||
|
||||
func (idx *MemoryIndex) getOffset(firstLevel, secondLevel int) uint64 {
|
||||
func (idx *MemoryIndex) getOffset(firstLevel, secondLevel int) (uint64, error) {
|
||||
offset := secondLevel << 2
|
||||
ofs := encbin.BigEndian.Uint32(idx.Offset32[firstLevel][offset : offset+4])
|
||||
|
||||
if (uint64(ofs) & isO64Mask) != 0 {
|
||||
offset := 8 * (uint64(ofs) & ^isO64Mask)
|
||||
n := encbin.BigEndian.Uint64(idx.Offset64[offset : offset+8])
|
||||
return n
|
||||
if l := uint64(len(idx.Offset64)); l < 8 || offset > l-8 {
|
||||
return 0, fmt.Errorf("%w: offset64 index out of range", ErrMalformedIdxFile)
|
||||
}
|
||||
return encbin.BigEndian.Uint64(idx.Offset64[offset : offset+8]), nil
|
||||
}
|
||||
|
||||
return uint64(ofs)
|
||||
return uint64(ofs), nil
|
||||
}
|
||||
|
||||
// FindCRC32 implements the Index interface.
|
||||
@@ -209,8 +215,11 @@ func (idx *MemoryIndex) genOffsetHash() error {
|
||||
mappedFirstLevel := idx.FanoutMapping[firstLevel]
|
||||
for secondLevel := uint32(0); i < fanoutValue; i++ {
|
||||
copy(hash[:], idx.Names[mappedFirstLevel][secondLevel*objectIDLength:])
|
||||
offset := int64(idx.getOffset(mappedFirstLevel, int(secondLevel)))
|
||||
offsetHash[offset] = hash
|
||||
off, err := idx.getOffset(mappedFirstLevel, int(secondLevel))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
offsetHash[int64(off)] = hash
|
||||
secondLevel++
|
||||
}
|
||||
}
|
||||
@@ -291,7 +300,11 @@ func (i *idxfileEntryIter) Next() (*Entry, error) {
|
||||
mappedFirstLevel := i.idx.FanoutMapping[i.firstLevel]
|
||||
entry := new(Entry)
|
||||
copy(entry.Hash[:], i.idx.Names[mappedFirstLevel][i.secondLevel*objectIDLength:])
|
||||
entry.Offset = i.idx.getOffset(mappedFirstLevel, i.secondLevel)
|
||||
var err error
|
||||
entry.Offset, err = i.idx.getOffset(mappedFirstLevel, i.secondLevel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry.CRC32 = i.idx.getCRC32(mappedFirstLevel, i.secondLevel)
|
||||
|
||||
i.secondLevel++
|
||||
|
||||
+15
-3
@@ -11,9 +11,10 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrClosed = errors.New("objfile: already closed")
|
||||
ErrHeader = errors.New("objfile: invalid header")
|
||||
ErrNegativeSize = errors.New("objfile: negative object size")
|
||||
ErrClosed = errors.New("objfile: already closed")
|
||||
ErrHeader = errors.New("objfile: invalid header")
|
||||
ErrHeaderNotRead = errors.New("objfile: Header must be called before Read")
|
||||
ErrNegativeSize = errors.New("objfile: negative object size")
|
||||
)
|
||||
|
||||
// Reader reads and decodes compressed objfile data from a provided io.Reader.
|
||||
@@ -100,12 +101,23 @@ func (r *Reader) prepareForRead(t plumbing.ObjectType, size int64) {
|
||||
//
|
||||
// If Read encounters the end of the data stream it will return err == io.EOF,
|
||||
// either in the current call if n > 0 or in a subsequent call.
|
||||
//
|
||||
// Read returns ErrHeaderNotRead if Header has not been called successfully.
|
||||
func (r *Reader) Read(p []byte) (n int, err error) {
|
||||
if r.multi == nil {
|
||||
return 0, ErrHeaderNotRead
|
||||
}
|
||||
return r.multi.Read(p)
|
||||
}
|
||||
|
||||
// Hash returns the hash of the object data stream that has been read so far.
|
||||
// It returns the zero plumbing.Hash if Header has not been called
|
||||
// successfully — guarding against the nil hasher that prepareForRead has
|
||||
// not yet allocated.
|
||||
func (r *Reader) Hash() plumbing.Hash {
|
||||
if r.multi == nil {
|
||||
return plumbing.ZeroHash
|
||||
}
|
||||
return r.hasher.Sum()
|
||||
}
|
||||
|
||||
|
||||
-3
@@ -19,9 +19,6 @@ const (
|
||||
// https://github.com/git/git/blob/f7466e94375b3be27f229c78873f0acf8301c0a5/diff-delta.c#L428
|
||||
// Max size of a copy operation (64KB).
|
||||
maxCopySize = 64 * 1024
|
||||
|
||||
// Min size of a copy operation.
|
||||
minCopySize = 4
|
||||
)
|
||||
|
||||
// GetDelta returns an EncodedObject of type OFSDeltaObject. Base and Target object,
|
||||
|
||||
+7
-1
@@ -78,7 +78,13 @@ func (o *FSObject) Reader() (io.ReadCloser, error) {
|
||||
_ = f.Close()
|
||||
return nil, err
|
||||
}
|
||||
return ioutil.NewReadCloserWithCloser(r, f.Close), nil
|
||||
// Cap the lazy stream at the resolved object size: well-formed
|
||||
// content reaches EOF inside the bound, an inflated stream that
|
||||
// runs past surfaces ErrInflatedSizeMismatch on the byte just
|
||||
// past the limit. For delta-resolved objects o.size is the
|
||||
// expanded size, which is what the caller is reading here.
|
||||
bounded := newBoundedReadCloser(r, o.size)
|
||||
return ioutil.NewReadCloserWithCloser(bounded, f.Close), nil
|
||||
}
|
||||
r, err := p.getObjectContent(o.offset)
|
||||
if err != nil {
|
||||
|
||||
+15
-6
@@ -126,11 +126,17 @@ func (p *Packfile) nextObjectHeader() (*ObjectHeader, error) {
|
||||
return h, err
|
||||
}
|
||||
|
||||
func (p *Packfile) getDeltaObjectSize(buf *bytes.Buffer) int64 {
|
||||
func (p *Packfile) getDeltaObjectSize(buf *bytes.Buffer) (int64, error) {
|
||||
delta := buf.Bytes()
|
||||
_, delta = decodeLEB128(delta) // skip src size
|
||||
sz, _ := decodeLEB128(delta)
|
||||
return int64(sz)
|
||||
_, delta, err := decodeLEB128(delta) // skip src size
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
sz, _, err := decodeLEB128(delta)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int64(sz), nil
|
||||
}
|
||||
|
||||
func (p *Packfile) getObjectSize(h *ObjectHeader) (int64, error) {
|
||||
@@ -145,7 +151,7 @@ func (p *Packfile) getObjectSize(h *ObjectHeader) (int64, error) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return p.getDeltaObjectSize(buf), nil
|
||||
return p.getDeltaObjectSize(buf)
|
||||
default:
|
||||
return 0, ErrInvalidObject.AddDetails("type %q", h.Type)
|
||||
}
|
||||
@@ -233,7 +239,10 @@ func (p *Packfile) getNextObject(h *ObjectHeader, hash plumbing.Hash) (plumbing.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
size = p.getDeltaObjectSize(buf)
|
||||
size, err = p.getDeltaObjectSize(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if size <= smallObjectThreshold {
|
||||
var obj = new(plumbing.MemoryObject)
|
||||
obj.SetSize(size)
|
||||
|
||||
+65
-7
@@ -26,6 +26,45 @@ var (
|
||||
ErrDeltaNotCached = errors.New("delta could not be found in cache")
|
||||
)
|
||||
|
||||
// maxObjectPreallocBytes caps the up-front size hint passed to
|
||||
// bytes.Buffer.Grow when staging an object's contents, so a malformed length
|
||||
// cannot trigger a huge or out-of-range allocation. The buffer still grows
|
||||
// dynamically as data is written; this is purely a hint cap.
|
||||
const maxObjectPreallocBytes = 1 << 30 // 1 GiB
|
||||
|
||||
// maxObjectsPrealloc caps the up-front capacity reserved from the pack's
|
||||
// declared object count, so a header advertising an absurd quantity cannot
|
||||
// trigger a multi-gigabyte allocation. The slice and maps still grow
|
||||
// organically beyond this hint.
|
||||
const maxObjectsPrealloc = 1 << 16 // 64 Ki entries
|
||||
|
||||
// Match upstream Git's pack depth ceiling: pack-objects.h OE_DEPTH_BITS,
|
||||
// enforced in builtin/pack-objects.c as (1 << OE_DEPTH_BITS) - 1.
|
||||
const maxDeltaChainDepth = 4095
|
||||
|
||||
// growHint returns a non-negative int64 size, clamped to a sane upper bound,
|
||||
// suitable for passing to bytes.Buffer.Grow.
|
||||
func growHint(n int64) int {
|
||||
switch {
|
||||
case n <= 0:
|
||||
return 0
|
||||
case n > maxObjectPreallocBytes:
|
||||
return maxObjectPreallocBytes
|
||||
default:
|
||||
return int(n)
|
||||
}
|
||||
}
|
||||
|
||||
// objectsHint returns a non-negative count, clamped to maxObjectsPrealloc,
|
||||
// suitable for passing to make() as the capacity hint for slices or maps
|
||||
// sized from a pack's declared object count.
|
||||
func objectsHint(n uint32) int {
|
||||
if n > maxObjectsPrealloc {
|
||||
return maxObjectsPrealloc
|
||||
}
|
||||
return int(n)
|
||||
}
|
||||
|
||||
// Observer interface is implemented by index encoders.
|
||||
type Observer interface {
|
||||
// OnHeader is called when a new packfile is opened.
|
||||
@@ -166,9 +205,10 @@ func (p *Parser) init() error {
|
||||
}
|
||||
|
||||
p.count = c
|
||||
p.oiByHash = make(map[plumbing.Hash]*objectInfo, p.count)
|
||||
p.oiByOffset = make(map[int64]*objectInfo, p.count)
|
||||
p.oi = make([]*objectInfo, p.count)
|
||||
hint := objectsHint(p.count)
|
||||
p.oiByHash = make(map[plumbing.Hash]*objectInfo, hint)
|
||||
p.oiByOffset = make(map[int64]*objectInfo, hint)
|
||||
p.oi = make([]*objectInfo, 0, hint)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -261,7 +301,7 @@ func (p *Parser) indexObjects() error {
|
||||
}
|
||||
if delta && !p.scanner.IsSeekable {
|
||||
buf.Reset()
|
||||
buf.Grow(int(oh.Length))
|
||||
buf.Grow(growHint(oh.Length))
|
||||
writers = append(writers, buf)
|
||||
}
|
||||
|
||||
@@ -306,7 +346,7 @@ func (p *Parser) indexObjects() error {
|
||||
}
|
||||
|
||||
p.oiByOffset[oh.Offset] = ota
|
||||
p.oi[i] = ota
|
||||
p.oi = append(p.oi, ota)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -317,8 +357,12 @@ func (p *Parser) resolveDeltas() error {
|
||||
defer sync.PutBytesBuffer(buf)
|
||||
|
||||
for _, obj := range p.oi {
|
||||
if err := checkDeltaChainDepth(obj); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
buf.Grow(int(obj.Length))
|
||||
buf.Grow(growHint(obj.Length))
|
||||
err := p.get(obj, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -337,6 +381,9 @@ func (p *Parser) resolveDeltas() error {
|
||||
// create it once and reuse across all children.
|
||||
r := bytes.NewReader(buf.Bytes())
|
||||
for _, child := range obj.Children {
|
||||
if err := checkDeltaChainDepth(child); err != nil {
|
||||
return err
|
||||
}
|
||||
// Even though we are discarding the output, we still need to read it to
|
||||
// so that the scanner can advance to the next object, and the SHA1 can be
|
||||
// calculated.
|
||||
@@ -356,6 +403,17 @@ func (p *Parser) resolveDeltas() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkDeltaChainDepth(o *objectInfo) error {
|
||||
var depth int
|
||||
for current := o; current != nil && current.DiskType.IsDelta(); current = current.Parent {
|
||||
depth++
|
||||
if depth > maxDeltaChainDepth {
|
||||
return fmt.Errorf("%w: delta chain depth exceeds %d", ErrMalformedPackFile, maxDeltaChainDepth)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) resolveExternalRef(o *objectInfo) {
|
||||
if ref, ok := p.oiByHash[o.SHA1]; ok && ref.ExternalRef {
|
||||
p.oiByHash[o.SHA1] = o
|
||||
@@ -405,7 +463,7 @@ func (p *Parser) get(o *objectInfo, buf *bytes.Buffer) (err error) {
|
||||
if o.DiskType.IsDelta() {
|
||||
b := sync.GetBytesBuffer()
|
||||
defer sync.PutBytesBuffer(b)
|
||||
buf.Grow(int(o.Length))
|
||||
buf.Grow(growHint(o.Length))
|
||||
err := p.get(o.Parent, b)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+72
-41
@@ -31,10 +31,15 @@ const (
|
||||
// premptively made available for a patch operation.
|
||||
maxPatchPreemptionSize uint = 65536
|
||||
|
||||
// minDeltaSize defines the smallest size for a delta.
|
||||
minDeltaSize = 4
|
||||
// minDeltaSize is the smallest valid delta: a 1-byte srcSz LEB128
|
||||
// header followed by a 1-byte targetSz LEB128 header (the
|
||||
// shortest case being targetSz=0 with no operations).
|
||||
minDeltaSize = 2
|
||||
)
|
||||
|
||||
// uintBits is the bit width of uint on the current platform (32 or 64).
|
||||
const uintBits = 32 << (^uint(0) >> 63)
|
||||
|
||||
type offset struct {
|
||||
mask byte
|
||||
shift uint
|
||||
@@ -142,7 +147,7 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo
|
||||
baseBuf := bufio.NewReader(baseRd)
|
||||
basePos := uint(0)
|
||||
|
||||
for {
|
||||
for remainingTargetSz > 0 {
|
||||
cmd, err := deltaBuf.ReadByte()
|
||||
if err == io.EOF {
|
||||
_ = dstWr.CloseWithError(ErrInvalidDelta)
|
||||
@@ -166,9 +171,9 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo
|
||||
return
|
||||
}
|
||||
|
||||
if invalidSize(sz, targetSz) ||
|
||||
if invalidSize(sz, remainingTargetSz) ||
|
||||
invalidOffsetSize(offset, sz, srcSz) {
|
||||
_ = dstWr.Close()
|
||||
_ = dstWr.CloseWithError(ErrInvalidDelta)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -210,7 +215,7 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo
|
||||
|
||||
case isCopyFromDelta(cmd):
|
||||
sz := uint(cmd) // cmd is the size itself
|
||||
if invalidSize(sz, targetSz) {
|
||||
if invalidSize(sz, remainingTargetSz) {
|
||||
_ = dstWr.CloseWithError(ErrInvalidDelta)
|
||||
return
|
||||
}
|
||||
@@ -225,40 +230,48 @@ func ReaderFromDelta(base plumbing.EncodedObject, deltaRC io.Reader) (io.ReadClo
|
||||
_ = dstWr.CloseWithError(ErrDeltaCmd)
|
||||
return
|
||||
}
|
||||
|
||||
if remainingTargetSz <= 0 {
|
||||
_ = dstWr.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Mirror upstream's `data != top` post-loop check: every byte
|
||||
// of the delta payload must be consumed.
|
||||
if _, err := deltaBuf.ReadByte(); err == nil {
|
||||
_ = dstWr.CloseWithError(ErrInvalidDelta)
|
||||
return
|
||||
} else if err != io.EOF {
|
||||
_ = dstWr.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = dstWr.Close()
|
||||
}()
|
||||
|
||||
return dstRd, nil
|
||||
}
|
||||
|
||||
func patchDelta(dst *bytes.Buffer, src, delta []byte) error {
|
||||
if len(delta) < minCopySize {
|
||||
return ErrInvalidDelta
|
||||
srcSz, delta, err := decodeLEB128(delta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrInvalidDelta, err)
|
||||
}
|
||||
|
||||
srcSz, delta := decodeLEB128(delta)
|
||||
if srcSz != uint(len(src)) {
|
||||
return ErrInvalidDelta
|
||||
}
|
||||
|
||||
targetSz, delta := decodeLEB128(delta)
|
||||
targetSz, delta, err := decodeLEB128(delta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrInvalidDelta, err)
|
||||
}
|
||||
remainingTargetSz := targetSz
|
||||
|
||||
var cmd byte
|
||||
|
||||
growSz := min(targetSz, maxPatchPreemptionSize)
|
||||
dst.Grow(int(growSz))
|
||||
for {
|
||||
|
||||
for remainingTargetSz > 0 {
|
||||
if len(delta) == 0 {
|
||||
return ErrInvalidDelta
|
||||
}
|
||||
|
||||
cmd = delta[0]
|
||||
cmd := delta[0]
|
||||
delta = delta[1:]
|
||||
|
||||
switch {
|
||||
@@ -275,16 +288,16 @@ func patchDelta(dst *bytes.Buffer, src, delta []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if invalidSize(sz, targetSz) ||
|
||||
if invalidSize(sz, remainingTargetSz) ||
|
||||
invalidOffsetSize(offset, sz, srcSz) {
|
||||
break
|
||||
return ErrInvalidDelta
|
||||
}
|
||||
dst.Write(src[offset : offset+sz])
|
||||
remainingTargetSz -= sz
|
||||
|
||||
case isCopyFromDelta(cmd):
|
||||
sz := uint(cmd) // cmd is the size itself
|
||||
if invalidSize(sz, targetSz) {
|
||||
if invalidSize(sz, remainingTargetSz) {
|
||||
return ErrInvalidDelta
|
||||
}
|
||||
|
||||
@@ -299,10 +312,12 @@ func patchDelta(dst *bytes.Buffer, src, delta []byte) error {
|
||||
default:
|
||||
return ErrDeltaCmd
|
||||
}
|
||||
}
|
||||
|
||||
if remainingTargetSz <= 0 {
|
||||
break
|
||||
}
|
||||
// Mirror upstream's `data != top` post-loop check: every byte of
|
||||
// the delta payload must be consumed.
|
||||
if len(delta) != 0 {
|
||||
return ErrInvalidDelta
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -354,7 +369,7 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader,
|
||||
baselr := io.LimitReader(sr, 0).(*io.LimitedReader)
|
||||
deltalr := io.LimitReader(deltaBuf, 0).(*io.LimitedReader)
|
||||
|
||||
for {
|
||||
for remainingTargetSz > 0 {
|
||||
buf := *bufp
|
||||
cmd, err := deltaBuf.ReadByte()
|
||||
if err == io.EOF {
|
||||
@@ -374,9 +389,9 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader,
|
||||
return 0, plumbing.ZeroHash, err
|
||||
}
|
||||
|
||||
if invalidSize(sz, targetSz) ||
|
||||
if invalidSize(sz, remainingTargetSz) ||
|
||||
invalidOffsetSize(offset, sz, srcSz) {
|
||||
return 0, plumbing.ZeroHash, err
|
||||
return 0, plumbing.ZeroHash, ErrInvalidDelta
|
||||
}
|
||||
|
||||
if _, err := sr.Seek(int64(offset), io.SeekStart); err != nil {
|
||||
@@ -389,7 +404,7 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader,
|
||||
remainingTargetSz -= sz
|
||||
} else if isCopyFromDelta(cmd) {
|
||||
sz := uint(cmd) // cmd is the size itself
|
||||
if invalidSize(sz, targetSz) {
|
||||
if invalidSize(sz, remainingTargetSz) {
|
||||
return 0, plumbing.ZeroHash, ErrInvalidDelta
|
||||
}
|
||||
deltalr.N = int64(sz)
|
||||
@@ -399,30 +414,41 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader,
|
||||
|
||||
remainingTargetSz -= sz
|
||||
} else {
|
||||
return 0, plumbing.ZeroHash, err
|
||||
}
|
||||
if remainingTargetSz <= 0 {
|
||||
break
|
||||
return 0, plumbing.ZeroHash, ErrDeltaCmd
|
||||
}
|
||||
}
|
||||
|
||||
// Mirror upstream's `data != top` post-loop check: every byte of
|
||||
// the delta payload must be consumed.
|
||||
if _, err := deltaBuf.ReadByte(); err == nil {
|
||||
return 0, plumbing.ZeroHash, ErrInvalidDelta
|
||||
} else if err != io.EOF {
|
||||
return 0, plumbing.ZeroHash, err
|
||||
}
|
||||
|
||||
return targetSz, hasher.Sum(), nil
|
||||
}
|
||||
|
||||
// Decodes a number encoded as an unsigned LEB128 at the start of some
|
||||
// binary data and returns the decoded number and the rest of the
|
||||
// stream.
|
||||
// binary data and returns the decoded number, the rest of the stream,
|
||||
// and an error if the encoded value does not fit in a uint.
|
||||
//
|
||||
// This must be called twice on the delta data buffer, first to get the
|
||||
// expected source buffer size, and again to get the target buffer size.
|
||||
func decodeLEB128(input []byte) (uint, []byte) {
|
||||
func decodeLEB128(input []byte) (uint, []byte, error) {
|
||||
if len(input) == 0 {
|
||||
return 0, input
|
||||
return 0, input, nil
|
||||
}
|
||||
|
||||
var num, sz uint
|
||||
var b byte
|
||||
for {
|
||||
// A continuation byte at shift > uintBits-7 cannot contribute
|
||||
// without overflowing the accumulator.
|
||||
if sz*7 > uintBits-7 {
|
||||
return 0, input, ErrLengthOverflow
|
||||
}
|
||||
|
||||
b = input[sz]
|
||||
num |= (uint(b) & payload) << (sz * 7) // concats 7 bits chunks
|
||||
sz++
|
||||
@@ -432,12 +458,16 @@ func decodeLEB128(input []byte) (uint, []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
return num, input[sz:]
|
||||
return num, input[sz:], nil
|
||||
}
|
||||
|
||||
func decodeLEB128ByteReader(input io.ByteReader) (uint, error) {
|
||||
var num, sz uint
|
||||
for {
|
||||
if sz*7 > uintBits-7 {
|
||||
return 0, ErrLengthOverflow
|
||||
}
|
||||
|
||||
b, err := input.ReadByte()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -529,8 +559,9 @@ func decodeSize(cmd byte, delta []byte) (uint, []byte, error) {
|
||||
return sz, delta, nil
|
||||
}
|
||||
|
||||
func invalidSize(sz, targetSz uint) bool {
|
||||
return sz > targetSz
|
||||
// invalidSize reports whether sz exceeds the remaining target size.
|
||||
func invalidSize(sz, remaining uint) bool {
|
||||
return sz > remaining
|
||||
}
|
||||
|
||||
func invalidOffsetSize(offset, sz, srcSz uint) bool {
|
||||
|
||||
+145
-9
@@ -29,8 +29,100 @@ var (
|
||||
ErrSeekNotSupported = NewError("not seek support")
|
||||
// ErrMalformedPackFile is returned by the parser when the pack file is corrupted.
|
||||
ErrMalformedPackFile = errors.New("malformed PACK file")
|
||||
// ErrLengthOverflow is returned when a variable-length integer would not
|
||||
// fit into its accumulator because the input declares more continuation
|
||||
// bytes than the type can hold.
|
||||
ErrLengthOverflow = errors.New("variable-length integer overflow")
|
||||
// ErrInflatedSizeMismatch is returned when a packfile object inflates to
|
||||
// more bytes than the size declared in its object header. A well-formed
|
||||
// packfile never produces more data than the declared size; exceeding it
|
||||
// indicates a structurally invalid entry.
|
||||
ErrInflatedSizeMismatch = errors.New("packfile: inflated object exceeds declared size")
|
||||
)
|
||||
|
||||
// boundedWriter passes writes through to w up to limit bytes total, then
|
||||
// returns ErrInflatedSizeMismatch. It is used to enforce that a packfile
|
||||
// object's inflated length does not exceed the size declared in its header.
|
||||
type boundedWriter struct {
|
||||
w io.Writer
|
||||
limit int64
|
||||
n int64
|
||||
}
|
||||
|
||||
// Write forwards p to the underlying writer while keeping the running total
|
||||
// at or below limit. On overrun it forwards the legal prefix and reports
|
||||
// the number of bytes actually consumed alongside ErrInflatedSizeMismatch,
|
||||
// matching the contract in io.Writer. A write error from the underlying
|
||||
// writer during overrun-handling is joined with ErrInflatedSizeMismatch so
|
||||
// it is not silently dropped.
|
||||
func (b *boundedWriter) Write(p []byte) (int, error) {
|
||||
if b.n+int64(len(p)) > b.limit {
|
||||
remain := int(b.limit - b.n)
|
||||
err := error(ErrInflatedSizeMismatch)
|
||||
if remain > 0 {
|
||||
n, werr := b.w.Write(p[:remain])
|
||||
b.n += int64(n)
|
||||
if werr != nil {
|
||||
err = errors.Join(ErrInflatedSizeMismatch, werr)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
n, err := b.w.Write(p)
|
||||
b.n += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// boundedReadCloser wraps a ReadCloser and reports ErrInflatedSizeMismatch
|
||||
// once more than limit bytes have been read. It is used by the on-demand
|
||||
// object reader returned from FSObject.Reader so that a lazy Read of a
|
||||
// packfile object cannot stream past its declared inflated size.
|
||||
//
|
||||
// The implementation builds on io.LimitedReader with the standard
|
||||
// overrun-detection trick: request limit+1 bytes from the underlying so
|
||||
// that the moment the sentinel byte materializes (LimitedReader.N drops
|
||||
// to zero) we know the source produced more than limit bytes.
|
||||
type boundedReadCloser struct {
|
||||
lr io.LimitedReader
|
||||
closer io.Closer
|
||||
overrun bool
|
||||
}
|
||||
|
||||
// newBoundedReadCloser wraps rc so that the cumulative bytes returned from
|
||||
// Read never exceed limit. The first call that would have returned a byte
|
||||
// past limit instead returns ErrInflatedSizeMismatch; subsequent calls
|
||||
// keep returning the same error. A negative limit is treated as zero, so
|
||||
// the first byte produced by rc surfaces ErrInflatedSizeMismatch.
|
||||
func newBoundedReadCloser(rc io.ReadCloser, limit int64) *boundedReadCloser {
|
||||
if limit < 0 {
|
||||
limit = 0
|
||||
}
|
||||
return &boundedReadCloser{
|
||||
lr: io.LimitedReader{R: rc, N: limit + 1},
|
||||
closer: rc,
|
||||
}
|
||||
}
|
||||
|
||||
// Read forwards Read up to the configured byte limit. When the underlying
|
||||
// stream produces the limit+1 sentinel byte, the legal prefix is returned
|
||||
// alongside ErrInflatedSizeMismatch; on subsequent calls only the error
|
||||
// is returned.
|
||||
func (b *boundedReadCloser) Read(p []byte) (int, error) {
|
||||
if b.overrun {
|
||||
return 0, ErrInflatedSizeMismatch
|
||||
}
|
||||
n, err := b.lr.Read(p)
|
||||
if b.lr.N == 0 {
|
||||
b.overrun = true
|
||||
return n - 1, ErrInflatedSizeMismatch
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Close closes the underlying ReadCloser.
|
||||
func (b *boundedReadCloser) Close() error { return b.closer.Close() }
|
||||
|
||||
// ObjectHeader contains the information related to the object, this information
|
||||
// is collected from the previous bytes to the content of the object.
|
||||
type ObjectHeader struct {
|
||||
@@ -220,6 +312,13 @@ func (s *Scanner) nextObjectHeader() (*ObjectHeader, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// An OFS-delta references a base object that appears earlier
|
||||
// in the pack; the negative offset must be strictly positive
|
||||
// and not larger than the current object's offset.
|
||||
if no <= 0 || no > h.Offset {
|
||||
return nil, fmt.Errorf("%w: invalid OFS delta offset", ErrMalformedPackFile)
|
||||
}
|
||||
|
||||
h.OffsetReference = h.Offset - no
|
||||
case plumbing.REFDeltaObject:
|
||||
var err error
|
||||
@@ -303,6 +402,13 @@ func (s *Scanner) readLength(first byte) (int64, error) {
|
||||
shift := firstLengthBits
|
||||
var err error
|
||||
for c&maskContinue > 0 {
|
||||
// Mirrors unpack_object_header_buffer in canonical Git's
|
||||
// packfile.c: a continuation byte at shift > 64-7 cannot
|
||||
// contribute without overflowing an int64.
|
||||
if shift > 64-lengthBits {
|
||||
return 0, fmt.Errorf("%w: %w", ErrMalformedPackFile, ErrLengthOverflow)
|
||||
}
|
||||
|
||||
if c, err = s.r.ReadByte(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -315,10 +421,18 @@ func (s *Scanner) readLength(first byte) (int64, error) {
|
||||
}
|
||||
|
||||
// NextObject writes the content of the next object into the reader, returns
|
||||
// the number of bytes written, the CRC32 of the content and an error, if any
|
||||
// the number of bytes written, the CRC32 of the content and an error, if any.
|
||||
//
|
||||
// When a prior NextObjectHeader has stashed the object header in
|
||||
// pendingObject, the inflated stream is bounded by the header's declared
|
||||
// length and surfaces ErrInflatedSizeMismatch on overrun.
|
||||
func (s *Scanner) NextObject(w io.Writer) (written int64, crc32 uint32, err error) {
|
||||
declaredSize := int64(-1)
|
||||
if s.pendingObject != nil {
|
||||
declaredSize = s.pendingObject.Length
|
||||
}
|
||||
s.pendingObject = nil
|
||||
written, err = s.copyObject(w)
|
||||
written, err = s.copyObject(w, declaredSize)
|
||||
|
||||
s.r.Flush()
|
||||
crc32 = s.crc.Sum32()
|
||||
@@ -327,23 +441,39 @@ func (s *Scanner) NextObject(w io.Writer) (written int64, crc32 uint32, err erro
|
||||
return
|
||||
}
|
||||
|
||||
// ReadObject returns a reader for the object content and an error
|
||||
// ReadObject returns a reader for the object content and an error.
|
||||
//
|
||||
// When a prior NextObjectHeader has stashed the object header in
|
||||
// pendingObject, the returned reader is bounded by the header's declared
|
||||
// length so callers cannot stream past the declared inflated size; an
|
||||
// overrun surfaces ErrInflatedSizeMismatch on the byte just past the
|
||||
// limit.
|
||||
func (s *Scanner) ReadObject() (io.ReadCloser, error) {
|
||||
declaredSize := int64(-1)
|
||||
if s.pendingObject != nil {
|
||||
declaredSize = s.pendingObject.Length
|
||||
}
|
||||
s.pendingObject = nil
|
||||
zr, err := sync.GetZlibReader(s.r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("zlib reset error: %s", err)
|
||||
}
|
||||
|
||||
return ioutil.NewReadCloserWithCloser(zr.Reader, func() error {
|
||||
rc := ioutil.NewReadCloserWithCloser(zr.Reader, func() error {
|
||||
sync.PutZlibReader(zr)
|
||||
return nil
|
||||
}), nil
|
||||
})
|
||||
if declaredSize >= 0 {
|
||||
return newBoundedReadCloser(rc, declaredSize), nil
|
||||
}
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// ReadRegularObject reads and write a non-deltified object
|
||||
// from it zlib stream in an object entry in the packfile.
|
||||
func (s *Scanner) copyObject(w io.Writer) (n int64, err error) {
|
||||
// copyObject inflates a non-deltified object's zlib stream into w. When
|
||||
// declaredSize is non-negative, the write sink is wrapped in a
|
||||
// boundedWriter so an overrun surfaces ErrInflatedSizeMismatch instead
|
||||
// of being silently appended.
|
||||
func (s *Scanner) copyObject(w io.Writer, declaredSize int64) (n int64, err error) {
|
||||
zr, err := sync.GetZlibReader(s.r)
|
||||
defer sync.PutZlibReader(zr)
|
||||
|
||||
@@ -352,8 +482,14 @@ func (s *Scanner) copyObject(w io.Writer) (n int64, err error) {
|
||||
}
|
||||
|
||||
defer ioutil.CheckClose(zr.Reader, &err)
|
||||
|
||||
sink := w
|
||||
if declaredSize >= 0 {
|
||||
sink = &boundedWriter{w: w, limit: declaredSize}
|
||||
}
|
||||
|
||||
buf := sync.GetByteSlice()
|
||||
n, err = io.CopyBuffer(w, zr.Reader, *buf)
|
||||
n, err = io.CopyBuffer(sink, zr.Reader, *buf)
|
||||
sync.PutByteSlice(buf)
|
||||
return
|
||||
}
|
||||
|
||||
+23
@@ -10,6 +10,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5/internal/pathutil"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||
"github.com/go-git/go-git/v5/plumbing/storer"
|
||||
@@ -118,7 +119,16 @@ func (t *Tree) Tree(path string) (*Tree, error) {
|
||||
}
|
||||
|
||||
// TreeEntryFile returns the *File for a given *TreeEntry.
|
||||
//
|
||||
// The entry's name is validated against pathutil.ValidTreePath for
|
||||
// the same reason FindEntry validates: TreeEntryFile is a boundary
|
||||
// where attacker-controlled tree data leaves the trusted store as a
|
||||
// *File whose Name a caller can hand to filesystem ops.
|
||||
func (t *Tree) TreeEntryFile(e *TreeEntry) (*File, error) {
|
||||
if err := pathutil.ValidTreePath(e.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blob, err := GetBlob(t.s, e.Hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -128,7 +138,16 @@ func (t *Tree) TreeEntryFile(e *TreeEntry) (*File, error) {
|
||||
}
|
||||
|
||||
// FindEntry search a TreeEntry in this tree or any subtree.
|
||||
//
|
||||
// The lookup path is validated against pathutil.ValidTreePath to
|
||||
// prevent attacker-controlled tree contents from leaking past this
|
||||
// boundary as `.git`-shaped or path-traversal-shaped names. Callers
|
||||
// that legitimately need to look up unsafe paths should walk the
|
||||
// tree manually.
|
||||
func (t *Tree) FindEntry(path string) (*TreeEntry, error) {
|
||||
if err := pathutil.ValidTreePath(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t.t == nil {
|
||||
t.t = make(map[string]*Tree)
|
||||
}
|
||||
@@ -517,6 +536,10 @@ func (w *TreeWalker) Next() (name string, entry TreeEntry, err error) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := pathutil.ValidTreePath(entry.Name); err != nil {
|
||||
return name, entry, err
|
||||
}
|
||||
|
||||
if entry.Mode == filemode.Dir {
|
||||
obj, err = GetTree(w.s, entry.Hash)
|
||||
}
|
||||
|
||||
+33
-1
@@ -252,7 +252,39 @@ func (c *command) setAuthFromEndpoint() error {
|
||||
}
|
||||
|
||||
func endpointToCommand(cmd string, ep *transport.Endpoint) string {
|
||||
return fmt.Sprintf("%s '%s'", cmd, ep.Path)
|
||||
var b strings.Builder
|
||||
b.WriteString(cmd)
|
||||
b.WriteByte(' ')
|
||||
writeShellQuote(&b, ep.Path)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// writeShellQuote writes s to b, wrapped in single quotes with
|
||||
// embedded single quotes and exclamation marks escaped using the
|
||||
// POSIX close-escape-reopen idiom:
|
||||
//
|
||||
// ' becomes '\''
|
||||
// ! becomes '\!'
|
||||
//
|
||||
// It is a direct port of canonical Git's sq_quote_buf (quote.c).
|
||||
// The bang escape keeps the result safe when re-evaluated under
|
||||
// csh-derived shells that perform history expansion. The output is
|
||||
// safe to pass as a single argument through any POSIX shell and
|
||||
// round-trips through git-shell's sq_dequote_to_argv.
|
||||
func writeShellQuote(b *strings.Builder, s string) {
|
||||
b.Grow(len(s) + 2)
|
||||
b.WriteByte('\'')
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if c == '\'' || c == '!' {
|
||||
b.WriteString(`'\`)
|
||||
b.WriteByte(c)
|
||||
b.WriteByte('\'')
|
||||
continue
|
||||
}
|
||||
b.WriteByte(c)
|
||||
}
|
||||
b.WriteByte('\'')
|
||||
}
|
||||
|
||||
func overrideConfig(overrides *ssh.ClientConfig, c *ssh.ClientConfig) {
|
||||
|
||||
+12
-1
@@ -1530,7 +1530,18 @@ func (r *Repository) Worktree() (*Worktree, error) {
|
||||
return nil, ErrIsBareRepository
|
||||
}
|
||||
|
||||
return &Worktree{r: r, Filesystem: r.wt}, nil
|
||||
protectNTFS := defaultProtectNTFS()
|
||||
protectHFS := defaultProtectHFS()
|
||||
if cfg, err := r.Config(); err == nil {
|
||||
if cfg.Core.ProtectNTFS.IsSet() {
|
||||
protectNTFS = cfg.Core.ProtectNTFS.IsTrue()
|
||||
}
|
||||
if cfg.Core.ProtectHFS.IsSet() {
|
||||
protectHFS = cfg.Core.ProtectHFS.IsTrue()
|
||||
}
|
||||
}
|
||||
|
||||
return &Worktree{r: r, Filesystem: newWorktreeFilesystem(r.wt, protectNTFS, protectHFS)}, nil
|
||||
}
|
||||
|
||||
func expand_ref(s storer.ReferenceStorer, ref plumbing.ReferenceName) (*plumbing.Reference, error) {
|
||||
|
||||
+17
-2
@@ -75,6 +75,10 @@ var (
|
||||
// ErrEmptyRefFile is returned when a reference file is attempted to be read,
|
||||
// but the file is empty
|
||||
ErrEmptyRefFile = errors.New("ref file is empty")
|
||||
// ErrModuleNameEscape is returned when a submodule name would
|
||||
// resolve outside the modules/ subtree, mirroring canonical Git's
|
||||
// "ignoring suspicious submodule name" defence.
|
||||
ErrModuleNameEscape = errors.New("submodule name escapes modules/ directory")
|
||||
)
|
||||
|
||||
// Options holds configuration for the storage.
|
||||
@@ -1127,9 +1131,20 @@ func (d *DotGit) PackRefs() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Module return a billy.Filesystem pointing to the module folder
|
||||
// Module returns a billy.Filesystem pointing to the module folder.
|
||||
//
|
||||
// As a defence in depth against submodule name path traversal,
|
||||
// refuse names whose joined path leaves the modules/ subtree once
|
||||
// cleaned. The config-layer parser also validates submodule names,
|
||||
// but Module may be reached from any caller that constructs a
|
||||
// Submodule struct programmatically and so bypasses the parser.
|
||||
func (d *DotGit) Module(name string) (billy.Filesystem, error) {
|
||||
return d.fs.Chroot(d.fs.Join(modulePath, name))
|
||||
p := d.fs.Join(modulePath, name)
|
||||
cleaned := path.Clean(filepath.ToSlash(p))
|
||||
if cleaned != modulePath && !strings.HasPrefix(cleaned, modulePath+"/") {
|
||||
return nil, ErrModuleNameEscape
|
||||
}
|
||||
return d.fs.Chroot(p)
|
||||
}
|
||||
|
||||
func (d *DotGit) AddAlternate(remote string) error {
|
||||
|
||||
+74
-8
@@ -6,9 +6,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-git/go-billy/v5"
|
||||
"github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/internal/pathutil"
|
||||
giturl "github.com/go-git/go-git/v5/internal/url"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/format/index"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||
@@ -119,6 +122,16 @@ func (s *Submodule) Repository() (*Repository, error) {
|
||||
exists = true
|
||||
}
|
||||
|
||||
// s.c.Path is sourced from the worktree's .gitmodules and is
|
||||
// therefore tree-controlled. Apply the strict tree-path validator
|
||||
// before chroot — the wrapper's tolerant validPath would let a
|
||||
// final-position .git component through (e.g. "submodule/.git"),
|
||||
// which a malicious .gitmodules could use to chroot the submodule
|
||||
// worktree into the repository's actual .git directory.
|
||||
if err := pathutil.ValidTreePath(s.c.Path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var worktree billy.Filesystem
|
||||
if worktree, err = s.w.Filesystem.Chroot(s.c.Path); err != nil {
|
||||
return nil, err
|
||||
@@ -138,18 +151,25 @@ func (s *Submodule) Repository() (*Repository, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !path.IsAbs(moduleEndpoint.Path) && moduleEndpoint.Protocol == "file" {
|
||||
remotes, err := s.w.r.Remotes()
|
||||
// A relative submodule URL such as "../X.git" must resolve against
|
||||
// the parent repository's remote URL, not against the process CWD.
|
||||
// Detect relativity from the raw configured URL because
|
||||
// transport.NewEndpoint normalizes local paths to absolute form via
|
||||
// filepath.Abs, which would otherwise mask the relative form here.
|
||||
if giturl.IsLocalEndpoint(s.c.URL) &&
|
||||
!path.IsAbs(s.c.URL) && !filepath.IsAbs(s.c.URL) {
|
||||
|
||||
base, err := defaultRemote(s.w.r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolving relative submodule URL: %w", err)
|
||||
}
|
||||
|
||||
rootEndpoint, err := transport.NewEndpoint(base.URLs[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rootEndpoint, err := transport.NewEndpoint(remotes[0].c.URLs[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rootEndpoint.Path = path.Join(rootEndpoint.Path, moduleEndpoint.Path)
|
||||
rootEndpoint.Path = path.Join(rootEndpoint.Path, s.c.URL)
|
||||
*moduleEndpoint = *rootEndpoint
|
||||
}
|
||||
|
||||
@@ -161,6 +181,52 @@ func (s *Submodule) Repository() (*Repository, error) {
|
||||
return r, err
|
||||
}
|
||||
|
||||
// defaultRemote returns the remote that relative submodule URLs are
|
||||
// resolved against, mirroring canonical Git's repo_default_remote
|
||||
// (remote.c) and resolve_relative_url (builtin/submodule--helper.c):
|
||||
//
|
||||
// 1. if HEAD is on a branch with branch.<name>.remote configured,
|
||||
// use that remote;
|
||||
// 2. else if exactly one remote is configured, use it;
|
||||
// 3. otherwise fall back to DefaultRemoteName ("origin").
|
||||
//
|
||||
// Each rule falls through unconditionally: a branch lookup that
|
||||
// finds the branch but with an empty Remote does not short-circuit
|
||||
// rule (2). Returns an error when the chosen remote is not configured.
|
||||
func defaultRemote(r *Repository) (*config.RemoteConfig, error) {
|
||||
cfg, err := r.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ref, err := r.Reference(plumbing.HEAD, false); err == nil &&
|
||||
ref.Type() == plumbing.SymbolicReference &&
|
||||
ref.Target().IsBranch() {
|
||||
if b, ok := cfg.Branches[ref.Target().Short()]; ok && b.Remote != "" {
|
||||
return lookupRemote(cfg, b.Remote)
|
||||
}
|
||||
}
|
||||
|
||||
if len(cfg.Remotes) == 1 {
|
||||
for name := range cfg.Remotes {
|
||||
return lookupRemote(cfg, name)
|
||||
}
|
||||
}
|
||||
|
||||
return lookupRemote(cfg, DefaultRemoteName)
|
||||
}
|
||||
|
||||
func lookupRemote(cfg *config.Config, name string) (*config.RemoteConfig, error) {
|
||||
rc, ok := cfg.Remotes[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("remote %q not found", name)
|
||||
}
|
||||
if len(rc.URLs) == 0 {
|
||||
return nil, fmt.Errorf("remote %q has no configured URL", name)
|
||||
}
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// Update the registered submodule to match what the superproject expects, the
|
||||
// submodule should be initialized first calling the Init method or setting in
|
||||
// the options SubmoduleUpdateOptions.Init equals true
|
||||
|
||||
+15
@@ -5,11 +5,18 @@ package binary
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// ErrIntegerOverflow is returned when a Git-format variable-width integer
|
||||
// would not fit into an int64 because the input declares more continuation
|
||||
// bytes than the type can hold.
|
||||
var ErrIntegerOverflow = errors.New("variable-width integer overflow")
|
||||
|
||||
// Read reads structured binary data from r into data. Bytes are read and
|
||||
// decoded in BigEndian order
|
||||
// https://golang.org/pkg/encoding/binary/#Read
|
||||
@@ -92,6 +99,14 @@ func ReadVariableWidthInt(r io.Reader) (int64, error) {
|
||||
|
||||
var v = int64(c & maskLength)
|
||||
for c&maskContinue > 0 {
|
||||
// Reject input that, after the v++ and shift below, would
|
||||
// not fit in an int64. With v < (MaxInt64-127)>>7, the
|
||||
// post-increment v is at most (MaxInt64-127)>>7 and the
|
||||
// final (v << 7) + (c & 0x7F) stays within int64.
|
||||
if v >= (math.MaxInt64-int64(maskLength))>>lengthBits {
|
||||
return 0, ErrIntegerOverflow
|
||||
}
|
||||
|
||||
v++
|
||||
if err := Read(r, &c); err != nil {
|
||||
return 0, err
|
||||
|
||||
+4
-111
@@ -7,7 +7,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-billy/v5"
|
||||
@@ -458,10 +457,6 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error {
|
||||
|
||||
filesMap := buildFilePathMap(files)
|
||||
for _, ch := range changes {
|
||||
if err := w.validChange(ch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(files) > 0 {
|
||||
file := ""
|
||||
if ch.From != nil {
|
||||
@@ -489,108 +484,6 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error {
|
||||
return w.r.Storer.SetIndex(idx)
|
||||
}
|
||||
|
||||
// worktreeDeny is a list of paths that are not allowed
|
||||
// to be used when resetting the worktree.
|
||||
var worktreeDeny = map[string]struct{}{
|
||||
// .git
|
||||
GitDirName: {},
|
||||
|
||||
// For other historical reasons, file names that do not conform to the 8.3
|
||||
// format (up to eight characters for the basename, three for the file
|
||||
// extension, certain characters not allowed such as `+`, etc) are associated
|
||||
// with a so-called "short name", at least on the `C:` drive by default.
|
||||
// Which means that `git~1/` is a valid way to refer to `.git/`.
|
||||
"git~1": {},
|
||||
}
|
||||
|
||||
// validPath checks whether paths are valid.
|
||||
// The rules around invalid paths could differ from upstream based on how
|
||||
// filesystems are managed within go-git, but they are largely the same.
|
||||
//
|
||||
// For upstream rules:
|
||||
// https://github.com/git/git/blob/564d0252ca632e0264ed670534a51d18a689ef5d/read-cache.c#L946
|
||||
// https://github.com/git/git/blob/564d0252ca632e0264ed670534a51d18a689ef5d/path.c#L1383
|
||||
func validPath(paths ...string) error {
|
||||
for _, p := range paths {
|
||||
parts := strings.FieldsFunc(p, func(r rune) bool { return (r == '\\' || r == '/') })
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("invalid path: %q", p)
|
||||
}
|
||||
|
||||
if _, denied := worktreeDeny[strings.ToLower(parts[0])]; denied {
|
||||
return fmt.Errorf("invalid path prefix: %q", p)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Volume names are not supported, in both formats: \\ and <DRIVE_LETTER>:.
|
||||
if vol := filepath.VolumeName(p); vol != "" {
|
||||
return fmt.Errorf("invalid path: %q", p)
|
||||
}
|
||||
|
||||
if !windowsValidPath(parts[0]) {
|
||||
return fmt.Errorf("invalid path: %q", p)
|
||||
}
|
||||
}
|
||||
|
||||
for _, part := range parts {
|
||||
if part == ".." {
|
||||
return fmt.Errorf("invalid path %q: cannot use '..'", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// windowsPathReplacer defines the chars that need to be replaced
|
||||
// as part of windowsValidPath.
|
||||
var windowsPathReplacer *strings.Replacer
|
||||
|
||||
func init() {
|
||||
windowsPathReplacer = strings.NewReplacer(" ", "", ".", "")
|
||||
}
|
||||
|
||||
func windowsValidPath(part string) bool {
|
||||
if len(part) > 3 && strings.EqualFold(part[:4], GitDirName) {
|
||||
// For historical reasons, file names that end in spaces or periods are
|
||||
// automatically trimmed. Therefore, `.git . . ./` is a valid way to refer
|
||||
// to `.git/`.
|
||||
if windowsPathReplacer.Replace(part[4:]) == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// For yet other historical reasons, NTFS supports so-called "Alternate Data
|
||||
// Streams", i.e. metadata associated with a given file, referred to via
|
||||
// `<filename>:<stream-name>:<stream-type>`. There exists a default stream
|
||||
// type for directories, allowing `.git/` to be accessed via
|
||||
// `.git::$INDEX_ALLOCATION/`.
|
||||
//
|
||||
// For performance reasons, _all_ Alternate Data Streams of `.git/` are
|
||||
// forbidden, not just `::$INDEX_ALLOCATION`.
|
||||
if len(part) > 4 && part[4:5] == ":" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *Worktree) validChange(ch merkletrie.Change) error {
|
||||
action, err := ch.Action()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch action {
|
||||
case merkletrie.Delete:
|
||||
return validPath(ch.From.String())
|
||||
case merkletrie.Insert:
|
||||
return validPath(ch.To.String())
|
||||
case merkletrie.Modify:
|
||||
return validPath(ch.From.String(), ch.To.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Worktree) checkoutChange(ch merkletrie.Change, t *object.Tree, idx *indexBuilder) error {
|
||||
a, err := ch.Action()
|
||||
if err != nil {
|
||||
@@ -763,10 +656,10 @@ func (w *Worktree) checkoutFile(f *object.File) (err error) {
|
||||
}
|
||||
|
||||
func (w *Worktree) checkoutFileSymlink(f *object.File) (err error) {
|
||||
// https://github.com/git/git/commit/10ecfa76491e4923988337b2e2243b05376b40de
|
||||
if strings.EqualFold(f.Name, gitmodulesFile) {
|
||||
return ErrGitModulesSymlink
|
||||
}
|
||||
// .gitmodules symlink rejection (and its NTFS / HFS variants) is
|
||||
// enforced by the worktreeFilesystem wrapper's Symlink method via
|
||||
// validSymlinkName. See https://github.com/git/git/commit/10ecfa7
|
||||
// for the upstream rationale.
|
||||
|
||||
from, err := f.Reader()
|
||||
if err != nil {
|
||||
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-billy/v5"
|
||||
|
||||
"github.com/go-git/go-git/v5/internal/pathutil"
|
||||
)
|
||||
|
||||
// defaultProtectHFS returns the default value for core.protectHFS
|
||||
// when not explicitly configured. Matches upstream Git's
|
||||
// PROTECT_HFS_DEFAULT[1], which the Makefile sets to 1 on Darwin
|
||||
// and leaves at 0 on every other platform.
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/config.mak.uname#L146
|
||||
func defaultProtectHFS() bool {
|
||||
return runtime.GOOS == "darwin"
|
||||
}
|
||||
|
||||
// defaultProtectNTFS returns the default value for core.protectNTFS
|
||||
// when not explicitly configured. Matches upstream Git's
|
||||
// PROTECT_NTFS_DEFAULT, which has been 1 on every platform since
|
||||
// 9102f958ee5 (CVE-2019-1353)[1]: WSL allows Linux processes to
|
||||
// reach NTFS-mounted worktrees on Windows hosts, so the
|
||||
// is_ntfs_dotgit guard cannot safely be gated on the runtime OS.
|
||||
//
|
||||
// [1]: https://github.com/git/git/commit/9102f958ee5
|
||||
func defaultProtectNTFS() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// worktreeFilesystem wraps a billy.Filesystem and validates every path passed
|
||||
// to a mutating operation. This prevents writing to, or deleting from,
|
||||
// dangerous locations (e.g. .git/*, ../) regardless of which worktree
|
||||
// code path triggers the operation.
|
||||
type worktreeFilesystem struct {
|
||||
billy.Filesystem
|
||||
protectNTFS bool
|
||||
protectHFS bool
|
||||
}
|
||||
|
||||
func newWorktreeFilesystem(fs billy.Filesystem, protectNTFS, protectHFS bool) *worktreeFilesystem {
|
||||
return &worktreeFilesystem{Filesystem: fs, protectNTFS: protectNTFS, protectHFS: protectHFS}
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Create(filename string) (billy.File, error) {
|
||||
if err := sfs.validPath(filename); err != nil {
|
||||
return nil, fmt.Errorf("create: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Create(filename)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Open(filename string) (billy.File, error) {
|
||||
if err := sfs.validReadPath(filename); err != nil {
|
||||
return nil, fmt.Errorf("open: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Open(filename)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
|
||||
if err := sfs.validPath(filename); err != nil {
|
||||
return nil, fmt.Errorf("openfile: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.OpenFile(filename, flag, perm)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Stat(filename string) (os.FileInfo, error) {
|
||||
if err := sfs.validReadPath(filename); err != nil {
|
||||
return nil, fmt.Errorf("stat: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Stat(filename)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Remove(filename string) error {
|
||||
if err := sfs.validPath(filename); err != nil {
|
||||
return fmt.Errorf("remove: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Remove(filename)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Rename(from, to string) error {
|
||||
if err := sfs.validPath(from, to); err != nil {
|
||||
return fmt.Errorf("rename: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Rename(from, to)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) ReadDir(path string) ([]os.FileInfo, error) {
|
||||
if err := sfs.validReadPath(path); err != nil {
|
||||
return nil, fmt.Errorf("readdir: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.ReadDir(path)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Lstat(filename string) (os.FileInfo, error) {
|
||||
if err := sfs.validReadPath(filename); err != nil {
|
||||
return nil, fmt.Errorf("lstat: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Lstat(filename)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Symlink(target, link string) error {
|
||||
if err := sfs.validPath(link); err != nil {
|
||||
return fmt.Errorf("symlink: %w", err)
|
||||
}
|
||||
if err := sfs.validSymlinkName(link); err != nil {
|
||||
return fmt.Errorf("symlink: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Symlink(target, link)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Readlink(link string) (string, error) {
|
||||
if err := sfs.validReadPath(link); err != nil {
|
||||
return "", fmt.Errorf("readlink: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Readlink(link)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) MkdirAll(path string, perm os.FileMode) error {
|
||||
// MkdirAll on the worktree root is a no-op: the root always exists,
|
||||
// so there is nothing to materialise. Mirroring the tolerance that
|
||||
// validReadPath gives to read-side operations avoids breaking callers
|
||||
// that walk a directory tree and pass the relative-to-root prefix
|
||||
// ("") through to the worktree FS.
|
||||
if path == "" || path == "." || path == "/" {
|
||||
return nil
|
||||
}
|
||||
if err := sfs.validPath(path); err != nil {
|
||||
return fmt.Errorf("mkdirall: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.MkdirAll(path, perm)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) TempFile(_, _ string) (billy.File, error) {
|
||||
return nil, fmt.Errorf("tempfile: %w", errUnsupportedOperation)
|
||||
}
|
||||
|
||||
func (sfs *worktreeFilesystem) Chroot(path string) (billy.Filesystem, error) {
|
||||
if err := sfs.validReadPath(path); err != nil {
|
||||
return nil, fmt.Errorf("chroot: %w", err)
|
||||
}
|
||||
return sfs.Filesystem.Chroot(path)
|
||||
}
|
||||
|
||||
// validReadPath is like validPath but treats the empty string and "." as
|
||||
// valid references to the worktree root. Read-side operations on the root
|
||||
// (e.g. ReadDir(""), Lstat(".")) are legitimate; mutating the root itself
|
||||
// is not, so write-side operations continue to use validPath directly.
|
||||
func (sfs *worktreeFilesystem) validReadPath(p string) error {
|
||||
if p == "" || p == "." || p == "/" {
|
||||
return nil
|
||||
}
|
||||
return sfs.validPath(p)
|
||||
}
|
||||
|
||||
var errUnsupportedOperation = errors.New("unsupported operation")
|
||||
|
||||
// isDotGitVariant reports whether part is .git, git~1, or an HFS+
|
||||
// equivalent of .git (when protectHFS is true). NTFS variants of .git
|
||||
// (e.g. ".git " with trailing space, ".git::$INDEX_ALLOCATION") are
|
||||
// detected separately by pathutil.WindowsValidPath, which applies
|
||||
// regardless of position in the path. Both validators reuse this
|
||||
// helper.
|
||||
func isDotGitVariant(part string, protectHFS bool) bool {
|
||||
if pathutil.IsDotGitName(part) {
|
||||
return true
|
||||
}
|
||||
if protectHFS && pathutil.IsHFSDotGit(part) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// validPath checks whether paths are valid for the worktree
|
||||
// filesystem abstraction. It is intentionally tolerant of .git as
|
||||
// the final path component of a multi-component path
|
||||
// (e.g. "submodule/.git"), so that legitimate gitlink pointer files
|
||||
// can still be Stat'd, Read, and Removed via the wrapper during
|
||||
// submodule cleanup. Attacker-controlled tree-entry paths are
|
||||
// validated separately by pathutil.ValidTreePath at the boundaries
|
||||
// where data leaves the trusted store (Tree.FindEntry, the explicit
|
||||
// callers in CherryPick and Submodule.Repository).
|
||||
//
|
||||
// For upstream rules:
|
||||
// https://github.com/git/git/blob/v2.54.0/read-cache.c#L987
|
||||
// https://github.com/git/git/blob/v2.54.0/path.c#L1419
|
||||
func (sfs *worktreeFilesystem) validPath(paths ...string) error {
|
||||
for _, p := range paths {
|
||||
for i := 0; i < len(p); i++ {
|
||||
if p[i] < 0x20 || p[i] == 0x7f {
|
||||
return fmt.Errorf("invalid path %q: contains control character", p)
|
||||
}
|
||||
}
|
||||
|
||||
parts := strings.FieldsFunc(p, func(r rune) bool { return (r == '\\' || r == '/') })
|
||||
if len(parts) == 0 {
|
||||
return fmt.Errorf("invalid path: %q", p)
|
||||
}
|
||||
|
||||
if sfs.protectNTFS {
|
||||
// Volume names are not supported, in both formats: \\ and <DRIVE_LETTER>:.
|
||||
if vol := filepath.VolumeName(p); vol != "" {
|
||||
return fmt.Errorf("invalid path: %q", p)
|
||||
}
|
||||
}
|
||||
|
||||
for i, part := range parts {
|
||||
if part == "." || part == ".." {
|
||||
return fmt.Errorf("invalid path %q: cannot use %q", p, part)
|
||||
}
|
||||
|
||||
// Reject .git (and equivalents) as a path component when it is
|
||||
// either the first component (root-level .git) or a non-final
|
||||
// component (traversal into a .git directory, e.g. "a/.git/config").
|
||||
// A final non-first .git component (e.g. "submodule/.git") is
|
||||
// allowed because submodule worktrees contain a .git pointer file.
|
||||
if isDotGitVariant(part, sfs.protectHFS) && (i == 0 || i < len(parts)-1) {
|
||||
return fmt.Errorf("invalid path component: %q", p)
|
||||
}
|
||||
|
||||
if sfs.protectNTFS && !pathutil.WindowsValidPath(part) {
|
||||
return fmt.Errorf("invalid path: %q", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validSymlinkName checks the per-component name of a symlink for
|
||||
// dotfile names that attackers can use to trick a checkout into
|
||||
// writing a dangerous symlink. Each path component is compared
|
||||
// against .gitmodules case-insensitively, against its NTFS variants
|
||||
// (e.g. ".gitmodules .", ".gitmodules::$INDEX_ALLOCATION", or 8.3
|
||||
// short-name forms) when protectNTFS is on, and against its HFS+
|
||||
// variants (Unicode ignored code points folded into ".gitmodules")
|
||||
// when protectHFS is on.
|
||||
//
|
||||
// Reference: upstream Git verify_path_internal at read-cache.c#L1004-L1024
|
||||
// in tag v2.54.0[1].
|
||||
//
|
||||
// [1]: https://github.com/git/git/blob/v2.54.0/read-cache.c#L1004-L1024
|
||||
func (sfs *worktreeFilesystem) validSymlinkName(name string) error {
|
||||
parts := strings.FieldsFunc(name, func(r rune) bool {
|
||||
return r == '/' || r == '\\'
|
||||
})
|
||||
for _, part := range parts {
|
||||
if strings.EqualFold(part, gitmodulesFile) {
|
||||
return ErrGitModulesSymlink
|
||||
}
|
||||
if sfs.protectNTFS && pathutil.IsNTFSDotGitmodules(part) {
|
||||
return ErrGitModulesSymlink
|
||||
}
|
||||
if sfs.protectHFS && pathutil.IsHFSDotGitmodules(part) {
|
||||
return ErrGitModulesSymlink
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+9
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-billy/v5/util"
|
||||
"github.com/go-git/go-git/v5/internal/pathutil"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
|
||||
@@ -545,6 +546,14 @@ func (w *Worktree) addOrUpdateFileToIndex(idx *index.Index, filename string, h p
|
||||
}
|
||||
|
||||
func (w *Worktree) doAddFileToIndex(idx *index.Index, filename string, h plumbing.Hash) error {
|
||||
// Mirror upstream's Index.Add gate at the v5 caller boundary: the
|
||||
// index feeds future trees, so a name that the tree-side
|
||||
// pathutil.ValidTreePath gate would reject must not enter the
|
||||
// index in the first place. v5 keeps Index.Add's existing signature
|
||||
// for API compatibility, so the validation happens here.
|
||||
if err := pathutil.ValidTreePath(filename); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.doUpdateFileToIndex(idx.Add(filename), filename, h)
|
||||
}
|
||||
|
||||
|
||||
Generated
Vendored
+4
-4
@@ -68,7 +68,7 @@ func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func getAuthManager(manager string, m map[string]map[string]interface{}) (auth.Manager, *plugin.RevaPlugin, error) {
|
||||
func getAuthManager(manager string, m map[string]map[string]any, logger *zerolog.Logger) (auth.Manager, *plugin.RevaPlugin, error) {
|
||||
if manager == "" {
|
||||
return nil, nil, errtypes.InternalError("authsvc: driver not configured for auth manager")
|
||||
}
|
||||
@@ -86,7 +86,7 @@ func getAuthManager(manager string, m map[string]map[string]interface{}) (auth.M
|
||||
return authManager, p, nil
|
||||
} else if _, ok := err.(errtypes.NotFound); ok {
|
||||
if f, ok := registry.NewFuncs[manager]; ok {
|
||||
authmgr, err := f(m[manager])
|
||||
authmgr, err := f(m[manager], logger)
|
||||
return authmgr, nil, err
|
||||
}
|
||||
} else {
|
||||
@@ -96,13 +96,13 @@ func getAuthManager(manager string, m map[string]map[string]interface{}) (auth.M
|
||||
}
|
||||
|
||||
// New returns a new AuthProviderServiceServer.
|
||||
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
|
||||
func New(m map[string]interface{}, ss *grpc.Server, logger *zerolog.Logger) (rgrpc.Service, error) {
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authManager, plug, err := getAuthManager(c.AuthManager, c.AuthManagers)
|
||||
authManager, plug, err := getAuthManager(c.AuthManager, c.AuthManagers, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Generated
Vendored
+17
-3
@@ -234,11 +234,25 @@ func (s *svc) CreateStorageSpace(ctx context.Context, req *provider.CreateStorag
|
||||
// here is happeing so that that grant is also visible when listing permission (shares) on the spaceroot.
|
||||
if req.GetType() != "personal" && createRes.GetStatus().GetCode() == rpc.Code_CODE_OK {
|
||||
rollbackFn := func() {
|
||||
dsRes, dsErr := s.DeleteStorageSpace(ctx, &provider.DeleteStorageSpaceRequest{
|
||||
// Deleting a space needs to DeleteRequests, one to disable the Space ...
|
||||
dr := &provider.DeleteStorageSpaceRequest{
|
||||
Id: createRes.GetStorageSpace().GetId(),
|
||||
})
|
||||
}
|
||||
dsRes, dsErr := s.DeleteStorageSpace(ctx, dr)
|
||||
if dsErr != nil || dsRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
log.Error().Err(dsErr).Interface("status", dsRes.GetStatus()).Interface("space_id", createRes.GetStorageSpace().GetId()).Msg("failed to delete space during rollback")
|
||||
log.Error().Err(dsErr).Interface("status", dsRes.GetStatus()).Interface("space_id", createRes.GetStorageSpace().GetId()).Msg("failed to disable space during rollback")
|
||||
// if disabling fails we won't be able to purge it either so give up here
|
||||
return
|
||||
}
|
||||
// ... and other one to finally purge it
|
||||
dr.Opaque = &typesv1beta1.Opaque{
|
||||
Map: map[string]*typesv1beta1.OpaqueEntry{
|
||||
"purge": {},
|
||||
},
|
||||
}
|
||||
dsRes, dsErr = s.DeleteStorageSpace(ctx, dr)
|
||||
if dsErr != nil || dsRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
|
||||
log.Error().Err(dsErr).Interface("status", dsRes.GetStatus()).Interface("space_id", createRes.GetStorageSpace().GetId()).Msg("failed to purge space during rollback")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Generated
Vendored
+4
-4
@@ -60,22 +60,22 @@ func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func getDriver(c *config) (group.Manager, error) {
|
||||
func getDriver(c *config, logger *zerolog.Logger) (group.Manager, error) {
|
||||
if f, ok := registry.NewFuncs[c.Driver]; ok {
|
||||
return f(c.Drivers[c.Driver])
|
||||
return f(c.Drivers[c.Driver], logger)
|
||||
}
|
||||
|
||||
return nil, errtypes.NotFound(fmt.Sprintf("driver %s not found for group manager", c.Driver))
|
||||
}
|
||||
|
||||
// New returns a new GroupProviderServiceServer.
|
||||
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
|
||||
func New(m map[string]any, ss *grpc.Server, logger *zerolog.Logger) (rgrpc.Service, error) {
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupManager, err := getDriver(c)
|
||||
groupManager, err := getDriver(c, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Generated
Vendored
+7
-7
@@ -84,7 +84,7 @@ func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func getDriver(c *config) (user.Manager, *plugin.RevaPlugin, error) {
|
||||
func getDriver(c *config, logger *zerolog.Logger) (user.Manager, *plugin.RevaPlugin, error) {
|
||||
p, err := plugin.Load("userprovider", c.Driver)
|
||||
if err == nil {
|
||||
manager, ok := p.Plugin.(user.Manager)
|
||||
@@ -100,7 +100,7 @@ func getDriver(c *config) (user.Manager, *plugin.RevaPlugin, error) {
|
||||
} else if _, ok := err.(errtypes.NotFound); ok {
|
||||
// plugin not found, fetch the driver from the in-memory registry
|
||||
if f, ok := userRegistry.NewFuncs[c.Driver]; ok {
|
||||
mgr, err := f(c.Drivers[c.Driver])
|
||||
mgr, err := f(c.Drivers[c.Driver], logger)
|
||||
return mgr, nil, err
|
||||
}
|
||||
} else {
|
||||
@@ -109,25 +109,25 @@ func getDriver(c *config) (user.Manager, *plugin.RevaPlugin, error) {
|
||||
return nil, nil, errtypes.NotFound(fmt.Sprintf("driver %s not found for user manager", c.Driver))
|
||||
}
|
||||
|
||||
func getTenantManager(c *config) (tenant.Manager, error) {
|
||||
func getTenantManager(c *config, logger *zerolog.Logger) (tenant.Manager, error) {
|
||||
if f, ok := tenantRegistry.NewFuncs[c.TenantDriver]; ok {
|
||||
mgr, err := f(c.TenantDrivers[c.TenantDriver])
|
||||
mgr, err := f(c.TenantDrivers[c.TenantDriver], logger)
|
||||
return mgr, err
|
||||
}
|
||||
return nil, errtypes.NotFound(fmt.Sprintf("driver %s not found for tenant manager", c.TenantDriver))
|
||||
}
|
||||
|
||||
// New returns a new UserProviderServiceServer.
|
||||
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
|
||||
func New(m map[string]interface{}, ss *grpc.Server, logger *zerolog.Logger) (rgrpc.Service, error) {
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userManager, plug, err := getDriver(c)
|
||||
userManager, plug, err := getDriver(c, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenantManager, err := getTenantManager(c)
|
||||
tenantManager, err := getTenantManager(c, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+2
-1
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -42,7 +43,7 @@ type manager struct {
|
||||
}
|
||||
|
||||
// New returns a new auth Manager.
|
||||
func New(m map[string]interface{}) (auth.Manager, error) {
|
||||
func New(m map[string]any, logger *zerolog.Logger) (auth.Manager, error) {
|
||||
mgr := &manager{}
|
||||
err := mgr.Configure(m)
|
||||
if err != nil {
|
||||
|
||||
+2
-1
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -44,7 +45,7 @@ type Credentials struct {
|
||||
}
|
||||
|
||||
// New returns a new auth Manager.
|
||||
func New(m map[string]interface{}) (auth.Manager, error) {
|
||||
func New(m map[string]any, _ *zerolog.Logger) (auth.Manager, error) {
|
||||
// m not used
|
||||
mgr := &manager{}
|
||||
err := mgr.Configure(m)
|
||||
|
||||
Generated
Vendored
+2
-1
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -36,7 +37,7 @@ func init() {
|
||||
type mgr struct{}
|
||||
|
||||
// New returns an auth manager implementation that allows to authenticate with any credentials.
|
||||
func New(c map[string]interface{}) (auth.Manager, error) {
|
||||
func New(c map[string]any, _ *zerolog.Logger) (auth.Manager, error) {
|
||||
return &mgr{}, nil
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -78,7 +79,7 @@ func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
}
|
||||
|
||||
// New returns a new auth Manager.
|
||||
func New(m map[string]interface{}) (auth.Manager, error) {
|
||||
func New(m map[string]any, _ *zerolog.Logger) (auth.Manager, error) {
|
||||
mgr := &manager{}
|
||||
err := mgr.Configure(m)
|
||||
if err != nil {
|
||||
|
||||
+3
-2
@@ -39,6 +39,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
ldapIdentity "github.com/opencloud-eu/reva/v2/pkg/utils/ldap"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -72,13 +73,13 @@ func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
}
|
||||
|
||||
// New returns an auth manager implementation that connects to a LDAP server to validate the user.
|
||||
func New(m map[string]interface{}) (auth.Manager, error) {
|
||||
func New(m map[string]any, logger *zerolog.Logger) (auth.Manager, error) {
|
||||
manager := &mgr{}
|
||||
err := manager.Configure(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manager.ldapClient, err = utils.GetLDAPClientWithReconnect(&manager.c.LDAPConn)
|
||||
manager.ldapClient, err = utils.GetLDAPClientWithReconnect(&manager.c.LDAPConn, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+2
-1
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// 'machine' is an authentication method used to impersonate users.
|
||||
@@ -60,7 +61,7 @@ func (m *manager) Configure(conf map[string]interface{}) error {
|
||||
}
|
||||
|
||||
// New creates a new manager for the 'machine' authentication
|
||||
func New(conf map[string]interface{}) (auth.Manager, error) {
|
||||
func New(conf map[string]any, _ *zerolog.Logger) (auth.Manager, error) {
|
||||
m := &manager{}
|
||||
err := m.Configure(conf)
|
||||
if err != nil {
|
||||
|
||||
+2
-1
@@ -39,6 +39,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -59,7 +60,7 @@ func (c *config) ApplyDefaults() {
|
||||
}
|
||||
|
||||
// New creates a new ocmshares authentication manager.
|
||||
func New(m map[string]interface{}) (auth.Manager, error) {
|
||||
func New(m map[string]any, _ *zerolog.Logger) (auth.Manager, error) {
|
||||
var mgr manager
|
||||
if err := mgr.Configure(m); err != nil {
|
||||
return nil, err
|
||||
|
||||
+2
-1
@@ -44,6 +44,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
@@ -102,7 +103,7 @@ func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
}
|
||||
|
||||
// New returns an auth manager implementation that verifies the oidc token and obtains the user claims.
|
||||
func New(m map[string]interface{}) (auth.Manager, error) {
|
||||
func New(m map[string]any, _ *zerolog.Logger) (auth.Manager, error) {
|
||||
manager := &mgr{}
|
||||
err := manager.Configure(m)
|
||||
if err != nil {
|
||||
|
||||
Generated
Vendored
+2
-1
@@ -36,6 +36,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -60,7 +61,7 @@ func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
}
|
||||
|
||||
// New returns a new auth Manager.
|
||||
func New(m map[string]interface{}) (auth.Manager, error) {
|
||||
func New(m map[string]any, _ *zerolog.Logger) (auth.Manager, error) {
|
||||
mgr := &manager{}
|
||||
err := mgr.Configure(m)
|
||||
if err != nil {
|
||||
|
||||
+2
-1
@@ -20,11 +20,12 @@ package registry
|
||||
|
||||
import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// NewFunc is the function that auth implementations
|
||||
// should register to at init time.
|
||||
type NewFunc func(map[string]interface{}) (auth.Manager, error)
|
||||
type NewFunc func(map[string]any, *zerolog.Logger) (auth.Manager, error)
|
||||
|
||||
// NewFuncs is a map containing all the registered auth managers.
|
||||
var NewFuncs = map[string]NewFunc{}
|
||||
|
||||
Generated
Vendored
+2
-1
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/auth"
|
||||
@@ -46,7 +47,7 @@ func (m *manager) Configure(config map[string]interface{}) error {
|
||||
}
|
||||
|
||||
// New creates a new manager for the 'service' authentication
|
||||
func New(conf map[string]interface{}) (auth.Manager, error) {
|
||||
func New(conf map[string]any, _ *zerolog.Logger) (auth.Manager, error) {
|
||||
m := &manager{}
|
||||
err := m.Configure(conf)
|
||||
if err != nil {
|
||||
|
||||
-22
@@ -30,9 +30,7 @@ import (
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/mime"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/publicshare"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
|
||||
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
@@ -42,8 +40,6 @@ import (
|
||||
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
publicsharemgr "github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/registry"
|
||||
usermgr "github.com/opencloud-eu/reva/v2/pkg/user/manager/registry"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -413,24 +409,6 @@ func LocalGroupIDToString(groupID *grouppb.GroupId) string {
|
||||
return groupID.OpaqueId
|
||||
}
|
||||
|
||||
// GetUserManager returns a connection to a user share manager
|
||||
func GetUserManager(manager string, m map[string]map[string]interface{}) (user.Manager, error) {
|
||||
if f, ok := usermgr.NewFuncs[manager]; ok {
|
||||
return f(m[manager])
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("driver %s not found for user manager", manager)
|
||||
}
|
||||
|
||||
// GetPublicShareManager returns a connection to a public share manager
|
||||
func GetPublicShareManager(manager string, m map[string]map[string]interface{}) (publicshare.Manager, error) {
|
||||
if f, ok := publicsharemgr.NewFuncs[manager]; ok {
|
||||
return f(m[manager])
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("driver %s not found for public shares manager", manager)
|
||||
}
|
||||
|
||||
// timestamp is assumed to be UTC ... just human readable ...
|
||||
// FIXME and ambiguous / error prone because there is no time zone ...
|
||||
func timestampToExpiration(t *types.Timestamp) string {
|
||||
|
||||
+2
-1
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/group"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/group/manager/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@@ -65,7 +66,7 @@ func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
}
|
||||
|
||||
// New returns a group manager implementation that reads a json file to provide group metadata.
|
||||
func New(m map[string]interface{}) (group.Manager, error) {
|
||||
func New(m map[string]any, _ *zerolog.Logger) (group.Manager, error) {
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+3
-2
@@ -36,6 +36,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
ldapIdentity "github.com/opencloud-eu/reva/v2/pkg/utils/ldap"
|
||||
"github.com/rs/zerolog"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
@@ -70,7 +71,7 @@ func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
}
|
||||
|
||||
// New returns a group manager implementation that connects to a LDAP server to provide group metadata.
|
||||
func New(m map[string]interface{}) (group.Manager, error) {
|
||||
func New(m map[string]any, logger *zerolog.Logger) (group.Manager, error) {
|
||||
if sharedconf.MultiTenantEnabled() {
|
||||
return nil, errtypes.NotSupported("ldap group manager does not support multi-tenancy")
|
||||
}
|
||||
@@ -81,7 +82,7 @@ func New(m map[string]interface{}) (group.Manager, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mgr.ldapClient, err = utils.GetLDAPClientWithReconnect(&mgr.c.LDAPConn)
|
||||
mgr.ldapClient, err = utils.GetLDAPClientWithReconnect(&mgr.c.LDAPConn, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+2
-1
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/group"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/group/manager/registry"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -37,7 +38,7 @@ type manager struct {
|
||||
}
|
||||
|
||||
// New returns a group manager implementation that return NOT FOUND or empty result set for every call
|
||||
func New(m map[string]interface{}) (group.Manager, error) {
|
||||
func New(m map[string]any, _ *zerolog.Logger) (group.Manager, error) {
|
||||
return &manager{}, nil
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -18,11 +18,14 @@
|
||||
|
||||
package registry
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/group"
|
||||
import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/group"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// NewFunc is the function that group managers
|
||||
// should register at init time.
|
||||
type NewFunc func(map[string]interface{}) (group.Manager, error)
|
||||
type NewFunc func(map[string]any, *zerolog.Logger) (group.Manager, error)
|
||||
|
||||
// NewFuncs is a map containing all the registered group managers.
|
||||
var NewFuncs = map[string]NewFunc{}
|
||||
|
||||
+25
-18
@@ -328,21 +328,13 @@ func (m *Manager) waitForInit(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// waitForMigrations blocks until both storage initialization and all data
|
||||
// migrations have completed on this instance, or until ctx is cancelled.
|
||||
// It is a strict superset of waitForInit and should be used by write operations
|
||||
// to ensure no writes race with an in-progress migration.
|
||||
func (m *Manager) waitForMigrations(ctx context.Context) error {
|
||||
select {
|
||||
case <-m.ready:
|
||||
case <-ctx.Done():
|
||||
return errors.Wrap(ctx.Err(), "share manager not yet initialized")
|
||||
}
|
||||
// migrationsRunning returns true if migrations have not yet completed on this instance.
|
||||
func (m *Manager) migrationsRunning() bool {
|
||||
select {
|
||||
case <-m.migrationsDone:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return errors.Wrap(ctx.Err(), "share manager migrations not yet complete")
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,11 +388,14 @@ func (m *Manager) ProcessEvents(ch <-chan events.Event) {
|
||||
func (m *Manager) Share(ctx context.Context, md *provider.ResourceInfo, g *collaboration.ShareGrant) (*collaboration.Share, error) {
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Share")
|
||||
defer span.End()
|
||||
if err := m.waitForMigrations(ctx); err != nil {
|
||||
if err := m.waitForInit(ctx); err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
if m.migrationsRunning() {
|
||||
return nil, errtypes.Aborted("share manager is currently migrating, please retry")
|
||||
}
|
||||
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
ts := utils.TSNow()
|
||||
@@ -603,9 +598,12 @@ func (m *Manager) Unshare(ctx context.Context, ref *collaboration.ShareReference
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "Unshare")
|
||||
defer span.End()
|
||||
|
||||
if err := m.waitForMigrations(ctx); err != nil {
|
||||
if err := m.waitForInit(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if m.migrationsRunning() {
|
||||
return errtypes.Aborted("share manager is currently migrating, please retry")
|
||||
}
|
||||
|
||||
s, err := m.get(ctx, ref)
|
||||
if err != nil {
|
||||
@@ -620,9 +618,12 @@ func (m *Manager) UpdateShare(ctx context.Context, ref *collaboration.ShareRefer
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "UpdateShare")
|
||||
defer span.End()
|
||||
|
||||
if err := m.waitForMigrations(ctx); err != nil {
|
||||
if err := m.waitForInit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if m.migrationsRunning() {
|
||||
return nil, errtypes.Aborted("share manager is currently migrating, please retry")
|
||||
}
|
||||
|
||||
var toUpdate *collaboration.Share
|
||||
|
||||
@@ -1165,9 +1166,12 @@ func (m *Manager) UpdateReceivedShare(ctx context.Context, receivedShare *collab
|
||||
ctx, span := appctx.GetTracerProvider(ctx).Tracer(tracerName).Start(ctx, "UpdateReceivedShare")
|
||||
defer span.End()
|
||||
|
||||
if err := m.waitForMigrations(ctx); err != nil {
|
||||
if err := m.waitForInit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if m.migrationsRunning() {
|
||||
return nil, errtypes.Aborted("share manager is currently migrating, please retry")
|
||||
}
|
||||
|
||||
rs, err := m.getReceived(ctx, &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{Id: receivedShare.Share.Id}})
|
||||
if err != nil {
|
||||
@@ -1330,7 +1334,10 @@ func (m *Manager) removeShare(ctx context.Context, s *collaboration.Share, skipS
|
||||
func (m *Manager) CleanupStaleShares(ctx context.Context) {
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
if err := m.waitForMigrations(ctx); err != nil {
|
||||
if err := m.waitForInit(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
if m.migrationsRunning() {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Generated
Vendored
+7
-7
@@ -144,7 +144,7 @@ func New(logger zerolog.Logger,
|
||||
// indefinitely until ctx is cancelled. A lock whose timestamp is older than
|
||||
// lockTTL is considered stale and will be taken over.
|
||||
func (m *Migrations) acquireLock(ctx context.Context) (string, error) {
|
||||
m.logger.Debug().Str("instance", m.instanceID).Msg("acquiring migration lock")
|
||||
m.logger.Info().Str("instance", m.instanceID).Msg("acquiring migration lock")
|
||||
for {
|
||||
// Fast path: create the lock file only if it does not exist yet.
|
||||
data, err := json.Marshal(lockData{Timestamp: time.Now(), InstanceID: m.instanceID})
|
||||
@@ -157,7 +157,7 @@ func (m *Migrations) acquireLock(ctx context.Context) (string, error) {
|
||||
IfNoneMatch: []string{"*"},
|
||||
})
|
||||
if err == nil {
|
||||
m.logger.Debug().Str("instance", m.instanceID).Msg("migration lock acquired")
|
||||
m.logger.Info().Str("instance", m.instanceID).Msg("migration lock acquired")
|
||||
return res.Etag, nil
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ func (m *Migrations) acquireLock(ctx context.Context) (string, error) {
|
||||
if _, ok := err.(errtypes.IsNotFound); ok {
|
||||
// Lock was released between our upload attempt and the download;
|
||||
// retry acquiring it immediately.
|
||||
m.logger.Debug().Str("instance", m.instanceID).Msg("migration lock vanished during read; retrying")
|
||||
m.logger.Info().Str("instance", m.instanceID).Msg("migration lock vanished during read; retrying")
|
||||
continue
|
||||
}
|
||||
return "", err
|
||||
@@ -192,7 +192,7 @@ func (m *Migrations) acquireLock(ctx context.Context) (string, error) {
|
||||
}
|
||||
|
||||
if stale {
|
||||
m.logger.Debug().
|
||||
m.logger.Info().
|
||||
Str("instance", m.instanceID).
|
||||
Str("held_by", existing.InstanceID).
|
||||
Time("lock_timestamp", existing.Timestamp).
|
||||
@@ -209,15 +209,15 @@ func (m *Migrations) acquireLock(ctx context.Context) (string, error) {
|
||||
IfMatchEtag: dl.Etag,
|
||||
})
|
||||
if err == nil {
|
||||
m.logger.Debug().Str("instance", m.instanceID).Msg("migration lock acquired via stale takeover")
|
||||
m.logger.Info().Str("instance", m.instanceID).Msg("migration lock acquired via stale takeover")
|
||||
return res.Etag, nil
|
||||
}
|
||||
// Another instance took the stale lock before us; loop and retry.
|
||||
m.logger.Debug().Str("instance", m.instanceID).Err(err).Msg("stale lock takeover lost race; retrying")
|
||||
m.logger.Info().Str("instance", m.instanceID).Err(err).Msg("stale lock takeover lost race; retrying")
|
||||
continue
|
||||
}
|
||||
|
||||
m.logger.Debug().
|
||||
m.logger.Info().
|
||||
Str("instance", m.instanceID).
|
||||
Str("held_by", existing.InstanceID).
|
||||
Time("lock_timestamp", existing.Timestamp).
|
||||
|
||||
+30
-2
@@ -8,21 +8,49 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func NewNatsKeyValue(c Config) (jetstream.KeyValue, error) {
|
||||
func NewNatsKeyValue(c Config, log *zerolog.Logger) (jetstream.KeyValue, error) {
|
||||
nodes := strings.Join(c.Nodes, ",")
|
||||
if nodes == "" {
|
||||
return nil, errors.New("at least one node is required")
|
||||
}
|
||||
opts := []nats.Option{}
|
||||
opts := []nats.Option{
|
||||
nats.DisconnectErrHandler(func(nc *nats.Conn, err error) {
|
||||
log.Error().Err(err).Msg("Disconnected from NATS. Trying to reconnect...")
|
||||
}),
|
||||
nats.ReconnectHandler(func(nc *nats.Conn) {
|
||||
log.Error().Msgf("Successfully reconnected to NATS at: %s", nc.ConnectedUrl())
|
||||
}),
|
||||
nats.ClosedHandler(func(nc *nats.Conn) {
|
||||
// Alright, it's time to give up. Send ourselves a SIGTERM to trigger a graceful
|
||||
// shutdown of the service.
|
||||
log.Error().Msg("NATS connection closed permanently. Shutting down...")
|
||||
|
||||
pid := os.Getpid()
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
fmt.Printf("Error finding process: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = process.Signal(syscall.SIGTERM)
|
||||
if err != nil {
|
||||
fmt.Printf("Error sending signal: %v\n", err)
|
||||
return
|
||||
}
|
||||
}),
|
||||
}
|
||||
if c.AuthUsername != "" || c.AuthPassword != "" {
|
||||
opts = append(opts, nats.UserInfo(c.AuthUsername, c.AuthPassword))
|
||||
}
|
||||
|
||||
+75
-13
@@ -21,9 +21,12 @@ package idcache
|
||||
import (
|
||||
"context"
|
||||
"encoding/base32"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cenkalti/backoff"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
@@ -43,12 +46,16 @@ func NewStoreIDCache(kv jetstream.KeyValue) (*IDCache, error) {
|
||||
// Delete removes an entry from the cache
|
||||
func (c *IDCache) Delete(ctx context.Context, spaceID, nodeID string) error {
|
||||
var rerr error
|
||||
v, err := c.kv.Get(ctx, cacheKey(spaceID, nodeID))
|
||||
v, err := retry(ctx, func() (jetstream.KeyValueEntry, error) {
|
||||
return c.kv.Get(ctx, cacheKey(spaceID, nodeID))
|
||||
})
|
||||
if err == nil {
|
||||
rerr = c.kv.Purge(ctx, reverseCacheKey(string(v.Value())))
|
||||
rerr = retryErr(ctx, func() error {
|
||||
return c.kv.Purge(ctx, reverseCacheKey(string(v.Value())))
|
||||
})
|
||||
}
|
||||
|
||||
err = c.kv.Purge(ctx, cacheKey(spaceID, nodeID))
|
||||
err = retryErr(ctx, func() error { return c.kv.Purge(ctx, cacheKey(spaceID, nodeID)) })
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -66,18 +73,22 @@ func (c *IDCache) DeleteByPath(ctx context.Context, path string) error {
|
||||
}
|
||||
appctx.GetLogger(ctx).Error().Err(err).Str("record", path).Msg("could not get spaceID and nodeID from cache")
|
||||
} else {
|
||||
err := c.kv.Purge(ctx, baseKey)
|
||||
err := retryErr(ctx, func() error {
|
||||
return c.kv.Purge(ctx, baseKey)
|
||||
})
|
||||
if err != nil && err != jetstream.ErrKeyNotFound {
|
||||
appctx.GetLogger(ctx).Error().Err(err).Str("record", baseKey).Str("spaceID", spaceID).Str("nodeID", nodeID).Msg("could not purge from cache")
|
||||
}
|
||||
|
||||
err = c.kv.Purge(ctx, cacheKey(spaceID, nodeID))
|
||||
err = retryErr(ctx, func() error {
|
||||
return c.kv.Purge(ctx, cacheKey(spaceID, nodeID))
|
||||
})
|
||||
if err != nil && err != jetstream.ErrKeyNotFound {
|
||||
appctx.GetLogger(ctx).Error().Err(err).Str("record", cacheKey(spaceID, nodeID)).Str("spaceID", spaceID).Str("nodeID", nodeID).Msg("could not purge from cache")
|
||||
}
|
||||
}
|
||||
|
||||
watcher, err := c.kv.Watch(ctx, baseKey+".>")
|
||||
watcher, err := retry(ctx, func() (jetstream.KeyWatcher, error) { return c.kv.Watch(ctx, baseKey+".>") })
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -94,12 +105,16 @@ func (c *IDCache) DeleteByPath(ctx context.Context, path string) error {
|
||||
continue
|
||||
}
|
||||
|
||||
err = c.kv.Purge(ctx, key)
|
||||
err = retryErr(ctx, func() error {
|
||||
return c.kv.Purge(ctx, key)
|
||||
})
|
||||
if err != nil && err != jetstream.ErrKeyNotFound {
|
||||
appctx.GetLogger(ctx).Error().Err(err).Str("record", key).Str("spaceID", spaceID).Str("nodeID", nodeID).Msg("could not purge from cache")
|
||||
}
|
||||
|
||||
err = c.kv.Purge(ctx, cacheKey(spaceID, nodeID))
|
||||
err = retryErr(ctx, func() error {
|
||||
return c.kv.Purge(ctx, cacheKey(spaceID, nodeID))
|
||||
})
|
||||
if err != nil && err != jetstream.ErrKeyNotFound {
|
||||
appctx.GetLogger(ctx).Error().Err(err).Str("record", cacheKey(spaceID, nodeID)).Str("spaceID", spaceID).Str("nodeID", nodeID).Msg("could not purge from cache")
|
||||
}
|
||||
@@ -109,23 +124,31 @@ func (c *IDCache) DeleteByPath(ctx context.Context, path string) error {
|
||||
|
||||
// DeletePath removes only the path entry from the cache
|
||||
func (c *IDCache) DeletePath(ctx context.Context, path string) error {
|
||||
return c.kv.Purge(ctx, reverseCacheKey(path))
|
||||
return retryErr(ctx, func() error {
|
||||
return c.kv.Purge(ctx, reverseCacheKey(path))
|
||||
})
|
||||
}
|
||||
|
||||
// Set adds a new entry to the cache
|
||||
func (c *IDCache) Set(ctx context.Context, spaceID, nodeID, val string) error {
|
||||
_, err := c.kv.Put(ctx, cacheKey(spaceID, nodeID), []byte(val))
|
||||
_, err := retry(ctx, func() (uint64, error) {
|
||||
return c.kv.Put(ctx, cacheKey(spaceID, nodeID), []byte(val))
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.kv.Put(ctx, reverseCacheKey(val), []byte(cacheKey(spaceID, nodeID)))
|
||||
_, err = retry(ctx, func() (uint64, error) {
|
||||
return c.kv.Put(ctx, reverseCacheKey(val), []byte(cacheKey(spaceID, nodeID)))
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Get returns the value for a given key
|
||||
func (c *IDCache) Get(ctx context.Context, spaceID, nodeID string) (string, error) {
|
||||
record, err := c.kv.Get(ctx, cacheKey(spaceID, nodeID))
|
||||
record, err := retry(ctx, func() (jetstream.KeyValueEntry, error) {
|
||||
return c.kv.Get(ctx, cacheKey(spaceID, nodeID))
|
||||
})
|
||||
if err != nil {
|
||||
if err == jetstream.ErrKeyNotFound {
|
||||
return "", errtypes.NotFound("record not found in cache")
|
||||
@@ -136,7 +159,9 @@ func (c *IDCache) Get(ctx context.Context, spaceID, nodeID string) (string, erro
|
||||
}
|
||||
|
||||
func (c *IDCache) getByReverseCacheKey(ctx context.Context, reverseKey string) (string, string, error) {
|
||||
record, err := c.kv.Get(ctx, reverseKey)
|
||||
record, err := retry(ctx, func() (jetstream.KeyValueEntry, error) {
|
||||
return c.kv.Get(ctx, reverseKey)
|
||||
})
|
||||
if err != nil {
|
||||
if err == jetstream.ErrKeyNotFound {
|
||||
return "", "", errtypes.NotFound("record not found in cache")
|
||||
@@ -172,3 +197,40 @@ func reverseCacheKey(path string) string {
|
||||
|
||||
return strings.Join(encoded, ".")
|
||||
}
|
||||
|
||||
func retry[T any](ctx context.Context, f func() (T, error)) (T, error) {
|
||||
var v T
|
||||
var err error
|
||||
b := backoff.NewExponentialBackOff()
|
||||
for range 5 {
|
||||
v, err = f()
|
||||
if err == nil || err == jetstream.ErrKeyNotFound {
|
||||
return v, err
|
||||
}
|
||||
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
time.Sleep(b.NextBackOff())
|
||||
}
|
||||
|
||||
appctx.GetLogger(ctx).Error().Err(err).Msg("error in jetstream kv operation, retrying")
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
func retryErr(ctx context.Context, f func() error) error {
|
||||
var err error
|
||||
b := backoff.NewExponentialBackOff()
|
||||
for range 5 {
|
||||
err = f()
|
||||
if err == nil || err == jetstream.ErrKeyNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
time.Sleep(b.NextBackOff())
|
||||
}
|
||||
|
||||
appctx.GetLogger(ctx).Error().Err(err).Msg("error in jetstream kv operation, retrying")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
-8
@@ -122,14 +122,6 @@ func (lu *Lookup) IDsForPath(ctx context.Context, path string) (string, string,
|
||||
// IDsForPath returns the space and opaque id for the given path
|
||||
spaceID, nodeID, err := lu.IDCache.GetByPath(ctx, path)
|
||||
if err != nil {
|
||||
if _, ok := err.(errtypes.NotFound); !ok {
|
||||
lu.log.Error().Err(err).Str("path", path).Msg("error looking up path in cache")
|
||||
}
|
||||
// fallback to disk
|
||||
sID, nID, _, _, mErr := lu.metadataBackend.IdentifyPath(ctx, path)
|
||||
if mErr == nil && nID != "" {
|
||||
return sID, nID, nil
|
||||
}
|
||||
return "", "", err
|
||||
}
|
||||
return spaceID, nodeID, nil
|
||||
|
||||
+2
-2
@@ -73,7 +73,7 @@ func NewDefault(m map[string]interface{}, stream events.Stream, log *zerolog.Log
|
||||
|
||||
o.IDCache.Database += "_v2" // Use a versioned bucket name to avoid conflicts with previous implementations
|
||||
o.IDCache.TTL = 0 // Disable TTL for the ID cache, as the posix driver relies on it for caching file IDs and we don't want them to expire
|
||||
kv, err := cache.NewNatsKeyValue(o.IDCache)
|
||||
kv, err := cache.NewNatsKeyValue(o.IDCache, log)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create nats key value store")
|
||||
}
|
||||
@@ -84,7 +84,7 @@ func NewDefault(m map[string]interface{}, stream events.Stream, log *zerolog.Log
|
||||
|
||||
o.IDCache.Database += "_history" // Use a versioned bucket name to avoid conflicts with previous implementations
|
||||
o.IDCache.TTL = 24 * 60 * time.Minute
|
||||
historyKv, err := cache.NewNatsKeyValue(o.IDCache)
|
||||
historyKv, err := cache.NewNatsKeyValue(o.IDCache, log)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create nats key value store")
|
||||
}
|
||||
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
import re
|
||||
with open("/home/andre/src/opencloud/reva/pkg/storage/fs/posix/tree/assimilation_test.go", "r") as f:
|
||||
text = f.read()
|
||||
|
||||
new_content = """
|
||||
Describe("WarmupIDCache", func() {
|
||||
var (
|
||||
tree *Tree
|
||||
tmpDir string
|
||||
logger zerolog.Logger
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
tmpDir, err = os.MkdirTemp("", "warmupidcache-*")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
logger = zerolog.Nop()
|
||||
|
||||
tree = &Tree{
|
||||
log: &logger,
|
||||
options: &options.Options{
|
||||
Options: decomposedoptions.Options{
|
||||
Root: tmpDir,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
os.RemoveAll(tmpDir)
|
||||
})
|
||||
|
||||
It("returns nil for an empty directory", func() {
|
||||
err := tree.WarmupIDCache(tmpDir, false, false)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
"""
|
||||
|
||||
text = re.sub(r'Describe\("WarmupIDCache", func\(\).*$', new_content, text, flags=re.DOTALL)
|
||||
text += "\n})\n"
|
||||
with open("/home/andre/src/opencloud/reva/pkg/storage/fs/posix/tree/assimilation_test.go", "w") as f:
|
||||
f.write(text)
|
||||
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import re
|
||||
|
||||
with open("assimilation_test.go", "r") as f:
|
||||
text = f.read()
|
||||
|
||||
# remove CreateTestStorageSpace which throws error
|
||||
text = text.replace('_, err = env.CreateTestStorageSpace("personal", nil)\n\t\tExpect(err).ToNot(HaveOccurred())', '')
|
||||
|
||||
with open("assimilation_test.go", "w") as f:
|
||||
f.write(text)
|
||||
Generated
Vendored
-128
@@ -1,128 +0,0 @@
|
||||
import sys
|
||||
|
||||
content = """
|
||||
Describe("WarmupIDCache", func() {
|
||||
var (
|
||||
tree *Tree
|
||||
tmpDir string
|
||||
logger zerolog.Logger
|
||||
ctx context.Context
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
var err error
|
||||
tmpDir, err = os.MkdirTemp("", "warmupidcache-*")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
logger = zerolog.Nop()
|
||||
|
||||
o := &options.Options{
|
||||
Options: decomposedoptions.Options{
|
||||
Root: tmpDir,
|
||||
},
|
||||
}
|
||||
|
||||
// We need a backend and caches
|
||||
c, _ := idcache.NewMemoryIDCache()
|
||||
historyCache, _ := idcache.NewMemoryIDCache()
|
||||
um := &usermapper.NullMapper{}
|
||||
|
||||
backend := metadata.NewMessagePackBackend(o.FileMetadataCache)
|
||||
lu, err := lookup.New(backend, um, o, &timemanager.Manager{}, c, historyCache)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
tree = &Tree{
|
||||
log: &logger,
|
||||
options: o,
|
||||
lookup: lu,
|
||||
}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
os.RemoveAll(tmpDir)
|
||||
})
|
||||
|
||||
It("returns nil for an empty directory", func() {
|
||||
err := tree.WarmupIDCache(tmpDir, false, false)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("picks up new files and directories", func() {
|
||||
subDir := filepath.Join(tmpDir, "sub")
|
||||
err := os.Mkdir(subDir, 0755)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
filePath := filepath.Join(subDir, "test.txt")
|
||||
err = os.WriteFile(filePath, []byte("hello world"), 0644)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Should not crash, tests basic traverse
|
||||
err = tree.WarmupIDCache(tmpDir, false, false)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("verifies tree sizes and recursion", func() {
|
||||
// Setup a small directory structure
|
||||
subDir := filepath.Join(tmpDir, "sub2")
|
||||
err := os.Mkdir(subDir, 0755)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
nestedDir := filepath.Join(subDir, "nested")
|
||||
err = os.Mkdir(nestedDir, 0755)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
filePath := filepath.Join(nestedDir, "test.txt")
|
||||
err = os.WriteFile(filePath, []byte("hello world"), 0644) // 11 bytes
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Run assimilation
|
||||
err = tree.WarmupIDCache(tmpDir, true, false)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// If assimilation runs, the files will have xattrs and tree sizes evaluated
|
||||
// wait, but without mocked idResolver for the Tree, it might fail? Let's check!
|
||||
})
|
||||
})
|
||||
"""
|
||||
|
||||
with open("assimilation_test.go", "r") as f:
|
||||
text = f.read()
|
||||
|
||||
import re
|
||||
|
||||
# Add imports
|
||||
imports = """
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/lookup"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/idcache"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/usermapper"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/timemanager"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/posix/options"
|
||||
decomposedoptions "github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/options"
|
||||
)
|
||||
"""
|
||||
|
||||
# Replace imports
|
||||
start_import = text.find('import (')
|
||||
end_import = text.find(')', start_import) + 1
|
||||
text = text[:start_import] + imports.strip() + text[end_import:]
|
||||
|
||||
start_idx = text.find('Describe("WarmupIDCache", func() {')
|
||||
end_idx = text.find('})\n\n})', start_idx) + 2
|
||||
|
||||
if start_idx != -1 and end_idx != -1:
|
||||
new_text = text[:start_idx] + content.strip() + text[end_idx:]
|
||||
with open("assimilation_test.go", "w") as f:
|
||||
f.write(new_text)
|
||||
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
with open("assimilation_test.go", "r") as f:
|
||||
text = f.read()
|
||||
|
||||
import re
|
||||
|
||||
# We will inject the xattr check.
|
||||
insert = """// verify that tree sizes are updated
|
||||
// Since we used assimilate=true, the treesize xattr on sub2 and nested should be 11.
|
||||
|
||||
b, err := env.Lookup.MetadataBackend().Get(env.Ctx, subDir, "user.oc.treesize")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(string(b)).To(Equal("11"))
|
||||
|
||||
b, err = env.Lookup.MetadataBackend().Get(env.Ctx, nestedDir, "user.oc.treesize")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(string(b)).To(Equal("11"))"""
|
||||
|
||||
# inject right after env.Tree.WarmupIDCache call
|
||||
text = text.replace("err = env.Tree.WarmupIDCache(tmpDir+\"/users/admin\", true, false)\n\t\tExpect(err).ToNot(HaveOccurred())", "err = env.Tree.WarmupIDCache(tmpDir+\"/users/admin\", true, false)\n\t\tExpect(err).ToNot(HaveOccurred())\n\n" + insert)
|
||||
|
||||
with open("assimilation_test.go", "w") as f:
|
||||
f.write(text)
|
||||
|
||||
+3
-2
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
ldapIdentity "github.com/opencloud-eu/reva/v2/pkg/utils/ldap"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -60,14 +61,14 @@ type manager struct {
|
||||
}
|
||||
|
||||
// New returns a new user manager.
|
||||
func New(m map[string]interface{}) (tenant.Manager, error) {
|
||||
func New(m map[string]any, logger *zerolog.Logger) (tenant.Manager, error) {
|
||||
mgr := &manager{}
|
||||
err := mgr.Configure(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mgr.ldap, err = utils.GetLDAPClientWithReconnect(&mgr.conf.LDAPConn)
|
||||
mgr.ldap, err = utils.GetLDAPClientWithReconnect(&mgr.conf.LDAPConn, logger)
|
||||
|
||||
return mgr, err
|
||||
}
|
||||
|
||||
+2
-1
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/tenant"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/tenant/manager/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -57,7 +58,7 @@ type manager struct {
|
||||
}
|
||||
|
||||
// New returns a new tenant manager.
|
||||
func New(m map[string]interface{}) (tenant.Manager, error) {
|
||||
func New(m map[string]any, _ *zerolog.Logger) (tenant.Manager, error) {
|
||||
mgr := &manager{}
|
||||
err := mgr.Configure(m)
|
||||
return mgr, err
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/tenant"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/tenant/manager/registry"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -36,7 +37,7 @@ type manager struct {
|
||||
}
|
||||
|
||||
// New returns a tenant manager implementation that return NOT FOUND or empty result set for every call
|
||||
func New(m map[string]interface{}) (tenant.Manager, error) {
|
||||
func New(m map[string]any, _ *zerolog.Logger) (tenant.Manager, error) {
|
||||
return &manager{}, nil
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -20,11 +20,12 @@ package registry
|
||||
|
||||
import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/tenant"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// NewFunc is the function that tenant managers
|
||||
// should register at init time.
|
||||
type NewFunc func(map[string]interface{}) (tenant.Manager, error)
|
||||
type NewFunc func(map[string]any, *zerolog.Logger) (tenant.Manager, error)
|
||||
|
||||
// NewFuncs is a map containing all the registered user managers.
|
||||
var NewFuncs = map[string]NewFunc{}
|
||||
|
||||
+2
-1
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user/manager/registry"
|
||||
"github.com/rs/zerolog"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@@ -40,7 +41,7 @@ type manager struct {
|
||||
}
|
||||
|
||||
// New returns a new user manager.
|
||||
func New(m map[string]interface{}) (user.Manager, error) {
|
||||
func New(m map[string]interface{}, _ *zerolog.Logger) (user.Manager, error) {
|
||||
mgr := &manager{}
|
||||
err := mgr.Configure(m)
|
||||
if err != nil {
|
||||
|
||||
+4
-4
@@ -25,14 +25,14 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user/manager/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -65,7 +65,7 @@ func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
}
|
||||
|
||||
// New returns a user manager implementation that reads a json file to provide user metadata.
|
||||
func New(m map[string]interface{}) (user.Manager, error) {
|
||||
func New(m map[string]interface{}, _ *zerolog.Logger) (user.Manager, error) {
|
||||
mgr := &manager{}
|
||||
err := mgr.Configure(m)
|
||||
if err != nil {
|
||||
|
||||
+7
-2
@@ -34,6 +34,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user/manager/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
ldapIdentity "github.com/opencloud-eu/reva/v2/pkg/utils/ldap"
|
||||
"github.com/rs/zerolog"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
@@ -68,14 +69,18 @@ func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
}
|
||||
|
||||
// New returns a user manager implementation that connects to a LDAP server to provide user metadata.
|
||||
func New(m map[string]interface{}) (user.Manager, error) {
|
||||
func New(m map[string]any, logger *zerolog.Logger) (user.Manager, error) {
|
||||
if logger == nil {
|
||||
nop := zerolog.Nop()
|
||||
logger = &nop
|
||||
}
|
||||
mgr := &manager{}
|
||||
err := mgr.Configure(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mgr.ldapClient, err = utils.GetLDAPClientWithReconnect(&mgr.c.LDAPConn)
|
||||
mgr.ldapClient, err = utils.GetLDAPClientWithReconnect(&mgr.c.LDAPConn, logger)
|
||||
return mgr, err
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user/manager/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -68,7 +69,7 @@ type manager struct {
|
||||
}
|
||||
|
||||
// New returns a new user manager.
|
||||
func New(m map[string]interface{}) (user.Manager, error) {
|
||||
func New(m map[string]any, _ *zerolog.Logger) (user.Manager, error) {
|
||||
mgr := &manager{}
|
||||
err := mgr.Configure(m)
|
||||
return mgr, err
|
||||
|
||||
+2
-1
@@ -20,11 +20,12 @@ package registry
|
||||
|
||||
import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// NewFunc is the function that user managers
|
||||
// should register at init time.
|
||||
type NewFunc func(map[string]interface{}) (user.Manager, error)
|
||||
type NewFunc func(map[string]any, *zerolog.Logger) (user.Manager, error)
|
||||
|
||||
// NewFuncs is a map containing all the registered user managers.
|
||||
var NewFuncs = map[string]NewFunc{}
|
||||
|
||||
+9
-3
@@ -26,9 +26,9 @@ import (
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/logger"
|
||||
ldapReconnect "github.com/opencloud-eu/reva/v2/pkg/utils/ldap"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// LDAPConn holds the basic parameter for setting up an
|
||||
@@ -44,10 +44,14 @@ type LDAPConn struct {
|
||||
// GetLDAPClientWithReconnect initializes a long-lived LDAP connection that
|
||||
// automatically reconnects on connection errors. It allows to set TLS options
|
||||
// e.g. to add trusted Certificates or disable Certificate verification
|
||||
func GetLDAPClientWithReconnect(c *LDAPConn) (ldap.Client, error) {
|
||||
func GetLDAPClientWithReconnect(c *LDAPConn, logger *zerolog.Logger) (ldap.Client, error) {
|
||||
var tlsConf *tls.Config
|
||||
if logger == nil {
|
||||
nop := zerolog.Nop()
|
||||
logger = &nop
|
||||
}
|
||||
if c.Insecure {
|
||||
logger.New().Warn().Msg("SSL Certificate verification is disabled. This is strongly discouraged for production environments.")
|
||||
logger.Warn().Msg("SSL Certificate verification is disabled. This is strongly discouraged for production environments.")
|
||||
tlsConf = &tls.Config{
|
||||
//nolint:gosec // We need the ability to run with "insecure" (dev/testing)
|
||||
InsecureSkipVerify: true,
|
||||
@@ -73,6 +77,8 @@ func GetLDAPClientWithReconnect(c *LDAPConn) (ldap.Client, error) {
|
||||
TLSConfig: tlsConf,
|
||||
},
|
||||
)
|
||||
|
||||
conn.SetLogger(logger)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -221,6 +221,7 @@ func (c *ConnWithReconnect) ldapConnect(config Config) (*ldap.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
c.logger.Debug().Msg("LDAP Connected")
|
||||
l.SetTimeout(30 * time.Second)
|
||||
if config.BindDN != "" {
|
||||
c.logger.Debug().Msgf("Binding as %s", config.BindDN)
|
||||
err = l.Bind(config.BindDN, config.BindPassword)
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
[tagpr]
|
||||
releaseBranch = v2
|
||||
versionFile = -
|
||||
vPrefix = true
|
||||
changelog = true
|
||||
release = true
|
||||
fixedMajorVersion = 2
|
||||
+23
-18
@@ -7,11 +7,13 @@
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fshamaton%2Fmsgpack?ref=badge_shield)
|
||||
|
||||
## 📣 Announcement: `time.Time` decoding defaults to **UTC** in v3
|
||||
|
||||
Starting with **v3.0.0**, when decoding MessagePack **Timestamp** into Go’s `time.Time`,
|
||||
the default `Location` will be **UTC** (previously `Local`). The instant is unchanged.
|
||||
To keep the old behavior, use `SetDecodedTimeAsLocal()`.
|
||||
|
||||
## Features
|
||||
|
||||
* Supported types : primitive / array / slice / struct / map / interface{} and time.Time
|
||||
* Renaming fields via `msgpack:"field_name"`
|
||||
* Omitting fields via `msgpack:"-"`
|
||||
@@ -22,11 +24,13 @@ To keep the old behavior, use `SetDecodedTimeAsLocal()`.
|
||||
## Installation
|
||||
|
||||
Current version is **msgpack/v2**.
|
||||
|
||||
```sh
|
||||
go get -u github.com/shamaton/msgpack/v2
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
@@ -36,36 +40,36 @@ import (
|
||||
)
|
||||
|
||||
type Struct struct {
|
||||
String string
|
||||
String string
|
||||
}
|
||||
|
||||
// simple
|
||||
func main() {
|
||||
v := Struct{String: "msgpack"}
|
||||
v := Struct{String: "msgpack"}
|
||||
|
||||
d, err := msgpack.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
r := Struct{}
|
||||
if err = msgpack.Unmarshal(d, &r); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
d, err := msgpack.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
r := Struct{}
|
||||
if err = msgpack.Unmarshal(d, &r); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// streaming
|
||||
func handle(w http.ResponseWriter, r *http.Request) {
|
||||
var body Struct
|
||||
if err := msgpack.UnmarshalRead(r, &body); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := msgpack.MarshalWrite(w, body); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var body Struct
|
||||
if err := msgpack.UnmarshalRead(r, &body); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := msgpack.MarshalWrite(w, body); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📣 Announcement: `time.Time` decoding defaults to **UTC** in v3
|
||||
## v3: `time.Time` decoding defaults to **UTC**
|
||||
|
||||
**TL;DR:** Starting with **v3.0.0**, when decoding MessagePack **Timestamp** into Go’s `time.Time`, the default `Location` will be **UTC** (previously `Local`). The **instant** is unchanged—only the display/location changes. This avoids host-dependent differences and aligns with common distributed systems practice.
|
||||
|
||||
@@ -111,6 +115,7 @@ msgpack.SetDecodedTimeAsUTC()
|
||||
```
|
||||
|
||||
## Benchmark
|
||||
|
||||
This result made from [shamaton/msgpack_bench](https://github.com/shamaton/msgpack_bench)
|
||||
|
||||

|
||||
|
||||
+1
-2
@@ -23,8 +23,7 @@ type Decoder interface {
|
||||
}
|
||||
|
||||
// DecoderCommon provides common utility methods for decoding data from bytes.
|
||||
type DecoderCommon struct {
|
||||
}
|
||||
type DecoderCommon struct{}
|
||||
|
||||
// ReadSize1 reads a single byte from the given index in the byte slice.
|
||||
// Returns the byte and the new index after reading.
|
||||
|
||||
+1
-2
@@ -26,8 +26,7 @@ type Encoder interface {
|
||||
// EncoderCommon provides utility methods for encoding various types of values into bytes.
|
||||
// It includes methods to encode integers and unsigned integers of different sizes,
|
||||
// as well as methods to write raw byte slices into a target byte slice.
|
||||
type EncoderCommon struct {
|
||||
}
|
||||
type EncoderCommon struct{}
|
||||
|
||||
// SetByte1Int64 encodes a single byte from the given int64 value into the byte slice at the specified offset.
|
||||
// Returns the new offset after writing the byte.
|
||||
|
||||
+24
-12
@@ -32,14 +32,16 @@ func CreateStreamWriter(w io.Writer, buf *common.Buffer) StreamWriter {
|
||||
|
||||
// WriteByte1Int64 writes a single byte representation of an int64 value.
|
||||
func (w *StreamWriter) WriteByte1Int64(value int64) error {
|
||||
return w.buf.Write(w.w,
|
||||
return w.buf.Write(
|
||||
w.w,
|
||||
byte(value),
|
||||
)
|
||||
}
|
||||
|
||||
// WriteByte2Int64 writes a two-byte representation of an int64 value.
|
||||
func (w *StreamWriter) WriteByte2Int64(value int64) error {
|
||||
return w.buf.Write(w.w,
|
||||
return w.buf.Write(
|
||||
w.w,
|
||||
byte(value>>8),
|
||||
byte(value),
|
||||
)
|
||||
@@ -47,7 +49,8 @@ func (w *StreamWriter) WriteByte2Int64(value int64) error {
|
||||
|
||||
// WriteByte4Int64 writes a four-byte representation of an int64 value.
|
||||
func (w *StreamWriter) WriteByte4Int64(value int64) error {
|
||||
return w.buf.Write(w.w,
|
||||
return w.buf.Write(
|
||||
w.w,
|
||||
byte(value>>24),
|
||||
byte(value>>16),
|
||||
byte(value>>8),
|
||||
@@ -57,7 +60,8 @@ func (w *StreamWriter) WriteByte4Int64(value int64) error {
|
||||
|
||||
// WriteByte8Int64 writes an eight-byte representation of an int64 value.
|
||||
func (w *StreamWriter) WriteByte8Int64(value int64) error {
|
||||
return w.buf.Write(w.w,
|
||||
return w.buf.Write(
|
||||
w.w,
|
||||
byte(value>>56),
|
||||
byte(value>>48),
|
||||
byte(value>>40),
|
||||
@@ -71,14 +75,16 @@ func (w *StreamWriter) WriteByte8Int64(value int64) error {
|
||||
|
||||
// WriteByte1Uint64 writes a single byte representation of a uint64 value.
|
||||
func (w *StreamWriter) WriteByte1Uint64(value uint64) error {
|
||||
return w.buf.Write(w.w,
|
||||
return w.buf.Write(
|
||||
w.w,
|
||||
byte(value),
|
||||
)
|
||||
}
|
||||
|
||||
// WriteByte2Uint64 writes a two-byte representation of a uint64 value.
|
||||
func (w *StreamWriter) WriteByte2Uint64(value uint64) error {
|
||||
return w.buf.Write(w.w,
|
||||
return w.buf.Write(
|
||||
w.w,
|
||||
byte(value>>8),
|
||||
byte(value),
|
||||
)
|
||||
@@ -86,7 +92,8 @@ func (w *StreamWriter) WriteByte2Uint64(value uint64) error {
|
||||
|
||||
// WriteByte4Uint64 writes a four-byte representation of a uint64 value.
|
||||
func (w *StreamWriter) WriteByte4Uint64(value uint64) error {
|
||||
return w.buf.Write(w.w,
|
||||
return w.buf.Write(
|
||||
w.w,
|
||||
byte(value>>24),
|
||||
byte(value>>16),
|
||||
byte(value>>8),
|
||||
@@ -96,7 +103,8 @@ func (w *StreamWriter) WriteByte4Uint64(value uint64) error {
|
||||
|
||||
// WriteByte8Uint64 writes an eight-byte representation of a uint64 value.
|
||||
func (w *StreamWriter) WriteByte8Uint64(value uint64) error {
|
||||
return w.buf.Write(w.w,
|
||||
return w.buf.Write(
|
||||
w.w,
|
||||
byte(value>>56),
|
||||
byte(value>>48),
|
||||
byte(value>>40),
|
||||
@@ -110,14 +118,16 @@ func (w *StreamWriter) WriteByte8Uint64(value uint64) error {
|
||||
|
||||
// WriteByte1Int writes a single byte representation of an int value.
|
||||
func (w *StreamWriter) WriteByte1Int(value int) error {
|
||||
return w.buf.Write(w.w,
|
||||
return w.buf.Write(
|
||||
w.w,
|
||||
byte(value),
|
||||
)
|
||||
}
|
||||
|
||||
// WriteByte2Int writes a two-byte representation of an int value.
|
||||
func (w *StreamWriter) WriteByte2Int(value int) error {
|
||||
return w.buf.Write(w.w,
|
||||
return w.buf.Write(
|
||||
w.w,
|
||||
byte(value>>8),
|
||||
byte(value),
|
||||
)
|
||||
@@ -125,7 +135,8 @@ func (w *StreamWriter) WriteByte2Int(value int) error {
|
||||
|
||||
// WriteByte4Int writes a four-byte representation of an int value.
|
||||
func (w *StreamWriter) WriteByte4Int(value int) error {
|
||||
return w.buf.Write(w.w,
|
||||
return w.buf.Write(
|
||||
w.w,
|
||||
byte(value>>24),
|
||||
byte(value>>16),
|
||||
byte(value>>8),
|
||||
@@ -135,7 +146,8 @@ func (w *StreamWriter) WriteByte4Int(value int) error {
|
||||
|
||||
// WriteByte4Uint32 writes a four-byte representation of a uint32 value.
|
||||
func (w *StreamWriter) WriteByte4Uint32(value uint32) error {
|
||||
return w.buf.Write(w.w,
|
||||
return w.buf.Write(
|
||||
w.w,
|
||||
byte(value>>24),
|
||||
byte(value>>16),
|
||||
byte(value>>8),
|
||||
|
||||
+1
-2
@@ -6,8 +6,7 @@ import (
|
||||
)
|
||||
|
||||
// Common is used encoding/decoding
|
||||
type Common struct {
|
||||
}
|
||||
type Common struct{}
|
||||
|
||||
// CheckField returns flag whether should encode/decode or not and field name
|
||||
func (c *Common) CheckField(field reflect.StructField) (public, omit bool, name string) {
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ type decoder struct {
|
||||
func Decode(data []byte, v interface{}, asArray bool) error {
|
||||
d := decoder{data: data, asArray: asArray}
|
||||
|
||||
if d.data == nil || len(d.data) < 1 {
|
||||
if len(d.data) < 1 {
|
||||
return def.ErrNoData
|
||||
}
|
||||
rv := reflect.ValueOf(v)
|
||||
|
||||
+58
-2
@@ -1,12 +1,17 @@
|
||||
package decoding
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/shamaton/msgpack/v2/def"
|
||||
"github.com/shamaton/msgpack/v2/ext"
|
||||
"github.com/shamaton/msgpack/v2/time"
|
||||
)
|
||||
|
||||
var extCoderMap = map[int8]ext.Decoder{time.Decoder.Code(): time.Decoder}
|
||||
var extCoders = []ext.Decoder{time.Decoder}
|
||||
var (
|
||||
extCoderMap = map[int8]ext.Decoder{time.Decoder.Code(): time.Decoder}
|
||||
extCoders = []ext.Decoder{time.Decoder}
|
||||
)
|
||||
|
||||
// AddExtDecoder adds decoders for extension types.
|
||||
func AddExtDecoder(f ext.Decoder) {
|
||||
@@ -45,6 +50,57 @@ func updateExtCoders() {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) extEndOffset(offset int) (bool, int, error) {
|
||||
code, offset, err := d.readSize1(offset)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
return d.extEndOffsetWithCode(code, offset)
|
||||
}
|
||||
|
||||
func (d *decoder) extEndOffsetWithCode(code byte, offset int) (bool, int, error) {
|
||||
switch code {
|
||||
case def.Fixext1:
|
||||
_, offset, err := d.readSizeN(offset, def.Byte1+def.Byte1)
|
||||
return true, offset, err
|
||||
case def.Fixext2:
|
||||
_, offset, err := d.readSizeN(offset, def.Byte1+def.Byte2)
|
||||
return true, offset, err
|
||||
case def.Fixext4:
|
||||
_, offset, err := d.readSizeN(offset, def.Byte1+def.Byte4)
|
||||
return true, offset, err
|
||||
case def.Fixext8:
|
||||
_, offset, err := d.readSizeN(offset, def.Byte1+def.Byte8)
|
||||
return true, offset, err
|
||||
case def.Fixext16:
|
||||
_, offset, err := d.readSizeN(offset, def.Byte1+def.Byte16)
|
||||
return true, offset, err
|
||||
case def.Ext8:
|
||||
size, offset, err := d.readSize1(offset)
|
||||
if err != nil {
|
||||
return true, 0, err
|
||||
}
|
||||
_, offset, err = d.readSizeN(offset, def.Byte1+int(size))
|
||||
return true, offset, err
|
||||
case def.Ext16:
|
||||
sizeBytes, offset, err := d.readSize2(offset)
|
||||
if err != nil {
|
||||
return true, 0, err
|
||||
}
|
||||
_, offset, err = d.readSizeN(offset, def.Byte1+int(binary.BigEndian.Uint16(sizeBytes)))
|
||||
return true, offset, err
|
||||
case def.Ext32:
|
||||
sizeBytes, offset, err := d.readSize4(offset)
|
||||
if err != nil {
|
||||
return true, 0, err
|
||||
}
|
||||
_, offset, err = d.readSizeN(offset, def.Byte1+int(binary.BigEndian.Uint32(sizeBytes)))
|
||||
return true, offset, err
|
||||
default:
|
||||
return false, 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
var zero = time.Unix(0,0)
|
||||
|
||||
|
||||
-1
@@ -16,7 +16,6 @@ func (d *decoder) isNegativeFixNum(v byte) bool {
|
||||
}
|
||||
|
||||
func (d *decoder) asInt(offset int, k reflect.Kind) (int64, int, error) {
|
||||
|
||||
code, _, err := d.readSize1(offset)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
|
||||
+12
-6
@@ -163,13 +163,19 @@ func (d *decoder) asInterface(offset int, k reflect.Kind) (interface{}, int, err
|
||||
*/
|
||||
|
||||
// ext
|
||||
for i := range extCoders {
|
||||
if extCoders[i].IsType(offset, &d.data) {
|
||||
v, offset, err := extCoders[i].AsValue(offset, k, &d.data)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
isExt, _, err := d.extEndOffset(offset)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if isExt {
|
||||
for i := range extCoders {
|
||||
if extCoders[i].IsType(offset, &d.data) {
|
||||
v, offset, err := extCoders[i].AsValue(offset, k, &d.data)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return v, offset, nil
|
||||
}
|
||||
return v, offset, nil
|
||||
}
|
||||
}
|
||||
return nil, 0, d.errorTemplate(code, k)
|
||||
|
||||
+4
-2
@@ -7,8 +7,10 @@ import (
|
||||
"github.com/shamaton/msgpack/v2/def"
|
||||
)
|
||||
|
||||
var emptyString = ""
|
||||
var emptyBytes = []byte{}
|
||||
var (
|
||||
emptyString = ""
|
||||
emptyBytes = []byte{}
|
||||
)
|
||||
|
||||
func (d *decoder) isCodeString(code byte) bool {
|
||||
return d.isFixString(code) || code == def.Str8 || code == def.Str16 || code == def.Str32
|
||||
|
||||
+24
-40
@@ -18,8 +18,10 @@ type structCacheTypeArray struct {
|
||||
}
|
||||
|
||||
// struct cache map
|
||||
var mapSCTM = sync.Map{}
|
||||
var mapSCTA = sync.Map{}
|
||||
var (
|
||||
mapSCTM = sync.Map{}
|
||||
mapSCTA = sync.Map{}
|
||||
)
|
||||
|
||||
func (d *decoder) setStruct(rv reflect.Value, offset int, k reflect.Kind) (int, error) {
|
||||
/*
|
||||
@@ -33,17 +35,23 @@ func (d *decoder) setStruct(rv reflect.Value, offset int, k reflect.Kind) (int,
|
||||
}
|
||||
*/
|
||||
|
||||
for i := range extCoders {
|
||||
if extCoders[i].IsType(offset, &d.data) {
|
||||
v, offset, err := extCoders[i].AsValue(offset, k, &d.data)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
isExt, _, err := d.extEndOffset(offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if isExt {
|
||||
for i := range extCoders {
|
||||
if extCoders[i].IsType(offset, &d.data) {
|
||||
v, offset, err := extCoders[i].AsValue(offset, k, &d.data)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Validate that the receptacle is of the right value type.
|
||||
if rv.Type() == reflect.TypeOf(v) {
|
||||
rv.Set(reflect.ValueOf(v))
|
||||
return offset, nil
|
||||
// Validate that the receptacle is of the right value type.
|
||||
if rv.Type() == reflect.TypeOf(v) {
|
||||
rv.Set(reflect.ValueOf(v))
|
||||
return offset, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,38 +285,14 @@ func (d *decoder) jumpOffset(offset int) (int, error) {
|
||||
}
|
||||
offset = o
|
||||
|
||||
case code == def.Fixext1:
|
||||
offset += def.Byte1 + def.Byte1
|
||||
case code == def.Fixext2:
|
||||
offset += def.Byte1 + def.Byte2
|
||||
case code == def.Fixext4:
|
||||
offset += def.Byte1 + def.Byte4
|
||||
case code == def.Fixext8:
|
||||
offset += def.Byte1 + def.Byte8
|
||||
case code == def.Fixext16:
|
||||
offset += def.Byte1 + def.Byte16
|
||||
|
||||
case code == def.Ext8:
|
||||
b, o, err := d.readSize1(offset)
|
||||
default:
|
||||
isExt, o, err := d.extEndOffsetWithCode(code, offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
o += def.Byte1 + int(b)
|
||||
offset = o
|
||||
case code == def.Ext16:
|
||||
bs, o, err := d.readSize2(offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
if isExt {
|
||||
offset = o
|
||||
}
|
||||
o += def.Byte1 + int(binary.BigEndian.Uint16(bs))
|
||||
offset = o
|
||||
case code == def.Ext32:
|
||||
bs, o, err := d.readSize4(offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
o += def.Byte1 + int(binary.BigEndian.Uint32(bs))
|
||||
offset = o
|
||||
|
||||
}
|
||||
return offset, nil
|
||||
|
||||
-1
@@ -8,7 +8,6 @@ import (
|
||||
)
|
||||
|
||||
func (d *decoder) asUint(offset int, k reflect.Kind) (uint64, int, error) {
|
||||
|
||||
code, _, err := d.readSize1(offset)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
|
||||
-1
@@ -253,7 +253,6 @@ func (e *encoder) calcLength(l int) (int, error) {
|
||||
}
|
||||
|
||||
func (e *encoder) create(rv reflect.Value, offset int) int {
|
||||
|
||||
switch rv.Kind() {
|
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||
v := rv.Uint()
|
||||
|
||||
+4
-2
@@ -7,8 +7,10 @@ import (
|
||||
"github.com/shamaton/msgpack/v2/time"
|
||||
)
|
||||
|
||||
var extCoderMap = map[reflect.Type]ext.Encoder{time.Encoder.Type(): time.Encoder}
|
||||
var extCoders = []ext.Encoder{time.Encoder}
|
||||
var (
|
||||
extCoderMap = map[reflect.Type]ext.Encoder{time.Encoder.Type(): time.Encoder}
|
||||
extCoders = []ext.Encoder{time.Encoder}
|
||||
)
|
||||
|
||||
// AddExtEncoder adds encoders for extension types.
|
||||
func AddExtEncoder(f ext.Encoder) {
|
||||
|
||||
-1
@@ -296,7 +296,6 @@ func (e *encoder) calcFixedMap(rv reflect.Value) (int, bool) {
|
||||
}
|
||||
|
||||
func (e *encoder) writeMapLength(l int, offset int) int {
|
||||
|
||||
// format
|
||||
if l <= 0x0f {
|
||||
offset = e.setByte1Int(def.FixMap+l, offset)
|
||||
|
||||
-1
@@ -127,7 +127,6 @@ func (e *encoder) writeSliceLength(l int, offset int) int {
|
||||
}
|
||||
|
||||
func (e *encoder) writeFixedSlice(rv reflect.Value, offset int) (int, bool) {
|
||||
|
||||
switch sli := rv.Interface().(type) {
|
||||
case []int:
|
||||
offset = e.writeSliceLength(len(sli), offset)
|
||||
|
||||
+4
-8
@@ -19,11 +19,12 @@ type structCache struct {
|
||||
|
||||
var cachemap = sync.Map{}
|
||||
|
||||
type structCalcFunc func(rv reflect.Value) (int, error)
|
||||
type structWriteFunc func(rv reflect.Value, offset int) int
|
||||
type (
|
||||
structCalcFunc func(rv reflect.Value) (int, error)
|
||||
structWriteFunc func(rv reflect.Value, offset int) int
|
||||
)
|
||||
|
||||
func (e *encoder) getStructCalc(typ reflect.Type) structCalcFunc {
|
||||
|
||||
for j := range extCoders {
|
||||
if extCoders[j].Type() == typ {
|
||||
return extCoders[j].CalcByteSize
|
||||
@@ -33,11 +34,9 @@ func (e *encoder) getStructCalc(typ reflect.Type) structCalcFunc {
|
||||
return e.calcStructArray
|
||||
}
|
||||
return e.calcStructMap
|
||||
|
||||
}
|
||||
|
||||
func (e *encoder) calcStruct(rv reflect.Value) (int, error) {
|
||||
|
||||
//if isTime, tm := e.isDateTime(rv); isTime {
|
||||
// size := e.calcTime(tm)
|
||||
// return size, nil
|
||||
@@ -178,7 +177,6 @@ func (e *encoder) calcSizeWithOmitEmpty(rv reflect.Value, name string, omit bool
|
||||
}
|
||||
|
||||
func (e *encoder) getStructWriter(typ reflect.Type) structWriteFunc {
|
||||
|
||||
for i := range extCoders {
|
||||
if extCoders[i].Type() == typ {
|
||||
return func(rv reflect.Value, offset int) int {
|
||||
@@ -213,7 +211,6 @@ func (e *encoder) writeStruct(rv reflect.Value, offset int) int {
|
||||
}
|
||||
|
||||
func (e *encoder) writeStructArray(rv reflect.Value, offset int) int {
|
||||
|
||||
cache, _ := cachemap.Load(rv.Type())
|
||||
c := cache.(*structCache)
|
||||
|
||||
@@ -236,7 +233,6 @@ func (e *encoder) writeStructArray(rv reflect.Value, offset int) int {
|
||||
}
|
||||
|
||||
func (e *encoder) writeStructMap(rv reflect.Value, offset int) int {
|
||||
|
||||
cache, _ := cachemap.Load(rv.Type())
|
||||
c := cache.(*structCache)
|
||||
|
||||
|
||||
-1
@@ -17,7 +17,6 @@ func (d *decoder) isCodeBin(v byte) bool {
|
||||
}
|
||||
|
||||
func (d *decoder) asBinWithCode(code byte, k reflect.Kind) ([]byte, error) {
|
||||
|
||||
switch code {
|
||||
case def.Bin8:
|
||||
l, err := d.readSize1()
|
||||
|
||||
+2
-1
@@ -29,7 +29,8 @@ func Decode(r io.Reader, v interface{}, asArray bool) error {
|
||||
|
||||
rv = rv.Elem()
|
||||
|
||||
d := decoder{r: r,
|
||||
d := decoder{
|
||||
r: r,
|
||||
buf: common.GetBuffer(),
|
||||
asArray: asArray,
|
||||
}
|
||||
|
||||
+5
-2
@@ -2,13 +2,16 @@ package decoding
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/shamaton/msgpack/v2/def"
|
||||
"github.com/shamaton/msgpack/v2/ext"
|
||||
"github.com/shamaton/msgpack/v2/time"
|
||||
)
|
||||
|
||||
var extCoderMap = map[int8]ext.StreamDecoder{time.StreamDecoder.Code(): time.StreamDecoder}
|
||||
var extCoders = []ext.StreamDecoder{time.StreamDecoder}
|
||||
var (
|
||||
extCoderMap = map[int8]ext.StreamDecoder{time.StreamDecoder.Code(): time.StreamDecoder}
|
||||
extCoders = []ext.StreamDecoder{time.StreamDecoder}
|
||||
)
|
||||
|
||||
// AddExtDecoder adds decoders for extension types.
|
||||
func AddExtDecoder(f ext.StreamDecoder) {
|
||||
|
||||
+5
-3
@@ -7,8 +7,10 @@ import (
|
||||
"github.com/shamaton/msgpack/v2/def"
|
||||
)
|
||||
|
||||
var emptyString = ""
|
||||
var emptyBytes = []byte{}
|
||||
var (
|
||||
emptyString = ""
|
||||
emptyBytes = []byte{}
|
||||
)
|
||||
|
||||
func (d *decoder) isCodeString(code byte) bool {
|
||||
return d.isFixString(code) || code == def.Str8 || code == def.Str16 || code == def.Str32
|
||||
@@ -83,7 +85,7 @@ func (d *decoder) asStringByteByLength(l int, _ reflect.Kind) ([]byte, error) {
|
||||
if l < 1 {
|
||||
return emptyBytes, nil
|
||||
}
|
||||
|
||||
|
||||
// avoid common buffer reference
|
||||
return d.copySizeN(l)
|
||||
}
|
||||
|
||||
+4
-2
@@ -18,8 +18,10 @@ type structCacheTypeArray struct {
|
||||
}
|
||||
|
||||
// struct cache map
|
||||
var mapSCTM = sync.Map{}
|
||||
var mapSCTA = sync.Map{}
|
||||
var (
|
||||
mapSCTM = sync.Map{}
|
||||
mapSCTA = sync.Map{}
|
||||
)
|
||||
|
||||
func (d *decoder) setStruct(code byte, rv reflect.Value, k reflect.Kind) error {
|
||||
if len(extCoders) > 0 {
|
||||
|
||||
-1
@@ -41,7 +41,6 @@ func Encode(w io.Writer, v any, asArray bool) error {
|
||||
}
|
||||
|
||||
func (e *encoder) create(rv reflect.Value) error {
|
||||
|
||||
switch rv.Kind() {
|
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
||||
v := rv.Uint()
|
||||
|
||||
+4
-2
@@ -7,8 +7,10 @@ import (
|
||||
"github.com/shamaton/msgpack/v2/time"
|
||||
)
|
||||
|
||||
var extCoderMap = map[reflect.Type]ext.StreamEncoder{time.StreamEncoder.Type(): time.StreamEncoder}
|
||||
var extCoders = []ext.StreamEncoder{time.StreamEncoder}
|
||||
var (
|
||||
extCoderMap = map[reflect.Type]ext.StreamEncoder{time.StreamEncoder.Type(): time.StreamEncoder}
|
||||
extCoders = []ext.StreamEncoder{time.StreamEncoder}
|
||||
)
|
||||
|
||||
// AddExtEncoder adds encoders for extension types.
|
||||
func AddExtEncoder(f ext.StreamEncoder) {
|
||||
|
||||
-1
@@ -8,7 +8,6 @@ import (
|
||||
)
|
||||
|
||||
func (e *encoder) writeMapLength(l int) error {
|
||||
|
||||
// format
|
||||
if l <= 0x0f {
|
||||
if err := e.setByte1Int(def.FixMap + l); err != nil {
|
||||
|
||||
+18
-9
@@ -5,14 +5,16 @@ func (e *encoder) setByte1Int64(value int64) error {
|
||||
}
|
||||
|
||||
func (e *encoder) setByte2Int64(value int64) error {
|
||||
return e.buf.Write(e.w,
|
||||
return e.buf.Write(
|
||||
e.w,
|
||||
byte(value>>8),
|
||||
byte(value),
|
||||
)
|
||||
}
|
||||
|
||||
func (e *encoder) setByte4Int64(value int64) error {
|
||||
return e.buf.Write(e.w,
|
||||
return e.buf.Write(
|
||||
e.w,
|
||||
byte(value>>24),
|
||||
byte(value>>16),
|
||||
byte(value>>8),
|
||||
@@ -21,7 +23,8 @@ func (e *encoder) setByte4Int64(value int64) error {
|
||||
}
|
||||
|
||||
func (e *encoder) setByte8Int64(value int64) error {
|
||||
return e.buf.Write(e.w,
|
||||
return e.buf.Write(
|
||||
e.w,
|
||||
byte(value>>56),
|
||||
byte(value>>48),
|
||||
byte(value>>40),
|
||||
@@ -38,14 +41,16 @@ func (e *encoder) setByte1Uint64(value uint64) error {
|
||||
}
|
||||
|
||||
func (e *encoder) setByte2Uint64(value uint64) error {
|
||||
return e.buf.Write(e.w,
|
||||
return e.buf.Write(
|
||||
e.w,
|
||||
byte(value>>8),
|
||||
byte(value),
|
||||
)
|
||||
}
|
||||
|
||||
func (e *encoder) setByte4Uint64(value uint64) error {
|
||||
return e.buf.Write(e.w,
|
||||
return e.buf.Write(
|
||||
e.w,
|
||||
byte(value>>24),
|
||||
byte(value>>16),
|
||||
byte(value>>8),
|
||||
@@ -54,7 +59,8 @@ func (e *encoder) setByte4Uint64(value uint64) error {
|
||||
}
|
||||
|
||||
func (e *encoder) setByte8Uint64(value uint64) error {
|
||||
return e.buf.Write(e.w,
|
||||
return e.buf.Write(
|
||||
e.w,
|
||||
byte(value>>56),
|
||||
byte(value>>48),
|
||||
byte(value>>40),
|
||||
@@ -67,20 +73,23 @@ func (e *encoder) setByte8Uint64(value uint64) error {
|
||||
}
|
||||
|
||||
func (e *encoder) setByte1Int(value int) error {
|
||||
return e.buf.Write(e.w,
|
||||
return e.buf.Write(
|
||||
e.w,
|
||||
byte(value),
|
||||
)
|
||||
}
|
||||
|
||||
func (e *encoder) setByte2Int(value int) error {
|
||||
return e.buf.Write(e.w,
|
||||
return e.buf.Write(
|
||||
e.w,
|
||||
byte(value>>8),
|
||||
byte(value),
|
||||
)
|
||||
}
|
||||
|
||||
func (e *encoder) setByte4Int(value int) error {
|
||||
return e.buf.Write(e.w,
|
||||
return e.buf.Write(
|
||||
e.w,
|
||||
byte(value>>24),
|
||||
byte(value>>16),
|
||||
byte(value>>8),
|
||||
|
||||
-1
@@ -32,7 +32,6 @@ func (e *encoder) writeSliceLength(l int) error {
|
||||
}
|
||||
|
||||
func (e *encoder) writeFixedSlice(rv reflect.Value) (bool, error) {
|
||||
|
||||
switch sli := rv.Interface().(type) {
|
||||
case []int:
|
||||
for _, v := range sli {
|
||||
|
||||
-2
@@ -23,7 +23,6 @@ var cachemap = sync.Map{}
|
||||
type structWriteFunc func(rv reflect.Value) error
|
||||
|
||||
func (e *encoder) getStructWriter(typ reflect.Type) structWriteFunc {
|
||||
|
||||
for i := range extCoders {
|
||||
if extCoders[i].Type() == typ {
|
||||
return func(rv reflect.Value) error {
|
||||
@@ -40,7 +39,6 @@ func (e *encoder) getStructWriter(typ reflect.Type) structWriteFunc {
|
||||
}
|
||||
|
||||
func (e *encoder) writeStruct(rv reflect.Value) error {
|
||||
|
||||
for i := range extCoders {
|
||||
if extCoders[i].Type() == rv.Type() {
|
||||
w := ext.CreateStreamWriter(e.w, e.buf)
|
||||
|
||||
+95
-21
@@ -24,30 +24,86 @@ func (td *timeDecoder) Code() int8 {
|
||||
return def.TimeStamp
|
||||
}
|
||||
|
||||
func (td *timeDecoder) IsType(offset int, d *[]byte) bool {
|
||||
code, offset := td.ReadSize1(offset, d)
|
||||
func (td *timeDecoder) readSize1Safe(index int, d *[]byte) (byte, int, bool) {
|
||||
if len(*d) < index+def.Byte1 {
|
||||
return 0, 0, false
|
||||
}
|
||||
v, next := td.ReadSize1(index, d)
|
||||
return v, next, true
|
||||
}
|
||||
|
||||
if code == def.Fixext4 {
|
||||
t, _ := td.ReadSize1(offset, d)
|
||||
return int8(t) == td.Code()
|
||||
} else if code == def.Fixext8 {
|
||||
t, _ := td.ReadSize1(offset, d)
|
||||
return int8(t) == td.Code()
|
||||
} else if code == def.Ext8 {
|
||||
l, offset := td.ReadSize1(offset, d)
|
||||
t, _ := td.ReadSize1(offset, d)
|
||||
return l == 12 && int8(t) == td.Code()
|
||||
func (td *timeDecoder) readSize4Safe(index int, d *[]byte) ([]byte, int, bool) {
|
||||
if len(*d) < index+def.Byte4 {
|
||||
return nil, 0, false
|
||||
}
|
||||
v, next := td.ReadSize4(index, d)
|
||||
return v, next, true
|
||||
}
|
||||
|
||||
func (td *timeDecoder) readSize8Safe(index int, d *[]byte) ([]byte, int, bool) {
|
||||
if len(*d) < index+def.Byte8 {
|
||||
return nil, 0, false
|
||||
}
|
||||
v, next := td.ReadSize8(index, d)
|
||||
return v, next, true
|
||||
}
|
||||
|
||||
func (td *timeDecoder) IsType(offset int, d *[]byte) bool {
|
||||
code, offset, ok := td.readSize1Safe(offset, d)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
switch code {
|
||||
case def.Fixext4:
|
||||
t, _, ok := td.readSize1Safe(offset, d)
|
||||
if !ok || int8(t) != td.Code() {
|
||||
return false
|
||||
}
|
||||
_, _, ok = td.readSize4Safe(offset+def.Byte1, d)
|
||||
return ok
|
||||
case def.Fixext8:
|
||||
t, _, ok := td.readSize1Safe(offset, d)
|
||||
if !ok || int8(t) != td.Code() {
|
||||
return false
|
||||
}
|
||||
_, _, ok = td.readSize8Safe(offset+def.Byte1, d)
|
||||
return ok
|
||||
case def.Ext8:
|
||||
l, offset, ok := td.readSize1Safe(offset, d)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
t, _, ok := td.readSize1Safe(offset, d)
|
||||
if !ok || l != 12 || int8(t) != td.Code() {
|
||||
return false
|
||||
}
|
||||
_, _, ok = td.readSize4Safe(offset+def.Byte1, d)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
_, _, ok = td.readSize8Safe(offset+def.Byte1+def.Byte4, d)
|
||||
return ok
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (td *timeDecoder) AsValue(offset int, k reflect.Kind, d *[]byte) (interface{}, int, error) {
|
||||
code, offset := td.ReadSize1(offset, d)
|
||||
code, offset, ok := td.readSize1Safe(offset, d)
|
||||
if !ok {
|
||||
return zero, 0, def.ErrTooShortBytes
|
||||
}
|
||||
|
||||
switch code {
|
||||
case def.Fixext4:
|
||||
_, offset = td.ReadSize1(offset, d)
|
||||
bs, offset := td.ReadSize4(offset, d)
|
||||
_, offset, ok = td.readSize1Safe(offset, d)
|
||||
if !ok {
|
||||
return zero, 0, def.ErrTooShortBytes
|
||||
}
|
||||
bs, offset, ok := td.readSize4Safe(offset, d)
|
||||
if !ok {
|
||||
return zero, 0, def.ErrTooShortBytes
|
||||
}
|
||||
v := time.Unix(int64(binary.BigEndian.Uint32(bs)), 0)
|
||||
if decodeAsLocal {
|
||||
return v, offset, nil
|
||||
@@ -55,8 +111,14 @@ func (td *timeDecoder) AsValue(offset int, k reflect.Kind, d *[]byte) (interface
|
||||
return v.UTC(), offset, nil
|
||||
|
||||
case def.Fixext8:
|
||||
_, offset = td.ReadSize1(offset, d)
|
||||
bs, offset := td.ReadSize8(offset, d)
|
||||
_, offset, ok = td.readSize1Safe(offset, d)
|
||||
if !ok {
|
||||
return zero, 0, def.ErrTooShortBytes
|
||||
}
|
||||
bs, offset, ok := td.readSize8Safe(offset, d)
|
||||
if !ok {
|
||||
return zero, 0, def.ErrTooShortBytes
|
||||
}
|
||||
data64 := binary.BigEndian.Uint64(bs)
|
||||
nano := int64(data64 >> 34)
|
||||
if nano > 999999999 {
|
||||
@@ -69,10 +131,22 @@ func (td *timeDecoder) AsValue(offset int, k reflect.Kind, d *[]byte) (interface
|
||||
return v.UTC(), offset, nil
|
||||
|
||||
case def.Ext8:
|
||||
_, offset = td.ReadSize1(offset, d)
|
||||
_, offset = td.ReadSize1(offset, d)
|
||||
nanobs, offset := td.ReadSize4(offset, d)
|
||||
secbs, offset := td.ReadSize8(offset, d)
|
||||
_, offset, ok = td.readSize1Safe(offset, d)
|
||||
if !ok {
|
||||
return zero, 0, def.ErrTooShortBytes
|
||||
}
|
||||
_, offset, ok = td.readSize1Safe(offset, d)
|
||||
if !ok {
|
||||
return zero, 0, def.ErrTooShortBytes
|
||||
}
|
||||
nanobs, offset, ok := td.readSize4Safe(offset, d)
|
||||
if !ok {
|
||||
return zero, 0, def.ErrTooShortBytes
|
||||
}
|
||||
secbs, offset, ok := td.readSize8Safe(offset, d)
|
||||
if !ok {
|
||||
return zero, 0, def.ErrTooShortBytes
|
||||
}
|
||||
nano := binary.BigEndian.Uint32(nanobs)
|
||||
if nano > 999999999 {
|
||||
return zero, 0, fmt.Errorf("in timestamp 96 formats, nanoseconds must not be larger than 999999999 : %d", nano)
|
||||
|
||||
+9
@@ -33,6 +33,9 @@ func (td *timeStreamDecoder) IsType(code byte, innerType int8, dataLength int) b
|
||||
func (td *timeStreamDecoder) ToValue(code byte, data []byte, k reflect.Kind) (interface{}, error) {
|
||||
switch code {
|
||||
case def.Fixext4:
|
||||
if len(data) < def.Byte4 {
|
||||
return zero, def.ErrTooShortBytes
|
||||
}
|
||||
v := time.Unix(int64(binary.BigEndian.Uint32(data)), 0)
|
||||
if decodeAsLocal {
|
||||
return v, nil
|
||||
@@ -40,6 +43,9 @@ func (td *timeStreamDecoder) ToValue(code byte, data []byte, k reflect.Kind) (in
|
||||
return v.UTC(), nil
|
||||
|
||||
case def.Fixext8:
|
||||
if len(data) < def.Byte8 {
|
||||
return zero, def.ErrTooShortBytes
|
||||
}
|
||||
data64 := binary.BigEndian.Uint64(data)
|
||||
nano := int64(data64 >> 34)
|
||||
if nano > 999999999 {
|
||||
@@ -52,6 +58,9 @@ func (td *timeStreamDecoder) ToValue(code byte, data []byte, k reflect.Kind) (in
|
||||
return v.UTC(), nil
|
||||
|
||||
case def.Ext8:
|
||||
if len(data) < def.Byte4+def.Byte8 {
|
||||
return zero, def.ErrTooShortBytes
|
||||
}
|
||||
nano := binary.BigEndian.Uint32(data[:4])
|
||||
if nano > 999999999 {
|
||||
return zero, fmt.Errorf("in timestamp 96 formats, nanoseconds must not be larger than 999999999 : %d", nano)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user