233 lines
5.9 KiB
Go
233 lines
5.9 KiB
Go
// Package unpack unpacks files and templates to a target directory.
|
|
package unpack
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"strings"
|
|
|
|
"github.com/FAU-CDI/wisski-distillery/pkg/fsx"
|
|
"golang.org/x/exp/maps"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
// ts represents state of the template parser
|
|
type ts int
|
|
|
|
const (
|
|
tsGobble ts = iota // gobble into dst
|
|
tsSawDollar // saw a '$'
|
|
tsGobbleVar // gobble into var
|
|
)
|
|
|
|
// MissingTemplateKeyError indicates [WriteTemplate] found missing keys in the context
|
|
type MissingTemplateKeyError struct {
|
|
Keys []string
|
|
}
|
|
|
|
func (mtke MissingTemplateKeyError) Error() string {
|
|
return fmt.Sprintf("missing template keys from context: %v", mtke.Keys)
|
|
}
|
|
|
|
// UnusuedTemplateKeyError indicates [WriteTemplate] found unusued keys in the context
|
|
type UnusuedTemplateKeyError struct {
|
|
Keys []string
|
|
}
|
|
|
|
func (utke UnusuedTemplateKeyError) Error() string {
|
|
return fmt.Sprintf("unused template keys from context: %v", utke.Keys)
|
|
}
|
|
|
|
// WriteTemplate writes the template defined by src with the given context into reader.
|
|
//
|
|
// To run the template, variables of the form ${NAME} are replaced with their corresponding value from the context.
|
|
//
|
|
// If an underlying read or write fails, it is returned as is.
|
|
// Missing template keys return a [MissingTemplateKeyError], but are replaced with the empty string.
|
|
// Unused template keys return a [UnusuedTemplateKeyError], but are replaced with the empty string.
|
|
//
|
|
// Reader / Writer errors are always returned first; next missing template keys, and finally unused template keys.
|
|
func WriteTemplate(dst io.Writer, context map[string]string, src io.Reader) error {
|
|
|
|
// We keep track of contect keys that have not been used.
|
|
//
|
|
// We first fill the map with all the keys from the context.
|
|
// Then when we use a key, we delete it from the map.
|
|
// If there are any keys left at the end of the replacement, that is an error.
|
|
unusedKeys := make(map[string]struct{}, len(context))
|
|
for key := range context {
|
|
unusedKeys[key] = struct{}{}
|
|
}
|
|
|
|
// When we encounter a missing key, put it into this map.
|
|
// This is so that we can build an error message below.
|
|
missingKeys := make(map[string]struct{}, 0)
|
|
|
|
// We use a new bufio reader to read data from the input.
|
|
// This is a cheap trick to get a ReadRune() method.
|
|
reader := bufio.NewReader(src)
|
|
|
|
//
|
|
// MAIN PARSING LOOP
|
|
//
|
|
|
|
// start out in gobble mode!
|
|
mode := tsGobble
|
|
|
|
// keep track of variable names
|
|
var varB strings.Builder
|
|
|
|
parseloop:
|
|
for {
|
|
r, _, err := reader.ReadRune()
|
|
switch {
|
|
case err == io.EOF:
|
|
// finished parsing the source
|
|
break parseloop
|
|
case err != nil:
|
|
// the reader broke
|
|
return err
|
|
|
|
case mode == tsGobble && r == '$':
|
|
// saw a '$' in gobble mode
|
|
mode = tsSawDollar
|
|
case mode == tsGobble:
|
|
// normal gobbleing
|
|
// => pass it through
|
|
if _, err := dst.Write([]byte{byte(r)}); err != nil {
|
|
return err
|
|
}
|
|
|
|
case mode == tsSawDollar && r == '{':
|
|
// saw '{', following the '$'
|
|
// => read everything else into the buffer
|
|
mode = tsGobbleVar
|
|
case mode == tsSawDollar && r == '$':
|
|
// saw a '$' following the '$'
|
|
// => write the first '$', and handle the case $${stuff}
|
|
if _, err := dst.Write([]byte("$")); err != nil {
|
|
return err
|
|
}
|
|
case mode == tsSawDollar:
|
|
// saw anything else following the '$'
|
|
// => write both back and switch back to gobble mode
|
|
if _, err := dst.Write([]byte{byte('$'), byte(r)}); err != nil {
|
|
return err
|
|
}
|
|
mode = tsGobble
|
|
|
|
case mode == tsGobbleVar && r != '}':
|
|
// saw anything except for closing bracket
|
|
// => keep it in the buffer
|
|
if _, err := varB.WriteRune(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
case mode == tsGobbleVar:
|
|
// saw a closing '}' inside tsGobbleVar mode
|
|
// => use the variable
|
|
|
|
name := varB.String()
|
|
|
|
// get the variable from the context
|
|
value, ok := context[name]
|
|
|
|
delete(unusedKeys, name) // mark the variable as used!
|
|
if !ok {
|
|
// store unusued variables
|
|
missingKeys[name] = struct{}{}
|
|
value = ""
|
|
}
|
|
|
|
// write the replacement into the string
|
|
if _, err := io.WriteString(dst, value); err != nil {
|
|
return err
|
|
}
|
|
|
|
// reset the builder and go back into normal mode
|
|
varB.Reset()
|
|
mode = tsGobble
|
|
}
|
|
}
|
|
|
|
//
|
|
// CLEANUP UNUSUED INPUT
|
|
//
|
|
|
|
switch mode {
|
|
case tsSawDollar:
|
|
// we had a '$', but no '{'
|
|
// => write the trailing '$' into dest
|
|
if _, err := dst.Write([]byte("$")); err != nil {
|
|
return err
|
|
}
|
|
case tsGobbleVar:
|
|
// we had a "${", followed by somthing unclosed
|
|
// => write everything back into the dst
|
|
if _, err := dst.Write([]byte("${")); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(dst, varB.String()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Check if there were missing template keys.
|
|
// If so, we sort them and return an appropriate error.
|
|
if len(missingKeys) != 0 {
|
|
keys := maps.Keys(missingKeys)
|
|
slices.Sort(keys)
|
|
return MissingTemplateKeyError{
|
|
Keys: keys,
|
|
}
|
|
}
|
|
|
|
// Check if there were unused template keys.
|
|
// If so, we sort them and return an appropriate error.
|
|
if len(unusedKeys) != 0 {
|
|
keys := maps.Keys(unusedKeys)
|
|
slices.Sort(keys)
|
|
return UnusuedTemplateKeyError{
|
|
Keys: keys,
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// InstallTemplate unpacks the resource located at src in fsys, then processes it as a template, and eventually writes it to dst.
|
|
// Any existing file is truncated and overwritten.
|
|
//
|
|
// See [WriteTemplate] for possible errors.
|
|
func InstallTemplate(dst string, context map[string]string, src string, fsys fs.FS) error {
|
|
|
|
// open the srcFile
|
|
srcFile, err := fsys.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer srcFile.Close()
|
|
|
|
// stat it
|
|
srcInfo, err := srcFile.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// check if it is a directory
|
|
if srcInfo.IsDir() {
|
|
return errExpectedFileButGotDirectory
|
|
}
|
|
|
|
// open the destination file
|
|
file, err := fsx.Create(dst, srcInfo.Mode())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
// write the file!
|
|
return WriteTemplate(file, context, srcFile)
|
|
}
|