funtemplates

package module
v0.0.0-...-8a9d0a7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 10, 2024 License: CC0-1.0 Imports: 14 Imported by: 2

README

funtemplates

Funtemplates is a templating library for go that uses the C preprocessor.

  • Calls the cpp binary on your system.

  • Can load templates from an fs.FS, or you can pass them in as structs.

  • Supports passing in variables when rendering the template, which will be inserted using #define.

Example

#define BODY(content) <html><body>content</body></html>

#define LINK(url, text) <a href=url>text</a>

BODY(                                                          \
  <h1>Rendered with the C preprocessor!</h1>                   \
                                                               \
  <!-- a variable set dynamically in go -->                    \
  <p>the current time is _V_time</p>                           \
  <p>unicode works!!  「ガー ガー」との🪿 </p>                      \
  <p>it works in variables too: _V_mouse</p>                   \
                                                               \
  <!-- make sure to put things with two slashes in them in     \
     quotes. otherwise bad things will happen - because        \
     it looks like a C comment. -->                            \
  <p>LINK("https://git.sr.ht/~bbbb/funtemplates", source)</p>  \
)
...
// you can load templates from a fs.FS. (or you can just
// pass in their content and name as strings).
t = ft.Must(t.ParseFSRecursive(templates, "templates"))

h := func(w http.ResponseWriter, req *http.Request) {
  now := time.Now().UTC().Format(time.RFC1123)

  err := t.ExecuteTemplate(w, "example.html", ft.FunData{
    "time": now,
    // unicode works in these variables too.
    "mouse": "🐁",
  })
}
...

(full code is at examples/simple)

This example is with html, but funtemplates isn't specific to html. it can be used with all text files.

(formats that have significant whitespace can be tricky though.)

Gotchas

To prevent it from evaulating something, you can put it in "quotes". The quotes will be in the output though; it won't remove them.

Another way is by putting a unicode codepoint literal instead of the actual character(s). It will be turned into the actual character in the output.

  • instead of "\U", you need to use "|U". this is so the preprocessor won't try to interpret the literal itself.

  • funtemplates only supports 8-character codepoint literals right now. instead of "|u002f", you'd need to write "|U0000002f" (this is a '/')

Special characters you should be aware of:

The C preprocessor has some special characters which you should escape if you're not using them for what they do.

  • In the body of macro functions, # must be followed by a parameter of the function and will put it in quotes. To escape it, the codepoint is |U00000023.

  • In function bodies, ## will concentrate the things to its left and to its right.

e.g #define A() 123 456 ## 789 would result in 123 456789, when A() is called.

  • Things that look like C comments will be treated like C comments. /* */ and //. /'s codepoint is |U0000002f.

Be aware of that with URLs. The // in the protocol can get treated like a comment. e.g. https://example.com could turn into https:. You can use the codepoint literal instead of a "/" to avoid this.

  • https:|U0000002f|U0000002fexample.com would turn into "https://example.com".

Debugging

You can set FtOptions.Debug_print to true (passed into ft.NewFunTemplate()), and it will print out each of the files it generates to stderr.

ExecCpp (what calls the cpp binary on your system) also has a debugging option.

Performance

Currently the only C preprocessor implementation we have is ExecCpp, which uses the cpp present on your system. It writes all the generated template files to the disk in a (configurable) tmp directory and calls cpp with them (the files are deleted after). I'm not sure how big a difference it would make, but if you want to use this in production (😰), you could set the tmp directory to something in-memory, like docker's tmpfs.

Security

I would not be surprised if this had some kind of RCE. The C compiler wasn't designed to be used as a templating engine for a web server (or whatever else), so it might have some unexpected behaviors.

funtemplates doesn't pass any user-generated input into the arguments when it calls cpp. but, you can pass in variables when you execute a template (which get turned into #define statements). so it still may deal with user-generated input.

Escaping text

