gui/pkg/display/manifest.go
Snider a2022d952c
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run
Implement GUI chat bindings and preload runtime wiring
2026-04-15 17:53:40 +01:00

167 lines
4.3 KiB
Go

package display
import (
"errors"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
core "dappco.re/go/core"
"gopkg.in/yaml.v3"
)
type ViewManifest struct {
AppID string `yaml:"app_id" json:"app_id"`
Name string `yaml:"name" json:"name"`
Preloads []ManifestPreload `yaml:"preloads" json:"preloads"`
Windows map[string]ManifestWindow `yaml:"windows" json:"windows"`
HLCRF []HLCRFTemplate `yaml:"hlcrf" json:"hlcrf"`
Permissions []string `yaml:"permissions" json:"permissions"`
}
type ManifestPreload struct {
Path string `yaml:"path" json:"path"`
Inline string `yaml:"inline" json:"inline"`
Enabled *bool `yaml:"enabled" json:"enabled,omitempty"`
}
type ManifestWindow struct {
Title string `yaml:"title" json:"title"`
Width int `yaml:"width" json:"width"`
Height int `yaml:"height" json:"height"`
Preload bool `yaml:"preload" json:"preload"`
}
type HLCRFTemplate struct {
Name string `yaml:"name" json:"name"`
Tag string `yaml:"tag" json:"tag"`
Template string `yaml:"template" json:"template"`
}
type loadedManifest struct {
Path string
BaseDir string
Manifest ViewManifest
}
func (s *Service) loadManifestForOrigin(pageURL string) (*loadedManifest, error) {
if s.manifestCache == nil {
s.manifestCache = make(map[string]*loadedManifest)
}
if cached, ok := s.manifestCache[pageURL]; ok {
return cached, nil
}
path, err := discoverManifestPath(pageURL)
if err != nil {
return nil, err
}
body, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var manifest ViewManifest
if err := yaml.Unmarshal(body, &manifest); err != nil {
return nil, err
}
loaded := &loadedManifest{
Path: path,
BaseDir: manifestBaseDir(path),
Manifest: manifest,
}
s.manifestCache[pageURL] = loaded
return loaded, nil
}
func manifestBaseDir(manifestPath string) string {
baseDir := filepath.Dir(manifestPath)
if filepath.Base(baseDir) == ".core" {
return filepath.Dir(baseDir)
}
return baseDir
}
func safeManifestPreloadPath(baseDir, preloadPath string) (string, error) {
trimmed := strings.TrimSpace(preloadPath)
if trimmed == "" {
return "", errors.New("preload path is empty")
}
if filepath.IsAbs(trimmed) {
return "", errors.New("preload path must be relative")
}
baseAbs, err := filepath.Abs(baseDir)
if err != nil {
return "", err
}
candidateAbs, err := filepath.Abs(filepath.Join(baseAbs, trimmed))
if err != nil {
return "", err
}
rel, err := filepath.Rel(baseAbs, candidateAbs)
if err != nil {
return "", err
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "", errors.New("preload path escapes manifest directory")
}
return candidateAbs, nil
}
func discoverManifestPath(pageURL string) (string, error) {
parsed, err := url.Parse(strings.TrimSpace(pageURL))
if err != nil {
return "", err
}
candidates := make([]string, 0, 4)
switch parsed.Scheme {
case "", "file":
path := parsed.Path
if path == "" {
path = pageURL
}
if info, err := os.Stat(path); err == nil {
if info.IsDir() {
candidates = append(candidates, filepath.Join(path, ".core", "view.yaml"))
} else {
dir := filepath.Dir(path)
candidates = append(candidates, filepath.Join(dir, ".core", "view.yaml"))
candidates = append(candidates, filepath.Join(filepath.Dir(dir), ".core", "view.yaml"))
}
}
default:
if parsed.Host != "" {
candidates = append(candidates, filepath.Join(core.Env("DIR_HOME"), ".core", "apps", parsed.Host, ".core", "view.yaml"))
}
}
for _, candidate := range candidates {
if _, err := os.Stat(candidate); err == nil {
return candidate, nil
}
}
return "", errors.New("view manifest not found")
}
func (s *Service) manifestWindowConfig(pageURL string) map[string]ManifestWindow {
s.manifestMu.Lock()
defer s.manifestMu.Unlock()
loaded, err := s.loadManifestForOrigin(pageURL)
if err != nil || loaded == nil {
return nil
}
return loaded.Manifest.Windows
}
func (s *Service) readManifestPreload(baseDir, preloadPath string) ([]byte, error) {
resolvedPath, err := safeManifestPreloadPath(baseDir, preloadPath)
if err != nil {
return nil, err
}
return os.ReadFile(resolvedPath)
}
type manifestCacheState struct {
manifestCache map[string]*loadedManifest
manifestMu sync.Mutex
}