268 lines
6.7 KiB
Go
268 lines
6.7 KiB
Go
package plugins
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
)
|
|
|
|
type droidPolicyLocation struct {
|
|
filePath string
|
|
label string
|
|
}
|
|
|
|
type droidPolicyLayer struct {
|
|
filePath string
|
|
label string
|
|
snapshot *managedFileSnapshot
|
|
hooksDisabled *bool
|
|
allowManagedHooksOnly *bool
|
|
showHookOutput *bool
|
|
}
|
|
|
|
type droidPolicyOrigin struct {
|
|
filePath string
|
|
label string
|
|
}
|
|
|
|
type droidPolicyState struct {
|
|
allowManagedHooksOnlyBy *droidPolicyOrigin
|
|
hooksDisabled *bool
|
|
hooksDisabledBy *droidPolicyOrigin
|
|
preconditions []filePrecondition
|
|
}
|
|
|
|
func defaultDroidManagedSettingsPath() string {
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
return "/Library/Application Support/Factory/settings.json"
|
|
case "windows":
|
|
programFiles := os.Getenv("ProgramFiles")
|
|
if programFiles == "" {
|
|
programFiles = `C:\Program Files`
|
|
}
|
|
return filepath.Join(programFiles, "Factory", "settings.json")
|
|
default:
|
|
return "/etc/factory/settings.json"
|
|
}
|
|
}
|
|
|
|
var droidManagedSettingsPath = defaultDroidManagedSettingsPath
|
|
|
|
func readDroidPolicyLayer(location droidPolicyLocation) (*droidPolicyLayer, error) {
|
|
if _, err := managedPhysicalDirectoryExists(
|
|
filepath.Dir(location.filePath),
|
|
location.label+" directory",
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
snapshot, err := readManagedFile(
|
|
location.filePath,
|
|
location.label,
|
|
maxManagedSourceBytes,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
layer := &droidPolicyLayer{
|
|
filePath: location.filePath,
|
|
label: location.label,
|
|
snapshot: snapshot,
|
|
}
|
|
if snapshot == nil {
|
|
return layer, nil
|
|
}
|
|
root, err := decodeJSONCObject(
|
|
snapshot.contents,
|
|
fmt.Sprintf("%s at %s", location.label, location.filePath),
|
|
true,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
layer.hooksDisabled, err = droidOptionalBoolean(root, "hooksDisabled", location.label)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
layer.allowManagedHooksOnly, err = droidOptionalBoolean(
|
|
root,
|
|
"allowManagedHooksOnly",
|
|
location.label,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
layer.showHookOutput, err = droidOptionalBoolean(root, "showHookOutput", location.label)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return layer, nil
|
|
}
|
|
|
|
func droidGitRoot(start string) (string, bool, error) {
|
|
current, err := filepath.Abs(start)
|
|
if err != nil {
|
|
return "", false, fmt.Errorf("resolve Factory Droid project directory: %w", err)
|
|
}
|
|
for {
|
|
marker := filepath.Join(current, ".git")
|
|
info, inspectErr := os.Lstat(marker)
|
|
switch {
|
|
case inspectErr == nil:
|
|
if info.Mode()&os.ModeSymlink != 0 || (!info.IsDir() && !info.Mode().IsRegular()) {
|
|
return "", false, fmt.Errorf(
|
|
"Factory Droid project marker is not physical: %s",
|
|
marker,
|
|
)
|
|
}
|
|
return current, true, nil
|
|
case !os.IsNotExist(inspectErr):
|
|
return "", false, fmt.Errorf(
|
|
"inspect Factory Droid project marker at %s: %w",
|
|
marker,
|
|
inspectErr,
|
|
)
|
|
}
|
|
parent := filepath.Dir(current)
|
|
if parent == current {
|
|
return "", false, nil
|
|
}
|
|
current = parent
|
|
}
|
|
}
|
|
|
|
func droidProjectDirectories(root, current string) []string {
|
|
directories := []string{root}
|
|
relative, err := filepath.Rel(root, current)
|
|
if err != nil || relative == "." || relative == "" || filepath.IsAbs(relative) {
|
|
return directories
|
|
}
|
|
if relative == ".." || len(relative) > 3 && relative[:3] == ".."+string(os.PathSeparator) {
|
|
return directories
|
|
}
|
|
directory := root
|
|
for _, segment := range splitDroidPath(relative) {
|
|
directory = filepath.Join(directory, segment)
|
|
directories = append(directories, directory)
|
|
}
|
|
return directories
|
|
}
|
|
|
|
func splitDroidPath(value string) []string {
|
|
parts := make([]string, 0)
|
|
for value != "." && value != "" {
|
|
directory, file := filepath.Split(value)
|
|
if file != "" {
|
|
parts = append([]string{file}, parts...)
|
|
}
|
|
value = filepath.Clean(directory)
|
|
if value == string(os.PathSeparator) {
|
|
break
|
|
}
|
|
}
|
|
return parts
|
|
}
|
|
|
|
func droidProjectPolicyLocations() ([][2]droidPolicyLocation, error) {
|
|
current, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve Factory Droid working directory: %w", err)
|
|
}
|
|
current, err = filepath.Abs(current)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve Factory Droid working directory: %w", err)
|
|
}
|
|
root, found, err := droidGitRoot(current)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
root = current
|
|
}
|
|
directories := droidProjectDirectories(root, current)
|
|
locations := make([][2]droidPolicyLocation, 0, len(directories))
|
|
for index, directory := range directories {
|
|
scope := "project"
|
|
if index > 0 {
|
|
scope = "folder " + directory
|
|
}
|
|
factory := filepath.Join(directory, ".factory")
|
|
locations = append(locations, [2]droidPolicyLocation{
|
|
{filepath.Join(factory, "settings.json"), "Factory Droid " + scope + " settings"},
|
|
{filepath.Join(factory, "settings.local.json"), "Factory Droid " + scope + " local settings"},
|
|
})
|
|
}
|
|
return locations, nil
|
|
}
|
|
|
|
func droidScopeHooksDisabled(
|
|
settings, local *droidPolicyLayer,
|
|
) *droidPolicyLayer {
|
|
if local.hooksDisabled != nil {
|
|
return local
|
|
}
|
|
if settings.hooksDisabled != nil {
|
|
return settings
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readDroidPolicy() (*droidPolicyState, error) {
|
|
managed, err := readDroidPolicyLayer(droidPolicyLocation{
|
|
droidManagedSettingsPath(),
|
|
"Factory Droid system-managed settings",
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
locations, err := droidProjectPolicyLocations()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
projectLayers := make([][2]*droidPolicyLayer, 0, len(locations))
|
|
for _, scope := range locations {
|
|
settings, readErr := readDroidPolicyLayer(scope[0])
|
|
if readErr != nil {
|
|
return nil, readErr
|
|
}
|
|
local, readErr := readDroidPolicyLayer(scope[1])
|
|
if readErr != nil {
|
|
return nil, readErr
|
|
}
|
|
projectLayers = append(projectLayers, [2]*droidPolicyLayer{settings, local})
|
|
}
|
|
state := &droidPolicyState{}
|
|
if managed.allowManagedHooksOnly != nil && *managed.allowManagedHooksOnly {
|
|
state.allowManagedHooksOnlyBy = &droidPolicyOrigin{managed.filePath, managed.label}
|
|
}
|
|
selected := managed
|
|
if selected.hooksDisabled == nil {
|
|
selected = nil
|
|
for _, scope := range projectLayers {
|
|
if layer := droidScopeHooksDisabled(scope[0], scope[1]); layer != nil {
|
|
selected = layer
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if selected != nil {
|
|
state.hooksDisabled = selected.hooksDisabled
|
|
state.hooksDisabledBy = &droidPolicyOrigin{selected.filePath, selected.label}
|
|
}
|
|
layers := []*droidPolicyLayer{managed}
|
|
for _, scope := range projectLayers {
|
|
layers = append(layers, scope[0], scope[1])
|
|
}
|
|
state.preconditions = make([]filePrecondition, 0, len(layers))
|
|
for _, layer := range layers {
|
|
state.preconditions = append(state.preconditions, filePrecondition{
|
|
filePath: layer.filePath,
|
|
label: layer.label,
|
|
snapshot: layer.snapshot,
|
|
maximumSize: maxManagedSourceBytes,
|
|
})
|
|
}
|
|
return state, nil
|
|
}
|