feat: publish the Sandwich Hime source preview
Signed-off-by: Cole Speelman <gamertan@noreply.localhost>
This commit is contained in:
+184
@@ -0,0 +1,184 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sando
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// ErrUnsafeURL is the sentinel wrapped by URLSafetyError when an ordinary URL
|
||||
// value is ambiguous or uses a scheme outside the v1 allowlist.
|
||||
var ErrUnsafeURL = errors.New("sando: unsafe URL")
|
||||
|
||||
// URLSafetyError reports why an ordinary URL was rejected. It intentionally
|
||||
// does not retain or print the complete value, which may contain sensitive
|
||||
// application data.
|
||||
type URLSafetyError struct {
|
||||
Scheme string
|
||||
Reason string
|
||||
}
|
||||
|
||||
// Error implements error.
|
||||
func (e *URLSafetyError) Error() string {
|
||||
if e.Scheme != "" {
|
||||
return fmt.Sprintf("%v: scheme %q is not allowed", ErrUnsafeURL, e.Scheme)
|
||||
}
|
||||
if e.Reason != "" {
|
||||
return fmt.Sprintf("%v: %s", ErrUnsafeURL, e.Reason)
|
||||
}
|
||||
return ErrUnsafeURL.Error()
|
||||
}
|
||||
|
||||
// Unwrap permits errors.Is(err, ErrUnsafeURL).
|
||||
func (e *URLSafetyError) Unwrap() error { return ErrUnsafeURL }
|
||||
|
||||
// WriteString writes a compiler-owned static literal and reports both writer
|
||||
// errors and contract-violating short writes. Application data must use the
|
||||
// context-specific helpers below instead.
|
||||
func WriteString(w io.Writer, value string) error {
|
||||
return writeString(w, value)
|
||||
}
|
||||
|
||||
// WriteText writes value escaped for an HTML text node.
|
||||
func WriteText(w io.Writer, value any) error {
|
||||
if trusted, ok := value.(TrustedHTML); ok {
|
||||
return writeString(w, trusted.value)
|
||||
}
|
||||
return writeString(w, html.EscapeString(normalizeText(stringValue(value))))
|
||||
}
|
||||
|
||||
// WriteRCDATA writes value escaped for an HTML RCDATA element such as title
|
||||
// or textarea. Trusted wrappers are intentionally not honored in this context:
|
||||
// their contents are escaped like every other value so they cannot close the
|
||||
// containing element.
|
||||
func WriteRCDATA(w io.Writer, value any) error {
|
||||
return writeString(w, html.EscapeString(normalizeText(stringValue(value))))
|
||||
}
|
||||
|
||||
// WriteAttr writes value escaped for a quoted HTML attribute. Generated code
|
||||
// must always place this output inside a syntactically complete quoted value.
|
||||
func WriteAttr(w io.Writer, value any) error {
|
||||
return writeString(w, html.EscapeString(normalizeText(stringValue(value))))
|
||||
}
|
||||
|
||||
// WriteURL writes value escaped for a quoted URL-bearing HTML attribute.
|
||||
// Ordinary values are canonicalized and checked before any bytes are written.
|
||||
// TrustedURL bypasses the scheme check but never attribute escaping.
|
||||
func WriteURL(w io.Writer, value any) error {
|
||||
if trusted, ok := value.(TrustedURL); ok {
|
||||
return writeString(w, html.EscapeString(normalizeText(trusted.value)))
|
||||
}
|
||||
|
||||
canonical, err := canonicalURL(stringValue(value))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeString(w, html.EscapeString(canonical))
|
||||
}
|
||||
|
||||
// WriteHTML writes deliberately trusted HTML without escaping.
|
||||
func WriteHTML(w io.Writer, value TrustedHTML) error {
|
||||
return writeString(w, value.value)
|
||||
}
|
||||
|
||||
// WriteJS writes deliberately trusted JavaScript without escaping.
|
||||
func WriteJS(w io.Writer, value TrustedJS) error {
|
||||
return writeString(w, value.value)
|
||||
}
|
||||
|
||||
// WriteCSS writes deliberately trusted CSS without escaping.
|
||||
func WriteCSS(w io.Writer, value TrustedCSS) error {
|
||||
return writeString(w, value.value)
|
||||
}
|
||||
|
||||
func writeString(w io.Writer, value string) error {
|
||||
if isNil(w) {
|
||||
return ErrNilWriter
|
||||
}
|
||||
|
||||
n, err := io.WriteString(w, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != len(value) {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stringValue(value any) string {
|
||||
switch value := value.(type) {
|
||||
case TrustedHTML:
|
||||
return value.value
|
||||
case TrustedURL:
|
||||
return value.value
|
||||
case TrustedJS:
|
||||
return value.value
|
||||
case TrustedCSS:
|
||||
return value.value
|
||||
default:
|
||||
return fmt.Sprint(value)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeText(value string) string {
|
||||
if !utf8.ValidString(value) {
|
||||
value = strings.ToValidUTF8(value, "\uFFFD")
|
||||
}
|
||||
return strings.ReplaceAll(value, "\x00", "\uFFFD")
|
||||
}
|
||||
|
||||
func canonicalURL(value string) (string, error) {
|
||||
value = normalizeText(value)
|
||||
value = strings.TrimFunc(value, func(r rune) bool {
|
||||
return unicode.IsSpace(r) || r == '\uFEFF'
|
||||
})
|
||||
|
||||
for _, r := range value {
|
||||
if r < 0x20 || r == 0x7f {
|
||||
return "", &URLSafetyError{Reason: "control characters are not allowed"}
|
||||
}
|
||||
}
|
||||
|
||||
colon := strings.IndexByte(value, ':')
|
||||
boundary := strings.IndexAny(value, "/?#")
|
||||
if colon < 0 || boundary >= 0 && boundary < colon {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
scheme := value[:colon]
|
||||
if !validScheme(scheme) {
|
||||
return "", &URLSafetyError{Reason: "ambiguous scheme syntax"}
|
||||
}
|
||||
|
||||
scheme = strings.ToLower(scheme)
|
||||
switch scheme {
|
||||
case "http", "https", "mailto", "tel":
|
||||
return value, nil
|
||||
default:
|
||||
return "", &URLSafetyError{Scheme: scheme}
|
||||
}
|
||||
}
|
||||
|
||||
func validScheme(value string) bool {
|
||||
if value == "" || !isASCIIAlpha(value[0]) {
|
||||
return false
|
||||
}
|
||||
for i := 1; i < len(value); i++ {
|
||||
c := value[i]
|
||||
if !isASCIIAlpha(c) && (c < '0' || c > '9') && c != '+' && c != '-' && c != '.' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isASCIIAlpha(c byte) bool {
|
||||
return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z'
|
||||
}
|
||||
Reference in New Issue
Block a user