151 lines
4.2 KiB
Go
151 lines
4.2 KiB
Go
package apigen
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
const importsFile = "omni.imports.json"
|
|
|
|
// SharedRoot explicitly authorizes reads from one canonical local directory.
|
|
// Version is a project-owned label; SHA256 optionally pins the imported source
|
|
// set, not a remote tag. The CLI never downloads or executes shared contracts.
|
|
type SharedRoot struct {
|
|
Path string `json:"path"`
|
|
Version string `json:"version,omitempty"`
|
|
SHA256 string `json:"sha256,omitempty"`
|
|
}
|
|
type sharedConfig struct {
|
|
Roots map[string]SharedRoot `json:"roots"`
|
|
}
|
|
|
|
func readSharedRoots(root string) (map[string]SharedRoot, error) {
|
|
stat, err := os.Lstat(filepath.Join(root, importsFile))
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !stat.Mode().IsRegular() {
|
|
return nil, errors.New("api: shared root configuration must be a regular file")
|
|
}
|
|
f, err := os.Open(filepath.Join(root, importsFile))
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
raw, err := io.ReadAll(io.LimitReader(f, 65537))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(raw) > 65536 {
|
|
return nil, errors.New("api: shared root config too large")
|
|
}
|
|
var cfg sharedConfig
|
|
if err = apierrors.DecodeCatalogJSON(raw, &cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(cfg.Roots) > 16 {
|
|
return nil, errors.New("api: at most 16 shared roots")
|
|
}
|
|
for name, r := range cfg.Roots {
|
|
if !lowerName.MatchString(name) || r.Path == "" || len(r.Version) > 128 {
|
|
return nil, errors.New("api: invalid shared root")
|
|
}
|
|
if r.SHA256 != "" && (len(r.SHA256) != 64 || strings.Trim(r.SHA256, "0123456789abcdef") != "") {
|
|
return nil, errors.New("api: shared sha256 must be 64 lowercase hex characters")
|
|
}
|
|
path := r.Path
|
|
if !filepath.IsAbs(path) {
|
|
path = filepath.Join(root, path)
|
|
}
|
|
r.Path, err = filepath.EvalSymlinks(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.Path, err = filepath.Abs(r.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stat, err := os.Stat(r.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !stat.IsDir() {
|
|
return nil, errors.New("api: shared root must be a directory")
|
|
}
|
|
cfg.Roots[name] = r
|
|
}
|
|
return cfg.Roots, nil
|
|
}
|
|
func sourceWithin(root, name string) (string, error) {
|
|
real, err := filepath.EvalSymlinks(name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
rel, err := filepath.Rel(root, real)
|
|
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
|
return "", errors.New("api: imports must stay within their declared contract root")
|
|
}
|
|
return filepath.ToSlash(rel), nil
|
|
}
|
|
func sharedHash(sources map[string][]byte, alias string) string {
|
|
values := map[string]string{}
|
|
for name, data := range sources {
|
|
if strings.HasPrefix(name, "@"+alias+"/") {
|
|
values[strings.TrimPrefix(name, "@"+alias+"/")] = digest(data)
|
|
}
|
|
}
|
|
raw, _ := json.Marshal(values)
|
|
return digest(raw)
|
|
}
|
|
func sourceMetadata(spec *Spec) map[string]any {
|
|
hashes := map[string]string{}
|
|
for name, data := range spec.Sources {
|
|
hashes[name] = digest(data)
|
|
}
|
|
shared := map[string]any{}
|
|
for alias, r := range spec.SharedRoots {
|
|
shared[alias] = map[string]string{"version": r.Version, "sha256": sharedHash(spec.Sources, alias)}
|
|
}
|
|
return map[string]any{"files": hashes, "shared": shared}
|
|
}
|
|
func sharedSeeds(spec *Spec, directory string) ([]byte, error) {
|
|
cfg := sharedConfig{Roots: map[string]SharedRoot{}}
|
|
for name, r := range spec.SharedRoots {
|
|
rel, err := filepath.Rel(filepath.Join(directory, "api"), r.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.Path = filepath.ToSlash(rel)
|
|
cfg.Roots[name] = r
|
|
}
|
|
data, err := json.MarshalIndent(cfg, "", " ")
|
|
return append(data, '\n'), err
|
|
}
|
|
func validateSharedPins(spec *Spec) error {
|
|
names := make([]string, 0, len(spec.SharedRoots))
|
|
for name := range spec.SharedRoots {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
r := spec.SharedRoots[name]
|
|
if r.SHA256 != "" && r.SHA256 != sharedHash(spec.Sources, name) {
|
|
return fmt.Errorf("api: shared contract %s checksum differs (got %s)", name, sharedHash(spec.Sources, name))
|
|
}
|
|
}
|
|
return nil
|
|
}
|