]> ruderich.org/simon Gitweb - nsscash/nsscash.git/blob - passwd.go
nsscash: improve comments
[nsscash/nsscash.git] / passwd.go
1 // Parse /etc/passwd files and serialize them
2
3 // Copyright (C) 2019  Simon Ruderich
4 //
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.
9 //
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.
14 //
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/>.
17
18 package main
19
20 import (
21         "bufio"
22         "bytes"
23         "encoding/binary"
24         "fmt"
25         "io"
26         "sort"
27         "strconv"
28         "strings"
29
30         "github.com/pkg/errors"
31 )
32
33 // Version written in SerializePasswds()
34 const PasswdVersion = 1
35
36 type Passwd struct {
37         Name   string
38         Passwd string
39         Uid    uint64
40         Gid    uint64
41         Gecos  string
42         Dir    string
43         Shell  string
44 }
45
46 // ParsePasswds parses a file in the format of /etc/passwd and returns all
47 // entries as slice of Passwd structs.
48 func ParsePasswds(r io.Reader) ([]Passwd, error) {
49         var res []Passwd
50
51         s := bufio.NewScanner(r)
52         for s.Scan() {
53                 t := s.Text()
54
55                 x := strings.Split(t, ":")
56                 if len(x) != 7 {
57                         return nil, fmt.Errorf("invalid line %q", t)
58                 }
59
60                 uid, err := strconv.ParseUint(x[2], 10, 64)
61                 if err != nil {
62                         return nil, errors.Wrapf(err, "invalid uid in line %q", t)
63                 }
64                 gid, err := strconv.ParseUint(x[3], 10, 64)
65                 if err != nil {
66                         return nil, errors.Wrapf(err, "invalid gid in line %q", t)
67                 }
68
69                 res = append(res, Passwd{
70                         Name:   x[0],
71                         Passwd: x[1],
72                         Uid:    uid,
73                         Gid:    gid,
74                         Gecos:  x[4],
75                         Dir:    x[5],
76                         Shell:  x[6],
77                 })
78         }
79         err := s.Err()
80         if err != nil {
81                 return nil, err
82         }
83
84         return res, nil
85 }
86
87 func SerializePasswd(p Passwd) []byte {
88         // Concatenate all (NUL-terminated) strings and store the offsets
89         var data bytes.Buffer
90         data.Write([]byte(p.Name))
91         data.WriteByte(0)
92         offPasswd := uint16(data.Len())
93         data.Write([]byte(p.Passwd))
94         data.WriteByte(0)
95         offGecos := uint16(data.Len())
96         data.Write([]byte(p.Gecos))
97         data.WriteByte(0)
98         offDir := uint16(data.Len())
99         data.Write([]byte(p.Dir))
100         data.WriteByte(0)
101         offShell := uint16(data.Len())
102         data.Write([]byte(p.Shell))
103         data.WriteByte(0)
104         size := uint16(data.Len())
105
106         var res bytes.Buffer // serialized result
107         le := binary.LittleEndian
108
109         id := make([]byte, 8)
110         // uid
111         le.PutUint64(id, p.Uid)
112         res.Write(id)
113         // gid
114         le.PutUint64(id, p.Gid)
115         res.Write(id)
116
117         off := make([]byte, 2)
118         // off_passwd
119         le.PutUint16(off, offPasswd)
120         res.Write(off)
121         // off_gecos
122         le.PutUint16(off, offGecos)
123         res.Write(off)
124         // off_dir
125         le.PutUint16(off, offDir)
126         res.Write(off)
127         // off_shell
128         le.PutUint16(off, offShell)
129         res.Write(off)
130         // data_size
131         le.PutUint16(off, size)
132         res.Write(off)
133
134         res.Write(data.Bytes())
135         // We must pad each entry so that all uint64 at the beginning of the
136         // struct are 8 byte aligned
137         alignBufferTo(&res, 8)
138
139         return res.Bytes()
140 }
141
142 func SerializePasswds(w io.Writer, pws []Passwd) error {
143         // Serialize passwords and store offsets
144         var data bytes.Buffer
145         offsets := make(map[Passwd]uint64)
146         for _, p := range pws {
147                 // TODO: warn about duplicate entries
148                 offsets[p] = uint64(data.Len())
149                 data.Write(SerializePasswd(p))
150         }
151
152         // Copy to prevent sorting from modifying the argument
153         sorted := make([]Passwd, len(pws))
154         copy(sorted, pws)
155
156         le := binary.LittleEndian
157         tmp := make([]byte, 8)
158
159         // Create index "sorted" in input order, used when iterating over all
160         // passwd entries (getpwent_r); keeping the original order makes
161         // debugging easier
162         var indexOrig bytes.Buffer
163         for _, p := range pws {
164                 le.PutUint64(tmp, offsets[p])
165                 indexOrig.Write(tmp)
166         }
167
168         // Create index sorted after id
169         var indexId bytes.Buffer
170         sort.Slice(sorted, func(i, j int) bool {
171                 return sorted[i].Uid < sorted[j].Uid
172         })
173         for _, p := range sorted {
174                 le.PutUint64(tmp, offsets[p])
175                 indexId.Write(tmp)
176         }
177
178         // Create index sorted after name
179         var indexName bytes.Buffer
180         sort.Slice(sorted, func(i, j int) bool {
181                 return sorted[i].Name < sorted[j].Name
182         })
183         for _, p := range sorted {
184                 le.PutUint64(tmp, offsets[p])
185                 indexName.Write(tmp)
186         }
187
188         // Sanity check
189         if len(pws)*8 != indexOrig.Len() ||
190                 indexOrig.Len() != indexId.Len() ||
191                 indexId.Len() != indexName.Len() {
192                 return fmt.Errorf("indexes have inconsistent length")
193         }
194
195         // Write result
196
197         // magic
198         w.Write([]byte("NSS-CASH"))
199         // version
200         le.PutUint64(tmp, PasswdVersion)
201         w.Write(tmp)
202         // count
203         le.PutUint64(tmp, uint64(len(pws)))
204         w.Write(tmp)
205         // off_orig_index
206         offset := uint64(0)
207         le.PutUint64(tmp, offset)
208         w.Write(tmp)
209         // off_id_index
210         offset += uint64(indexOrig.Len())
211         le.PutUint64(tmp, offset)
212         w.Write(tmp)
213         // off_name_index
214         offset += uint64(indexId.Len())
215         le.PutUint64(tmp, offset)
216         w.Write(tmp)
217         // off_data
218         offset += uint64(indexName.Len())
219         le.PutUint64(tmp, offset)
220         w.Write(tmp)
221
222         _, err := indexOrig.WriteTo(w)
223         if err != nil {
224                 return err
225         }
226         _, err = indexId.WriteTo(w)
227         if err != nil {
228                 return err
229         }
230         _, err = indexName.WriteTo(w)
231         if err != nil {
232                 return err
233         }
234         _, err = data.WriteTo(w)
235         if err != nil {
236                 return err
237         }
238
239         return nil
240 }