// Package kc extracts and parses the silo/ metadata directory from .kc files. // // A .kc file is a ZIP archive (superset of .fcstd) that contains a silo/ // directory with JSON metadata entries. This package handles extraction only — // no database or HTTP dependencies. package kc import ( "archive/zip" "bytes" "encoding/json" "fmt" "io" "strings" ) // Manifest represents the contents of silo/manifest.json. type Manifest struct { UUID string `json:"uuid"` KCVersion string `json:"kc_version"` RevisionHash string `json:"revision_hash"` SiloInstance string `json:"silo_instance"` } // Metadata represents the contents of silo/metadata.json. type Metadata struct { SchemaName string `json:"schema_name"` Tags []string `json:"tags"` LifecycleState string `json:"lifecycle_state"` Fields map[string]any `json:"fields"` } // ExtractResult holds the parsed silo/ directory contents from a .kc file. type ExtractResult struct { Manifest *Manifest Metadata *Metadata } // Extract opens a ZIP archive from data and parses the silo/ directory. // Returns nil, nil if no silo/ directory is found (plain .fcstd file). // Returns nil, error if silo/ entries exist but fail to parse. func Extract(data []byte) (*ExtractResult, error) { r, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) if err != nil { return nil, fmt.Errorf("kc: open zip: %w", err) } var manifestFile, metadataFile *zip.File hasSiloDir := false for _, f := range r.File { if f.Name == "silo/" || strings.HasPrefix(f.Name, "silo/") { hasSiloDir = true } switch f.Name { case "silo/manifest.json": manifestFile = f case "silo/metadata.json": metadataFile = f } } if !hasSiloDir { return nil, nil // plain .fcstd, no extraction } result := &ExtractResult{} if manifestFile != nil { m, err := readJSON[Manifest](manifestFile) if err != nil { return nil, fmt.Errorf("kc: parse manifest.json: %w", err) } result.Manifest = m } if metadataFile != nil { m, err := readJSON[Metadata](metadataFile) if err != nil { return nil, fmt.Errorf("kc: parse metadata.json: %w", err) } result.Metadata = m } return result, nil } // readJSON opens a zip.File and decodes its contents as JSON into T. func readJSON[T any](f *zip.File) (*T, error) { rc, err := f.Open() if err != nil { return nil, err } defer rc.Close() data, err := io.ReadAll(rc) if err != nil { return nil, err } var v T if err := json.Unmarshal(data, &v); err != nil { return nil, err } return &v, nil }