1package gitattributes
2
3import (
4	"errors"
5	"io"
6	"io/ioutil"
7	"strings"
8)
9
10const (
11	commentPrefix = "#"
12	eol           = "\n"
13	macroPrefix   = "[attr]"
14)
15
16var (
17	ErrMacroNotAllowed      = errors.New("macro not allowed")
18	ErrInvalidAttributeName = errors.New("Invalid attribute name")
19)
20
21type MatchAttribute struct {
22	Name       string
23	Pattern    Pattern
24	Attributes []Attribute
25}
26
27type attributeState byte
28
29const (
30	attributeUnknown     attributeState = 0
31	attributeSet         attributeState = 1
32	attributeUnspecified attributeState = '!'
33	attributeUnset       attributeState = '-'
34	attributeSetValue    attributeState = '='
35)
36
37type Attribute interface {
38	Name() string
39	IsSet() bool
40	IsUnset() bool
41	IsUnspecified() bool
42	IsValueSet() bool
43	Value() string
44	String() string
45}
46
47type attribute struct {
48	name  string
49	state attributeState
50	value string
51}
52
53func (a attribute) Name() string {
54	return a.name
55}
56
57func (a attribute) IsSet() bool {
58	return a.state == attributeSet
59}
60
61func (a attribute) IsUnset() bool {
62	return a.state == attributeUnset
63}
64
65func (a attribute) IsUnspecified() bool {
66	return a.state == attributeUnspecified
67}
68
69func (a attribute) IsValueSet() bool {
70	return a.state == attributeSetValue
71}
72
73func (a attribute) Value() string {
74	return a.value
75}
76
77func (a attribute) String() string {
78	switch a.state {
79	case attributeSet:
80		return a.name + ": set"
81	case attributeUnset:
82		return a.name + ": unset"
83	case attributeUnspecified:
84		return a.name + ": unspecified"
85	default:
86		return a.name + ": " + a.value
87	}
88}
89
90// ReadAttributes reads patterns and attributes from the gitattributes format.
91func ReadAttributes(r io.Reader, domain []string, allowMacro bool) (attributes []MatchAttribute, err error) {
92	data, err := ioutil.ReadAll(r)
93	if err != nil {
94		return nil, err
95	}
96
97	for _, line := range strings.Split(string(data), eol) {
98		attribute, err := ParseAttributesLine(line, domain, allowMacro)
99		if err != nil {
100			return attributes, err
101		}
102		if len(attribute.Name) == 0 {
103			continue
104		}
105
106		attributes = append(attributes, attribute)
107	}
108
109	return attributes, nil
110}
111
112// ParseAttributesLine parses a gitattribute line, extracting path pattern and
113// attributes.
114func ParseAttributesLine(line string, domain []string, allowMacro bool) (m MatchAttribute, err error) {
115	line = strings.TrimSpace(line)
116
117	if strings.HasPrefix(line, commentPrefix) || len(line) == 0 {
118		return
119	}
120
121	name, unquoted := unquote(line)
122	attrs := strings.Fields(unquoted)
123	if len(name) == 0 {
124		name = attrs[0]
125		attrs = attrs[1:]
126	}
127
128	var macro bool
129	macro, name, err = checkMacro(name, allowMacro)
130	if err != nil {
131		return
132	}
133
134	m.Name = name
135	m.Attributes = make([]Attribute, 0, len(attrs))
136
137	for _, attrName := range attrs {
138		attr := attribute{
139			name:  attrName,
140			state: attributeSet,
141		}
142
143		// ! and - prefixes
144		state := attributeState(attr.name[0])
145		if state == attributeUnspecified || state == attributeUnset {
146			attr.state = state
147			attr.name = attr.name[1:]
148		}
149
150		kv := strings.SplitN(attrName, "=", 2)
151		if len(kv) == 2 {
152			attr.name = kv[0]
153			attr.value = kv[1]
154			attr.state = attributeSetValue
155		}
156
157		if !validAttributeName(attr.name) {
158			return m, ErrInvalidAttributeName
159		}
160		m.Attributes = append(m.Attributes, attr)
161	}
162
163	if !macro {
164		m.Pattern = ParsePattern(name, domain)
165	}
166	return
167}
168
169func checkMacro(name string, allowMacro bool) (macro bool, macroName string, err error) {
170	if !strings.HasPrefix(name, macroPrefix) {
171		return false, name, nil
172	}
173	if !allowMacro {
174		return true, name, ErrMacroNotAllowed
175	}
176
177	macroName = name[len(macroPrefix):]
178	if !validAttributeName(macroName) {
179		return true, name, ErrInvalidAttributeName
180	}
181	return true, macroName, nil
182}
183
184func validAttributeName(name string) bool {
185	if len(name) == 0 || name[0] == '-' {
186		return false
187	}
188
189	for _, ch := range name {
190		if !(ch == '-' || ch == '.' || ch == '_' ||
191			('0' <= ch && ch <= '9') ||
192			('a' <= ch && ch <= 'z') ||
193			('A' <= ch && ch <= 'Z')) {
194			return false
195		}
196	}
197	return true
198}
199
200func unquote(str string) (string, string) {
201	if str[0] != '"' {
202		return "", str
203	}
204
205	for i := 1; i < len(str); i++ {
206		switch str[i] {
207		case '\\':
208			i++
209		case '"':
210			return str[1:i], str[i+1:]
211		}
212	}
213	return "", str
214}
215