1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
6 "bytes"
7 "errors"
8 "io/fs"
9 "os"
10 "strconv"
11 "strings"
12)
13
14// maxConfigSize bounds the config file that is read.
15const maxConfigSize = 1 << 20
16
17// checkConfig reads the few settings that decide whether the repository is
18// readable: core.repositoryformatversion and the extensions.* keys. Other
19// settings, includes and conditional includes are ignored.
20func checkConfig(path string) error {
21 data, err := os.ReadFile(path)
22 if errors.Is(err, fs.ErrNotExist) {
23 return nil
24 }
25 if err != nil {
26 return err
27 }
28 if len(data) > maxConfigSize {
29 return unsupported("config file larger than %d bytes", maxConfigSize)
30 }
31 return checkConfigData(data)
32}
33
34func checkConfigData(data []byte) error {
35 section := ""
36 version := int64(0)
37 for len(data) > 0 {
38 var line []byte
39 line, data, _ = bytes.Cut(data, []byte{'\n'})
40 line = bytes.TrimSpace(line)
41 if len(line) == 0 || line[0] == '#' || line[0] == ';' {
42 continue
43 }
44 if line[0] == '[' {
45 end := bytes.IndexByte(line, ']')
46 if end < 0 {
47 return corrupt("config: bad section line %q", truncate(string(line)))
48 }
49 head := string(bytes.TrimSpace(line[1:end]))
50 // "[core]", "[remote \"origin\"]", "[branch.main]"
51 name, _, _ := strings.Cut(head, " ")
52 name, _, _ = strings.Cut(name, ".")
53 section = strings.ToLower(name)
54 line = bytes.TrimSpace(line[end+1:])
55 if len(line) == 0 || line[0] == '#' || line[0] == ';' {
56 continue
57 }
58 // "[core] bare = true" on one line.
59 }
60 keyb, valb, _ := bytes.Cut(line, []byte{'='})
61 key := strings.ToLower(string(bytes.TrimSpace(keyb)))
62 val := configValue(valb)
63 switch section {
64 case "core":
65 if key == "repositoryformatversion" {
66 v, err := strconv.ParseInt(val, 10, 64)
67 if err != nil {
68 return corrupt("config: core.repositoryformatversion %q", truncate(val))
69 }
70 version = v
71 }
72 case "extensions":
73 switch key {
74 case "objectformat":
75 if !strings.EqualFold(val, "sha1") {
76 return unsupported("extensions.objectformat = %s", truncate(val))
77 }
78 case "refstorage":
79 if !strings.EqualFold(val, "files") {
80 return unsupported("extensions.refstorage = %s", truncate(val))
81 }
82 case "noop", "worktreeconfig", "preciousobjects", "relativeworktrees":
83 // No effect on reading: preciousobjects only forbids
84 // deleting objects, relativeworktrees changes how linked
85 // worktrees point at the repository (they are rejected
86 // anyway).
87 default:
88 return unsupported("extensions.%s", truncate(key))
89 }
90 }
91 }
92 if version != 0 && version != 1 {
93 return unsupported("core.repositoryformatversion = %d", version)
94 }
95 return nil
96}
97
98// configValue returns the unquoted value of a config line, without a trailing
99// comment.
100func configValue(b []byte) string {
101 b = bytes.TrimSpace(b)
102 var sb strings.Builder
103 inQuote := false
104 for i := 0; i < len(b); i++ {
105 c := b[i]
106 switch {
107 case c == '\\' && i+1 < len(b):
108 i++
109 sb.WriteByte(b[i])
110 case c == '"':
111 inQuote = !inQuote
112 case !inQuote && (c == '#' || c == ';'):
113 return strings.TrimSpace(sb.String())
114 default:
115 sb.WriteByte(c)
116 }
117 }
118 return strings.TrimSpace(sb.String())
119}