X-Git-Url: http://www.git.cypherpunks.ru/?a=blobdiff_plain;f=dep.go;h=77c248ec9026c6ca1e325c11468e643c030654ed;hb=HEAD;hp=0f62d30243f0256e2a0be731de93f010be45052a;hpb=67e92d5a298516292bbbee1f7714708cdc0766f8;p=goredo.git diff --git a/dep.go b/dep.go index 0f62d30..77c248e 100644 --- a/dep.go +++ b/dep.go @@ -1,19 +1,17 @@ -/* -goredo -- redo implementation on pure Go -Copyright (C) 2020 Sergey Matveev - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, version 3 of the License. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ +// goredo -- djb's redo implementation on pure Go +// Copyright (C) 2020-2024 Sergey Matveev +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . // Dependencies saver @@ -21,126 +19,338 @@ package main import ( "bufio" - "encoding/hex" - "fmt" + "bytes" + "encoding/binary" + "errors" "io" "os" "path" - "strings" - "syscall" - "go.cypherpunks.ru/recfile" - "golang.org/x/crypto/blake2b" + "github.com/google/uuid" + "lukechampine.com/blake3" ) -const EnvNoHash = "REDO_NO_HASH" +const ( + BinMagic = "GOREDO" + BinVersionV1 = 0x01 + UUIDLen = 16 -var NoHash bool + DepTypeIfcreate = 0x01 + DepTypeIfchange = 0x02 + DepTypeAlways = 0x03 + DepTypeStamp = 0x04 + DepTypeIfchangeNonex = 0x05 +) -func recfileWrite(fdDep *os.File, fields ...recfile.Field) error { - w := recfile.NewWriter(fdDep) - if _, err := w.RecordStart(); err != nil { - return err - } - if _, err := w.WriteFields(fields...); err != nil { - return err +var ( + DirPrefix string + DepCwd string + + IfchangeCache = make(map[string][]*Ifchange) + DepCache = make(map[string]*Dep) + + NullUUID uuid.UUID +) + +func chunkWrite(in []byte) (out []byte) { + l := len(in) + 2 + if l > 1<<16 { + panic("too long") } - return nil + out = make([]byte, l) + binary.BigEndian.PutUint16(out[:2], uint16(l)) + copy(out[2:], in) + return } -func ifcreate(fdDep *os.File, tgt string) error { - trace(CDebug, "ifcreate: %s <- %s", fdDep.Name(), tgt) - return recfileWrite( - fdDep, - recfile.Field{Name: "Type", Value: DepTypeIfcreate}, - recfile.Field{Name: "Target", Value: tgt}, - ) +type Ifchange struct { + tgt *Tgt + meta [InodeLen + HashLen]byte } -func always(fdDep *os.File) error { - trace(CDebug, "always: %s", fdDep.Name()) - return recfileWrite(fdDep, recfile.Field{Name: "Type", Value: DepTypeAlways}) +func (ifchange *Ifchange) Inode() *Inode { + inode := Inode(ifchange.meta[:InodeLen]) + return &inode } -func stamp(fdDep, src *os.File) error { - var hsh string - hsh, err := fileHash(os.Stdin) - if err != nil { - return err - } - trace(CDebug, "stamp: %s <- %s", fdDep.Name(), hsh) - return recfileWrite( - fdDep, - recfile.Field{Name: "Type", Value: DepTypeStamp}, - recfile.Field{Name: "Hash", Value: hsh}, - ) +func (ifchange *Ifchange) Hash() Hash { + return Hash(ifchange.meta[InodeLen:]) } -func fileCtime(fd *os.File) (string, error) { - fi, err := fd.Stat() - if err != nil { - return "", err - } - stat := fi.Sys().(*syscall.Stat_t) - sec, nsec := stat.Ctimespec.Unix() - return fmt.Sprintf("%d.%d", sec, nsec), nil +type Dep struct { + build uuid.UUID + always bool + stamp Hash + ifcreates []*Tgt + ifchanges []*Ifchange } -func fileHash(fd *os.File) (string, error) { - h, err := blake2b.New256(nil) - if err != nil { - panic(err) - } - if _, err = io.Copy(h, bufio.NewReader(fd)); err != nil { +func ifcreate(w io.Writer, fdDepName string, tgt string) (err error) { + tracef(CDebug, "ifcreate: %s <- %s", fdDepName, tgt) + _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(append( + []byte{DepTypeIfcreate}, []byte(tgt)..., + )))) + return +} + +func always(w io.Writer, fdDepName string) (err error) { + tracef(CDebug, "always: %s", fdDepName) + _, err = io.Copy(w, bytes.NewBuffer(chunkWrite( + []byte{DepTypeAlways}, + ))) + return +} + +func stamp(w io.Writer, fdDepName string, hsh Hash) (err error) { + tracef(CDebug, "stamp: %s <- %s", fdDepName, hsh) + _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(append( + []byte{DepTypeStamp}, []byte(hsh)..., + )))) + return +} + +func fileHash(fd io.Reader) (Hash, error) { + h := blake3.New(HashLen, nil) + if _, err := io.Copy(h, bufio.NewReader(fd)); err != nil { return "", err } - return hex.EncodeToString(h.Sum(nil)), nil + return Hash(h.Sum(nil)), nil } -func writeDep(fdDep *os.File, cwd, tgt string) error { - trace(CDebug, "ifchange: %s <- %s", fdDep.Name(), tgt) - fd, err := os.Open(path.Join(cwd, tgt)) +func depWrite(w io.Writer, fdDepName, cwd string, tgt *Tgt, hsh Hash) (err error) { + tracef(CDebug, "ifchange: %s <- %s", fdDepName, tgt) + fd, err := os.Open(tgt.a) if err != nil { - return err + return ErrLine(err) } defer fd.Close() - ts, err := fileCtime(fd) + inode, isDir, err := inodeFromFileByFd(fd) if err != nil { - return err + return ErrLine(err) } - fields := []recfile.Field{ - recfile.Field{Name: "Type", Value: DepTypeIfchange}, - recfile.Field{Name: "Target", Value: tgt}, - recfile.Field{Name: "Ctime", Value: ts}, + if isDir { + return nil } - var hsh string - if !NoHash { + if hsh == "" { hsh, err = fileHash(fd) if err != nil { - return err + return ErrLine(err) } - fields = append(fields, recfile.Field{Name: "Hash", Value: hsh}) } - return recfileWrite(fdDep, fields...) + _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(bytes.Join([][]byte{ + {DepTypeIfchange}, + inode[:], + []byte(hsh), + []byte(tgt.RelTo(cwd)), + }, nil)))) + return } -func writeDeps(fdDep *os.File, tgts []string) error { +func depWriteNonex(w io.Writer, fdDepName, tgtRel string) (err error) { + tracef(CDebug, "ifchange: %s <- %s (non-existing)", fdDepName, tgtRel) + _, err = io.Copy(w, bytes.NewBuffer(chunkWrite(bytes.Join([][]byte{ + {DepTypeIfchangeNonex}, + []byte(tgtRel), + }, nil)))) + return +} + +func depsWrite(fdDep *os.File, tgts []*Tgt) error { if fdDep == nil { - trace(CDebug, "no opened fdDep: %s", tgts) + tracef(CDebug, "no opened fdDep: %s", tgts) return nil } - ups := []string{} - upLevels := strings.Count(os.Getenv(EnvDirPrefix), "/") - for i := 0; i < upLevels; i++ { - ups = append(ups, "..") - } - up := path.Join(ups...) + var err error + var cwd string + fdDepW := bufio.NewWriter(fdDep) for _, tgt := range tgts { - if _, err := os.Stat(tgt); err == nil { - if err = writeDep(fdDep, Cwd, path.Join(up, tgt)); err != nil { - return err + cwd = Cwd + if DepCwd != "" && Cwd != DepCwd { + cwd = DepCwd + } + tgtDir := path.Join(cwd, DirPrefix) + if _, errStat := os.Stat(tgt.a); errStat == nil { + err = ErrLine(depWrite(fdDepW, fdDep.Name(), tgtDir, tgt, "")) + } else { + tgtRel := tgt.RelTo(tgtDir) + err = ErrLine(depWriteNonex(fdDepW, fdDep.Name(), tgtRel)) + } + if err != nil { + return err + } + } + return fdDepW.Flush() +} + +func depHeadParse(data []byte) (build uuid.UUID, tail []byte, err error) { + if len(data) < len(BinMagic)+1+UUIDLen { + err = errors.New("too short header") + return + } + if !bytes.Equal(data[:len(BinMagic)], []byte(BinMagic)) { + err = errors.New("bad magic") + return + } + data = data[len(BinMagic):] + switch data[0] { + case BinVersionV1: + default: + err = errors.New("unknown version") + return + } + build = uuid.Must(uuid.FromBytes(data[1 : 1+UUIDLen])) + tail = data[1+UUIDLen:] + return +} + +func chunkRead(data []byte) (typ byte, chunk []byte, tail []byte, err error) { + if len(data) < 2 { + err = errors.New("no length") + return + } + l := binary.BigEndian.Uint16(data[:2]) + if l == 0 { + err = errors.New("zero length chunk") + return + } + if len(data) < int(l) { + err = errors.New("not enough data") + return + } + typ, chunk, tail = data[2], data[3:l], data[l:] + return +} + +func depBinIfchangeParse(tgt *Tgt, chunk []byte) (*Ifchange, string, error) { + if len(chunk) < InodeLen+HashLen+1 { + return nil, "", errors.New("too short \"ifchange\" format") + } + + tgtH, _ := pathSplit(tgt.a) + name := string(chunk[InodeLen+HashLen:]) + ifchange := &Ifchange{ + tgt: NewTgt(path.Join(tgtH, name)), + meta: ([InodeLen + HashLen]byte)(chunk), + } + for _, cached := range IfchangeCache[ifchange.tgt.rel] { + if ifchange.meta == cached.meta { + return cached, name, nil + } + } + if IfchangeCache != nil { + IfchangeCache[ifchange.tgt.rel] = append(IfchangeCache[ifchange.tgt.rel], ifchange) + } + return ifchange, name, nil +} + +func depParse(tgt *Tgt, data []byte) (*Dep, error) { + build, data, err := depHeadParse(data) + if err != nil { + return nil, err + } + dep := Dep{build: build} + var typ byte + var chunk []byte + for len(data) > 0 { + typ, chunk, data, err = chunkRead(data) + if err != nil { + return nil, ErrLine(err) + } + switch typ { + case DepTypeAlways: + if len(chunk) != 0 { + return nil, ErrLine(errors.New("bad \"always\" format")) + } + dep.always = true + case DepTypeStamp: + if len(chunk) != HashLen { + return nil, ErrLine(errors.New("bad \"stamp\" format")) + } + dep.stamp = Hash(chunk) + case DepTypeIfcreate: + if len(chunk) < 1 { + return nil, ErrLine(errors.New("too short \"ifcreate\" format")) + } + tgtH, _ := pathSplit(tgt.a) + dep.ifcreates = append(dep.ifcreates, NewTgt(path.Join(tgtH, string(chunk)))) + case DepTypeIfchange: + ifchange, _, err := depBinIfchangeParse(tgt, chunk) + if err != nil { + return nil, ErrLine(err) + } + dep.ifchanges = append(dep.ifchanges, ifchange) + case DepTypeIfchangeNonex: + if len(chunk) < 1 { + return nil, ErrLine(errors.New("too short \"ifchange\" format")) + } + dep.ifchanges = append(dep.ifchanges, &Ifchange{tgt: NewTgt(string(chunk))}) + default: + return nil, ErrLine(errors.New("unknown type")) + } + } + return &dep, nil +} + +func depRead(tgt *Tgt) (*Dep, error) { + data, err := os.ReadFile(tgt.dep) + if err != nil { + return nil, err + } + return depParse(tgt, data) +} + +func depReadOnlyIfchanges(pth string) (ifchanges []string, err error) { + data, err := os.ReadFile(pth) + if err != nil { + return + } + _, data, err = depHeadParse(data) + if err != nil { + return nil, err + } + var typ byte + var chunk []byte + var tgt string + tgtDummy := NewTgt("") + for len(data) > 0 { + typ, chunk, data, err = chunkRead(data) + if err != nil { + return nil, ErrLine(err) + } + switch typ { + case DepTypeIfchange: + _, tgt, err = depBinIfchangeParse(tgtDummy, chunk) + if err != nil { + return nil, ErrLine(err) } + ifchanges = append(ifchanges, tgt) + case DepTypeIfchangeNonex: + ifchanges = append(ifchanges, string(chunk)) } } - return nil + return +} + +func depBuildRead(pth string) (uuid.UUID, error) { + fd, err := os.Open(pth) + if err != nil { + return NullUUID, err + } + data := make([]byte, len(BinMagic)+1+UUIDLen) + _, err = io.ReadFull(fd, data) + fd.Close() + if err != nil { + return NullUUID, err + } + build, _, err := depHeadParse(data) + return build, err +} + +func depBuildWrite(w io.Writer, build uuid.UUID) (err error) { + _, err = io.Copy(w, bytes.NewBuffer(bytes.Join([][]byte{ + []byte(BinMagic), + {BinVersionV1}, + build[:], + }, nil))) + return }