package bnp import ( "fmt" "os" "path/filepath" "sort" "strings" ) // Presentation members: a root preview.png plus an optional screenshots/ dir. // Neither ships behavior — the registry ingests them at publish into the // plugin's managed gallery (spec 2026-07-05-registry-presentation-reviews). // The .so-era source archives carried preview.png implicitly; the .bnp // packers dropped it, which silently wiped previews on republish — these // entries restore that and add the multi-screenshot surface. const ( filePreview = "preview.png" dirScreenshots = "screenshots" // maxScreenshotBytes caps one presentation image (registry gallery cap). maxScreenshotBytes = 8 << 20 // maxScreenshotFiles caps the screenshots/ dir (registry gallery cap). maxScreenshotFiles = 12 ) // screenshotExtOK reports whether a filename carries an accepted gallery // image extension. func screenshotExtOK(name string) bool { switch strings.ToLower(filepath.Ext(name)) { case ".png", ".jpg", ".jpeg", ".webp": return true } return false } // validateScreenshotsDir checks the optional screenshots/ dir: accepted // extensions only, ≤ maxScreenshotBytes per file, ≤ maxScreenshotFiles total, // no subdirectories or irregular files. Returns the file count (0 when the // dir is absent). func validateScreenshotsDir(dir string) (int, error) { root := filepath.Join(dir, dirScreenshots) info, err := os.Stat(root) if os.IsNotExist(err) { return 0, nil } if err != nil { return 0, fmt.Errorf("stat %s/: %w", dirScreenshots, err) } if !info.IsDir() { return 0, fmt.Errorf("%s exists but is not a directory", dirScreenshots) } entries, err := os.ReadDir(root) if err != nil { return 0, fmt.Errorf("read %s/: %w", dirScreenshots, err) } count := 0 for _, e := range entries { if e.IsDir() { return 0, fmt.Errorf("%s/%s: subdirectories are not allowed in screenshots/", dirScreenshots, e.Name()) } fi, err := e.Info() if err != nil { return 0, fmt.Errorf("%s/%s: %w", dirScreenshots, e.Name(), err) } if !fi.Mode().IsRegular() { return 0, fmt.Errorf("%s/%s: not a regular file", dirScreenshots, e.Name()) } if !screenshotExtOK(e.Name()) { return 0, fmt.Errorf("%s/%s: unsupported image type (allowed: .png .jpg .jpeg .webp)", dirScreenshots, e.Name()) } if fi.Size() > maxScreenshotBytes { return 0, fmt.Errorf("%s/%s: %d bytes exceeds the %d MB per-image cap", dirScreenshots, e.Name(), fi.Size(), maxScreenshotBytes>>20) } count++ } if count > maxScreenshotFiles { return 0, fmt.Errorf("%s/ holds %d images; the gallery cap is %d", dirScreenshots, count, maxScreenshotFiles) } return count, nil } // presentationEntries validates and returns pack entries for the optional // root preview.png and screenshots/ dir. Both are optional; absence is not an // error. func presentationEntries(dir string) (entries []packEntry, previewIncluded bool, screenshotCount int, err error) { previewPath := filepath.Join(dir, filePreview) if fi, statErr := os.Stat(previewPath); statErr == nil { if !fi.Mode().IsRegular() { return nil, false, 0, fmt.Errorf("%s is not a regular file", filePreview) } if fi.Size() > maxScreenshotBytes { return nil, false, 0, fmt.Errorf("%s: %d bytes exceeds the %d MB per-image cap", filePreview, fi.Size(), maxScreenshotBytes>>20) } entries = append(entries, packEntry{ArtifactPath: filePreview, Source: previewPath}) previewIncluded = true } screenshotCount, err = validateScreenshotsDir(dir) if err != nil { return nil, false, 0, err } if screenshotCount > 0 { root := filepath.Join(dir, dirScreenshots) files, readErr := os.ReadDir(root) if readErr != nil { return nil, false, 0, fmt.Errorf("read %s/: %w", dirScreenshots, readErr) } names := make([]string, 0, len(files)) for _, f := range files { names = append(names, f.Name()) } sort.Strings(names) for _, name := range names { entries = append(entries, packEntry{ ArtifactPath: dirScreenshots + "/" + name, Source: filepath.Join(root, name), }) } } return entries, previewIncluded, screenshotCount, nil }