]> Cypherpunks.ru repositories - goredo.git/blob - dep.go
Binary format and many optimisations
[goredo.git] / dep.go
1 /*
2 goredo -- djb's redo implementation on pure Go
3 Copyright (C) 2020-2023 Sergey Matveev <stargrave@stargrave.org>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, version 3 of the License.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 // Dependencies saver
19
20 package main
21
22 import (
23         "bufio"
24         "bytes"
25         "encoding/binary"
26         "errors"
27         "io"
28         "os"
29         "path"
30
31         "github.com/google/uuid"
32         "lukechampine.com/blake3"
33 )
34
35 const (
36         BinMagic     = "GOREDO"
37         BinVersionV1 = 0x01
38         UUIDLen      = 16
39
40         DepTypeIfcreate      = 0x01
41         DepTypeIfchange      = 0x02
42         DepTypeAlways        = 0x03
43         DepTypeStamp         = 0x04
44         DepTypeIfchangeDummy = 0x05
45 )
46
47 var (
48         DirPrefix string
49         DepCwd    string
50
51         IfchangeCache = make(map[string][]*Ifchange)
52         DepCache      = make(map[string]*Dep)
53
54         NullUUID uuid.UUID
55 )
56
57 func chunkWrite(in []byte) (out []byte) {
58         l := len(in) + 2
59         if l > 1<<16 {
60                 panic("too long")
61         }
62         out = make([]byte, l)
63         binary.BigEndian.PutUint16(out[:2], uint16(l))
64         copy(out[2:], in)
65         return
66 }
67
68 type Ifchange struct {
69         tgt  *Tgt
70         meta string
71 }
72
73 func (ifchange *Ifchange) Inode() Inode {
74         return Inode(ifchange.meta[:InodeLen])
75 }
76
77 func (ifchange *Ifchange) Hash() Hash {
78         return Hash(ifchange.meta[InodeLen:])
79 }
80
81 type Dep struct {
82         build     uuid.UUID
83         always    bool
84         stamp     Hash
85         ifcreates []*Tgt
86         ifchanges []*Ifchange
87 }
88
89 func ifcreate(w io.Writer, fdDepName string, tgt string) (err error) {
90         tracef(CDebug, "ifcreate: %s <- %s", fdDepName, tgt)
91         _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(append(
92                 []byte{DepTypeIfcreate}, []byte(tgt)...,
93         ))))
94         return
95 }
96
97 func always(w io.Writer, fdDepName string) (err error) {
98         tracef(CDebug, "always: %s", fdDepName)
99         _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(
100                 []byte{DepTypeAlways},
101         )))
102         return
103 }
104
105 func stamp(w io.Writer, fdDepName string, hsh Hash) (err error) {
106         tracef(CDebug, "stamp: %s <- %s", fdDepName, hsh)
107         _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(append(
108                 []byte{DepTypeStamp}, []byte(hsh)...,
109         ))))
110         return
111 }
112
113 func fileHash(fd io.Reader) (Hash, error) {
114         h := blake3.New(HashLen, nil)
115         if _, err := io.Copy(h, bufio.NewReader(fd)); err != nil {
116                 return "", err
117         }
118         return Hash(h.Sum(nil)), nil
119 }
120
121 func depWrite(w io.Writer, fdDepName, cwd string, tgt *Tgt, hsh Hash) (err error) {
122         tracef(CDebug, "ifchange: %s <- %s", fdDepName, tgt)
123         fd, err := os.Open(tgt.a)
124         if err != nil {
125                 return ErrLine(err)
126         }
127         defer fd.Close()
128         inode, isDir, err := inodeFromFileByFd(fd)
129         if err != nil {
130                 return ErrLine(err)
131         }
132         if isDir {
133                 return nil
134         }
135         if hsh == "" {
136                 hsh, err = fileHash(fd)
137                 if err != nil {
138                         return ErrLine(err)
139                 }
140         }
141         _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(bytes.Join([][]byte{
142                 {DepTypeIfchange},
143                 []byte(inode),
144                 []byte(hsh),
145                 []byte(tgt.RelTo(cwd)),
146         }, nil))))
147         return
148 }
149
150 func depWriteDummy(w io.Writer, fdDepName, tgtRel string) (err error) {
151         tracef(CDebug, "ifchange: %s <- %s (non-existing)", fdDepName, tgtRel)
152         _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(bytes.Join([][]byte{
153                 {DepTypeIfchangeDummy},
154                 []byte(tgtRel),
155         }, nil))))
156         return
157 }
158
159 func depsWrite(fdDep *os.File, tgts []*Tgt) error {
160         if fdDep == nil {
161                 tracef(CDebug, "no opened fdDep: %s", tgts)
162                 return nil
163         }
164         var err error
165         var cwd string
166         fdDepW := bufio.NewWriter(fdDep)
167         for _, tgt := range tgts {
168                 cwd = Cwd
169                 if DepCwd != "" && Cwd != DepCwd {
170                         cwd = DepCwd
171                 }
172                 tgtDir := path.Join(cwd, DirPrefix)
173                 if _, errStat := os.Stat(tgt.a); errStat == nil {
174                         err = ErrLine(depWrite(fdDepW, fdDep.Name(), tgtDir, tgt, ""))
175                 } else {
176                         tgtRel := tgt.RelTo(tgtDir)
177                         err = ErrLine(depWriteDummy(fdDepW, fdDep.Name(), tgtRel))
178                 }
179                 if err != nil {
180                         return err
181                 }
182         }
183         return fdDepW.Flush()
184 }
185
186 func depHeadParse(data []byte) (build uuid.UUID, tail []byte, err error) {
187         if len(data) < len(BinMagic)+1+UUIDLen {
188                 err = errors.New("too short header")
189                 return
190         }
191         if !bytes.Equal(data[:len(BinMagic)], []byte(BinMagic)) {
192                 err = errors.New("bad magic")
193                 return
194         }
195         data = data[len(BinMagic):]
196         switch data[0] {
197         case BinVersionV1:
198         default:
199                 err = errors.New("unknown version")
200                 return
201         }
202         build = uuid.Must(uuid.FromBytes(data[1 : 1+UUIDLen]))
203         tail = data[1+UUIDLen:]
204         return
205 }
206
207 func chunkRead(data []byte) (typ byte, chunk []byte, tail []byte, err error) {
208         if len(data) < 2 {
209                 err = errors.New("no length")
210                 return
211         }
212         l := binary.BigEndian.Uint16(data[:2])
213         if l == 0 {
214                 err = errors.New("zero length chunk")
215                 return
216         }
217         if len(data) < int(l) {
218                 err = errors.New("not enough data")
219                 return
220         }
221         typ, chunk, tail = data[2], data[3:l], data[l:]
222         return
223 }
224
225 func depBinIfchangeParse(tgt *Tgt, chunk []byte) (*Ifchange, string, error) {
226         if len(chunk) < InodeLen+HashLen+1 {
227                 return nil, "", errors.New("too short \"ifchange\" format")
228         }
229         name := string(chunk[InodeLen+HashLen:])
230         meta := string(chunk[:InodeLen+HashLen])
231
232         tgtH, _ := pathSplit(tgt.a)
233         ifchange := &Ifchange{tgt: NewTgt(path.Join(tgtH, name)), meta: meta}
234         cachedFound := false
235         for _, cached := range IfchangeCache[ifchange.tgt.rel] {
236                 if ifchange.meta == cached.meta {
237                         ifchange = cached
238                         cachedFound = true
239                         break
240                 }
241         }
242         if IfchangeCache != nil && !cachedFound {
243                 IfchangeCache[ifchange.tgt.rel] = append(IfchangeCache[ifchange.tgt.rel], ifchange)
244         }
245         return ifchange, name, nil
246 }
247
248 func depParse(tgt *Tgt, data []byte) (*Dep, error) {
249         build, data, err := depHeadParse(data)
250         if err != nil {
251                 return nil, err
252         }
253         dep := Dep{build: build}
254         var typ byte
255         var chunk []byte
256         for len(data) > 0 {
257                 typ, chunk, data, err = chunkRead(data)
258                 if err != nil {
259                         return nil, ErrLine(err)
260                 }
261                 switch typ {
262                 case DepTypeAlways:
263                         if len(chunk) != 0 {
264                                 return nil, ErrLine(errors.New("bad \"always\" format"))
265                         }
266                         dep.always = true
267                 case DepTypeStamp:
268                         if len(chunk) != HashLen {
269                                 return nil, ErrLine(errors.New("bad \"stamp\" format"))
270                         }
271                         dep.stamp = Hash(chunk)
272                 case DepTypeIfcreate:
273                         if len(chunk) < 1 {
274                                 return nil, ErrLine(errors.New("too short \"ifcreate\" format"))
275                         }
276                         tgtH, _ := pathSplit(tgt.a)
277                         dep.ifcreates = append(dep.ifcreates, NewTgt(path.Join(tgtH, string(chunk))))
278                 case DepTypeIfchange:
279                         ifchange, _, err := depBinIfchangeParse(tgt, chunk)
280                         if err != nil {
281                                 return nil, ErrLine(err)
282                         }
283                         dep.ifchanges = append(dep.ifchanges, ifchange)
284                 case DepTypeIfchangeDummy:
285                         if len(chunk) < 1 {
286                                 return nil, ErrLine(errors.New("too short \"ifchange\" format"))
287                         }
288                         dep.ifchanges = append(dep.ifchanges, &Ifchange{tgt: NewTgt(string(chunk))})
289                 default:
290                         return nil, ErrLine(errors.New("unknown type"))
291                 }
292         }
293         return &dep, nil
294 }
295
296 func depRead(tgt *Tgt) (*Dep, error) {
297         data, err := os.ReadFile(tgt.dep)
298         if err != nil {
299                 return nil, err
300         }
301         return depParse(tgt, data)
302 }
303
304 func depReadOnlyIfchanges(pth string) (ifchanges []string, err error) {
305         data, err := os.ReadFile(pth)
306         if err != nil {
307                 return
308         }
309         _, data, err = depHeadParse(data)
310         if err != nil {
311                 return nil, err
312         }
313         var typ byte
314         var chunk []byte
315         var tgt string
316         tgtDummy := NewTgt("")
317         for len(data) > 0 {
318                 typ, chunk, data, err = chunkRead(data)
319                 if err != nil {
320                         return nil, ErrLine(err)
321                 }
322                 switch typ {
323                 case DepTypeIfchange:
324                         _, tgt, err = depBinIfchangeParse(tgtDummy, chunk)
325                         if err != nil {
326                                 return nil, ErrLine(err)
327                         }
328                         ifchanges = append(ifchanges, tgt)
329                 case DepTypeIfchangeDummy:
330                         ifchanges = append(ifchanges, string(chunk))
331                 }
332         }
333         return
334 }
335
336 func depBuildRead(pth string) (uuid.UUID, error) {
337         fd, err := os.Open(pth)
338         if err != nil {
339                 return NullUUID, err
340         }
341         data := make([]byte, len(BinMagic)+1+UUIDLen)
342         _, err = io.ReadFull(fd, data)
343         fd.Close()
344         if err != nil {
345                 return NullUUID, err
346         }
347         build, _, err := depHeadParse(data)
348         return build, err
349 }
350
351 func depBuildWrite(w io.Writer, build uuid.UUID) (err error) {
352         _, err = io.Copy(w, bytes.NewBuffer(bytes.Join([][]byte{
353                 []byte(BinMagic),
354                 {BinVersionV1},
355                 build[:],
356         }, nil)))
357         return
358 }