| // Copyright 2024 The BoringSSL Authors | 
 | // | 
 | // Licensed under the Apache License, Version 2.0 (the "License"); | 
 | // you may not use this file except in compliance with the License. | 
 | // You may obtain a copy of the License at | 
 | // | 
 | //     https://www.apache.org/licenses/LICENSE-2.0 | 
 | // | 
 | // Unless required by applicable law or agreed to in writing, software | 
 | // distributed under the License is distributed on an "AS IS" BASIS, | 
 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
 | // See the License for the specific language governing permissions and | 
 | // limitations under the License. | 
 |  | 
 | package main | 
 |  | 
 | import ( | 
 | 	"bytes" | 
 | 	"cmp" | 
 | 	"crypto/sha256" | 
 | 	"fmt" | 
 | 	"os/exec" | 
 | 	"slices" | 
 | 	"strings" | 
 | 	"sync" | 
 | ) | 
 |  | 
 | type treeEntryMode int | 
 |  | 
 | const ( | 
 | 	treeEntryRegular treeEntryMode = iota | 
 | 	treeEntryExecutable | 
 | 	treeEntrySymlink | 
 | ) | 
 |  | 
 | func (m treeEntryMode) String() string { | 
 | 	switch m { | 
 | 	case treeEntryRegular: | 
 | 		return "regular file" | 
 | 	case treeEntryExecutable: | 
 | 		return "executable file" | 
 | 	case treeEntrySymlink: | 
 | 		return "symbolic link" | 
 | 	} | 
 | 	panic(fmt.Sprintf("unknown mode %d", m)) | 
 | } | 
 |  | 
 | type treeEntry struct { | 
 | 	path   string | 
 | 	mode   treeEntryMode | 
 | 	sha256 []byte | 
 | } | 
 |  | 
 | func sortTree(tree []treeEntry) { | 
 | 	slices.SortFunc(tree, func(a, b treeEntry) int { return cmp.Compare(a.path, b.path) }) | 
 | } | 
 |  | 
 | func compareTrees(got, want []treeEntry) error { | 
 | 	// Check for duplicate files. | 
 | 	for i := 0; i < len(got)-1; i++ { | 
 | 		if got[i].path == got[i+1].path { | 
 | 			return fmt.Errorf("duplicate file %q in archive", got[i].path) | 
 | 		} | 
 | 	} | 
 |  | 
 | 	// Check for differences between the two trees. | 
 | 	for i := 0; i < len(got) && i < len(want); i++ { | 
 | 		if got[i].path == want[i].path { | 
 | 			if got[i].mode != want[i].mode { | 
 | 				return fmt.Errorf("file %q was a %s but should have been a %s", got[i].path, got[i].mode, want[i].mode) | 
 | 			} | 
 | 			if !bytes.Equal(got[i].sha256, want[i].sha256) { | 
 | 				return fmt.Errorf("hash of %q was %x but should have been %x", got[i].path, got[i].sha256, want[i].sha256) | 
 | 			} | 
 | 		} else if got[i].path < want[i].path { | 
 | 			return fmt.Errorf("unexpected file %q", got[i].path) | 
 | 		} else { | 
 | 			return fmt.Errorf("missing file %q", want[i].path) | 
 | 		} | 
 | 	} | 
 | 	if len(want) < len(got) { | 
 | 		return fmt.Errorf("unexpected file %q", got[len(want)].path) | 
 | 	} | 
 | 	if len(got) < len(want) { | 
 | 		return fmt.Errorf("missing file %q", want[len(got)].path) | 
 | 	} | 
 | 	return nil | 
 | } | 
 |  | 
 | type gitTreeEntry struct { | 
 | 	path       string | 
 | 	mode       treeEntryMode | 
 | 	objectName string | 
 | } | 
 |  | 
 | func gitListTree(treeish string) ([]gitTreeEntry, error) { | 
 | 	var stdout, stderr bytes.Buffer | 
 | 	cmd := exec.Command("git", "ls-tree", "-r", "-z", treeish) | 
 | 	cmd.Stdout = &stdout | 
 | 	cmd.Stderr = &stderr | 
 | 	if err := cmd.Run(); err != nil { | 
 | 		return nil, fmt.Errorf("error listing git tree %q: %w\n%s\n", treeish, err, stderr.String()) | 
 | 	} | 
 | 	lines := strings.Split(stdout.String(), "\x00") | 
 | 	ret := make([]gitTreeEntry, 0, len(lines)) | 
 | 	for _, line := range lines { | 
 | 		if len(line) == 0 { | 
 | 			continue | 
 | 		} | 
 |  | 
 | 		idx := strings.IndexByte(line, '\t') | 
 | 		if idx < 0 { | 
 | 			return nil, fmt.Errorf("could not parse ls-tree output %q", line) | 
 | 		} | 
 |  | 
 | 		info, path := line[:idx], line[idx+1:] | 
 | 		infos := strings.Split(info, " ") | 
 | 		if len(infos) != 3 { | 
 | 			return nil, fmt.Errorf("could not parse ls-tree output %q", line) | 
 | 		} | 
 |  | 
 | 		perms, objectType, objectName := infos[0], infos[1], infos[2] | 
 | 		if objectType != "blob" { | 
 | 			return nil, fmt.Errorf("unexpected object type in ls-tree output %q", line) | 
 | 		} | 
 |  | 
 | 		var mode treeEntryMode | 
 | 		switch perms { | 
 | 		case "100644": | 
 | 			mode = treeEntryRegular | 
 | 		case "100755": | 
 | 			mode = treeEntryExecutable | 
 | 		case "120000": | 
 | 			mode = treeEntrySymlink | 
 | 		default: | 
 | 			return nil, fmt.Errorf("unexpected file mode in ls-tree output %q", line) | 
 | 		} | 
 |  | 
 | 		ret = append(ret, gitTreeEntry{path: path, mode: mode, objectName: objectName}) | 
 | 	} | 
 | 	return ret, nil | 
 | } | 
 |  | 
 | func gitHashBlob(objectName string) ([]byte, error) { | 
 | 	h := sha256.New() | 
 | 	var stderr bytes.Buffer | 
 | 	cmd := exec.Command("git", "cat-file", "blob", objectName) | 
 | 	cmd.Stdout = h | 
 | 	cmd.Stderr = &stderr | 
 | 	if err := cmd.Run(); err != nil { | 
 | 		return nil, fmt.Errorf("error hashing git object %q: %w\n%s\n", objectName, err, stderr.String()) | 
 | 	} | 
 | 	return h.Sum(nil), nil | 
 | } | 
 |  | 
 | func gitHashTree(s *stepPrinter, treeish string) ([]treeEntry, error) { | 
 | 	gitTree, err := gitListTree(treeish) | 
 | 	if err != nil { | 
 | 		return nil, err | 
 | 	} | 
 |  | 
 | 	s.setTotal(len(gitTree)) | 
 |  | 
 | 	// Hashing objects one by one is slow, so parallelize. Ideally we could | 
 | 	// just use the object name, but git uses SHA-1, so checking a SHA-265 | 
 | 	// hash seems prudent. | 
 | 	var workerErr error | 
 | 	var workerLock sync.Mutex | 
 |  | 
 | 	var wg sync.WaitGroup | 
 | 	jobs := make(chan gitTreeEntry, *numWorkers) | 
 | 	results := make(chan treeEntry, *numWorkers) | 
 | 	for i := 0; i < *numWorkers; i++ { | 
 | 		wg.Add(1) | 
 | 		go func() { | 
 | 			defer wg.Done() | 
 | 			for job := range jobs { | 
 | 				workerLock.Lock() | 
 | 				shouldStop := workerErr != nil | 
 | 				workerLock.Unlock() | 
 | 				if shouldStop { | 
 | 					break | 
 | 				} | 
 |  | 
 | 				sha256, err := gitHashBlob(job.objectName) | 
 | 				if err != nil { | 
 | 					workerLock.Lock() | 
 | 					if workerErr == nil { | 
 | 						workerErr = err | 
 | 					} | 
 | 					workerLock.Unlock() | 
 | 					break | 
 | 				} | 
 |  | 
 | 				results <- treeEntry{path: job.path, mode: job.mode, sha256: sha256} | 
 | 			} | 
 | 		}() | 
 | 	} | 
 |  | 
 | 	go func() { | 
 | 		for _, job := range gitTree { | 
 | 			jobs <- job | 
 | 		} | 
 | 		close(jobs) | 
 | 		wg.Wait() | 
 | 		close(results) | 
 | 	}() | 
 |  | 
 | 	tree := make([]treeEntry, 0, len(gitTree)) | 
 | 	for result := range results { | 
 | 		s.addProgress(1) | 
 | 		tree = append(tree, result) | 
 | 	} | 
 |  | 
 | 	if workerErr != nil { | 
 | 		return nil, workerErr | 
 | 	} | 
 |  | 
 | 	if len(tree) != len(gitTree) { | 
 | 		panic("input and output sizes did not match") | 
 | 	} | 
 |  | 
 | 	sortTree(tree) | 
 | 	return tree, nil | 
 | } |