405 lines
11 KiB
Go
405 lines
11 KiB
Go
package plugins
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
)
|
|
|
|
type droidDocument struct {
|
|
kind string
|
|
filePath string
|
|
exists bool
|
|
raw []byte
|
|
snapshot *managedFileSnapshot
|
|
root map[string]any
|
|
hooks droidHookSettings
|
|
basePath []any
|
|
hasHooksContainer bool
|
|
hooksDisabled *bool
|
|
showHookOutput *bool
|
|
ownedFile bool
|
|
}
|
|
|
|
type droidSources struct {
|
|
root *droidDocument
|
|
legacy *droidDocument
|
|
settings *droidDocument
|
|
localSettings *droidDocument
|
|
policy *droidPolicyState
|
|
}
|
|
|
|
type droidRenderedDocument struct {
|
|
document *droidDocument
|
|
changed bool
|
|
next []byte
|
|
remove bool
|
|
}
|
|
|
|
type droidHookBlock struct {
|
|
field string
|
|
filePath string
|
|
label string
|
|
}
|
|
|
|
func parseDroidDocument(
|
|
exists bool,
|
|
filePath, kind string,
|
|
raw []byte,
|
|
snapshot *managedFileSnapshot,
|
|
) (*droidDocument, error) {
|
|
label := droidDocumentLabel(kind, filePath)
|
|
root, err := decodeJSONCObject(raw, label, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
document := &droidDocument{
|
|
kind: kind, filePath: filePath, exists: exists,
|
|
raw: append([]byte(nil), raw...), snapshot: snapshot, root: root,
|
|
ownedFile: bytes.HasPrefix(raw, []byte(droidOwnedFileMarker)),
|
|
}
|
|
if kind == "settings" || kind == "local-settings" {
|
|
document.basePath = []any{"hooks"}
|
|
document.hooksDisabled, err = droidOptionalBoolean(root, "hooksDisabled", label)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
document.showHookOutput, err = droidOptionalBoolean(root, "showHookOutput", label)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hooksValue, hasHooks := root["hooks"]
|
|
document.hasHooksContainer = hasHooks
|
|
if !hasHooks {
|
|
document.hooks = droidHookSettings{}
|
|
return document, nil
|
|
}
|
|
document.hooks, err = readDroidHookSettings(hooksValue, label+` field "hooks"`)
|
|
return document, err
|
|
}
|
|
if hooksValue, hasHooks := root["hooks"]; hasHooks {
|
|
document.basePath = []any{"hooks"}
|
|
document.hasHooksContainer = true
|
|
document.hooks, err = readDroidHookSettings(hooksValue, label+` field "hooks"`)
|
|
return document, err
|
|
}
|
|
document.hooksDisabled, err = droidOptionalBoolean(root, "hooksDisabled", label)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
document.showHookOutput, err = droidOptionalBoolean(root, "showHookOutput", label)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
direct := make(map[string]any, len(root))
|
|
for key, value := range root {
|
|
if key != "hooksDisabled" && key != "showHookOutput" {
|
|
direct[key] = value
|
|
}
|
|
}
|
|
document.hooks, err = readDroidHookSettings(direct, label)
|
|
return document, err
|
|
}
|
|
|
|
func droidOptionalBoolean(
|
|
root map[string]any,
|
|
field, label string,
|
|
) (*bool, error) {
|
|
value, exists := root[field]
|
|
if !exists {
|
|
return nil, nil
|
|
}
|
|
boolean, ok := value.(bool)
|
|
if !ok {
|
|
return nil, fmt.Errorf(`%s field %q must be a boolean`, label, field)
|
|
}
|
|
return &boolean, nil
|
|
}
|
|
|
|
func droidDocumentLabel(kind, filePath string) string {
|
|
switch kind {
|
|
case "settings":
|
|
return fmt.Sprintf("Factory Droid settings at %s", filePath)
|
|
case "local-settings":
|
|
return fmt.Sprintf("Factory Droid local settings at %s", filePath)
|
|
case "legacy":
|
|
return fmt.Sprintf("Factory Droid legacy hooks at %s", filePath)
|
|
default:
|
|
return fmt.Sprintf("Factory Droid hooks at %s", filePath)
|
|
}
|
|
}
|
|
|
|
func createDroidDocument(filePath, kind string, raw []byte) (*droidDocument, error) {
|
|
return parseDroidDocument(false, filePath, kind, raw, nil)
|
|
}
|
|
|
|
func createOwnedDroidDocument(filePath string) (*droidDocument, error) {
|
|
return createDroidDocument(
|
|
filePath,
|
|
"hooks",
|
|
[]byte(droidOwnedFileMarker+"\n{\n \"hooks\": {}\n}\n"),
|
|
)
|
|
}
|
|
|
|
func activeDroidDocument(sources *droidSources) *droidDocument {
|
|
switch {
|
|
case sources.root.exists:
|
|
return sources.root
|
|
case sources.legacy.exists:
|
|
return sources.legacy
|
|
case sources.localSettings.hasHooksContainer:
|
|
return sources.localSettings
|
|
case sources.settings.hasHooksContainer:
|
|
return sources.settings
|
|
default:
|
|
return sources.root
|
|
}
|
|
}
|
|
|
|
func effectiveDroidHooks(sources *droidSources) droidHookSettings {
|
|
return activeDroidDocument(sources).hooks
|
|
}
|
|
|
|
func droidHookBlocked(sources *droidSources) *droidHookBlock {
|
|
if sources.policy != nil && sources.policy.allowManagedHooksOnlyBy != nil {
|
|
origin := sources.policy.allowManagedHooksOnlyBy
|
|
return &droidHookBlock{"allowManagedHooksOnly", origin.filePath, origin.label}
|
|
}
|
|
if sources.policy != nil && sources.policy.hooksDisabled != nil {
|
|
if *sources.policy.hooksDisabled {
|
|
origin := sources.policy.hooksDisabledBy
|
|
return &droidHookBlock{"hooksDisabled", origin.filePath, origin.label}
|
|
}
|
|
return nil
|
|
}
|
|
selected := sources.settings
|
|
if sources.localSettings.hooksDisabled != nil {
|
|
selected = sources.localSettings
|
|
}
|
|
if selected.hooksDisabled != nil && *selected.hooksDisabled {
|
|
return &droidHookBlock{
|
|
"hooksDisabled", selected.filePath,
|
|
droidDocumentLabel(selected.kind, selected.filePath),
|
|
}
|
|
}
|
|
active := activeDroidDocument(sources)
|
|
if active.hooksDisabled != nil && *active.hooksDisabled {
|
|
return &droidHookBlock{
|
|
"hooksDisabled", active.filePath,
|
|
droidDocumentLabel(active.kind, active.filePath),
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func droidSourceDocuments(sources *droidSources) []*droidDocument {
|
|
return []*droidDocument{
|
|
sources.root, sources.legacy, sources.settings, sources.localSettings,
|
|
}
|
|
}
|
|
|
|
func droidInstallationDocuments(sources *droidSources) []*droidDocument {
|
|
target := activeDroidDocument(sources)
|
|
candidates := []*droidDocument{target}
|
|
if sources.root.exists {
|
|
candidates = append(candidates, sources.root)
|
|
}
|
|
if sources.legacy.exists {
|
|
candidates = append(candidates, sources.legacy)
|
|
}
|
|
if sources.settings.hasHooksContainer {
|
|
candidates = append(candidates, sources.settings)
|
|
}
|
|
if sources.localSettings.hasHooksContainer {
|
|
candidates = append(candidates, sources.localSettings)
|
|
}
|
|
return uniqueDroidDocuments(candidates...)
|
|
}
|
|
|
|
func uniqueDroidDocuments(documents ...*droidDocument) []*droidDocument {
|
|
result := make([]*droidDocument, 0, len(documents))
|
|
seen := map[string]bool{}
|
|
for _, document := range documents {
|
|
if document == nil {
|
|
continue
|
|
}
|
|
normalized := filepath.Clean(document.filePath)
|
|
if seen[normalized] {
|
|
continue
|
|
}
|
|
seen[normalized] = true
|
|
result = append(result, document)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func droidAdditionsFor(
|
|
document, target *droidDocument,
|
|
groups map[string]map[string]any,
|
|
) map[string]map[string]any {
|
|
if sameDroidPath(document.filePath, target.filePath) {
|
|
return groups
|
|
}
|
|
return map[string]map[string]any{}
|
|
}
|
|
|
|
func renderDroidDocument(
|
|
document *droidDocument,
|
|
agentID, runtimeRoot string,
|
|
additions map[string]map[string]any,
|
|
) (*droidRenderedDocument, error) {
|
|
if len(additions) > 0 && droidHasExactInstallation(document, additions, runtimeRoot) {
|
|
return &droidRenderedDocument{document: document, next: document.raw}, nil
|
|
}
|
|
editor, err := newJSONCEditor(
|
|
document.raw,
|
|
droidDocumentLabel(document.kind, document.filePath),
|
|
true,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
removals := managedDroidRemovals(document.hooks, agentID, runtimeRoot)
|
|
sort.SliceStable(removals, func(left, right int) bool {
|
|
if removals[left].event == removals[right].event {
|
|
return removals[left].groupIndex > removals[right].groupIndex
|
|
}
|
|
return removals[left].event < removals[right].event
|
|
})
|
|
for _, event := range droidToolEvents {
|
|
removedEvent := false
|
|
for _, removal := range removals {
|
|
if removal.event != event {
|
|
continue
|
|
}
|
|
removedEvent = true
|
|
groupPath := appendDroidPath(document.basePath, removal.event, removal.groupIndex)
|
|
if removal.removeGroup {
|
|
if err := editor.remove(groupPath); err != nil {
|
|
return nil, err
|
|
}
|
|
continue
|
|
}
|
|
sort.Sort(sort.Reverse(sort.IntSlice(removal.handlerIndexes)))
|
|
for _, handlerIndex := range removal.handlerIndexes {
|
|
if err := editor.remove(appendDroidPath(groupPath, "hooks", handlerIndex)); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
if removedEvent {
|
|
current, parseErr := droidHooksFromRaw(document, editor.pack())
|
|
if parseErr != nil {
|
|
return nil, parseErr
|
|
}
|
|
groups, exists := current[event].([]any)
|
|
if exists && len(groups) == 0 {
|
|
if err := editor.remove(appendDroidPath(document.basePath, event)); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, event := range droidToolEvents {
|
|
group, exists := additions[event]
|
|
if !exists {
|
|
continue
|
|
}
|
|
current, parseErr := droidHooksFromRaw(document, editor.pack())
|
|
if parseErr != nil {
|
|
return nil, parseErr
|
|
}
|
|
eventPath := appendDroidPath(document.basePath, event)
|
|
if _, exists := current[event]; exists {
|
|
err = editor.appendArray(eventPath, group)
|
|
} else {
|
|
err = editor.addProperty(document.basePath, event, []any{group})
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
next := editor.pack()
|
|
nextDocument, err := parseDroidDocument(
|
|
document.exists, document.filePath, document.kind, next, document.snapshot,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
remove := len(additions) == 0 && document.exists && document.ownedFile &&
|
|
droidHookDocumentEmpty(nextDocument)
|
|
return &droidRenderedDocument{
|
|
document: document,
|
|
changed: remove || !bytes.Equal(next, document.raw),
|
|
next: next,
|
|
remove: remove,
|
|
}, nil
|
|
}
|
|
|
|
func droidHasExactInstallation(
|
|
document *droidDocument,
|
|
additions map[string]map[string]any,
|
|
runtimeRoot string,
|
|
) bool {
|
|
if len(additions) != len(droidToolEvents) {
|
|
return false
|
|
}
|
|
removals := managedDroidRemovals(document.hooks, "", runtimeRoot)
|
|
if len(removals) != len(droidToolEvents) {
|
|
return false
|
|
}
|
|
for _, event := range droidToolEvents {
|
|
matches := make([]droidManagedRemoval, 0, 1)
|
|
for _, removal := range removals {
|
|
if removal.event == event {
|
|
matches = append(matches, removal)
|
|
}
|
|
}
|
|
if len(matches) != 1 || !matches[0].removeGroup {
|
|
return false
|
|
}
|
|
groups := document.hooks[event].([]any)
|
|
if !reflect.DeepEqual(groups[matches[0].groupIndex], additions[event]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func droidHooksFromRaw(
|
|
document *droidDocument,
|
|
raw []byte,
|
|
) (droidHookSettings, error) {
|
|
current, err := parseDroidDocument(
|
|
document.exists, document.filePath, document.kind, raw, document.snapshot,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return current.hooks, nil
|
|
}
|
|
|
|
func droidHookDocumentEmpty(document *droidDocument) bool {
|
|
if document.kind == "settings" || document.kind == "local-settings" || len(document.hooks) > 0 {
|
|
return false
|
|
}
|
|
for field := range document.root {
|
|
if field != "hooks" {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func appendDroidPath(base []any, parts ...any) []any {
|
|
path := make([]any, 0, len(base)+len(parts))
|
|
path = append(path, base...)
|
|
return append(path, parts...)
|
|
}
|
|
|
|
func displayDroidConfigPath(sources *droidSources) string {
|
|
return activeDroidDocument(sources).filePath
|
|
}
|