Ctrl K
ComunidadGuías…

coderd/render/escape.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package render

import "strings"

// Character classes for EscapeMarkdown, split by where each character carries
// structural meaning. The split is what keeps escaping away from the enum-like
// label values that body templates compare with `eq`, such as "user_override",
// "bobby-workspace" and "1.5": escaping one changes template control flow.
const (
	// inlineCritical characters can produce a link, an image, an angle autolink,
	// or forge an escape from anywhere in a value, so they are always escaped.
	//
	// Backtick is here, not in blockStart, because a fence's info string is an
	// HTML sink: gomarkdown writes it into class="language-..." unescaped, and
	// SkipHTML does not apply to a CodeBlock node. Escaping only the leading
	// backtick would leave two, which open an inline code span.
	inlineCritical = "\\[]()!<`"

	// blockStart characters carry structural meaning only as the first
	// non-space character of a line, so they are escaped only there.
	blockStart = `#-+.>|`

	// leadingEmphasis characters carry inline meaning anywhere but also open a
	// block construct in leading position: "* " starts a bullet list, and three
	// or more of either character starts a thematic break. Escaping them only
	// there costs emphasis that begins on a line boundary.
	leadingEmphasis = `*_`

	// foldStart characters also carry meaning only at the start of a line, but
	// glamour does not honor a backslash before them, so escaping would leave a
	// literal backslash in the plaintext part. The preceding line break becomes
	// a space instead, denying them the line-start position.
	//
	// ":" is here for the GFM delimiter row ":-- | --:" as well as definition
	// lists. Escaping "|" does not reach that row: its pipes are mid-line.
	foldStart = `=~:`

	// maxLeadingSpaces is the widest indentation a line may keep, since four
	// spaces open an indented code block and a space cannot be escaped.
	maxLeadingSpaces = 3
)

// EscapeMarkdown neutralizes Markdown structure in an untrusted value so that it
// renders as literal text through both HTMLFromNotificationMarkdown and
// PlaintextFromMarkdown. Line breaks are preserved so multi-line values keep
// their shape. Other control characters are dropped, being the carrier for SMTP
// header injection.
//
// Known residual: a template that wraps the value in a code span. CommonMark
// does not process escapes inside one, so the backslashes emitted here reach
// the reader. Nothing here can detect that, since the sink is decided after
// this runs.
func EscapeMarkdown(s string) string {
	if s == "" {
		return s
	}

	lines := strings.Split(stripControl(s), "\n")
	var b strings.Builder
	b.Grow(len(s) + len(s)/8)

	for i, line := range lines {
		if i > 0 {
			// Joining a fold-start line to the previous one takes it out of
			// leading position.
			if opensFoldConstruct(line) {
				_ = b.WriteByte(' ')
			} else {
				_ = b.WriteByte('\n')
			}
		}
		// The first line has no preceding break to fold, so escaping is the only
		// lever left there.
		_, _ = b.WriteString(escapeLine(line, i == 0 && isLeadingFoldConstruct(line)))
	}
	return b.String()
}

// stripControl keeps line breaks, turns the other whitespace controls into
// spaces and drops the rest. Carriage returns are folded rather than kept so a
// value cannot terminate an SMTP header.
func stripControl(s string) string {
	if strings.IndexFunc(s, isStrippable) < 0 {
		return s
	}
	var b strings.Builder
	b.Grow(len(s))
	for _, r := range s {
		switch {
		case r == '\n':
			_, _ = b.WriteRune(r)
		case r == '\r' || r == '\t' || r == '\v' || r == '\f':
			_, _ = b.WriteRune(' ')
		case r < 0x20 || r == 0x7f:
			// Dropped.
		default:
			_, _ = b.WriteRune(r)
		}
	}
	return b.String()
}

func isStrippable(r rune) bool {
	return r != '\n' && (r < 0x20 || r == 0x7f)
}

// escapeLine escapes one line's structural characters and truncates its
// indentation to maxLeadingSpaces.
//
// escapeFold additionally escapes a leading "=" or "~". Only EscapeMarkdown's
// first line passes it, and only when that line really is a fold construct.
func escapeLine(line string, escapeFold bool) string {
	var b strings.Builder
	b.Grow(len(line))

	leading := true
	spaces := 0
	// digitRun reports whether the line so far is nothing but indentation and
	// digits, which is the only position where "." opens an ordered list.
	digitRun := false
	for i, r := range line {
		switch {
		case r < 0x80 && strings.ContainsRune(inlineCritical, r):
			_ = b.WriteByte('\\')
			_, _ = b.WriteRune(r)
		case leading && r == ' ':
			// Indentation keeps the next character in leading position.
			if spaces < maxLeadingSpaces {
				_, _ = b.WriteRune(r)
				spaces++
			}
			continue
		case leading && r < 0x80 && strings.ContainsRune(blockStart+leadingEmphasis, r):
			_ = b.WriteByte('\\')
			_, _ = b.WriteRune(r)
		case leading && escapeFold && (r == '=' || r == '~'):
			_ = b.WriteByte('\\')
			_, _ = b.WriteRune(r)
		case digitRun && r == '.' && closesMarker(line, i):
			// The "1." of an ordered list. Its sibling "1)" needs no case
			// because ")" is inlineCritical and is always escaped.
			_ = b.WriteByte('\\')
			_, _ = b.WriteRune(r)
		default:
			_, _ = b.WriteRune(r)
		}
		digitRun = (leading || digitRun) && r >= '0' && r <= '9'
		leading = false
	}
	return b.String()
}

// closesMarker reports whether the single-byte list-marker delimiter at i is
// followed by a space or ends the line, as CommonMark requires of a marker.
// That requirement is what keeps a value such as "1.5" out of the escaped set.
// Tabs need no handling: stripControl has already folded them into spaces.
func closesMarker(line string, i int) bool {
	return i+1 == len(line) || line[i+1] == ' '
}

// opensFoldConstruct reports whether a line's first non-space character is a
// foldStart character. Approximate on purpose: it only decides whether to drop
// a line break, which costs nothing.
func opensFoldConstruct(line string) bool {
	t := strings.TrimLeft(line, " ")
	if t == "" {
		return false
	}
	return strings.ContainsRune(foldStart, rune(t[0]))
}

// isLeadingFoldConstruct reports whether a line is itself a tilde fence opener
// or a Setext "=" underline, rather than merely starting with one of those
// characters. Exact, because it governs escaping and the backslash is visible:
// "=> next" must not acquire one, while "~~~" must, since an unterminated fence
// at the start of a title renders the Subject, <title> and heading empty.
//
// Indentation is ignored: escapeLine truncates it to maxLeadingSpaces, which
// still leaves the line able to open a block. ":" is excluded because a
// definition list or table needs a preceding line that a first line lacks.
func isLeadingFoldConstruct(line string) bool {
	t := strings.TrimLeft(line, " ")
	if strings.HasPrefix(t, "~~~") {
		return true
	}
	t = strings.TrimRight(t, " ")
	return t != "" && strings.Trim(t, "=") == ""
}