mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-02-14 07:55:03 -05:00
Update version to v1.4.128 and commit
This commit is contained in:
@@ -14,15 +14,15 @@ import (
|
||||
// ExtensionExecutor handles the secure execution of extensions
|
||||
// It uses the registry to verify extensions before running them
|
||||
type ExtensionExecutor struct {
|
||||
registry *ExtensionRegistry
|
||||
registry *ExtensionRegistry
|
||||
}
|
||||
|
||||
// NewExtensionExecutor creates a new executor instance
|
||||
// It requires a registry to verify extensions
|
||||
func NewExtensionExecutor(registry *ExtensionRegistry) *ExtensionExecutor {
|
||||
return &ExtensionExecutor{
|
||||
registry: registry,
|
||||
}
|
||||
return &ExtensionExecutor{
|
||||
registry: registry,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute runs an extension with the given operation and value string
|
||||
@@ -34,19 +34,19 @@ func (e *ExtensionExecutor) Execute(name, operation, value string) (string, erro
|
||||
// Get and verify extension from registry
|
||||
ext, err := e.registry.GetExtension(name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get extension: %w", err)
|
||||
return "", fmt.Errorf("failed to get extension: %w", err)
|
||||
}
|
||||
|
||||
// Format the command using our template system
|
||||
cmdStr, err := e.formatCommand(ext, operation, value)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to format command: %w", err)
|
||||
return "", fmt.Errorf("failed to format command: %w", err)
|
||||
}
|
||||
|
||||
// Split the command string into command and arguments
|
||||
cmdParts := strings.Fields(cmdStr)
|
||||
if len(cmdParts) < 1 {
|
||||
return "", fmt.Errorf("empty command after formatting")
|
||||
return "", fmt.Errorf("empty command after formatting")
|
||||
}
|
||||
|
||||
// Create command with the Executable and formatted arguments
|
||||
@@ -55,13 +55,13 @@ func (e *ExtensionExecutor) Execute(name, operation, value string) (string, erro
|
||||
|
||||
// Set up environment if specified
|
||||
if len(ext.Env) > 0 {
|
||||
cmd.Env = append(os.Environ(), ext.Env...)
|
||||
cmd.Env = append(os.Environ(), ext.Env...)
|
||||
}
|
||||
|
||||
// Execute based on output method
|
||||
outputMethod := ext.GetOutputMethod()
|
||||
if outputMethod == "file" {
|
||||
return e.executeWithFile(cmd, ext)
|
||||
return e.executeWithFile(cmd, ext)
|
||||
}
|
||||
return e.executeStdout(cmd, ext)
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func (e *ExtensionExecutor) formatCommand(ext *ExtensionDefinition, operation st
|
||||
// Get operation config
|
||||
opConfig, exists := ext.Operations[operation]
|
||||
if !exists {
|
||||
return "", fmt.Errorf("operation %s not found for extension %s", operation, ext.Name)
|
||||
return "", fmt.Errorf("operation %s not found for extension %s", operation, ext.Name)
|
||||
}
|
||||
|
||||
vars := make(map[string]string)
|
||||
@@ -83,27 +83,27 @@ func (e *ExtensionExecutor) formatCommand(ext *ExtensionDefinition, operation st
|
||||
// Split on pipe for numbered variables
|
||||
values := strings.Split(value, "|")
|
||||
for i, val := range values {
|
||||
vars[fmt.Sprintf("%d", i+1)] = val
|
||||
vars[fmt.Sprintf("%d", i+1)] = val
|
||||
}
|
||||
|
||||
|
||||
return ApplyTemplate(opConfig.CmdTemplate, vars, "")
|
||||
}
|
||||
|
||||
// executeStdout runs the command and captures its stdout
|
||||
func (e *ExtensionExecutor) executeStdout(cmd *exec.Cmd, ext *ExtensionDefinition) (string, error) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
//debug output
|
||||
fmt.Printf("Executing command: %s\n", cmd.String())
|
||||
//debug output
|
||||
fmt.Printf("Executing command: %s\n", cmd.String())
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", fmt.Errorf("execution failed: %w\nstderr: %s", err, stderr.String())
|
||||
}
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", fmt.Errorf("execution failed: %w\nstderr: %s", err, stderr.String())
|
||||
}
|
||||
|
||||
return stdout.String(), nil
|
||||
return stdout.String(), nil
|
||||
}
|
||||
|
||||
// executeWithFile runs the command and handles file-based output
|
||||
@@ -111,7 +111,7 @@ func (e *ExtensionExecutor) executeWithFile(cmd *exec.Cmd, ext *ExtensionDefinit
|
||||
// Parse timeout - this is now a first-class field
|
||||
timeout, err := time.ParseDuration(ext.Timeout)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid timeout format: %w", err)
|
||||
return "", fmt.Errorf("invalid timeout format: %w", err)
|
||||
}
|
||||
|
||||
// Create context with timeout
|
||||
@@ -122,51 +122,51 @@ func (e *ExtensionExecutor) executeWithFile(cmd *exec.Cmd, ext *ExtensionDefinit
|
||||
|
||||
fileConfig := ext.GetFileConfig()
|
||||
if fileConfig == nil {
|
||||
return "", fmt.Errorf("no file configuration found")
|
||||
return "", fmt.Errorf("no file configuration found")
|
||||
}
|
||||
|
||||
// Handle path from stdout case
|
||||
if pathFromStdout, ok := fileConfig["path_from_stdout"].(bool); ok && pathFromStdout {
|
||||
return e.handlePathFromStdout(cmd, ext)
|
||||
return e.handlePathFromStdout(cmd, ext)
|
||||
}
|
||||
|
||||
// Handle fixed file case
|
||||
workDir, _ := fileConfig["work_dir"].(string)
|
||||
outputFile, _ := fileConfig["output_file"].(string)
|
||||
|
||||
|
||||
if outputFile == "" {
|
||||
return "", fmt.Errorf("no output file specified in configuration")
|
||||
return "", fmt.Errorf("no output file specified in configuration")
|
||||
}
|
||||
|
||||
// Set working directory if specified
|
||||
if workDir != "" {
|
||||
cmd.Dir = workDir
|
||||
cmd.Dir = workDir
|
||||
}
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return "", fmt.Errorf("execution timed out after %v", timeout)
|
||||
}
|
||||
return "", fmt.Errorf("execution failed: %w\nerr: %s", err, stderr.String())
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return "", fmt.Errorf("execution timed out after %v", timeout)
|
||||
}
|
||||
return "", fmt.Errorf("execution failed: %w\nerr: %s", err, stderr.String())
|
||||
}
|
||||
|
||||
// Construct full file path
|
||||
outputPath := outputFile
|
||||
if workDir != "" {
|
||||
outputPath = filepath.Join(workDir, outputFile)
|
||||
outputPath = filepath.Join(workDir, outputFile)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(outputPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read output file: %w", err)
|
||||
return "", fmt.Errorf("failed to read output file: %w", err)
|
||||
}
|
||||
|
||||
// Handle cleanup if enabled
|
||||
if ext.IsCleanupEnabled() {
|
||||
defer os.Remove(outputPath)
|
||||
defer os.Remove(outputPath)
|
||||
}
|
||||
|
||||
return string(content), nil
|
||||
@@ -179,18 +179,18 @@ func (e *ExtensionExecutor) handlePathFromStdout(cmd *exec.Cmd, ext *ExtensionDe
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", fmt.Errorf("failed to get output path: %w\nerr: %s", err, stderr.String())
|
||||
return "", fmt.Errorf("failed to get output path: %w\nerr: %s", err, stderr.String())
|
||||
}
|
||||
|
||||
outputPath := strings.TrimSpace(stdout.String())
|
||||
content, err := os.ReadFile(outputPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read output file: %w", err)
|
||||
return "", fmt.Errorf("failed to read output file: %w", err)
|
||||
}
|
||||
|
||||
if ext.IsCleanupEnabled() {
|
||||
defer os.Remove(outputPath)
|
||||
defer os.Remove(outputPath)
|
||||
}
|
||||
|
||||
return string(content), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ config:
|
||||
"cleanup": "true",
|
||||
}
|
||||
|
||||
err := createExtension("basic-test", "write",
|
||||
err := createExtension("basic-test", "write",
|
||||
"{{executable}} write {{1}} "+outputFile, config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create extension: %v", err)
|
||||
@@ -261,7 +261,7 @@ config:
|
||||
// Test cleanup behavior
|
||||
t.Run("CleanupBehavior", func(t *testing.T) {
|
||||
outputFile := filepath.Join(tmpDir, "cleanup-test.txt")
|
||||
|
||||
|
||||
// Test with cleanup enabled
|
||||
config := map[string]interface{}{
|
||||
"output_file": `"cleanup-test.txt"`,
|
||||
@@ -357,4 +357,4 @@ config:
|
||||
t.Error("Expected error from missing output_file, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,59 +11,59 @@ import (
|
||||
|
||||
// ExtensionManager handles the high-level operations of the extension system
|
||||
type ExtensionManager struct {
|
||||
registry *ExtensionRegistry
|
||||
executor *ExtensionExecutor
|
||||
configDir string
|
||||
registry *ExtensionRegistry
|
||||
executor *ExtensionExecutor
|
||||
configDir string
|
||||
}
|
||||
|
||||
// NewExtensionManager creates a new extension manager instance
|
||||
func NewExtensionManager(configDir string) *ExtensionManager {
|
||||
registry := NewExtensionRegistry(configDir)
|
||||
return &ExtensionManager{
|
||||
registry: registry,
|
||||
executor: NewExtensionExecutor(registry),
|
||||
configDir: configDir,
|
||||
}
|
||||
registry := NewExtensionRegistry(configDir)
|
||||
return &ExtensionManager{
|
||||
registry: registry,
|
||||
executor: NewExtensionExecutor(registry),
|
||||
configDir: configDir,
|
||||
}
|
||||
}
|
||||
|
||||
// ListExtensions handles the listextensions flag action
|
||||
func (em *ExtensionManager) ListExtensions() error {
|
||||
if em.registry == nil || em.registry.registry.Extensions == nil {
|
||||
return fmt.Errorf("extension registry not initialized")
|
||||
return fmt.Errorf("extension registry not initialized")
|
||||
}
|
||||
|
||||
for name, entry := range em.registry.registry.Extensions {
|
||||
fmt.Printf("Extension: %s\n", name)
|
||||
|
||||
// Try to load extension details
|
||||
ext, err := em.registry.GetExtension(name)
|
||||
if err != nil {
|
||||
fmt.Printf(" Status: DISABLED - Hash verification failed: %v\n", err)
|
||||
fmt.Printf(" Config Path: %s\n\n", entry.ConfigPath)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("Extension: %s\n", name)
|
||||
|
||||
// Print extension details if verification succeeded
|
||||
fmt.Printf(" Status: ENABLED\n")
|
||||
fmt.Printf(" Executable: %s\n", ext.Executable)
|
||||
fmt.Printf(" Type: %s\n", ext.Type)
|
||||
fmt.Printf(" Timeout: %s\n", ext.Timeout)
|
||||
fmt.Printf(" Description: %s\n", ext.Description)
|
||||
fmt.Printf(" Version: %s\n", ext.Version)
|
||||
|
||||
fmt.Printf(" Operations:\n")
|
||||
for opName, opConfig := range ext.Operations {
|
||||
fmt.Printf(" %s:\n", opName)
|
||||
fmt.Printf(" Command Template: %s\n", opConfig.CmdTemplate)
|
||||
// Try to load extension details
|
||||
ext, err := em.registry.GetExtension(name)
|
||||
if err != nil {
|
||||
fmt.Printf(" Status: DISABLED - Hash verification failed: %v\n", err)
|
||||
fmt.Printf(" Config Path: %s\n\n", entry.ConfigPath)
|
||||
continue
|
||||
}
|
||||
|
||||
// Print extension details if verification succeeded
|
||||
fmt.Printf(" Status: ENABLED\n")
|
||||
fmt.Printf(" Executable: %s\n", ext.Executable)
|
||||
fmt.Printf(" Type: %s\n", ext.Type)
|
||||
fmt.Printf(" Timeout: %s\n", ext.Timeout)
|
||||
fmt.Printf(" Description: %s\n", ext.Description)
|
||||
fmt.Printf(" Version: %s\n", ext.Version)
|
||||
|
||||
fmt.Printf(" Operations:\n")
|
||||
for opName, opConfig := range ext.Operations {
|
||||
fmt.Printf(" %s:\n", opName)
|
||||
fmt.Printf(" Command Template: %s\n", opConfig.CmdTemplate)
|
||||
}
|
||||
|
||||
if fileConfig := ext.GetFileConfig(); fileConfig != nil {
|
||||
fmt.Printf(" File Configuration:\n")
|
||||
for k, v := range fileConfig {
|
||||
fmt.Printf(" %s: %v\n", k, v)
|
||||
}
|
||||
|
||||
if fileConfig := ext.GetFileConfig(); fileConfig != nil {
|
||||
fmt.Printf(" File Configuration:\n")
|
||||
for k, v := range fileConfig {
|
||||
fmt.Printf(" %s: %v\n", k, v)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -73,27 +73,27 @@ func (em *ExtensionManager) ListExtensions() error {
|
||||
func (em *ExtensionManager) RegisterExtension(configPath string) error {
|
||||
absPath, err := filepath.Abs(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid config path: %w", err)
|
||||
return fmt.Errorf("invalid config path: %w", err)
|
||||
}
|
||||
|
||||
// Get extension name before registration for status message
|
||||
data, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read config file: %w", err)
|
||||
return fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
var ext ExtensionDefinition
|
||||
if err := yaml.Unmarshal(data, &ext); err != nil {
|
||||
return fmt.Errorf("failed to parse config file: %w", err)
|
||||
return fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
if err := em.registry.Register(absPath); err != nil {
|
||||
return fmt.Errorf("failed to register extension: %w", err)
|
||||
return fmt.Errorf("failed to register extension: %w", err)
|
||||
}
|
||||
|
||||
if _, err := time.ParseDuration(ext.Timeout); err != nil {
|
||||
return fmt.Errorf("invalid timeout value '%s': must be a duration like '30s' or '1m': %w", ext.Timeout, err)
|
||||
}
|
||||
return fmt.Errorf("invalid timeout value '%s': must be a duration like '30s' or '1m': %w", ext.Timeout, err)
|
||||
}
|
||||
|
||||
// Print success message with extension details
|
||||
fmt.Printf("Successfully registered extension:\n")
|
||||
@@ -103,18 +103,18 @@ func (em *ExtensionManager) RegisterExtension(configPath string) error {
|
||||
fmt.Printf(" Timeout: %s\n", ext.Timeout)
|
||||
fmt.Printf(" Description: %s\n", ext.Description)
|
||||
fmt.Printf(" Version: %s\n", ext.Version)
|
||||
|
||||
|
||||
fmt.Printf(" Operations:\n")
|
||||
for opName, opConfig := range ext.Operations {
|
||||
fmt.Printf(" %s:\n", opName)
|
||||
fmt.Printf(" Command Template: %s\n", opConfig.CmdTemplate)
|
||||
fmt.Printf(" %s:\n", opName)
|
||||
fmt.Printf(" Command Template: %s\n", opConfig.CmdTemplate)
|
||||
}
|
||||
|
||||
if fileConfig := ext.GetFileConfig(); fileConfig != nil {
|
||||
fmt.Printf(" File Configuration:\n")
|
||||
for k, v := range fileConfig {
|
||||
fmt.Printf(" %s: %v\n", k, v)
|
||||
}
|
||||
fmt.Printf(" File Configuration:\n")
|
||||
for k, v := range fileConfig {
|
||||
fmt.Printf(" %s: %v\n", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -122,14 +122,14 @@ func (em *ExtensionManager) RegisterExtension(configPath string) error {
|
||||
|
||||
// RemoveExtension handles the rmextension flag action
|
||||
func (em *ExtensionManager) RemoveExtension(name string) error {
|
||||
if err := em.registry.Remove(name); err != nil {
|
||||
return fmt.Errorf("failed to remove extension: %w", err)
|
||||
}
|
||||
if err := em.registry.Remove(name); err != nil {
|
||||
return fmt.Errorf("failed to remove extension: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessExtension handles template processing for extension directives
|
||||
func (em *ExtensionManager) ProcessExtension(name, operation, value string) (string, error) {
|
||||
return em.executor.Execute(name, operation, value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,4 +181,4 @@ timeout: 30s`,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,321 +16,314 @@ import (
|
||||
|
||||
// ExtensionDefinition represents a single extension configuration
|
||||
type ExtensionDefinition struct {
|
||||
// Global properties
|
||||
Name string `yaml:"name"`
|
||||
Executable string `yaml:"executable"`
|
||||
Type string `yaml:"type"`
|
||||
Timeout string `yaml:"timeout"`
|
||||
Description string `yaml:"description"`
|
||||
Version string `yaml:"version"`
|
||||
Env []string `yaml:"env"`
|
||||
|
||||
// Operation-specific commands
|
||||
Operations map[string]OperationConfig `yaml:"operations"`
|
||||
|
||||
// Additional config
|
||||
Config map[string]interface{} `yaml:"config"`
|
||||
// Global properties
|
||||
Name string `yaml:"name"`
|
||||
Executable string `yaml:"executable"`
|
||||
Type string `yaml:"type"`
|
||||
Timeout string `yaml:"timeout"`
|
||||
Description string `yaml:"description"`
|
||||
Version string `yaml:"version"`
|
||||
Env []string `yaml:"env"`
|
||||
|
||||
// Operation-specific commands
|
||||
Operations map[string]OperationConfig `yaml:"operations"`
|
||||
|
||||
// Additional config
|
||||
Config map[string]interface{} `yaml:"config"`
|
||||
}
|
||||
|
||||
type OperationConfig struct {
|
||||
CmdTemplate string `yaml:"cmd_template"`
|
||||
CmdTemplate string `yaml:"cmd_template"`
|
||||
}
|
||||
|
||||
|
||||
|
||||
// RegistryEntry represents a registered extension
|
||||
type RegistryEntry struct {
|
||||
ConfigPath string `yaml:"config_path"`
|
||||
ConfigHash string `yaml:"config_hash"`
|
||||
ExecutableHash string `yaml:"executable_hash"`
|
||||
ConfigPath string `yaml:"config_path"`
|
||||
ConfigHash string `yaml:"config_hash"`
|
||||
ExecutableHash string `yaml:"executable_hash"`
|
||||
}
|
||||
|
||||
type ExtensionRegistry struct {
|
||||
configDir string
|
||||
registry struct {
|
||||
Extensions map[string]*RegistryEntry `yaml:"extensions"`
|
||||
}
|
||||
configDir string
|
||||
registry struct {
|
||||
Extensions map[string]*RegistryEntry `yaml:"extensions"`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Helper methods for Config access
|
||||
func (e *ExtensionDefinition) GetOutputMethod() string {
|
||||
if output, ok := e.Config["output"].(map[string]interface{}); ok {
|
||||
if method, ok := output["method"].(string); ok {
|
||||
return method
|
||||
}
|
||||
if method, ok := output["method"].(string); ok {
|
||||
return method
|
||||
}
|
||||
}
|
||||
return "stdout" // default to stdout if not specified
|
||||
}
|
||||
|
||||
func (e *ExtensionDefinition) GetFileConfig() map[string]interface{} {
|
||||
if output, ok := e.Config["output"].(map[string]interface{}); ok {
|
||||
if fileConfig, ok := output["file_config"].(map[string]interface{}); ok {
|
||||
return fileConfig
|
||||
}
|
||||
if fileConfig, ok := output["file_config"].(map[string]interface{}); ok {
|
||||
return fileConfig
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *ExtensionDefinition) IsCleanupEnabled() bool {
|
||||
if fc := e.GetFileConfig(); fc != nil {
|
||||
if cleanup, ok := fc["cleanup"].(bool); ok {
|
||||
return cleanup
|
||||
}
|
||||
if cleanup, ok := fc["cleanup"].(bool); ok {
|
||||
return cleanup
|
||||
}
|
||||
}
|
||||
return false // default to no cleanup
|
||||
}
|
||||
|
||||
|
||||
func NewExtensionRegistry(configDir string) *ExtensionRegistry {
|
||||
r := &ExtensionRegistry{
|
||||
configDir: configDir,
|
||||
}
|
||||
r.registry.Extensions = make(map[string]*RegistryEntry)
|
||||
|
||||
r.ensureConfigDir()
|
||||
|
||||
if err := r.loadRegistry(); err != nil {
|
||||
if Debug {
|
||||
fmt.Printf("Warning: could not load extension registry: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
r := &ExtensionRegistry{
|
||||
configDir: configDir,
|
||||
}
|
||||
r.registry.Extensions = make(map[string]*RegistryEntry)
|
||||
|
||||
r.ensureConfigDir()
|
||||
|
||||
if err := r.loadRegistry(); err != nil {
|
||||
if Debug {
|
||||
fmt.Printf("Warning: could not load extension registry: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ExtensionRegistry) ensureConfigDir() error {
|
||||
extDir := filepath.Join(r.configDir, "extensions")
|
||||
return os.MkdirAll(extDir, 0755)
|
||||
extDir := filepath.Join(r.configDir, "extensions")
|
||||
return os.MkdirAll(extDir, 0755)
|
||||
}
|
||||
|
||||
// Update the Register method in extension_registry.go
|
||||
|
||||
func (r *ExtensionRegistry) Register(configPath string) error {
|
||||
// Read and parse the extension definition to verify it
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
// Read and parse the extension definition to verify it
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
var ext ExtensionDefinition
|
||||
if err := yaml.Unmarshal(data, &ext); err != nil {
|
||||
return fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
var ext ExtensionDefinition
|
||||
if err := yaml.Unmarshal(data, &ext); err != nil {
|
||||
return fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
// Validate extension name
|
||||
if ext.Name == "" {
|
||||
return fmt.Errorf("extension name cannot be empty")
|
||||
}
|
||||
|
||||
if strings.Contains(ext.Name, " ") {
|
||||
return fmt.Errorf("extension name '%s' contains spaces - names must not contain spaces", ext.Name)
|
||||
}
|
||||
// Validate extension name
|
||||
if ext.Name == "" {
|
||||
return fmt.Errorf("extension name cannot be empty")
|
||||
}
|
||||
|
||||
// Verify executable exists
|
||||
if _, err := os.Stat(ext.Executable); err != nil {
|
||||
return fmt.Errorf("executable not found: %w", err)
|
||||
}
|
||||
if strings.Contains(ext.Name, " ") {
|
||||
return fmt.Errorf("extension name '%s' contains spaces - names must not contain spaces", ext.Name)
|
||||
}
|
||||
|
||||
// Get absolute path to config
|
||||
absPath, err := filepath.Abs(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
// Verify executable exists
|
||||
if _, err := os.Stat(ext.Executable); err != nil {
|
||||
return fmt.Errorf("executable not found: %w", err)
|
||||
}
|
||||
|
||||
// Calculate hashes
|
||||
configHash := ComputeStringHash(string(data))
|
||||
executableHash, err := ComputeHash(ext.Executable)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash executable: %w", err)
|
||||
}
|
||||
// Get absolute path to config
|
||||
absPath, err := filepath.Abs(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
|
||||
// Store entry
|
||||
r.registry.Extensions[ext.Name] = &RegistryEntry{
|
||||
ConfigPath: absPath,
|
||||
ConfigHash: configHash,
|
||||
ExecutableHash: executableHash,
|
||||
}
|
||||
// Calculate hashes
|
||||
configHash := ComputeStringHash(string(data))
|
||||
executableHash, err := ComputeHash(ext.Executable)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash executable: %w", err)
|
||||
}
|
||||
|
||||
return r.saveRegistry()
|
||||
// Store entry
|
||||
r.registry.Extensions[ext.Name] = &RegistryEntry{
|
||||
ConfigPath: absPath,
|
||||
ConfigHash: configHash,
|
||||
ExecutableHash: executableHash,
|
||||
}
|
||||
|
||||
return r.saveRegistry()
|
||||
}
|
||||
|
||||
func (r *ExtensionRegistry) validateExtensionDefinition(ext *ExtensionDefinition) error {
|
||||
// Validate required fields
|
||||
if ext.Name == "" {
|
||||
return fmt.Errorf("extension name is required")
|
||||
}
|
||||
if ext.Executable == "" {
|
||||
return fmt.Errorf("executable path is required")
|
||||
}
|
||||
if ext.Type == "" {
|
||||
return fmt.Errorf("extension type is required")
|
||||
}
|
||||
// Validate required fields
|
||||
if ext.Name == "" {
|
||||
return fmt.Errorf("extension name is required")
|
||||
}
|
||||
if ext.Executable == "" {
|
||||
return fmt.Errorf("executable path is required")
|
||||
}
|
||||
if ext.Type == "" {
|
||||
return fmt.Errorf("extension type is required")
|
||||
}
|
||||
|
||||
// Validate timeout format
|
||||
if ext.Timeout != "" {
|
||||
if _, err := time.ParseDuration(ext.Timeout); err != nil {
|
||||
return fmt.Errorf("invalid timeout format: %w", err)
|
||||
}
|
||||
}
|
||||
// Validate timeout format
|
||||
if ext.Timeout != "" {
|
||||
if _, err := time.ParseDuration(ext.Timeout); err != nil {
|
||||
return fmt.Errorf("invalid timeout format: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate operations
|
||||
if len(ext.Operations) == 0 {
|
||||
return fmt.Errorf("at least one operation must be defined")
|
||||
}
|
||||
for name, op := range ext.Operations {
|
||||
if op.CmdTemplate == "" {
|
||||
return fmt.Errorf("command template is required for operation %s", name)
|
||||
}
|
||||
}
|
||||
// Validate operations
|
||||
if len(ext.Operations) == 0 {
|
||||
return fmt.Errorf("at least one operation must be defined")
|
||||
}
|
||||
for name, op := range ext.Operations {
|
||||
if op.CmdTemplate == "" {
|
||||
return fmt.Errorf("command template is required for operation %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func (r *ExtensionRegistry) Remove(name string) error {
|
||||
if _, exists := r.registry.Extensions[name]; !exists {
|
||||
return fmt.Errorf("extension %s not found", name)
|
||||
}
|
||||
if _, exists := r.registry.Extensions[name]; !exists {
|
||||
return fmt.Errorf("extension %s not found", name)
|
||||
}
|
||||
|
||||
delete(r.registry.Extensions, name)
|
||||
delete(r.registry.Extensions, name)
|
||||
|
||||
return r.saveRegistry()
|
||||
return r.saveRegistry()
|
||||
}
|
||||
|
||||
func (r *ExtensionRegistry) Verify(name string) error {
|
||||
// Get the registry entry
|
||||
entry, exists := r.registry.Extensions[name]
|
||||
if !exists {
|
||||
return fmt.Errorf("extension %s not found", name)
|
||||
}
|
||||
// Get the registry entry
|
||||
entry, exists := r.registry.Extensions[name]
|
||||
if !exists {
|
||||
return fmt.Errorf("extension %s not found", name)
|
||||
}
|
||||
|
||||
// Load and parse the config file
|
||||
data, err := os.ReadFile(entry.ConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
// Load and parse the config file
|
||||
data, err := os.ReadFile(entry.ConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
// Verify config hash
|
||||
currentConfigHash := ComputeStringHash(string(data))
|
||||
if currentConfigHash != entry.ConfigHash {
|
||||
return fmt.Errorf("config file hash mismatch for %s", name)
|
||||
}
|
||||
// Verify config hash
|
||||
currentConfigHash := ComputeStringHash(string(data))
|
||||
if currentConfigHash != entry.ConfigHash {
|
||||
return fmt.Errorf("config file hash mismatch for %s", name)
|
||||
}
|
||||
|
||||
// Parse to get executable path
|
||||
var ext ExtensionDefinition
|
||||
if err := yaml.Unmarshal(data, &ext); err != nil {
|
||||
return fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
// Parse to get executable path
|
||||
var ext ExtensionDefinition
|
||||
if err := yaml.Unmarshal(data, &ext); err != nil {
|
||||
return fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
// Verify executable hash
|
||||
currentExecutableHash, err := ComputeHash(ext.Executable)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify executable: %w", err)
|
||||
}
|
||||
// Verify executable hash
|
||||
currentExecutableHash, err := ComputeHash(ext.Executable)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify executable: %w", err)
|
||||
}
|
||||
|
||||
if currentExecutableHash != entry.ExecutableHash {
|
||||
return fmt.Errorf("executable hash mismatch for %s", name)
|
||||
}
|
||||
if currentExecutableHash != entry.ExecutableHash {
|
||||
return fmt.Errorf("executable hash mismatch for %s", name)
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ExtensionRegistry) GetExtension(name string) (*ExtensionDefinition, error) {
|
||||
entry, exists := r.registry.Extensions[name]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("extension %s not found", name)
|
||||
}
|
||||
entry, exists := r.registry.Extensions[name]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("extension %s not found", name)
|
||||
}
|
||||
|
||||
// Read current config file
|
||||
data, err := os.ReadFile(entry.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
// Read current config file
|
||||
data, err := os.ReadFile(entry.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
// Verify config hash
|
||||
currentHash := ComputeStringHash(string(data))
|
||||
if currentHash != entry.ConfigHash {
|
||||
return nil, fmt.Errorf("config file hash mismatch for %s", name)
|
||||
}
|
||||
// Verify config hash
|
||||
currentHash := ComputeStringHash(string(data))
|
||||
if currentHash != entry.ConfigHash {
|
||||
return nil, fmt.Errorf("config file hash mismatch for %s", name)
|
||||
}
|
||||
|
||||
// Parse config
|
||||
var ext ExtensionDefinition
|
||||
if err := yaml.Unmarshal(data, &ext); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
// Parse config
|
||||
var ext ExtensionDefinition
|
||||
if err := yaml.Unmarshal(data, &ext); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
// Verify executable hash
|
||||
currentExecHash, err := ComputeHash(ext.Executable)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to verify executable: %w", err)
|
||||
}
|
||||
// Verify executable hash
|
||||
currentExecHash, err := ComputeHash(ext.Executable)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to verify executable: %w", err)
|
||||
}
|
||||
|
||||
if currentExecHash != entry.ExecutableHash {
|
||||
return nil, fmt.Errorf("executable hash mismatch for %s", name)
|
||||
}
|
||||
if currentExecHash != entry.ExecutableHash {
|
||||
return nil, fmt.Errorf("executable hash mismatch for %s", name)
|
||||
}
|
||||
|
||||
return &ext, nil
|
||||
return &ext, nil
|
||||
}
|
||||
|
||||
|
||||
func (r *ExtensionRegistry) ListExtensions() ([]*ExtensionDefinition, error) {
|
||||
var exts []*ExtensionDefinition
|
||||
|
||||
for name := range r.registry.Extensions {
|
||||
ext, err := r.GetExtension(name)
|
||||
if err != nil {
|
||||
// Instead of failing, we'll return nil for this extension
|
||||
// The manager will handle displaying the error
|
||||
exts = append(exts, nil)
|
||||
continue
|
||||
}
|
||||
exts = append(exts, ext)
|
||||
}
|
||||
|
||||
return exts, nil
|
||||
var exts []*ExtensionDefinition
|
||||
|
||||
for name := range r.registry.Extensions {
|
||||
ext, err := r.GetExtension(name)
|
||||
if err != nil {
|
||||
// Instead of failing, we'll return nil for this extension
|
||||
// The manager will handle displaying the error
|
||||
exts = append(exts, nil)
|
||||
continue
|
||||
}
|
||||
exts = append(exts, ext)
|
||||
}
|
||||
|
||||
return exts, nil
|
||||
}
|
||||
|
||||
func (r *ExtensionRegistry) calculateFileHash(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func (r *ExtensionRegistry) saveRegistry() error {
|
||||
data, err := yaml.Marshal(r.registry)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal extension registry: %w", err)
|
||||
}
|
||||
data, err := yaml.Marshal(r.registry)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal extension registry: %w", err)
|
||||
}
|
||||
|
||||
registryPath := filepath.Join(r.configDir, "extensions", "extensions.yaml")
|
||||
return os.WriteFile(registryPath, data, 0644)
|
||||
registryPath := filepath.Join(r.configDir, "extensions", "extensions.yaml")
|
||||
return os.WriteFile(registryPath, data, 0644)
|
||||
}
|
||||
|
||||
func (r *ExtensionRegistry) loadRegistry() error {
|
||||
registryPath := filepath.Join(r.configDir, "extensions", "extensions.yaml")
|
||||
data, err := os.ReadFile(registryPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil // New registry
|
||||
}
|
||||
return fmt.Errorf("failed to read extension registry: %w", err)
|
||||
if os.IsNotExist(err) {
|
||||
return nil // New registry
|
||||
}
|
||||
return fmt.Errorf("failed to read extension registry: %w", err)
|
||||
}
|
||||
|
||||
// Need to unmarshal the data into our registry
|
||||
if err := yaml.Unmarshal(data, &r.registry); err != nil {
|
||||
return fmt.Errorf("failed to parse extension registry: %w", err)
|
||||
return fmt.Errorf("failed to parse extension registry: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ operations:
|
||||
// Test hash verification
|
||||
t.Run("HashVerification", func(t *testing.T) {
|
||||
registry := NewExtensionRegistry(tmpDir)
|
||||
|
||||
|
||||
// Modify executable after registration
|
||||
modifiedExecContent := []byte("#!/bin/bash\necho \"modified\"")
|
||||
err := os.WriteFile(execPath, modifiedExecContent, 0755)
|
||||
@@ -72,4 +72,4 @@ operations:
|
||||
t.Error("Expected error when executable modified, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,23 +11,23 @@ import (
|
||||
// ComputeHash computes SHA-256 hash of a file at given path.
|
||||
// Returns the hex-encoded hash string or an error if the operation fails.
|
||||
func ComputeHash(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("open file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("open file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", fmt.Errorf("read file: %w", err)
|
||||
}
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", fmt.Errorf("read file: %w", err)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// ComputeStringHash returns hex-encoded SHA-256 hash of the given string
|
||||
func ComputeStringHash(s string) string {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(s))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
h := sha256.New()
|
||||
h.Write([]byte(s))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
@@ -8,112 +8,112 @@ import (
|
||||
)
|
||||
|
||||
func TestComputeHash(t *testing.T) {
|
||||
// Create a temporary test file
|
||||
content := []byte("test content for hashing")
|
||||
tmpfile, err := os.CreateTemp("", "hashtest")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
// Create a temporary test file
|
||||
content := []byte("test content for hashing")
|
||||
tmpfile, err := os.CreateTemp("", "hashtest")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
if _, err := tmpfile.Write(content); err != nil {
|
||||
t.Fatalf("failed to write to temp file: %v", err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
t.Fatalf("failed to close temp file: %v", err)
|
||||
}
|
||||
if _, err := tmpfile.Write(content); err != nil {
|
||||
t.Fatalf("failed to write to temp file: %v", err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
t.Fatalf("failed to close temp file: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
want string // known hash for test content
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid file",
|
||||
path: tmpfile.Name(),
|
||||
want: "e25dd806d495b413931f4eea50b677a7a5c02d00460924661283f211a37f7e7f", // pre-computed hash of "test content for hashing"
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nonexistent file",
|
||||
path: filepath.Join(os.TempDir(), "nonexistent"),
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
want string // known hash for test content
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid file",
|
||||
path: tmpfile.Name(),
|
||||
want: "e25dd806d495b413931f4eea50b677a7a5c02d00460924661283f211a37f7e7f", // pre-computed hash of "test content for hashing"
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "nonexistent file",
|
||||
path: filepath.Join(os.TempDir(), "nonexistent"),
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ComputeHash(tt.path)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ComputeHash() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want && !tt.wantErr {
|
||||
t.Errorf("ComputeHash() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ComputeHash(tt.path)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ComputeHash() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want && !tt.wantErr {
|
||||
t.Errorf("ComputeHash() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeStringHash(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
want: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
},
|
||||
{
|
||||
name: "simple string",
|
||||
input: "test",
|
||||
want: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
|
||||
},
|
||||
{
|
||||
name: "longer string with spaces",
|
||||
input: "this is a test string",
|
||||
want: "f6774519d1c7a3389ef327e9c04766b999db8cdfb85d1346c471ee86d65885bc",
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
want: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
},
|
||||
{
|
||||
name: "simple string",
|
||||
input: "test",
|
||||
want: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
|
||||
},
|
||||
{
|
||||
name: "longer string with spaces",
|
||||
input: "this is a test string",
|
||||
want: "f6774519d1c7a3389ef327e9c04766b999db8cdfb85d1346c471ee86d65885bc",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ComputeStringHash(tt.input); got != tt.want {
|
||||
t.Errorf("ComputeStringHash() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ComputeStringHash(tt.input); got != tt.want {
|
||||
t.Errorf("ComputeStringHash() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHashConsistency ensures both hash functions produce same results for same content
|
||||
func TestHashConsistency(t *testing.T) {
|
||||
content := "test content for consistency check"
|
||||
|
||||
// Create a file with the test content
|
||||
tmpfile, err := os.CreateTemp("", "hashconsistency")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
content := "test content for consistency check"
|
||||
|
||||
if err := os.WriteFile(tmpfile.Name(), []byte(content), 0644); err != nil {
|
||||
t.Fatalf("failed to write to temp file: %v", err)
|
||||
}
|
||||
// Create a file with the test content
|
||||
tmpfile, err := os.CreateTemp("", "hashconsistency")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
// Get hashes using both methods
|
||||
fileHash, err := ComputeHash(tmpfile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("ComputeHash failed: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(tmpfile.Name(), []byte(content), 0644); err != nil {
|
||||
t.Fatalf("failed to write to temp file: %v", err)
|
||||
}
|
||||
|
||||
stringHash := ComputeStringHash(content)
|
||||
// Get hashes using both methods
|
||||
fileHash, err := ComputeHash(tmpfile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("ComputeHash failed: %v", err)
|
||||
}
|
||||
|
||||
// Compare results
|
||||
if fileHash != stringHash {
|
||||
t.Errorf("Hash inconsistency: file hash %v != string hash %v", fileHash, stringHash)
|
||||
}
|
||||
}
|
||||
stringHash := ComputeStringHash(content)
|
||||
|
||||
// Compare results
|
||||
if fileHash != stringHash {
|
||||
t.Errorf("Hash inconsistency: file hash %v != string hash %v", fileHash, stringHash)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,13 +20,13 @@ var (
|
||||
var extensionManager *ExtensionManager
|
||||
|
||||
func init() {
|
||||
homedir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
debugf("Warning: could not initialize extension manager: %v\n", err)
|
||||
}
|
||||
configDir := filepath.Join(homedir, ".config/fabric")
|
||||
extensionManager = NewExtensionManager(configDir)
|
||||
// Extensions will work if registry exists, otherwise they'll just fail gracefully
|
||||
homedir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
debugf("Warning: could not initialize extension manager: %v\n", err)
|
||||
}
|
||||
configDir := filepath.Join(homedir, ".config/fabric")
|
||||
extensionManager = NewExtensionManager(configDir)
|
||||
// Extensions will work if registry exists, otherwise they'll just fail gracefully
|
||||
}
|
||||
|
||||
var pluginPattern = regexp.MustCompile(`\{\{plugin:([^:]+):([^:]+)(?::([^}]+))?\}\}`)
|
||||
@@ -40,120 +40,118 @@ func debugf(format string, a ...interface{}) {
|
||||
|
||||
func ApplyTemplate(content string, variables map[string]string, input string) (string, error) {
|
||||
|
||||
var missingVars []string
|
||||
r := regexp.MustCompile(`\{\{([^{}]+)\}\}`)
|
||||
|
||||
debugf("Starting template processing\n")
|
||||
for strings.Contains(content, "{{") {
|
||||
matches := r.FindAllStringSubmatch(content, -1)
|
||||
if len(matches) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
replaced := false
|
||||
for _, match := range matches {
|
||||
fullMatch := match[0]
|
||||
varName := match[1]
|
||||
|
||||
// Check if this is a plugin call
|
||||
if strings.HasPrefix(varName, "plugin:") {
|
||||
pluginMatches := pluginPattern.FindStringSubmatch(fullMatch)
|
||||
if len(pluginMatches) >= 3 {
|
||||
namespace := pluginMatches[1]
|
||||
operation := pluginMatches[2]
|
||||
value := ""
|
||||
if len(pluginMatches) == 4 {
|
||||
value = pluginMatches[3]
|
||||
}
|
||||
|
||||
debugf("\nPlugin call:\n")
|
||||
debugf(" Namespace: %s\n", namespace)
|
||||
debugf(" Operation: %s\n", operation)
|
||||
debugf(" Value: %s\n", value)
|
||||
|
||||
var result string
|
||||
var err error
|
||||
|
||||
switch namespace {
|
||||
case "text":
|
||||
debugf("Executing text plugin\n")
|
||||
result, err = textPlugin.Apply(operation, value)
|
||||
case "datetime":
|
||||
debugf("Executing datetime plugin\n")
|
||||
result, err = datetimePlugin.Apply(operation, value)
|
||||
case "file":
|
||||
debugf("Executing file plugin\n")
|
||||
result, err = filePlugin.Apply(operation, value)
|
||||
debugf("File plugin result: %#v\n", result)
|
||||
case "fetch":
|
||||
debugf("Executing fetch plugin\n")
|
||||
result, err = fetchPlugin.Apply(operation, value)
|
||||
case "sys":
|
||||
debugf("Executing sys plugin\n")
|
||||
result, err = sysPlugin.Apply(operation, value)
|
||||
default:
|
||||
return "", fmt.Errorf("unknown plugin namespace: %s", namespace)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
debugf("Plugin error: %v\n", err)
|
||||
return "", fmt.Errorf("plugin %s error: %v", namespace, err)
|
||||
}
|
||||
|
||||
debugf("Plugin result: %s\n", result)
|
||||
content = strings.ReplaceAll(content, fullMatch, result)
|
||||
debugf("Content after replacement: %s\n", content)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if pluginMatches := extensionPattern.FindStringSubmatch(fullMatch); len(pluginMatches) >= 3 {
|
||||
name := pluginMatches[1]
|
||||
operation := pluginMatches[2]
|
||||
value := ""
|
||||
if len(pluginMatches) == 4 {
|
||||
value = pluginMatches[3]
|
||||
}
|
||||
|
||||
debugf("\nExtension call:\n")
|
||||
debugf(" Name: %s\n", name)
|
||||
debugf(" Operation: %s\n", operation)
|
||||
debugf(" Value: %s\n", value)
|
||||
|
||||
result, err := extensionManager.ProcessExtension(name, operation, value)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("extension %s error: %v", name, err)
|
||||
}
|
||||
|
||||
content = strings.ReplaceAll(content, fullMatch, result)
|
||||
replaced = true
|
||||
continue
|
||||
}
|
||||
var missingVars []string
|
||||
r := regexp.MustCompile(`\{\{([^{}]+)\}\}`)
|
||||
|
||||
debugf("Starting template processing\n")
|
||||
for strings.Contains(content, "{{") {
|
||||
matches := r.FindAllStringSubmatch(content, -1)
|
||||
if len(matches) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
replaced := false
|
||||
for _, match := range matches {
|
||||
fullMatch := match[0]
|
||||
varName := match[1]
|
||||
|
||||
// Handle regular variables and input
|
||||
debugf("Processing variable: %s\n", varName)
|
||||
if varName == "input" {
|
||||
debugf("Replacing {{input}}\n")
|
||||
replaced = true
|
||||
content = strings.ReplaceAll(content, fullMatch, input)
|
||||
} else {
|
||||
if val, ok := variables[varName]; !ok {
|
||||
debugf("Missing variable: %s\n", varName)
|
||||
missingVars = append(missingVars, varName)
|
||||
return "", fmt.Errorf("missing required variable: %s", varName)
|
||||
} else {
|
||||
debugf("Replacing variable %s with value: %s\n", varName, val)
|
||||
content = strings.ReplaceAll(content, fullMatch, val)
|
||||
replaced = true
|
||||
}
|
||||
}
|
||||
if !replaced {
|
||||
return "", fmt.Errorf("template processing stuck - potential infinite loop")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if this is a plugin call
|
||||
if strings.HasPrefix(varName, "plugin:") {
|
||||
pluginMatches := pluginPattern.FindStringSubmatch(fullMatch)
|
||||
if len(pluginMatches) >= 3 {
|
||||
namespace := pluginMatches[1]
|
||||
operation := pluginMatches[2]
|
||||
value := ""
|
||||
if len(pluginMatches) == 4 {
|
||||
value = pluginMatches[3]
|
||||
}
|
||||
|
||||
debugf("\nPlugin call:\n")
|
||||
debugf(" Namespace: %s\n", namespace)
|
||||
debugf(" Operation: %s\n", operation)
|
||||
debugf(" Value: %s\n", value)
|
||||
|
||||
var result string
|
||||
var err error
|
||||
|
||||
switch namespace {
|
||||
case "text":
|
||||
debugf("Executing text plugin\n")
|
||||
result, err = textPlugin.Apply(operation, value)
|
||||
case "datetime":
|
||||
debugf("Executing datetime plugin\n")
|
||||
result, err = datetimePlugin.Apply(operation, value)
|
||||
case "file":
|
||||
debugf("Executing file plugin\n")
|
||||
result, err = filePlugin.Apply(operation, value)
|
||||
debugf("File plugin result: %#v\n", result)
|
||||
case "fetch":
|
||||
debugf("Executing fetch plugin\n")
|
||||
result, err = fetchPlugin.Apply(operation, value)
|
||||
case "sys":
|
||||
debugf("Executing sys plugin\n")
|
||||
result, err = sysPlugin.Apply(operation, value)
|
||||
default:
|
||||
return "", fmt.Errorf("unknown plugin namespace: %s", namespace)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
debugf("Plugin error: %v\n", err)
|
||||
return "", fmt.Errorf("plugin %s error: %v", namespace, err)
|
||||
}
|
||||
|
||||
debugf("Plugin result: %s\n", result)
|
||||
content = strings.ReplaceAll(content, fullMatch, result)
|
||||
debugf("Content after replacement: %s\n", content)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if pluginMatches := extensionPattern.FindStringSubmatch(fullMatch); len(pluginMatches) >= 3 {
|
||||
name := pluginMatches[1]
|
||||
operation := pluginMatches[2]
|
||||
value := ""
|
||||
if len(pluginMatches) == 4 {
|
||||
value = pluginMatches[3]
|
||||
}
|
||||
|
||||
debugf("\nExtension call:\n")
|
||||
debugf(" Name: %s\n", name)
|
||||
debugf(" Operation: %s\n", operation)
|
||||
debugf(" Value: %s\n", value)
|
||||
|
||||
result, err := extensionManager.ProcessExtension(name, operation, value)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("extension %s error: %v", name, err)
|
||||
}
|
||||
|
||||
content = strings.ReplaceAll(content, fullMatch, result)
|
||||
replaced = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle regular variables and input
|
||||
debugf("Processing variable: %s\n", varName)
|
||||
if varName == "input" {
|
||||
debugf("Replacing {{input}}\n")
|
||||
replaced = true
|
||||
content = strings.ReplaceAll(content, fullMatch, input)
|
||||
} else {
|
||||
if val, ok := variables[varName]; !ok {
|
||||
debugf("Missing variable: %s\n", varName)
|
||||
missingVars = append(missingVars, varName)
|
||||
return "", fmt.Errorf("missing required variable: %s", varName)
|
||||
} else {
|
||||
debugf("Replacing variable %s with value: %s\n", varName, val)
|
||||
content = strings.ReplaceAll(content, fullMatch, val)
|
||||
replaced = true
|
||||
}
|
||||
}
|
||||
if !replaced {
|
||||
return "", fmt.Errorf("template processing stuck - potential infinite loop")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debugf("Starting template processing\n")
|
||||
for strings.Contains(content, "{{") {
|
||||
|
||||
@@ -16,26 +16,26 @@ import (
|
||||
// - cannot convert to absolute path
|
||||
// - path doesn't exist
|
||||
func ExpandPath(path string) (string, error) {
|
||||
// If path starts with ~
|
||||
if strings.HasPrefix(path, "~/") {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
// Replace ~/ with actual home directory
|
||||
path = filepath.Join(usr.HomeDir, path[2:])
|
||||
}
|
||||
// If path starts with ~
|
||||
if strings.HasPrefix(path, "~/") {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
// Replace ~/ with actual home directory
|
||||
path = filepath.Join(usr.HomeDir, path[2:])
|
||||
}
|
||||
|
||||
// Convert to absolute path
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
// Convert to absolute path
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
|
||||
// Check if path exists
|
||||
if _, err := os.Stat(absPath); err != nil {
|
||||
return "", fmt.Errorf("path does not exist: %w", err)
|
||||
}
|
||||
// Check if path exists
|
||||
if _, err := os.Stat(absPath); err != nil {
|
||||
return "", fmt.Errorf("path does not exist: %w", err)
|
||||
}
|
||||
|
||||
return absPath, nil
|
||||
return absPath, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user