From f48ccbb9e20e7fb0fe383e82cd041a94d449b497 Mon Sep 17 00:00:00 2001
From: Sergey Matveev <stargrave@stargrave.org>
Date: Thu, 30 Sep 2021 14:40:15 +0300
Subject: [PATCH] meta4-check

---
 cmd/meta4-check/main.go  | 123 +++++++++++++++++++++++++++++++++++++++
 cmd/meta4-create/main.go |  94 +++++++++++-------------------
 common.go                |  72 +++++++++++++++++++++++
 3 files changed, 228 insertions(+), 61 deletions(-)
 create mode 100644 cmd/meta4-check/main.go
 create mode 100644 common.go

diff --git a/cmd/meta4-check/main.go b/cmd/meta4-check/main.go
new file mode 100644
index 0000000..3615c0e
--- /dev/null
+++ b/cmd/meta4-check/main.go
@@ -0,0 +1,123 @@
+/*
+meta4a -- Metalink 4.0 checker
+Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+// Metalink 4.0 checker
+package main
+
+import (
+	"bufio"
+	"crypto/sha256"
+	"crypto/sha512"
+	"encoding/hex"
+	"encoding/xml"
+	"flag"
+	"fmt"
+	"hash"
+	"io"
+	"io/fs"
+	"io/ioutil"
+	"log"
+	"os"
+
+	"go.stargrave.org/meta4ra"
+)
+
+func main() {
+	extractSig := flag.Bool("extract-sig", false, "Extract signature files")
+	log.SetFlags(log.Lshortfile)
+	flag.Parse()
+	sha256Hasher := sha256.New()
+	sha512Hasher := sha512.New()
+	bad := false
+	for _, metaPath := range flag.Args() {
+		data, err := ioutil.ReadFile(metaPath)
+		if err != nil {
+			log.Fatalln(err)
+		}
+		var meta meta4ra.Metalink
+		err = xml.Unmarshal(data, &meta)
+		if err != nil {
+			log.Fatalln(err)
+		}
+		for _, f := range meta.Files {
+			if f.Signature != nil && *extractSig {
+				if err = ioutil.WriteFile(
+					f.Name+".asc",
+					[]byte(f.Signature.Signature),
+					fs.FileMode(0666),
+				); err != nil {
+					fmt.Println("Error:", f.Name, "can not save signature:", err)
+					bad = true
+				}
+			}
+			var sha256Digest string
+			var sha512Digest string
+			fd, err := os.Open(f.Name)
+			if err != nil {
+				continue
+			}
+			for _, h := range f.Hashes {
+				switch h.Type {
+				case meta4ra.HashSHA256:
+					sha256Digest = h.Hash
+				case meta4ra.HashSHA512:
+					sha512Digest = h.Hash
+				}
+			}
+			var digestTheir string
+			var digestName string
+			var hasher hash.Hash
+			if sha512Digest != "" {
+				digestName = meta4ra.HashSHA512
+				digestTheir = sha512Digest
+				hasher = sha512Hasher
+			} else if sha256Digest != "" {
+				digestName = meta4ra.HashSHA256
+				digestTheir = sha256Digest
+				hasher = sha256Hasher
+			} else {
+				fd.Close()
+				fmt.Println("Error:", f.Name, "no satisfiable hash algorithm found")
+				bad = true
+				continue
+			}
+			hasher.Reset()
+			_, err = io.Copy(hasher, bufio.NewReader(fd))
+			fd.Close()
+			if err != nil {
+				fmt.Println("Error:", f.Name, err)
+				bad = true
+				continue
+			}
+			digestOur := hex.EncodeToString(hasher.Sum(nil))
+			if digestOur == digestTheir {
+				fmt.Println(f.Name, digestName, "good")
+			} else {
+				fmt.Println(
+					"Hash does not match:", f.Name, digestName,
+					"our:", digestOur,
+					"their:", digestTheir,
+				)
+				bad = true
+				continue
+			}
+		}
+	}
+	if bad {
+		os.Exit(1)
+	}
+}
diff --git a/cmd/meta4-create/main.go b/cmd/meta4-create/main.go
index 2289e8a..f11fa70 100644
--- a/cmd/meta4-create/main.go
+++ b/cmd/meta4-create/main.go
@@ -1,5 +1,5 @@
 /*
-meta4a -- Metalink 4.0 utility
+meta4a -- Metalink 4.0 creator
 Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
 
 This program is free software: you can redistribute it and/or modify
@@ -15,10 +15,11 @@ You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-// Metalink 4.0 utility
+// Metalink 4.0 creator
 package main
 
 import (
+	"bufio"
 	"crypto/sha256"
 	"crypto/sha512"
 	"encoding/hex"
@@ -30,64 +31,21 @@ import (
 	"os"
 	"path/filepath"
 	"time"
-)
 
-const (
-	Generator       = "meta4ra/0.1.0"
-	GPGSigMediaType = "application/pgp-signature"
+	"go.stargrave.org/meta4ra"
 )
 
-type Metalink struct {
-	XMLName   xml.Name  `xml:"urn:ietf:params:xml:ns:metalink metalink"`
-	Files     []File    `xml:"file"`
-	Generator string    `xml:"generator,,omitempty"`
-	Published time.Time `xml:"published,,omitempty"`
-}
-
-type File struct {
-	XMLName     xml.Name   `xml:"file"`
-	Name        string     `xml:"name,attr"`
-	Description string     `xml:"description,,omitempty"`
-	Hashes      []Hash     `xml:"hash,,omitempty"`
-	MetaURLs    []MetaURL  `xml:"metaurl,,omitempty"`
-	Signature   *Signature `xml:"signature"`
-	Size        uint64     `xml:"size,,omitempty"`
-	URLs        []URL      `xml:"url,,omitempty"`
-}
-
-type URL struct {
-	XMLName xml.Name `xml:"url"`
-	URL     string   `xml:",chardata"`
-}
-
-type Signature struct {
-	XMLName   xml.Name `xml:"signature"`
-	MediaType string   `xml:"mediatype,attr"`
-	Signature string   `xml:",cdata"`
-}
-
-type Hash struct {
-	XMLName xml.Name `xml:"hash"`
-	Type    string   `xml:"type,attr"`
-	Hash    string   `xml:",chardata"`
-}
-
-type MetaURL struct {
-	XMLName   xml.Name `xml:"metaurl"`
-	MediaType string   `xml:"mediatype,attr"`
-	URL       string   `xml:",chardata"`
-}
-
 func main() {
 	file := flag.String("file", "", "Path to file")
+	mtime := flag.String("mtime", "", "Take that file's mtime as a Published date")
 	desc := flag.String("desc", "", "Description")
 	sig := flag.String("sig", "", "Path to signature file")
 	torrent := flag.String("torrent", "", "Torrent URL")
 	log.SetFlags(log.Lshortfile)
 	flag.Parse()
-	urls := make([]URL, 0, len(flag.Args()))
+	urls := make([]meta4ra.URL, 0, len(flag.Args()))
 	for _, u := range flag.Args() {
-		urls = append(urls, URL{URL: u})
+		urls = append(urls, meta4ra.URL{URL: u})
 	}
 	fd, err := os.Open(*file)
 	if err != nil {
@@ -99,18 +57,21 @@ func main() {
 	}
 	sha256Hasher := sha256.New()
 	sha512Hasher := sha512.New()
-	_, err = io.Copy(io.MultiWriter(sha256Hasher, sha512Hasher), fd)
+	_, err = io.Copy(
+		io.MultiWriter(sha256Hasher, sha512Hasher),
+		bufio.NewReader(fd),
+	)
 	if err != nil {
 		log.Fatalln(err)
 	}
-	f := File{
+	f := meta4ra.File{
 		Name:        filepath.Base(*file),
 		Description: *desc,
 		Size:        uint64(fi.Size()),
 		URLs:        urls,
-		Hashes: []Hash{
-			{Type: "sha-256", Hash: hex.EncodeToString(sha256Hasher.Sum(nil))},
-			{Type: "sha-512", Hash: hex.EncodeToString(sha512Hasher.Sum(nil))},
+		Hashes: []meta4ra.Hash{
+			{Type: meta4ra.HashSHA256, Hash: hex.EncodeToString(sha256Hasher.Sum(nil))},
+			{Type: meta4ra.HashSHA512, Hash: hex.EncodeToString(sha512Hasher.Sum(nil))},
 		},
 	}
 	if *sig != "" {
@@ -118,18 +79,29 @@ func main() {
 		if err != nil {
 			log.Fatalln(err)
 		}
-		f.Signature = &Signature{
-			MediaType: GPGSigMediaType,
+		f.Signature = &meta4ra.Signature{
+			MediaType: meta4ra.GPGSigMediaType,
 			Signature: "\n" + string(sigData),
 		}
 	}
 	if *torrent != "" {
-		f.MetaURLs = []MetaURL{{MediaType: "torrent", URL: *torrent}}
+		f.MetaURLs = []meta4ra.MetaURL{{MediaType: "torrent", URL: *torrent}}
+	}
+	var published time.Time
+	if *mtime == "" {
+		published = time.Now()
+	} else {
+		fi, err := os.Stat(*mtime)
+		if err != nil {
+			log.Fatalln(err)
+		}
+		published = fi.ModTime()
 	}
-	m := Metalink{
-		Files:     []File{f},
-		Generator: Generator,
-		Published: time.Now().UTC().Truncate(time.Second),
+	published = published.UTC().Truncate(time.Second)
+	m := meta4ra.Metalink{
+		Files:     []meta4ra.File{f},
+		Generator: meta4ra.Generator,
+		Published: published,
 	}
 	out, err := xml.MarshalIndent(&m, "", "  ")
 	if err != nil {
diff --git a/common.go b/common.go
new file mode 100644
index 0000000..da28885
--- /dev/null
+++ b/common.go
@@ -0,0 +1,72 @@
+/*
+meta4a -- Metalink 4.0 creator
+Copyright (C) 2021 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+// Metalink 4.0 utilities
+package meta4ra
+
+import (
+	"encoding/xml"
+	"time"
+)
+
+const (
+	Generator       = "meta4ra/0.1.0"
+	GPGSigMediaType = "application/pgp-signature"
+	HashSHA256      = "sha-256"
+	HashSHA512      = "sha-512"
+)
+
+type Metalink struct {
+	XMLName   xml.Name  `xml:"urn:ietf:params:xml:ns:metalink metalink"`
+	Files     []File    `xml:"file"`
+	Generator string    `xml:"generator,,omitempty"`
+	Published time.Time `xml:"published,,omitempty"`
+}
+
+type File struct {
+	XMLName     xml.Name   `xml:"file"`
+	Name        string     `xml:"name,attr"`
+	Description string     `xml:"description,,omitempty"`
+	Hashes      []Hash     `xml:"hash,,omitempty"`
+	MetaURLs    []MetaURL  `xml:"metaurl,,omitempty"`
+	Signature   *Signature `xml:"signature"`
+	Size        uint64     `xml:"size,,omitempty"`
+	URLs        []URL      `xml:"url,,omitempty"`
+}
+
+type URL struct {
+	XMLName xml.Name `xml:"url"`
+	URL     string   `xml:",chardata"`
+}
+
+type Signature struct {
+	XMLName   xml.Name `xml:"signature"`
+	MediaType string   `xml:"mediatype,attr"`
+	Signature string   `xml:",cdata"`
+}
+
+type Hash struct {
+	XMLName xml.Name `xml:"hash"`
+	Type    string   `xml:"type,attr"`
+	Hash    string   `xml:",chardata"`
+}
+
+type MetaURL struct {
+	XMLName   xml.Name `xml:"metaurl"`
+	MediaType string   `xml:"mediatype,attr"`
+	URL       string   `xml:",chardata"`
+}
-- 
2.51.0