Refactor: migrate POSIX scaffolding to CBOR and JSONC
This commit is contained in:
@@ -11,6 +11,13 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/tailscale/hujson"
|
||||
)
|
||||
|
||||
const (
|
||||
settingsCBORPath = "settings.cbor"
|
||||
)
|
||||
|
||||
func ScanRoot(root string) ([]Node, error) {
|
||||
@@ -77,18 +84,21 @@ func ScanRoot(root string) ([]Node, error) {
|
||||
}
|
||||
|
||||
func loadRootScope(rootPath string) (Scope, error) {
|
||||
settingsPath := filepath.Join(rootPath, "settings.json")
|
||||
settingsPath := filepath.Join(rootPath, settingsCBORPath)
|
||||
content, err := os.ReadFile(settingsPath)
|
||||
if err != nil {
|
||||
if errorsIsNotExist(err) {
|
||||
return Scope{}, nil
|
||||
}
|
||||
return Scope{}, fmt.Errorf("read root settings.json: %w", err)
|
||||
return Scope{}, fmt.Errorf("read root %s: %w", filepath.Base(settingsPath), err)
|
||||
}
|
||||
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(content, &payload); err != nil {
|
||||
return Scope{}, fmt.Errorf("decode root settings.json: %w", err)
|
||||
payload, err := decodeStructuredMap(settingsPath, content)
|
||||
if err != nil {
|
||||
return Scope{}, fmt.Errorf("decode root %s: %w", filepath.Base(settingsPath), err)
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
return Scope{}, nil
|
||||
}
|
||||
|
||||
installation, _ := payload["installation"].(map[string]any)
|
||||
@@ -138,9 +148,9 @@ func buildNode(rootPath, relPath string, entry fs.DirEntry, rootScope Scope) (No
|
||||
node.SizeBytes = int64(len(content))
|
||||
node.Checksum = hex.EncodeToString(hash[:])
|
||||
|
||||
if strings.EqualFold(filepath.Ext(entry.Name()), ".json") {
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(content, &payload); err == nil {
|
||||
if isStructuredProjectionFile(entry.Name()) {
|
||||
payload, err := decodeStructuredMap(absPath, content)
|
||||
if err == nil {
|
||||
jsonContent, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return Node{}, fmt.Errorf("remarshal POSIX file %s: %w", relPath, err)
|
||||
@@ -171,6 +181,84 @@ func buildNode(rootPath, relPath string, entry fs.DirEntry, rootScope Scope) (No
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func isStructuredProjectionFile(name string) bool {
|
||||
switch strings.ToLower(filepath.Ext(name)) {
|
||||
case ".json", ".jsonc", ".cbor":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func decodeStructuredMap(path string, content []byte) (map[string]any, error) {
|
||||
var payload any
|
||||
switch strings.ToLower(filepath.Ext(path)) {
|
||||
case ".cbor":
|
||||
if err := cbor.Unmarshal(content, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case ".json":
|
||||
if err := json.Unmarshal(content, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case ".jsonc":
|
||||
decoded, err := decodeJSONCToAny(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload = decoded
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported structured file extension %q", filepath.Ext(path))
|
||||
}
|
||||
if payload == nil {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
normalized, ok := normalizeStructuredValue(payload).(map[string]any)
|
||||
if !ok || normalized == nil {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func decodeJSONCToAny(content []byte) (any, error) {
|
||||
ast, err := hujson.Parse(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ast.Standardize()
|
||||
standardized := ast.Pack()
|
||||
var payload any
|
||||
if err := json.Unmarshal(standardized, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func normalizeStructuredValue(value any) any {
|
||||
switch typed := value.(type) {
|
||||
case map[string]any:
|
||||
normalized := make(map[string]any, len(typed))
|
||||
for key, child := range typed {
|
||||
normalized[key] = normalizeStructuredValue(child)
|
||||
}
|
||||
return normalized
|
||||
case map[any]any:
|
||||
normalized := make(map[string]any, len(typed))
|
||||
for key, child := range typed {
|
||||
normalized[fmt.Sprint(key)] = normalizeStructuredValue(child)
|
||||
}
|
||||
return normalized
|
||||
case []any:
|
||||
normalized := make([]any, len(typed))
|
||||
for index, child := range typed {
|
||||
normalized[index] = normalizeStructuredValue(child)
|
||||
}
|
||||
return normalized
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
func summarizeNodes(nodes []Node) RebuildSummary {
|
||||
summary := RebuildSummary{TotalNodes: len(nodes)}
|
||||
for _, node := range nodes {
|
||||
|
||||
Reference in New Issue
Block a user