github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/platform/runtime/envdef/file_transform.go (about) 1 package envdef 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "path/filepath" 8 "regexp" 9 "strings" 10 11 "github.com/ActiveState/cli/internal/errs" 12 "github.com/ActiveState/cli/internal/fileutils" 13 "github.com/ActiveState/cli/internal/locale" 14 "github.com/ActiveState/cli/internal/logging" 15 "github.com/ActiveState/cli/internal/multilog" 16 "github.com/ActiveState/cli/internal/rollbar" 17 ) 18 19 // FileTransform specifies a single transformation to be performed on files in artifacts post-installation 20 type FileTransform struct { 21 Pattern string `json:"pattern"` 22 In []string `json:"in"` 23 With string `json:"with"` 24 ConstTransforms []ConstTransform `json:"const_transforms"` 25 PadWith *string `json:"pad_with"` 26 } 27 28 // ConstTransform is a transformation that should be applied to substituted constants prior to substitution in files 29 type ConstTransform struct { 30 In []string `json:"in"` // List of constants to apply this transform to 31 Pattern string `json:"pattern"` 32 With string `json:"with"` 33 } 34 35 // applyConstTransforms applies the constant transforms to the Constants values 36 func (ft *FileTransform) applyConstTransforms(constants Constants) (Constants, error) { 37 // copy constants, such that we don't change it 38 cs := make(Constants) 39 for k, v := range constants { 40 cs[k] = v 41 } 42 for _, ct := range ft.ConstTransforms { 43 for _, inVar := range ct.In { 44 inSubst, ok := cs[inVar] 45 if !ok { 46 return cs, errs.New("Do not know what to replace constant %s with.", inVar) 47 } 48 cs[inVar] = strings.ReplaceAll(inSubst, string(ct.Pattern), string(ct.With)) 49 } 50 } 51 52 return cs, nil 53 } 54 55 func (ft *FileTransform) relocateFile(fileBytes []byte, replacement string) ([]byte, error) { 56 findBytes := []byte(ft.Pattern) 57 replacementBytes := []byte(replacement) 58 59 // If `pad_width == null`, no padding is necessary and we can just replace the string and return 60 if ft.PadWith == nil { 61 return bytes.ReplaceAll(fileBytes, findBytes, replacementBytes), nil 62 } 63 64 // padding should be one byte 65 if len(*ft.PadWith) != 1 { 66 return fileBytes, errs.New("Padding character needs to have exactly one byte, got %d", len(*ft.PadWith)) 67 } 68 pad := []byte(*ft.PadWith)[0] 69 70 // replacement should be shorter than search string 71 if len(replacementBytes) > len(findBytes) { 72 multilog.Log(logging.ErrorNoStacktrace, rollbar.Error)("Replacement text too long: %s, original text: %s", ft.Pattern, replacement) 73 return fileBytes, locale.NewError("file_transform_replacement_too_long", "Replacement text cannot be longer than search text in a binary file.") 74 } 75 76 // Must account for the expand characters (ie. '${1}') in the 77 // replacement bytes in order for the binary paddding to be correct 78 regexExpandBytes := []byte("${1}") 79 replacementBytes = append(replacementBytes, regexExpandBytes...) 80 81 // paddedReplaceBytes is the replacement string plus the padding bytes added to the end 82 // It shall look like this: `<replacementBytes>${1}<padding>` with `len(replacementBytes)+len(padding)=len(findBytes)` 83 paddedReplaceBytes := bytes.Repeat([]byte{pad}, len(findBytes)+len(regexExpandBytes)) 84 copy(paddedReplaceBytes, replacementBytes) 85 86 quoteEscapeFind := regexp.QuoteMeta(ft.Pattern) 87 // replacementRegex matches the search Pattern plus subsequent text up to the string termination character (pad, which usually is 0x00) 88 replacementRegex, err := regexp.Compile(fmt.Sprintf(`%s([^\x%02x]*)`, quoteEscapeFind, pad)) 89 if err != nil { 90 return fileBytes, errs.Wrap(err, "Failed to compile replacement regular expression.") 91 } 92 return replacementRegex.ReplaceAll(fileBytes, paddedReplaceBytes), nil 93 } 94 95 func expandConstants(in string, constants Constants) string { 96 res := in 97 for k, v := range constants { 98 res = strings.ReplaceAll(res, fmt.Sprintf("${%s}", k), v) 99 } 100 return res 101 } 102 103 // ApplyTransform applies a file transformation to all specified files 104 func (ft *FileTransform) ApplyTransform(baseDir string, constants Constants) error { 105 // compute transformed constants 106 tcs, err := ft.applyConstTransforms(constants) 107 if err != nil { 108 return errs.Wrap(err, "Failed to apply the constant transformation to replacement text.") 109 } 110 replacement := expandConstants(ft.With, tcs) 111 112 for _, f := range ft.In { 113 fp := filepath.Join(baseDir, f) 114 fileBytes, err := os.ReadFile(fp) 115 if err != nil { 116 return errs.Wrap(err, "Could not read file contents of %s.", fp) 117 } 118 119 replaced, err := ft.relocateFile(fileBytes, replacement) 120 if err != nil { 121 return errs.Wrap(err, "relocateFile failed") 122 } 123 124 // skip writing back to file if contents remain the same after transformation 125 if bytes.Equal(replaced, fileBytes) { 126 continue 127 } 128 129 err = fileutils.WriteFile(fp, replaced) 130 if err != nil { 131 return errs.Wrap(err, "Could not write file contents.") 132 } 133 } 134 135 return nil 136 } 137 138 // ApplyFileTransforms applies all file transformations to the files in the base directory 139 func (ed *EnvironmentDefinition) ApplyFileTransforms(installDir string, constants Constants) error { 140 for _, ft := range ed.Transforms { 141 err := ft.ApplyTransform(installDir, constants) 142 if err != nil { 143 return err 144 } 145 } 146 return nil 147 }