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