1 // Parse /etc/group files and serialize them
3 // Copyright (C) 2019 Simon Ruderich
5 // This program is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU Affero General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU Affero General Public License for more details.
15 // You should have received a copy of the GNU Affero General Public License
16 // along with this program. If not, see <https://www.gnu.org/licenses/>.
31 "github.com/pkg/errors"
34 // Version written in SerializeGroups()
35 const GroupVersion = 1
44 // Go does not support slices in map keys; therefore, use this separate struct
45 type GroupKey struct {
49 Members string // "," separated
52 func toKey(g Group) GroupKey {
57 Members: strings.Join(g.Members, ","),
61 // ParseGroups parses a file in the format of /etc/group and returns all
62 // entries as slice of Group structs.
63 func ParseGroups(r io.Reader) ([]Group, error) {
66 s := bufio.NewReader(r)
68 t, err := s.ReadString('\n')
72 return nil, fmt.Errorf(
73 "no newline in last line: %q",
81 x := strings.Split(t, ":")
83 return nil, fmt.Errorf("invalid line %q", t)
86 gid, err := strconv.ParseUint(x[2], 10, 64)
88 return nil, errors.Wrapf(err, "invalid gid in line %q", t)
91 // ReadString() contains the delimiter
92 x[3] = strings.TrimSuffix(x[3], "\n")
95 // No members must result in empty slice, not slice with the
98 members = strings.Split(x[3], ",")
100 res = append(res, Group{
111 func SerializeGroup(g Group) ([]byte, error) {
112 le := binary.LittleEndian
114 // Concatenate all (NUL-terminated) strings and store the offsets
115 var mems bytes.Buffer
116 var mems_off []uint16
117 for _, m := range g.Members {
118 mems_off = append(mems_off, uint16(mems.Len()))
119 mems.Write([]byte(m))
122 var data bytes.Buffer
123 data.Write([]byte(g.Name))
125 offPasswd := uint16(data.Len())
126 data.Write([]byte(g.Passwd))
128 alignBufferTo(&data, 2) // align the following uint16
129 offMemOff := uint16(data.Len())
130 // Offsets for group members
131 offMem := offMemOff + 2*uint16(len(mems_off))
132 for _, o := range mems_off {
133 tmp := make([]byte, 2)
134 le.PutUint16(tmp, offMem+o)
137 // And the group members concatenated as above
138 data.Write(mems.Bytes())
139 // Ensure the offsets can fit the length of this entry
140 if data.Len() > math.MaxUint16 {
141 return nil, fmt.Errorf("group too large to serialize: %v, %v",
144 size := uint16(data.Len())
146 var res bytes.Buffer // serialized result
148 id := make([]byte, 8)
150 le.PutUint64(id, g.Gid)
153 off := make([]byte, 2)
155 le.PutUint16(off, offPasswd)
158 le.PutUint16(off, offMemOff)
161 le.PutUint16(off, uint16(len(g.Members)))
164 le.PutUint16(off, size)
167 res.Write(data.Bytes())
168 // We must pad each entry so that all uint64 at the beginning of the
169 // struct are 8 byte aligned
170 alignBufferTo(&res, 8)
172 return res.Bytes(), nil
175 func SerializeGroups(w io.Writer, grs []Group) error {
176 // Serialize groups and store offsets
177 var data bytes.Buffer
178 offsets := make(map[GroupKey]uint64)
179 for _, g := range grs {
180 // TODO: warn about duplicate entries
181 offsets[toKey(g)] = uint64(data.Len())
182 x, err := SerializeGroup(g)
189 // Copy to prevent sorting from modifying the argument
190 sorted := make([]Group, len(grs))
193 le := binary.LittleEndian
194 tmp := make([]byte, 8)
196 // Create index "sorted" in input order, used when iterating over all
197 // passwd entries (getgrent_r); keeping the original order makes
199 var indexOrig bytes.Buffer
200 for _, g := range grs {
201 le.PutUint64(tmp, offsets[toKey(g)])
205 // Create index sorted after id
206 var indexId bytes.Buffer
207 sort.Slice(sorted, func(i, j int) bool {
208 return sorted[i].Gid < sorted[j].Gid
210 for _, g := range sorted {
211 le.PutUint64(tmp, offsets[toKey(g)])
215 // Create index sorted after name
216 var indexName bytes.Buffer
217 sort.Slice(sorted, func(i, j int) bool {
218 return sorted[i].Name < sorted[j].Name
220 for _, g := range sorted {
221 le.PutUint64(tmp, offsets[toKey(g)])
226 if len(grs)*8 != indexOrig.Len() ||
227 indexOrig.Len() != indexId.Len() ||
228 indexId.Len() != indexName.Len() {
229 return fmt.Errorf("indexes have inconsistent length")
235 w.Write([]byte("NSS-CASH"))
237 le.PutUint64(tmp, GroupVersion)
240 le.PutUint64(tmp, uint64(len(grs)))
244 le.PutUint64(tmp, offset)
247 offset += uint64(indexOrig.Len())
248 le.PutUint64(tmp, offset)
251 offset += uint64(indexId.Len())
252 le.PutUint64(tmp, offset)
255 offset += uint64(indexName.Len())
256 le.PutUint64(tmp, offset)
259 _, err := indexOrig.WriteTo(w)
263 _, err = indexId.WriteTo(w)
267 _, err = indexName.WriteTo(w)
271 _, err = data.WriteTo(w)