refactor: keep compiler source standalone
Signed-off-by: Cole Speelman <crspeelman@gmail.com>
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
Zero-Clause BSD
|
||||
|
||||
Copyright (c) 2025-2026 Cole Speelman
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
@@ -1,15 +0,0 @@
|
||||
<!-- SPDX-FileCopyrightText: 2025-2026 Cole Speelman -->
|
||||
<!-- SPDX-License-Identifier: 0BSD -->
|
||||
|
||||
# Example license map
|
||||
|
||||
The original files in this synthetic, copyable example are licensed under the
|
||||
0BSD terms in `LICENSE`, including `.sando` templates, handwritten Go,
|
||||
`himesan.json`, and committed `.sando.go` output.
|
||||
|
||||
The generated files intentionally have no inline SPDX header because Hime-san
|
||||
replaces the whole file. This module-level license map covers them. Generator
|
||||
scaffolding copied into those files also has the additional permission in the
|
||||
root repository's `OUTPUT_EXCEPTION.md`; that permission is not required to
|
||||
keep this example under 0BSD when the example author already owns the relevant
|
||||
input code.
|
||||
@@ -1,15 +0,0 @@
|
||||
<!-- SPDX-License-Identifier: 0BSD -->
|
||||
|
||||
# Synthetic EQL-shaped fixture
|
||||
|
||||
This copyable 0BSD example exercises a shared document layout, typed page data,
|
||||
component composition, loops, text escaping, attribute escaping, and URL
|
||||
policy without containing EQL Wiki code, data, routes, or its database.
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
go run ./cmd/himesan generate ./examples/eql-shaped
|
||||
go run ./cmd/himesan check ./examples/eql-shaped
|
||||
(cd examples/eql-shaped && go test ./... && go run ./cmd/example)
|
||||
```
|
||||
@@ -1,83 +0,0 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"example.com/eql-shaped/views"
|
||||
"gamertan.com/sandwich-hime/sando"
|
||||
)
|
||||
|
||||
func main() {
|
||||
address := os.Getenv("HIMESAN_LISTEN_ADDR")
|
||||
if address != "" {
|
||||
serve(address)
|
||||
return
|
||||
}
|
||||
|
||||
output, err := renderPage(context.Background())
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
_, _ = output.WriteTo(os.Stdout)
|
||||
}
|
||||
|
||||
func serve(address string) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /healthz", func(response http.ResponseWriter, _ *http.Request) {
|
||||
response.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
response.WriteHeader(http.StatusOK)
|
||||
_, _ = response.Write([]byte("ok\n"))
|
||||
})
|
||||
mux.HandleFunc("GET /", func(response http.ResponseWriter, request *http.Request) {
|
||||
output, err := renderPage(request.Context())
|
||||
if err != nil {
|
||||
http.Error(response, "render failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
response.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
response.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'self'")
|
||||
response.WriteHeader(http.StatusOK)
|
||||
_, _ = output.WriteTo(response)
|
||||
})
|
||||
|
||||
server := &http.Server{Addr: address, Handler: mux, ReadHeaderTimeout: 5 * time.Second}
|
||||
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func renderPage(ctx context.Context) (*bytes.Buffer, error) {
|
||||
body := views.Home(views.HomeView{
|
||||
Heading: "EQL-shaped records",
|
||||
Intro: "Typed markup without making the template compiler your web framework.",
|
||||
Browse: views.BrowseView{
|
||||
Query: "pioneer & archivist",
|
||||
Records: []views.RecordView{
|
||||
{URL: "/items/1?from=home&kind=book", Title: "A <field> guide", Kind: "book", Featured: true},
|
||||
{URL: "/items/2", Title: "Community memory", Kind: "archive"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
page := views.Layout(views.LayoutView{
|
||||
SiteName: "EQL Wiki Fixture",
|
||||
Title: "Home",
|
||||
Body: body,
|
||||
})
|
||||
|
||||
var output bytes.Buffer
|
||||
if err := sando.Render(ctx, &output, page); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &output, nil
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
|
||||
module example.com/eql-shaped
|
||||
|
||||
go 1.25
|
||||
|
||||
require gamertan.com/sandwich-hime/sando v0.0.0
|
||||
|
||||
replace gamertan.com/sandwich-hime/sando => ../../sando
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sourceRoots": ["views"],
|
||||
"goPackage": "./cmd/example",
|
||||
"listenAddressEnv": "HIMESAN_LISTEN_ADDR",
|
||||
"healthPath": "/healthz",
|
||||
"proxyAddress": "127.0.0.1:7331",
|
||||
"additionalWatchRoots": []
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<?sando go
|
||||
package views
|
||||
|
||||
func Badge(label string)
|
||||
?>
|
||||
<?# SPDX-License-Identifier: 0BSD ?>
|
||||
<strong class="badge"><?= label ?></strong>
|
||||
@@ -1,37 +0,0 @@
|
||||
// Code generated by himesan; DO NOT EDIT.
|
||||
// himesan:compiler 0.1.0-dev
|
||||
// himesan:runtime-abi sando.v1
|
||||
// himesan:source-sha256 2c0063ed8724520f146d6717bb7d828ce69be17b41cbfb8f4866664ba09a58a8
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
__himesan_context "context"
|
||||
__himesan_sando "gamertan.com/sandwich-hime/sando"
|
||||
__himesan_io "io"
|
||||
)
|
||||
|
||||
var _ = __himesan_sando.ABI
|
||||
|
||||
func Badge(label string) __himesan_sando.Component {
|
||||
return __himesan_sando.ComponentFunc(func(__himesan_render_context __himesan_context.Context, __himesan_writer __himesan_io.Writer) error {
|
||||
_ = __himesan_render_context
|
||||
//line views/badge.sando:5:3
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/badge.sando:6:37
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n<strong class=\"badge\">"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/badge.sando:7:27
|
||||
if __himesan_error := __himesan_sando.WriteText(__himesan_writer, (label)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/badge.sando:7:35
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "</strong>\n"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<?sando go
|
||||
package views
|
||||
|
||||
func BrowseResults(view BrowseView)
|
||||
?>
|
||||
<?# SPDX-License-Identifier: 0BSD ?>
|
||||
<section aria-label="Browse results" data-query="<?= view.Query ?>">
|
||||
<ul>
|
||||
<? for _, record := range view.Records { ?>
|
||||
<li>
|
||||
<a href="<?= record.URL ?>"><?= record.Title ?></a>
|
||||
<span class="kind"><?= record.Kind ?></span>
|
||||
<? if record.Featured { ?><?~ Badge("featured") ?><? } ?>
|
||||
</li>
|
||||
<? } ?>
|
||||
</ul>
|
||||
</section>
|
||||
@@ -1,85 +0,0 @@
|
||||
// Code generated by himesan; DO NOT EDIT.
|
||||
// himesan:compiler 0.1.0-dev
|
||||
// himesan:runtime-abi sando.v1
|
||||
// himesan:source-sha256 c5b402971618747c906c84508d5af9fd00aa33391b9b59f093b001a3cb0b2164
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
__himesan_context "context"
|
||||
__himesan_sando "gamertan.com/sandwich-hime/sando"
|
||||
__himesan_io "io"
|
||||
)
|
||||
|
||||
var _ = __himesan_sando.ABI
|
||||
|
||||
func BrowseResults(view BrowseView) __himesan_sando.Component {
|
||||
return __himesan_sando.ComponentFunc(func(__himesan_render_context __himesan_context.Context, __himesan_writer __himesan_io.Writer) error {
|
||||
_ = __himesan_render_context
|
||||
//line views/browse_results.sando:5:3
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/browse_results.sando:6:37
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n<section aria-label=\"Browse results\" data-query=\""); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/browse_results.sando:7:54
|
||||
if __himesan_error := __himesan_sando.WriteAttr(__himesan_writer, (view.Query)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/browse_results.sando:7:67
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\">\n <ul>\n "); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/browse_results.sando:9:6
|
||||
for _, record := range view.Records {
|
||||
//line views/browse_results.sando:9:46
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n <li>\n <a href=\""); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/browse_results.sando:11:20
|
||||
if __himesan_error := __himesan_sando.WriteURL(__himesan_writer, (record.URL)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/browse_results.sando:11:33
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\">"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/browse_results.sando:11:39
|
||||
if __himesan_error := __himesan_sando.WriteText(__himesan_writer, (record.Title)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/browse_results.sando:11:54
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "</a>\n <span class=\"kind\">"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/browse_results.sando:12:30
|
||||
if __himesan_error := __himesan_sando.WriteText(__himesan_writer, (record.Kind)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/browse_results.sando:12:44
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "</span>\n "); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/browse_results.sando:13:10
|
||||
if record.Featured {
|
||||
//line views/browse_results.sando:13:37
|
||||
if __himesan_error := __himesan_sando.Render(__himesan_render_context, __himesan_writer, (Badge("featured"))); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/browse_results.sando:13:60
|
||||
}
|
||||
//line views/browse_results.sando:13:64
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n </li>\n "); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/browse_results.sando:15:6
|
||||
}
|
||||
//line views/browse_results.sando:15:10
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n </ul>\n</section>\n"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?sando go
|
||||
package views
|
||||
|
||||
func Home(view HomeView)
|
||||
?>
|
||||
<?# SPDX-License-Identifier: 0BSD ?>
|
||||
<section aria-labelledby="home-heading">
|
||||
<h1 id="home-heading"><?= view.Heading ?></h1>
|
||||
<p><?= view.Intro ?></p>
|
||||
<?~ BrowseResults(view.Browse) ?>
|
||||
</section>
|
||||
@@ -1,53 +0,0 @@
|
||||
// Code generated by himesan; DO NOT EDIT.
|
||||
// himesan:compiler 0.1.0-dev
|
||||
// himesan:runtime-abi sando.v1
|
||||
// himesan:source-sha256 696c391538cdc50c5ab1abea23b561fa354e56eaf73b7ad6aabe130389715a8c
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
__himesan_context "context"
|
||||
__himesan_sando "gamertan.com/sandwich-hime/sando"
|
||||
__himesan_io "io"
|
||||
)
|
||||
|
||||
var _ = __himesan_sando.ABI
|
||||
|
||||
func Home(view HomeView) __himesan_sando.Component {
|
||||
return __himesan_sando.ComponentFunc(func(__himesan_render_context __himesan_context.Context, __himesan_writer __himesan_io.Writer) error {
|
||||
_ = __himesan_render_context
|
||||
//line views/home.sando:5:3
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/home.sando:6:37
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n<section aria-labelledby=\"home-heading\">\n <h1 id=\"home-heading\">"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/home.sando:8:29
|
||||
if __himesan_error := __himesan_sando.WriteText(__himesan_writer, (view.Heading)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/home.sando:8:44
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "</h1>\n <p>"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/home.sando:9:10
|
||||
if __himesan_error := __himesan_sando.WriteText(__himesan_writer, (view.Intro)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/home.sando:9:23
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "</p>\n "); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/home.sando:10:7
|
||||
if __himesan_error := __himesan_sando.Render(__himesan_render_context, __himesan_writer, (BrowseResults(view.Browse))); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/home.sando:10:36
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n</section>\n"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<?sando go
|
||||
package views
|
||||
|
||||
func Layout(view LayoutView)
|
||||
?>
|
||||
<?# SPDX-License-Identifier: 0BSD ?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= view.Title ?> · <?= view.SiteName ?></title>
|
||||
</head>
|
||||
<body>
|
||||
<a href="#main">Skip to content</a>
|
||||
<header><a href="/"><?= view.SiteName ?></a></header>
|
||||
<main id="main"><?~ view.Body ?></main>
|
||||
<footer>Built with Sandwich Hime.</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,61 +0,0 @@
|
||||
// Code generated by himesan; DO NOT EDIT.
|
||||
// himesan:compiler 0.1.0-dev
|
||||
// himesan:runtime-abi sando.v1
|
||||
// himesan:source-sha256 c9a8102588cb94f946a36414291d12cb681e605acc93c2b5dc8d74fd33ae5603
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
__himesan_context "context"
|
||||
__himesan_sando "gamertan.com/sandwich-hime/sando"
|
||||
__himesan_io "io"
|
||||
)
|
||||
|
||||
var _ = __himesan_sando.ABI
|
||||
|
||||
func Layout(view LayoutView) __himesan_sando.Component {
|
||||
return __himesan_sando.ComponentFunc(func(__himesan_render_context __himesan_context.Context, __himesan_writer __himesan_io.Writer) error {
|
||||
_ = __himesan_render_context
|
||||
//line views/layout.sando:5:3
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/layout.sando:6:37
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <title>"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/layout.sando:12:14
|
||||
if __himesan_error := __himesan_sando.WriteRCDATA(__himesan_writer, (view.Title)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/layout.sando:12:27
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, " · "); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/layout.sando:12:35
|
||||
if __himesan_error := __himesan_sando.WriteRCDATA(__himesan_writer, (view.SiteName)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/layout.sando:12:51
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "</title>\n</head>\n<body>\n <a href=\"#main\">Skip to content</a>\n <header><a href=\"/\">"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/layout.sando:16:27
|
||||
if __himesan_error := __himesan_sando.WriteText(__himesan_writer, (view.SiteName)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/layout.sando:16:43
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "</a></header>\n <main id=\"main\">"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/layout.sando:17:23
|
||||
if __himesan_error := __himesan_sando.Render(__himesan_render_context, __himesan_writer, (view.Body)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/layout.sando:17:35
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "</main>\n <footer>Built with Sandwich Hime.</footer>\n</body>\n</html>\n"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
|
||||
package views
|
||||
|
||||
import "gamertan.com/sandwich-hime/sando"
|
||||
|
||||
type LayoutView struct {
|
||||
SiteName string
|
||||
Title string
|
||||
Body sando.Component
|
||||
}
|
||||
|
||||
type HomeView struct {
|
||||
Heading string
|
||||
Intro string
|
||||
Browse BrowseView
|
||||
}
|
||||
|
||||
type BrowseView struct {
|
||||
Query string
|
||||
Records []RecordView
|
||||
}
|
||||
|
||||
type RecordView struct {
|
||||
URL string
|
||||
Title string
|
||||
Kind string
|
||||
Featured bool
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
// SPDX-License-Identifier: 0BSD
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"html/template"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gamertan.com/sandwich-hime/sando"
|
||||
)
|
||||
|
||||
func TestBrowseResultsEscapesUntrustedValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
component := BrowseResults(BrowseView{
|
||||
Query: `"><script>alert(1)</script>`,
|
||||
Records: []RecordView{{
|
||||
URL: `/item?q=" onclick="alert(1)`,
|
||||
Title: `<img src=x onerror=alert(1)>`,
|
||||
Kind: `" aria-label="injected`,
|
||||
}},
|
||||
})
|
||||
|
||||
var output bytes.Buffer
|
||||
if err := sando.Render(context.Background(), &output, component); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := output.String()
|
||||
if strings.Contains(got, "<script>") || strings.Contains(got, "<img") || strings.Contains(got, `" onclick=`) {
|
||||
t.Fatalf("untrusted markup survived escaping: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowseResultsRejectsDangerousURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
component := BrowseResults(BrowseView{Records: []RecordView{{
|
||||
URL: " JaVaScRiPt:\talert(1)",
|
||||
Title: "unsafe",
|
||||
}}})
|
||||
var output bytes.Buffer
|
||||
err := sando.Render(context.Background(), &output, component)
|
||||
if !errors.Is(err, sando.ErrUnsafeURL) {
|
||||
t.Fatalf("expected unsafe URL error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratedComponentPropagatesWriterFailures(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
component := Badge("proof")
|
||||
if err := sando.Render(context.Background(), shortWriter{}, component); !errors.Is(err, io.ErrShortWrite) {
|
||||
t.Fatalf("short writer error = %v, want io.ErrShortWrite", err)
|
||||
}
|
||||
|
||||
want := errors.New("connection closed")
|
||||
if err := sando.Render(context.Background(), errorWriter{err: want}, component); !errors.Is(err, want) {
|
||||
t.Fatalf("writer error = %v, want %v", err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextEscapingAgainstHTMLTemplateBaseline(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
baseline := template.Must(template.New("probe").Parse(`<a href="{{ .URL }}" title="{{ .Title }}">{{ .Text }}</a>`))
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
title string
|
||||
text string
|
||||
}{
|
||||
{name: "entities", url: "/items?q=one&kind=book", title: `say "hello" & goodbye`, text: `<strong>not markup</strong>`},
|
||||
{name: "unicode", url: "/items/cole", title: "Café & 雪", text: "Cole's archive ♥"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
data := struct{ URL, Title, Text string }{test.url, test.title, test.text}
|
||||
var legacy bytes.Buffer
|
||||
if err := baseline.Execute(&legacy, data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var generated bytes.Buffer
|
||||
if err := sando.Render(context.Background(), &generated, SecurityProbe(test.url, test.title, test.text)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := strings.TrimSpace(generated.String()), legacy.String(); got != want {
|
||||
t.Fatalf("generated escaping differs from html/template baseline\n got: %s\nwant: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDangerousURLIsBlockedAtLeastAsStrictlyAsHTMLTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const dangerous = " JaVaScRiPt:\talert(1)"
|
||||
baseline := template.Must(template.New("probe").Parse(`<a href="{{ . }}">link</a>`))
|
||||
var legacy bytes.Buffer
|
||||
if err := baseline.Execute(&legacy, dangerous); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(legacy.String(), "#ZgotmplZ") {
|
||||
t.Fatalf("html/template baseline unexpectedly accepted dangerous URL: %s", legacy.String())
|
||||
}
|
||||
|
||||
var generated bytes.Buffer
|
||||
err := sando.Render(context.Background(), &generated, SecurityProbe(dangerous, "title", "link"))
|
||||
if !errors.Is(err, sando.ErrUnsafeURL) {
|
||||
t.Fatalf("generated renderer error = %v, want ErrUnsafeURL", err)
|
||||
}
|
||||
}
|
||||
|
||||
type shortWriter struct{}
|
||||
|
||||
func (shortWriter) Write(value []byte) (int, error) {
|
||||
if len(value) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return len(value) - 1, nil
|
||||
}
|
||||
|
||||
type errorWriter struct{ err error }
|
||||
|
||||
func (writer errorWriter) Write([]byte) (int, error) { return 0, writer.err }
|
||||
@@ -1,7 +0,0 @@
|
||||
<?sando go
|
||||
package views
|
||||
|
||||
func SecurityProbe(url string, title string, text string)
|
||||
?>
|
||||
<?# SPDX-License-Identifier: 0BSD ?>
|
||||
<a href="<?= url ?>" title="<?= title ?>"><?= text ?></a>
|
||||
@@ -1,53 +0,0 @@
|
||||
// Code generated by himesan; DO NOT EDIT.
|
||||
// himesan:compiler 0.1.0-dev
|
||||
// himesan:runtime-abi sando.v1
|
||||
// himesan:source-sha256 8d59b45799d0ed2e9e6997f286315f2cb794ea660940978ec39efa3b9a3b94ab
|
||||
|
||||
package views
|
||||
|
||||
import (
|
||||
__himesan_context "context"
|
||||
__himesan_sando "gamertan.com/sandwich-hime/sando"
|
||||
__himesan_io "io"
|
||||
)
|
||||
|
||||
var _ = __himesan_sando.ABI
|
||||
|
||||
func SecurityProbe(url string, title string, text string) __himesan_sando.Component {
|
||||
return __himesan_sando.ComponentFunc(func(__himesan_render_context __himesan_context.Context, __himesan_writer __himesan_io.Writer) error {
|
||||
_ = __himesan_render_context
|
||||
//line views/security_probe.sando:5:3
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/security_probe.sando:6:37
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\n<a href=\""); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/security_probe.sando:7:14
|
||||
if __himesan_error := __himesan_sando.WriteURL(__himesan_writer, (url)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/security_probe.sando:7:20
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\" title=\""); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/security_probe.sando:7:33
|
||||
if __himesan_error := __himesan_sando.WriteAttr(__himesan_writer, (title)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/security_probe.sando:7:41
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "\">"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/security_probe.sando:7:47
|
||||
if __himesan_error := __himesan_sando.WriteText(__himesan_writer, (text)); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
//line views/security_probe.sando:7:54
|
||||
if __himesan_error := __himesan_sando.WriteString(__himesan_writer, "</a>\n"); __himesan_error != nil {
|
||||
return __himesan_error
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user