Files
go-omni/cmd/go-omni/apigen/generatorConfig.go

111 lines
3.6 KiB
Go

package apigen
import (
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
)
const generatorConfigFile = "omni.gen.json"
// GeneratorConfig contains developer-owned scaffolding options only.
type GeneratorConfig struct {
Repository string `json:"repository,omitempty"`
Middleware string `json:"middleware,omitempty"`
inputDigest string
}
func readGeneratorConfig(root string) (GeneratorConfig, bool, error) {
cfg := GeneratorConfig{Repository: "none", Middleware: "factories"}
if err := safePath(root, generatorConfigFile); err != nil {
return cfg, false, err
}
stat, err := os.Lstat(filepath.Join(root, generatorConfigFile))
if errors.Is(err, os.ErrNotExist) {
// Existing projects keep their handwritten middleware signature until they
// explicitly select factories; do not rewrite or adopt developer files.
if raw, e := os.ReadFile(filepath.Join(root, manifestPath)); e == nil {
var previous manifest
if err := json.Unmarshal(raw, &previous); err != nil {
return cfg, false, err
}
if previous.GeneratorConfig {
return cfg, false, errors.New("omni.gen.json is missing; restore the project-owned generation options before regenerating")
}
cfg.Middleware = "handlers"
} else if !errors.Is(e, os.ErrNotExist) {
return cfg, false, e
}
return cfg, false, nil
}
if err != nil {
return cfg, false, err
}
if !stat.Mode().IsRegular() || stat.Size() > 1<<20 {
return cfg, false, errors.New("omni.gen.json must be a regular file of at most 1 MiB")
}
raw, err := readGeneratorConfigFile(root, generatorConfigFile)
if err != nil {
return cfg, false, err
}
var keys map[string]json.RawMessage
if err = apierrors.DecodeCatalogJSON(raw, &keys); err != nil {
return cfg, false, fmt.Errorf("omni.gen.json: %w", err)
}
for _, key := range []string{"mappings", "mappingFiles"} {
if _, exists := keys[key]; exists {
return cfg, false, errors.New("mapping generation was removed: migrate callers to copyx or ordinary Go, remove mappings/mappingFiles from omni.gen.json, then remove internal/mapping/mappingsGen.go; see COPY.md")
}
}
if err = apierrors.DecodeCatalogJSON(raw, &cfg); err != nil {
return cfg, false, fmt.Errorf("omni.gen.json: %w", err)
}
digest := sha256.Sum256(raw)
cfg.inputDigest = fmt.Sprintf("%x", digest)
if cfg.Repository != "none" && cfg.Repository != "group" {
return cfg, false, errors.New("omni.gen.json: repository must be none or group")
}
if cfg.Middleware != "factories" && cfg.Middleware != "handlers" {
return cfg, false, errors.New("omni.gen.json: middleware must be factories or handlers")
}
return cfg, true, nil
}
// Read through a bounded reader as well as checking stat: a growing file cannot
// make configuration loading unbounded. safePath also rejects symlink escapes.
func readGeneratorConfigFile(root, name string) ([]byte, error) {
if err := safePath(root, name); err != nil {
return nil, err
}
f, err := os.Open(filepath.Join(root, filepath.FromSlash(name)))
if err != nil {
return nil, fmt.Errorf("%s: %w", name, err)
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
return nil, err
}
if !stat.Mode().IsRegular() || stat.Size() > 1<<20 {
return nil, fmt.Errorf("%s must be a regular file of at most 1 MiB", name)
}
raw, err := io.ReadAll(io.LimitReader(f, (1<<20)+1))
if err != nil {
return nil, err
}
if len(raw) > 1<<20 {
return nil, fmt.Errorf("%s exceeds 1 MiB", name)
}
if trimmed := strings.TrimSpace(string(raw)); !strings.HasPrefix(trimmed, "{") {
return nil, fmt.Errorf("%s must contain a JSON object", name)
}
return raw, nil
}