When you pass variables into the templaes it does some escaping by default. Currently it replaces all '#'s and '/'s with the codepoint literal (so the preprocessor won't treat them like a special character), and replaces the "i" in "include" with the codepoint literal.

(This is to make it harder for someone to get it to print a file on your system by #including it. It could still be possible though.)

It doesn't do any escaping of html in the input, since this isn't specific to html. If you're putting user input into the templates, you should probably escape it.

(It could be a good idea to run it in a sandbox/vm if you're passing untrusted input into templates. A TODO is to compile a c preprocessor to webassembly and embed it, which would reduce the security risk a lot and improve portability & probably performance too.)

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrInvalidIdentifier = errors.New("invalid identifier in FunData")

Functions

func EscapeCpp

func EscapeCpp(text string) string

currently the escaping is very rudimentary.

escape user input, making it suitable to be passed into a template with FunData. `ExecuteTemplate` automatically calls this on all input. currently we just remove all '#'s and the word "include" from anywhere in `text`. They are replaced with a sad face emoji, to shame your users for trying to leak your private files.

if the value contains any non-unicode text, it will wrap it in quotes. "value".

TODO: log a warning when something is escaped? we'd probably want to pass in a logger though...

func Url

func Url(url string) string

put a url (or anything else) in quotes so the preprocessor won't think the "//" in the protocol is a comment. (e.g. "https://")

Types

type Cpp

type Cpp interface {
	// TODO: should file_to_run be put in as a CppFile instead of just the name to
	// the one in `files`? idk.
	Run(files []CppFile, file_to_run string, options CppOptions) (string, error)
}

TODO: rename to Cpp or something

func NewExecCpp

func NewExecCpp(_options *ExecOptions) Cpp

_options as nil for the default values.

type CppFile

type CppFile struct {
	// the name of the file including the path (starting in the working directory).
	// it should be the same as what you put in #include statments.
	// so like `#include "util/abc.h"`, the name should be "util/abc.h".
	Name string

	// the contents of the file.
	Contents string
}

func FilesFromFS

func FilesFromFS(_fs fs.FS, base_path string) ([]CppFile, error)

TODO: is the `_fs` messy? reads all the files recursivly starting at base path and turns them into `CppFile`s.

type CppOptions

type CppOptions struct{}

TODO: for now we're just using some kind of hardcoded default thing i guess..

type ExecCpp

type ExecCpp struct {
	// contains filtered or unexported fields
}

a Cpp implementation that uses the OS `cpp` command.

func (*ExecCpp) Run

func (e *ExecCpp) Run(
	files []CppFile,
	file_to_run string,
	options CppOptions,
) (string, error)

runs all the files through the preprocessor.

type ExecOptions

type ExecOptions struct {
	// whether it logs debug messages to the console.
	// (it logs at debug logging level.)
	// default false.
	Debug bool

	// will use the default slog logger if unset.
	Logger *slog.Logger

	// the name of the `cpp` command in the os.
	// default "cpp".
	Cpp_cmd string

	// the arguments that are always passed to `cpp`
	// default "-P" (no line numbers in output).
	Args []string

	// the root directory to store temporary files in.
	// e.g. /tmp.
	// this should ideally be loaded in memory and not an
	// actual directory in the disk. (e.g. docker tmpfs).
	// defaults to the the OS default.
	Tmp_root string
}

Options for ExecCpp

type FtOptions

type FtOptions struct {
	// if true, will log all the files that it generates
	// in stderr.
	// defaults to false.
	Debug_print bool

	// if unset, will use the default slog.Logger.
	Logger *slog.Logger
}

options for a FunTemplate.

type FunData

type FunData map[string]string

variables/data passed into a template execution

type FunTemplate

type FunTemplate struct {
	// contains filtered or unexported fields
}

func Must

func Must(e *FunTemplate, err error) *FunTemplate

wraps a call to a function returning (*FunTemplate, error) and panics if the error isn't nil.

func NewFunTemplate

func NewFunTemplate(cpp Cpp, _options *FtOptions) *FunTemplate

if `debug` is true, will log all the files it generates in stderr.

func (*FunTemplate) ExecuteTemplate

func (e *FunTemplate) ExecuteTemplate(
	wr io.Writer,
	name string,
	data FunData,
) error

execute the template `name`, returning the output.

this escapes all the values in `data` (not the variable names though). the escaping is very rudimentary, so don't rely on it. and it's just C preprocessor escaping, not html escaping.

func (*FunTemplate) ExecuteTemplateWithoutEscaping

func (e *FunTemplate) ExecuteTemplateWithoutEscaping(
	wr io.Writer,
	name string,
	data FunData,
) error

func (*FunTemplate) Parse

func (e *FunTemplate) Parse(text string, name string) (*FunTemplate, error)

func (*FunTemplate) ParseFSRecursive

func (e *FunTemplate) ParseFSRecursive(_fs fs.FS, base_path string) (*FunTemplate, error)

recursively parse all of the files in `base_path`

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL