// meta4ra -- Metalink 4.0 utilities // Copyright (C) 2021-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 . // Metalink 4.0 checker package main import ( "bufio" "encoding/xml" "flag" "fmt" "io" "io/fs" "log" "os" "path" "strings" "go.stargrave.org/meta4ra" ) func main() { hashes := flag.String("hashes", strings.Join(meta4ra.HashesDefault, ","), "hash-name:command-s") extractSig := flag.Bool("extract-sig", false, "Extract signature files") metaPath := flag.String("meta4", "file.meta4", "Metalink file") flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] [FILE ...]\n", os.Args[0]) flag.PrintDefaults() fmt.Fprint(flag.CommandLine.Output(), ` If no FILEs are specified, then all s from metalink are searched and verified if they exist. Otherwise only specified FILEs are checked. If you want to skip any verification (for example only to validate the format and -extract-sig, then you can just specify empty ("") FILE. `) } flag.Parse() log.SetFlags(log.Lshortfile) data, err := os.ReadFile(*metaPath) if err != nil { log.Fatalln(err) } var meta meta4ra.Metalink err = xml.Unmarshal(data, &meta) if err != nil { log.Fatalln(err) } toCheck := make(map[string]string) for _, fn := range flag.Args() { toCheck[path.Base(fn)] = fn } bad := false for _, f := range meta.Files { for _, sig := range f.Signature { if !*extractSig { continue } var fn string switch sig.MediaType { case meta4ra.SigMediaTypePGP: fn = f.Name + ".asc" case meta4ra.SigMediaTypeSSH: fn = f.Name + ".sig" } if fn == "" { continue } if err = os.WriteFile( fn, []byte(strings.TrimPrefix(sig.Signature, "\n")), fs.FileMode(0666), ); err != nil { fmt.Println("Error:", f.Name, "can not save signature:", err) bad = true } } hasher := meta4ra.NewHasher(*hashes) var hashTheir string var hashName string for i, name := range hasher.Names { for _, h := range f.Hashes { if h.Type == name { hasher.Names = []string{name} hasher.Cmds = append(hasher.Cmds[:0], hasher.Cmds[i]) hasher.Ins = append(hasher.Ins[:0], hasher.Ins[i]) hasher.Outs = append(hasher.Outs[:0], hasher.Outs[i]) hashName = name hashTheir = h.Hash goto HashFound } } } log.Fatalln("no common hashes found for:", f.Name) HashFound: fullPath := toCheck[f.Name] if !(len(toCheck) == 0 || fullPath != "") { continue } if fullPath == "" { fullPath = f.Name } fd, err := os.Open(fullPath) if err != nil { fmt.Println("Error:", f.Name, err) bad = true continue } hasher.Start() _, err = io.Copy(hasher, bufio.NewReaderSize(fd, 1<<20)) fd.Close() sums := hasher.Sums() if err != nil { fmt.Println("Error:", f.Name, err) bad = true continue } hashOur := sums[0].Hash if hashOur == hashTheir { fmt.Println(f.Name, hashName, "good") } else { fmt.Println( "Hash does not match:", f.Name, hashName, "our:", hashOur, "their:", hashTheir, ) bad = true continue } } if bad { os.Exit(1) } }