// Package upstream loads and explicitly synchronizes the reviewed Gitea snapshot. package upstream import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io/fs" "os" "path" "path/filepath" "sort" "strings" ) type Lock struct { Schema int `json:"schema"` Repository string `json:"repository"` Tag string `json:"tag"` Commit string `json:"commit"` Files map[string]string `json:"files"` } type Snapshot struct { Lock Lock Files map[string][]byte Templates map[string][]byte // mail/... paths, including shared partials Locales map[string]map[string]string } // AdapterSources also binds the preview's formatting and rendering semantics to // the upstream implementation reviewed during this migration. A changed source // requires reviewing the adapter before accepting another snapshot. var AdapterSources = map[string]string{ "modules/translation/translation.go": "4217cbd9beb79c0e1be52dd9a7d7705b9ff4becc2d9515d49236db126da0f7ee", "modules/translation/i18n/localestore.go": "e55a7832d01753622a7d47e904de95e5aed7ad3535c59bf6ac3fec4346e40e9d", "modules/translation/i18n/format.go": "f440236eaf1f73fe0a9aa36a2c036a4b34c6ab28c9e4cf971bef7fceee701cb4", "modules/templates/mail.go": "fb8204b79f700f88c7c3f0da16e50c3d678a2216e62e98f712805ed1d42ab3a9", } func Digest(data []byte) string { h := sha256.Sum256(data) return hex.EncodeToString(h[:]) } func SafePath(p string) bool { return p != "." && fs.ValidPath(p) && !strings.ContainsAny(p, "\\:") } func Load(dir string) (*Snapshot, error) { s, err := loadFiles(dir) if err != nil { return nil, err } return s, s.validate() } // Ownership/checksum validation is separate so explicitly syncing after an // adapter review can replace an older, still intact snapshot. func loadFiles(dir string) (*Snapshot, error) { data, err := os.ReadFile(filepath.Join(dir, "lock.json")) if err != nil { return nil, fmt.Errorf("snapshot lock: %w (run upstream sync explicitly)", err) } s := &Snapshot{Files: map[string][]byte{}} if err := json.Unmarshal(data, &s.Lock); err != nil { return nil, err } if err := s.Lock.validateIdentity(); err != nil { return nil, err } for p, expected := range s.Lock.Files { if !SafePath(p) { return nil, fmt.Errorf("unsafe snapshot path %q", p) } b, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(p))) if err != nil { return nil, err } if Digest(b) != expected { return nil, fmt.Errorf("snapshot checksum mismatch: %s", p) } s.Files[p] = b } // Unlocked files must not silently become input to a build. err = filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error { if err != nil { return err } if d.Type()&os.ModeSymlink != 0 { return fmt.Errorf("snapshot symlinks are unsupported: %s", p) } if d.IsDir() { return nil } rel, _ := filepath.Rel(dir, p) rel = filepath.ToSlash(rel) if rel != "lock.json" { if _, ok := s.Files[rel]; !ok { return fmt.Errorf("unlocked snapshot file: %s", rel) } } return nil }) if err != nil { return nil, err } return s, nil } func (lock Lock) validateIdentity() error { if lock.Schema != 1 || lock.Repository != "https://github.com/go-gitea/gitea" || !validTag(lock.Tag) || len(lock.Commit) != 40 || len(lock.Files) == 0 { return fmt.Errorf("invalid snapshot identity or unsupported version") } if _, err := hex.DecodeString(lock.Commit); err != nil { return fmt.Errorf("invalid snapshot commit: %w", err) } return nil } func (s *Snapshot) validate() error { if err := s.Lock.validateIdentity(); err != nil { return err } for p, hash := range AdapterSources { if Digest(s.Files[p]) != hash { return fmt.Errorf("upstream adapter changed: %s; review preview semantics before syncing", p) } } if len(s.Files["LICENSE"]) == 0 { return fmt.Errorf("missing upstream license") } s.Templates = map[string][]byte{} s.Locales = map[string]map[string]string{} for p, b := range s.Files { switch { case p == "public/assets/img/favicon.png": if len(b) == 0 { return fmt.Errorf("missing preview favicon") } case strings.HasPrefix(p, "templates/mail/") && strings.HasSuffix(p, ".tmpl"): s.Templates[strings.TrimPrefix(p, "templates/")] = b case strings.HasPrefix(p, "options/locale/locale_") && strings.HasSuffix(p, ".json"): locale := strings.TrimSuffix(strings.TrimPrefix(p, "options/locale/locale_"), ".json") values, err := DecodeLocale(b) if err != nil { return fmt.Errorf("%s: %w", p, err) } s.Locales[locale] = values default: if p != "LICENSE" { if _, ok := AdapterSources[p]; !ok { return fmt.Errorf("unsupported snapshot file: %s", p) } } } } if len(s.Entrypoints()) == 0 || len(s.Templates["mail/base/head.tmpl"]) == 0 || len(s.Templates["mail/base/footer.tmpl"]) == 0 || len(s.Locales["en-US"]) == 0 { return fmt.Errorf("snapshot is missing entrypoints, shared partials, or English translations") } return ValidateKeys(s) } // DecodeLocale mirrors Gitea's flat or one-level nested JSON catalog layout. func DecodeLocale(data []byte) (map[string]string, error) { var raw map[string]any if err := json.Unmarshal(data, &raw); err != nil { return nil, err } result := map[string]string{} for k, v := range raw { switch v := v.(type) { case string: result[k] = v case map[string]any: for inner, value := range v { str, ok := value.(string) if !ok { return nil, fmt.Errorf("unsupported translation value: %s.%s", k, inner) } result[k+"."+inner] = str } default: return nil, fmt.Errorf("unsupported translation value: %s", k) } } return result, nil } func (s *Snapshot) Entrypoints() []string { var result []string for p := range s.Templates { if !strings.HasPrefix(p, "mail/base/") { result = append(result, p) } } sort.Strings(result) return result } func (s *Snapshot) Languages() []string { var langs []string for l := range s.Locales { langs = append(langs, l) } sort.Strings(langs) return langs } func TemplateID(p string) string { return strings.TrimSuffix(path.Base(p), ".tmpl") }