This commit updates the handling of expanding of templates. This is to prepare embeding templates directly as opposed to embedding a single resource directory.
172 lines
4.7 KiB
Go
172 lines
4.7 KiB
Go
// Package unpack unpacks files and templates to a target directory.
|
|
package unpack
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"io"
|
|
"io/fs"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/exp/maps"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
var errExpectedFileButGotDirectory = errors.New("Expected a file, but got a directory")
|
|
|
|
// UnpackTemplate unpacks the given file template and template
|
|
func UnpackTemplate(context map[string]string, src fs.File) ([]byte, fs.FileMode, error) {
|
|
// stat the source file to install
|
|
srcStat, srcErr := src.Stat()
|
|
if srcErr != nil {
|
|
return nil, 0, errors.Wrapf(srcErr, "Error calling stat on source")
|
|
}
|
|
|
|
// should not be a directory
|
|
if srcStat.IsDir() {
|
|
return nil, 0, errors.Wrapf(errExpectedFileButGotDirectory, "Error calling stat on source %s", srcStat.Name())
|
|
}
|
|
|
|
// read all the bytes into a buffer
|
|
var buffer bytes.Buffer
|
|
WriteTemplate(&buffer, context, src)
|
|
return buffer.Bytes(), srcStat.Mode(), nil
|
|
}
|
|
|
|
type templateMode int
|
|
|
|
const (
|
|
templateModeNormal templateMode = iota // normal mode
|
|
templateModeDollar // saw '$'
|
|
templateModeOpen // saw '${'
|
|
)
|
|
|
|
// 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.
|
|
//
|
|
// Extra or missing variables from the context are an error.
|
|
func WriteTemplate(dst io.Writer, context map[string]string, src io.Reader) error {
|
|
// keep track of context keys that have not been used
|
|
unuusedContext := make(map[string]struct{}, len(context))
|
|
for key := range context {
|
|
unuusedContext[key] = struct{}{}
|
|
}
|
|
|
|
reader := bufio.NewReader(src) // a new reader
|
|
var missingKeyErr error // error for missing keys
|
|
var builder strings.Builder // holding variable names
|
|
mode := templateModeNormal // the current mode of the reader
|
|
parseloop:
|
|
for {
|
|
r, _, err := reader.ReadRune()
|
|
switch {
|
|
case err == io.EOF:
|
|
/* finished the source, see below */
|
|
break parseloop
|
|
case err != nil:
|
|
/* something went wrong */
|
|
return err
|
|
|
|
case mode == templateModeNormal && r == '$':
|
|
// saw a '$' in normal mode
|
|
// => switch to the dollar mode
|
|
mode = templateModeDollar
|
|
case mode == templateModeNormal:
|
|
// saw anything else
|
|
// => just pass it through
|
|
if _, err := dst.Write([]byte{byte(r)}); err != nil {
|
|
return err
|
|
}
|
|
|
|
case mode == templateModeDollar && r == '{':
|
|
// saw '{', following the '$'
|
|
// => read everything else into the buffer
|
|
mode = templateModeOpen
|
|
case mode == templateModeDollar && r == '$':
|
|
// saw a '$' following the '$'
|
|
// => write the first '$', and handle the case $${stuff}
|
|
if _, err := dst.Write([]byte("$")); err != nil {
|
|
return err
|
|
}
|
|
case mode == templateModeDollar:
|
|
// saw anything else following the '$'
|
|
// => write both back and switch back to normal mode
|
|
if _, err := dst.Write([]byte{byte('$'), byte(r)}); err != nil {
|
|
return err
|
|
}
|
|
mode = templateModeNormal
|
|
|
|
case mode == templateModeOpen && r != '}':
|
|
// saw anything except for closing bracket
|
|
// => keep it in the buffer
|
|
if _, err := builder.WriteRune(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
case mode == templateModeOpen:
|
|
// saw a closing '}' inside the open mode
|
|
// => use the variable
|
|
|
|
name := builder.String()
|
|
delete(unuusedContext, name) // mark the variable as used
|
|
|
|
// get the variable from the context
|
|
value, ok := context[name]
|
|
if missingKeyErr != nil && !ok {
|
|
missingKeyErr = errors.Errorf("key %s missing in context", name)
|
|
}
|
|
|
|
// 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
|
|
builder.Reset()
|
|
mode = templateModeNormal
|
|
|
|
default:
|
|
panic("never reached")
|
|
}
|
|
}
|
|
|
|
// cleanup at end of input
|
|
|
|
switch mode {
|
|
case templateModeNormal:
|
|
// => everything is fine
|
|
case templateModeDollar:
|
|
// we had a '$', but no '{'
|
|
// => write the trailing '$' into dest
|
|
if _, err := dst.Write([]byte("$")); err != nil {
|
|
return err
|
|
}
|
|
case templateModeOpen:
|
|
// 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, builder.String()); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
panic("never reached")
|
|
}
|
|
|
|
// check if there was a missing key!
|
|
if missingKeyErr != nil {
|
|
return missingKeyErr
|
|
}
|
|
|
|
// check if there was an unused key!
|
|
if len(unuusedContext) != 0 {
|
|
keys := maps.Keys(unuusedContext)
|
|
slices.Sort(keys)
|
|
return errors.Errorf("additional keys %s in context", strings.Join(keys, ","))
|
|
}
|
|
|
|
return nil
|
|
}
|