1package catfile
2
3import (
4	"bufio"
5	"bytes"
6	"context"
7	"fmt"
8	"io"
9	"strings"
10
11	"gitlab.com/gitlab-org/gitaly/v14/internal/git"
12	"gitlab.com/gitlab-org/gitaly/v14/internal/helper"
13	"gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb"
14)
15
16const (
17	// MaxTagReferenceDepth is the maximum depth of tag references we will dereference
18	MaxTagReferenceDepth = 10
19)
20
21// GetTag looks up a commit by tagID using an existing catfile.Batch instance. When 'trim' is
22// 'true', the tag message will be trimmed to fit in a gRPC message. When 'trimRightNewLine' is
23// 'true', the tag message will be trimmed to remove all '\n' characters from right. note: we pass
24// in the tagName because the tag name from refs/tags may be different than the name found in the
25// actual tag object. We want to use the tagName found in refs/tags
26func GetTag(ctx context.Context, c Batch, tagID git.Revision, tagName string, trimLen, trimRightNewLine bool) (*gitalypb.Tag, error) {
27	tagObj, err := c.Tag(ctx, tagID)
28	if err != nil {
29		return nil, err
30	}
31
32	tag, err := buildAnnotatedTag(ctx, c, tagObj, []byte(tagName), trimLen, trimRightNewLine)
33	if err != nil {
34		return nil, err
35	}
36
37	return tag, nil
38}
39
40// ExtractTagSignature extracts the signature from a content and returns both the signature
41// and the remaining content. If no signature is found, nil as the signature and the entire
42// content are returned. note: tags contain the signature block at the end of the message
43// https://github.com/git/git/blob/master/Documentation/technical/signature-format.txt#L12
44func ExtractTagSignature(content []byte) ([]byte, []byte) {
45	index := bytes.Index(content, []byte("-----BEGIN"))
46
47	if index > 0 {
48		return bytes.TrimSuffix(content[index:], []byte("\n")), content[:index]
49	}
50	return nil, content
51}
52
53type tagHeader struct {
54	oid     string
55	tagType string
56	tag     string
57	tagger  string
58}
59
60func splitRawTag(r io.Reader, trimRightNewLine bool) (*tagHeader, []byte, error) {
61	raw, err := io.ReadAll(r)
62	if err != nil {
63		return nil, nil, err
64	}
65
66	var body []byte
67	split := bytes.SplitN(raw, []byte("\n\n"), 2)
68	if len(split) == 2 {
69		body = split[1]
70		if trimRightNewLine {
71			// Remove trailing newline, if any, to preserve existing behavior the old GitLab tag finding code.
72			// See https://gitlab.com/gitlab-org/gitaly/blob/5e94dc966ac1900c11794b107a77496552591f9b/ruby/lib/gitlab/git/repository.rb#L211.
73			// Maybe this belongs in the FindAllTags handler, or even on the gitlab-ce client side, instead of here?
74			body = bytes.TrimRight(body, "\n")
75		}
76	}
77
78	var header tagHeader
79	s := bufio.NewScanner(bytes.NewReader(split[0]))
80	for s.Scan() {
81		headerSplit := strings.SplitN(s.Text(), " ", 2)
82		if len(headerSplit) != 2 {
83			continue
84		}
85
86		key, value := headerSplit[0], headerSplit[1]
87		switch key {
88		case "object":
89			header.oid = value
90		case "type":
91			header.tagType = value
92		case "tag":
93			header.tag = value
94		case "tagger":
95			header.tagger = value
96		}
97	}
98
99	return &header, body, nil
100}
101
102// ParseTag parses the tag from the given Reader. The tag's tagged commit is not populated. The
103// given object ID shall refer to the tag itself such that the returned Tag structure has the
104// correct OID.
105func ParseTag(r io.Reader, oid git.ObjectID) (*gitalypb.Tag, error) {
106	tag, _, err := parseTag(r, oid, nil, true, true)
107	return tag, err
108}
109
110func parseTag(r io.Reader, oid git.ObjectID, name []byte, trimLen, trimRightNewLine bool) (*gitalypb.Tag, *tagHeader, error) {
111	header, body, err := splitRawTag(r, trimRightNewLine)
112	if err != nil {
113		return nil, nil, err
114	}
115
116	if len(name) == 0 {
117		name = []byte(header.tag)
118	}
119
120	tag := &gitalypb.Tag{
121		Id:          oid.String(),
122		Name:        name,
123		MessageSize: int64(len(body)),
124		Message:     body,
125	}
126
127	if max := helper.MaxCommitOrTagMessageSize; trimLen && len(body) > max {
128		tag.Message = tag.Message[:max]
129	}
130
131	signature, _ := ExtractTagSignature(body)
132	if signature != nil {
133		length := bytes.Index(signature, []byte("\n"))
134
135		if length > 0 {
136			signature := string(signature[:length])
137			tag.SignatureType = detectSignatureType(signature)
138		}
139	}
140
141	tag.Tagger = parseCommitAuthor(header.tagger)
142
143	return tag, header, nil
144}
145
146func buildAnnotatedTag(ctx context.Context, b Batch, object *Object, name []byte, trimLen, trimRightNewLine bool) (*gitalypb.Tag, error) {
147	tag, header, err := parseTag(object.Reader, object.ObjectInfo.Oid, name, trimLen, trimRightNewLine)
148	if err != nil {
149		return nil, err
150	}
151
152	switch header.tagType {
153	case "commit":
154		tag.TargetCommit, err = GetCommit(ctx, b, git.Revision(header.oid))
155		if err != nil {
156			return nil, fmt.Errorf("buildAnnotatedTag error when getting target commit: %v", err)
157		}
158
159	case "tag":
160		tag.TargetCommit, err = dereferenceTag(ctx, b, git.Revision(header.oid))
161		if err != nil {
162			return nil, fmt.Errorf("buildAnnotatedTag error when dereferencing tag: %v", err)
163		}
164	}
165
166	return tag, nil
167}
168
169// dereferenceTag recursively dereferences annotated tags until it finds a commit.
170// This matches the original behavior in the ruby implementation.
171// we also protect against circular tag references. Even though this is not possible in git,
172// we still want to protect against an infinite looop
173func dereferenceTag(ctx context.Context, b Batch, oid git.Revision) (*gitalypb.GitCommit, error) {
174	for depth := 0; depth < MaxTagReferenceDepth; depth++ {
175		i, err := b.Info(ctx, oid)
176		if err != nil {
177			return nil, err
178		}
179
180		switch i.Type {
181		case "tag":
182			tagObj, err := b.Tag(ctx, oid)
183			if err != nil {
184				return nil, err
185			}
186
187			header, _, err := splitRawTag(tagObj.Reader, true)
188			if err != nil {
189				return nil, err
190			}
191
192			oid = git.Revision(header.oid)
193			continue
194		case "commit":
195			return GetCommit(ctx, b, oid)
196		default: // This current tag points to a tree or a blob
197			return nil, nil
198		}
199	}
200
201	// at this point the tag nesting has gone too deep. We want to return silently here however, as we don't
202	// want to fail the entire request if one tag is nested too deeply.
203	return nil, nil
204}
205