mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-18 00:59:38 +00:00
154 lines
3.9 KiB
Go
154 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"hash/fnv"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/photoprism/photoprism/pkg/fs"
|
|
)
|
|
|
|
// scanPathEntry stores the source line and hash for deterministic output ordering.
|
|
type scanPathEntry struct {
|
|
Hash uint64
|
|
Path string
|
|
}
|
|
|
|
// main reads scanner paths from a text file and writes a hashed Go map.
|
|
func main() {
|
|
if err := run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "scanpathsgen: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// run parses command-line arguments and executes generation.
|
|
func run() error {
|
|
inPath := flag.String("in", "resources/scan-paths.txt", "input file with one path per line")
|
|
outPath := flag.String("out", "scan-paths.go", "output Go source file")
|
|
flag.Parse()
|
|
|
|
entries, err := loadEntries(*inPath)
|
|
if err != nil {
|
|
// Allow public/community contributors to run "make generate"
|
|
// without requiring private scanner path resources.
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
source := generateSource(entries)
|
|
|
|
// Repository output directory should be readable/executable for maintainers.
|
|
//nolint:gosec // G301: Non-secret generated source path.
|
|
if err := os.MkdirAll(filepath.Dir(*outPath), 0o755); err != nil {
|
|
return fmt.Errorf("create output directory: %w", err)
|
|
}
|
|
|
|
// Generated Go source is committed to the repository and intentionally readable.
|
|
//nolint:gosec // G306: Non-secret generated source file.
|
|
if err := os.WriteFile(*outPath, source, 0o644); err != nil {
|
|
return fmt.Errorf("write output file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// loadEntries loads, validates, hashes, and sorts scan path entries.
|
|
func loadEntries(path string) ([]scanPathEntry, error) {
|
|
file, err := openInput(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open input file %s: %w", path, err)
|
|
}
|
|
defer file.Close()
|
|
|
|
dedup := make(map[string]struct{})
|
|
byHash := make(map[uint64]string)
|
|
entries := make([]scanPathEntry, 0, 256)
|
|
scanner := bufio.NewScanner(file)
|
|
lineNo := 0
|
|
|
|
for scanner.Scan() {
|
|
lineNo++
|
|
line := strings.TrimSpace(scanner.Text())
|
|
line = strings.TrimSuffix(line, "\r")
|
|
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
|
|
if !strings.HasPrefix(line, "/") {
|
|
return nil, fmt.Errorf("line %d must start with '/': %q", lineNo, line)
|
|
}
|
|
|
|
if _, ok := dedup[line]; ok {
|
|
continue
|
|
}
|
|
dedup[line] = struct{}{}
|
|
|
|
hash := hashPath(line)
|
|
|
|
if prev, collision := byHash[hash]; collision && prev != line {
|
|
return nil, fmt.Errorf("hash collision detected between %q and %q", prev, line)
|
|
}
|
|
byHash[hash] = line
|
|
|
|
entries = append(entries, scanPathEntry{Hash: hash, Path: line})
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, fmt.Errorf("read input file: %w", err)
|
|
}
|
|
|
|
sort.Slice(entries, func(i, j int) bool {
|
|
if entries[i].Hash == entries[j].Hash {
|
|
return entries[i].Path < entries[j].Path
|
|
}
|
|
return entries[i].Hash < entries[j].Hash
|
|
})
|
|
|
|
return entries, nil
|
|
}
|
|
|
|
// openInput opens a regular input file from a normalized path.
|
|
func openInput(path string) (*os.File, error) {
|
|
cleanPath := filepath.Clean(path)
|
|
if _, err := fs.StatFile(cleanPath); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
//nolint:gosec // G304: User-provided path is normalized and validated above.
|
|
return os.Open(cleanPath)
|
|
}
|
|
|
|
// generateSource renders Go source code for hashed scan path lookup.
|
|
func generateSource(entries []scanPathEntry) []byte {
|
|
var out bytes.Buffer
|
|
|
|
out.WriteString("// Code generated by go generate ./pkg/http/security; DO NOT EDIT.\n\n")
|
|
out.WriteString("package security\n\n")
|
|
out.WriteString("// ScanPathHashes contains lightweight hash keys for scanner probe paths.\n")
|
|
out.WriteString("var ScanPathHashes = map[uint64]bool{\n")
|
|
|
|
for _, entry := range entries {
|
|
fmt.Fprintf(&out, "\t0x%016x: true,\n", entry.Hash)
|
|
}
|
|
|
|
out.WriteString("}\n")
|
|
return out.Bytes()
|
|
}
|
|
|
|
// hashPath returns the same FNV-1a hash used at runtime for path matching.
|
|
func hashPath(path string) uint64 {
|
|
h := fnv.New64a()
|
|
_, _ = h.Write([]byte(path))
|
|
return h.Sum64()
|
|
}
|