1// Copyright 2018 The Gitea Authors. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package git
5
6import (
7 "bufio"
8 "bytes"
9 "fmt"
10 "io"
11 "strconv"
12 "strings"
13
14 "forgejo.org/modules/log"
15)
16
17// ParseTreeEntries parses the output of a `git ls-tree -l` command.
18func ParseTreeEntries(data []byte) ([]*TreeEntry, error) {
19 return parseTreeEntries(data, nil)
20}
21
22var sepSpace = []byte{' '}
23
24func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
25 var err error
26 entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1)
27 for pos := 0; pos < len(data); {
28 // expect line to be of the form:
29 // <mode> <type> <sha> <space-padded-size>\t<filename>
30 // <mode> <type> <sha>\t<filename>
31 posEnd := bytes.IndexByte(data[pos:], '\n')
32 if posEnd == -1 {
33 posEnd = len(data)
34 } else {
35 posEnd += pos
36 }
37 line := data[pos:posEnd]
38 posTab := bytes.IndexByte(line, '\t')
39 if posTab == -1 {
40 return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line)
41 }
42
43 entry := new(TreeEntry)
44 entry.ptree = ptree
45
46 entryAttrs := line[:posTab]
47 entryName := line[posTab+1:]
48
49 entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace)
50 _ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type
51 entryObjectID, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace)
52 if len(entryAttrs) > 0 {
53 entrySize := entryAttrs // the last field is the space-padded-size
54 entry.size, _ = strconv.ParseInt(strings.TrimSpace(string(entrySize)), 10, 64)
55 entry.sized = true
56 }
57
58 switch string(entryMode) {
59 case "100644":
60 entry.entryMode = EntryModeBlob
61 case "100755":
62 entry.entryMode = EntryModeExec
63 case "120000":
64 entry.entryMode = EntryModeSymlink
65 case "160000":
66 entry.entryMode = EntryModeCommit
67 case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons
68 entry.entryMode = EntryModeTree
69 default:
70 return nil, fmt.Errorf("unknown type: %v", string(entryMode))
71 }
72
73 entry.ID, err = NewIDFromString(string(entryObjectID))
74 if err != nil {
75 return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err)
76 }
77
78 if len(entryName) > 0 && entryName[0] == '"' {
79 entry.name, err = strconv.Unquote(string(entryName))
80 if err != nil {
81 return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err)
82 }
83 } else {
84 entry.name = string(entryName)
85 }
86
87 pos = posEnd + 1
88 entries = append(entries, entry)
89 }
90 return entries, nil
91}
92
93func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd *bufio.Reader, sz int64) ([]*TreeEntry, error) {
94 fnameBuf := make([]byte, 4096)
95 modeBuf := make([]byte, 40)
96 shaBuf := make([]byte, objectFormat.FullLength())
97 entries := make([]*TreeEntry, 0, 10)
98
99loop:
100 for sz > 0 {
101 mode, fname, sha, count, err := ParseTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf)
102 if err != nil {
103 if err == io.EOF {
104 break loop
105 }
106 return nil, err
107 }
108 sz -= int64(count)
109 entry := new(TreeEntry)
110 entry.ptree = ptree
111
112 switch string(mode) {
113 case "100644":
114 entry.entryMode = EntryModeBlob
115 case "100755":
116 entry.entryMode = EntryModeExec
117 case "120000":
118 entry.entryMode = EntryModeSymlink
119 case "160000":
120 entry.entryMode = EntryModeCommit
121 case "40000", "40755": // git uses 40000 for tree object, but some users may get 40755 for unknown reasons
122 entry.entryMode = EntryModeTree
123 default:
124 log.Debug("Unknown mode: %v", string(mode))
125 return nil, fmt.Errorf("unknown mode: %v", string(mode))
126 }
127
128 entry.ID = objectFormat.MustID(sha)
129 entry.name = string(fname)
130 entries = append(entries, entry)
131 }
132 if _, err := rd.Discard(1); err != nil {
133 return entries, err
134 }
135
136 return entries, nil
137}