Implement fetching dependencies in QUICHE depstool
PiperOrigin-RevId: 541021571
diff --git a/depstool/deps/fetch.go b/depstool/deps/fetch.go
new file mode 100644
index 0000000..39fc0ee
--- /dev/null
+++ b/depstool/deps/fetch.go
@@ -0,0 +1,70 @@
+package deps
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "path"
+)
+
+func fetchIntoFile(url string, file *os.File) error {
+ resp, err := http.Get(url)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ _, err = io.Copy(file, resp.Body)
+ return err
+}
+
+func fileSHA256(file *os.File) (string, error) {
+ file.Seek(0, 0)
+ hasher := sha256.New()
+ if _, err := io.Copy(hasher, file); err != nil {
+ return "", nil
+ }
+ return hex.EncodeToString(hasher.Sum(nil)), nil
+}
+
+// FetchURL fetches the specified URL into the specified file path, and returns
+// the SHA-256 hash of the file fetched.
+func FetchURL(url string, path string) (string, error) {
+ file, err := os.Create(path)
+ if err != nil {
+ return "", err
+ }
+ defer file.Close()
+
+ if err = fetchIntoFile(url, file); err != nil {
+ os.Remove(path)
+ return "", err
+ }
+
+ checksum, err := fileSHA256(file)
+ if err != nil {
+ os.Remove(path)
+ return "", err
+ }
+
+ return checksum, nil
+}
+
+// FetchEntry retrieves an existing WORKSPACE file entry into a specified directory,
+// verifies its checksum, and then returns the full path to the resulting file.
+func FetchEntry(entry *Entry, dir string) (string, error) {
+ filename := path.Join(dir, entry.SHA256+".tar.gz")
+ checksum, err := FetchURL(entry.URL, filename)
+ if err != nil {
+ return "", err
+ }
+
+ if checksum != entry.SHA256 {
+ os.Remove(filename)
+ return "", fmt.Errorf("SHA-256 mismatch: expected %s, got %s", entry.SHA256, checksum)
+ }
+
+ return filename, nil
+}
diff --git a/depstool/deps/fetch_test.go b/depstool/deps/fetch_test.go
new file mode 100644
index 0000000..07c161e
--- /dev/null
+++ b/depstool/deps/fetch_test.go
@@ -0,0 +1,55 @@
+package deps
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "os"
+ "testing"
+)
+
+func serveTestString(w http.ResponseWriter, _ *http.Request) {
+ io.WriteString(w, "test")
+}
+
+func TestFetch(t *testing.T) {
+ http.HandleFunc("/test", serveTestString)
+
+ listener, err := net.Listen("tcp", ":0")
+ if err != nil {
+ t.Fatal(err)
+ }
+ port := listener.Addr().(*net.TCPAddr).Port
+ url := fmt.Sprintf("http://localhost:%d/test", port)
+ go http.Serve(listener, nil)
+
+ tmpdir, err := os.MkdirTemp("", "*")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tmpdir)
+
+ entry := Entry{
+ Name: "com_example",
+ SHA256: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", // SHA256("test")
+ Prefix: "",
+ URL: url,
+ LastUpdated: "2022-05-18",
+ }
+
+ filename, err := FetchEntry(&entry, tmpdir)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ contents, err := os.ReadFile(filename)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !bytes.Equal(contents, []byte("test")) {
+ t.Errorf("Expected to get 'test', instead got '%s'", contents)
+ }
+}
diff --git a/depstool/depstool.go b/depstool/depstool.go
index 8ada2d9..7733841 100644
--- a/depstool/depstool.go
+++ b/depstool/depstool.go
@@ -59,6 +59,22 @@
os.Exit(0)
}
+func fetch(path string, contents []byte, outdir string) {
+ rules, err := deps.ParseHTTPArchiveRules(contents)
+ if err != nil {
+ log.Fatalf("Failed to parse %s: %v", path, err)
+ }
+
+ for _, rule := range rules {
+ log.Printf("Fetching %s into %s...", rule.Name, outdir)
+ outfile, err := deps.FetchEntry(rule, outdir)
+ if err != nil {
+ log.Fatalf("Failed to fetch %s: %s", rule.Name, err)
+ }
+ log.Printf("Successfully fetched %s into %s", rule.Name, outfile)
+ }
+}
+
func usage() {
fmt.Fprintf(flag.CommandLine.Output(), `
usage: depstool [WORKSPACE file] [subcommand]
@@ -66,6 +82,7 @@
Available subcommands:
list Lists all of the rules in the file
validate Validates that the WORKSPACE file is parsable
+ fetch Fetches all dependencies into the specified directory
If no subcommand is specified, "list" is assumed.
`)
@@ -93,6 +110,8 @@
list(path, contents)
case "validate":
validate(path, contents)
+ case "fetch":
+ fetch(path, contents, flag.Arg(2))
default:
log.Fatalf("Unknown command: %s", subcommand)
}