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  }