Search Apps Documentation Source Content File Folder Download Copy Actions Download

foreign_test.gno

11.41 Kb · 323 lines
  1package foreign
  2
  3import (
  4	"strings"
  5	"testing"
  6)
  7
  8func TestForeign_BasicWrap(t *testing.T) {
  9	got := Foreign("hello")
 10	want := "\n\n<gno-foreign>\nhello\n</gno-foreign>\n\n"
 11	if got != want {
 12		t.Errorf("got %q, want %q", got, want)
 13	}
 14}
 15
 16func TestForeign_LeadingTrailingBlankLines(t *testing.T) {
 17	// Parser requires a blank line before the opener (CM §4.6). Verify
 18	// the helper emits exactly that.
 19	got := Foreign("body")
 20	if !strings.HasPrefix(got, "\n\n<gno-foreign>\n") {
 21		t.Errorf("missing blank line before opener: %q", got)
 22	}
 23	if !strings.HasSuffix(got, "\n</gno-foreign>\n\n") {
 24		t.Errorf("missing blank line after closer: %q", got)
 25	}
 26}
 27
 28func TestForeign_NormalizesCRLF(t *testing.T) {
 29	got := Foreign("line1\r\nline2\r\nline3")
 30	if strings.Contains(got, "\r") {
 31		t.Errorf("CRLF not normalized: %q", got)
 32	}
 33	if !strings.Contains(got, "line1\nline2\nline3") {
 34		t.Errorf("body content lost: %q", got)
 35	}
 36}
 37
 38func TestForeign_NormalizesBareCR(t *testing.T) {
 39	got := Foreign("line1\rline2")
 40	if strings.Contains(got, "\r") {
 41		t.Errorf("bare CR not normalized: %q", got)
 42	}
 43	if !strings.Contains(got, "line1\nline2") {
 44		t.Errorf("body content lost: %q", got)
 45	}
 46}
 47
 48func TestForeign_EscapesSentinelClose(t *testing.T) {
 49	// Unbalanced </gno-foreign> in body would terminate the outer
 50	// block early. Verify it's escaped to literal text — only the
 51	// leading "<" is replaced with "&lt;"; the trailing ">" stays
 52	// literal (rendering as visible ">" in HTML).
 53	got := Foreign("foo\n</gno-foreign>\nbar")
 54	if strings.Contains(got, "\n</gno-foreign>\nbar") {
 55		t.Errorf("unbalanced close not escaped: %q", got)
 56	}
 57	if !strings.Contains(got, "&lt;/gno-foreign>") {
 58		t.Errorf("expected escaped close: %q", got)
 59	}
 60}
 61
 62func TestForeign_EscapesSentinelOpen(t *testing.T) {
 63	// A bare opener inside body (without matching close in body)
 64	// would be picked up as a nested inner foreign. Escape it.
 65	got := Foreign("foo\n<gno-foreign>\nbar")
 66	if !strings.Contains(got, "\n&lt;gno-foreign>\n") {
 67		t.Errorf("expected escaped inner opener: %q", got)
 68	}
 69}
 70
 71func TestForeign_EscapesIndentedSentinel(t *testing.T) {
 72	// Parser strips 0-3 leading spaces before matching; helper
 73	// must mirror that to catch indented sentinels.
 74	got := Foreign("foo\n   </gno-foreign>\nbar")
 75	if !strings.Contains(got, "&lt;/gno-foreign>") {
 76		t.Errorf("indented sentinel not escaped: %q", got)
 77	}
 78}
 79
 80func TestForeign_EscapesAttributeBearingOpener(t *testing.T) {
 81	// `<gno-foreign label="x">` IS recognized by the parser as a
 82	// nested opener. If left unmangled, the parser bumps framingDepth
 83	// when it sees this line in body bytes, and then consumes the
 84	// helper's own outer-close as a fake "inner close" — capturing
 85	// realm content that follows the helper's output INTO the
 86	// sandbox. Verify the helper escapes the leading "<".
 87	got := Foreign(`<gno-foreign label="x">`)
 88	if strings.Contains(got, `<gno-foreign label="x">`) {
 89		t.Errorf("attribute-bearing opener was NOT mangled — escape vector:\n%q", got)
 90	}
 91	if !strings.Contains(got, `&lt;gno-foreign label="x">`) {
 92		t.Errorf("expected leading '<' escaped: %q", got)
 93	}
 94}
 95
 96func TestForeign_EscapesAttributeBearingCloser(t *testing.T) {
 97	// golang.org/x/net/html zeroes the Attr slice on end tags, so the
 98	// parser recognizes ANY `</gno-foreign attr…>` form as a sentinel
 99	// close. Leaving such a line unmangled in body bytes would close
100	// the outer block early (framingDepth=0 case in the parser's
101	// Continue), letting body content that follows render OUTSIDE
102	// the sandbox at top level. Verify the helper escapes it.
103	got := Foreign(`</gno-foreign label="x">`)
104	if strings.Contains(got, `> </gno-foreign label="x"> </gno-foreign>`) {
105		t.Errorf("attribute-bearing closer was NOT mangled — escape vector:\n%q", got)
106	}
107	if !strings.Contains(got, `&lt;/gno-foreign label="x">`) {
108		t.Errorf("expected leading '<' escaped: %q", got)
109	}
110}
111
112func TestForeign_EscapesGnoForeignWithExtraContent(t *testing.T) {
113	// `<gno-foreign> extra` would not be recognized as a sentinel by
114	// the parser (tokenizer yields 2 tokens). But the helper is
115	// over-inclusive on purpose: anything that looks like a
116	// gno-foreign tag opener or closer gets mangled. Predictable
117	// behavior beats narrow matching that diverges from the parser
118	// on attribute-bearing forms.
119	got := Foreign("<gno-foreign> extra text")
120	if !strings.Contains(got, "&lt;gno-foreign> extra text") {
121		t.Errorf("expected over-inclusive mangle of `<gno-foreign> extra`: %q", got)
122	}
123}
124
125func TestForeign_EscapesCaseVariantClose(t *testing.T) {
126	// The renderer-side parser recognizes tags case-insensitively
127	// (goldmark's tokenizer lowercases tag names). A case-variant close
128	// left unescaped would terminate the outer block early and let
129	// trailing body render OUTSIDE the sandbox. The helper must escape
130	// `</GNO-FOREIGN>` just like `</gno-foreign>`.
131	got := Foreign("foo\n</GNO-FOREIGN>\nbar")
132	if strings.Contains(got, "\n</GNO-FOREIGN>\nbar") {
133		t.Errorf("case-variant close not escaped — sandbox-escape vector: %q", got)
134	}
135	if !strings.Contains(got, "&lt;/GNO-FOREIGN>") {
136		t.Errorf("expected escaped case-variant close: %q", got)
137	}
138}
139
140func TestForeign_EscapesCaseVariantOpen(t *testing.T) {
141	// A case-variant opener left unescaped would be picked up by the
142	// parser as a nested inner foreign (framingDepth++), causing the
143	// helper's own outer-close to be consumed as a fake inner close and
144	// capturing trailing realm content into the sandbox.
145	got := Foreign("foo\n<GNO-FOREIGN>\nbar")
146	if !strings.Contains(got, "\n&lt;GNO-FOREIGN>\n") {
147		t.Errorf("case-variant opener not escaped — capture vector: %q", got)
148	}
149}
150
151func TestForeign_EscapesMixedCaseSentinel(t *testing.T) {
152	// Mixed-case forms (the most likely hand-crafted evasion) must also
153	// be caught, including the attribute-bearing mixed-case opener.
154	for _, in := range []string{
155		"<Gno-Foreign>",
156		"</Gno-Foreign>",
157		`<GNO-foreign label="x">`,
158		"</gNo-fOrEiGn>",
159	} {
160		got := Foreign(in)
161		if !strings.Contains(got, "&lt;") {
162			t.Errorf("mixed-case sentinel %q not escaped: %q", in, got)
163		}
164	}
165}
166
167func TestForeignWithLabel_StripsBidiAndZeroWidth(t *testing.T) {
168	// A bidi-override (U+202E) or zero-width (U+200C) run in the label
169	// is invisible-spoof payload; it must be stripped before the label
170	// is spliced into the opener attribute.
171	got := ForeignWithLabel("a\u202eb\u200cc", "body")
172	if !strings.Contains(got, `label="abc"`) {
173		t.Errorf("expected bidi/zero-width-stripped label: %q", got)
174	}
175}
176
177func TestForeignWithLabel_UnicodeSeparatorsBecomeSpaces(t *testing.T) {
178	// U+2028 / U+2029 / U+0085 must not survive in the single-line
179	// opener attribute; they fold to spaces like ASCII line breaks.
180	got := ForeignWithLabel("a\u2028b\u2029c\u0085d", "body")
181	if !strings.Contains(got, `label="a b c d"`) {
182		t.Errorf("expected space-folded label: %q", got)
183	}
184}
185
186func TestForeign_EscapesUnrelatedForeignPrefix(t *testing.T) {
187	// `<gno-foreignx>` is a different tag the parser would NOT treat as
188	// a sentinel, but the escaper is deliberately over-inclusive (it
189	// matches the `<gno-foreign` prefix alone), so it is escaped to
190	// visible literal text. Harmless, and the safe side of the trade.
191	got := Foreign("<gno-foreignx>")
192	if !strings.Contains(got, "&lt;gno-foreignx>") {
193		t.Errorf("expected over-inclusive escape of <gno-foreignx>: %q", got)
194	}
195}
196
197func TestForeign_EscapesSlashAndFormFeedClosers(t *testing.T) {
198	// REGRESSION: goldmark's tokenizer ends a tag name at `/` and at
199	// form-feed (both HTML tag-name terminators), so `</gno-foreign/>`
200	// and `</gno-foreign\f>` are recognized as CLOSERS by the parser. A
201	// body line carrying one of these, if it reached the parser
202	// unescaped, would close the outer block early — a sandbox escape.
203	// The over-inclusive escaper must neutralize every such variant.
204	for _, in := range []string{
205		"foo\n</gno-foreign/>\nbar",
206		"foo\n</gno-foreign/ >\nbar",
207		"foo\n</gno-foreign\f>\nbar",
208		"foo\n<gno-foreign/>\nbar",
209	} {
210		got := Foreign(in)
211		if !strings.Contains(got, "&lt;") {
212			t.Errorf("variant not escaped (sandbox-escape vector): %q -> %q", in, got)
213		}
214	}
215}
216
217func TestForeign_EmptyBody(t *testing.T) {
218	got := Foreign("")
219	want := "\n\n<gno-foreign>\n\n</gno-foreign>\n\n"
220	if got != want {
221		t.Errorf("got %q, want %q", got, want)
222	}
223}
224
225func TestForeign_PreservesInternalBlankLines(t *testing.T) {
226	got := Foreign("para1\n\npara2")
227	if !strings.Contains(got, "para1\n\npara2") {
228		t.Errorf("internal blank line lost: %q", got)
229	}
230}
231
232func TestForeignWithLabel_BasicWrap(t *testing.T) {
233	got := ForeignWithLabel("My Label", "body")
234	want := "\n\n<gno-foreign label=\"My Label\">\nbody\n</gno-foreign>\n\n"
235	if got != want {
236		t.Errorf("got %q, want %q", got, want)
237	}
238}
239
240func TestForeignWithLabel_EmptyLabelFallsBack(t *testing.T) {
241	// An empty label (or one that becomes empty after sanitization)
242	// should be identical to Foreign — no attribute on the opener.
243	got := ForeignWithLabel("", "body")
244	if strings.Contains(got, "label=") {
245		t.Errorf("empty label should not emit attribute: %q", got)
246	}
247	if got != Foreign("body") {
248		t.Errorf("empty label should match Foreign: got %q vs %q", got, Foreign("body"))
249	}
250}
251
252func TestForeignWithLabel_WhitespaceOnlyLabelFallsBack(t *testing.T) {
253	got := ForeignWithLabel("   ", "body")
254	if strings.Contains(got, "label=") {
255		t.Errorf("whitespace label should not emit attribute: %q", got)
256	}
257}
258
259func TestForeignWithLabel_EscapesAmpAndQuote(t *testing.T) {
260	got := ForeignWithLabel(`Tom & "Jerry"`, "body")
261	if !strings.Contains(got, `label="Tom &amp; &quot;Jerry&quot;"`) {
262		t.Errorf("expected & and \" escaped: %q", got)
263	}
264}
265
266func TestForeignWithLabel_EscapesAngleBrackets(t *testing.T) {
267	// An unescaped `>` would close the opener tag early. Must be
268	// escaped.
269	got := ForeignWithLabel(`a<b>c`, "body")
270	if !strings.Contains(got, `label="a&lt;b&gt;c"`) {
271		t.Errorf("expected < and > escaped: %q", got)
272	}
273}
274
275func TestForeignWithLabel_StripsNUL(t *testing.T) {
276	got := ForeignWithLabel("a\x00b\x00c", "body")
277	if strings.Contains(got, "\x00") {
278		t.Errorf("NUL not stripped: %q", got)
279	}
280	if !strings.Contains(got, `label="abc"`) {
281		t.Errorf("expected NUL-stripped label: %q", got)
282	}
283}
284
285func TestForeignWithLabel_ControlCharsBecomeSpaces(t *testing.T) {
286	// Newlines and tabs would break the single-line opener required
287	// by the parser. They must become spaces.
288	got := ForeignWithLabel("line1\nline2\ttab", "body")
289	if strings.Contains(got, "\nline2") || strings.Contains(got, "\t") {
290		t.Errorf("control chars not converted: %q", got)
291	}
292	if !strings.Contains(got, "line1 line2 tab") {
293		t.Errorf("expected space-separated label: %q", got)
294	}
295}
296
297func TestForeignWithLabel_TrimsLeadingTrailingSpace(t *testing.T) {
298	got := ForeignWithLabel("  hello  ", "body")
299	if !strings.Contains(got, `label="hello"`) {
300		t.Errorf("expected trimmed label: %q", got)
301	}
302}
303
304func TestForeignWithLabel_PreservesBodyNormalization(t *testing.T) {
305	// Body normalization (CRLF, sentinel escaping) must still apply
306	// in the labeled variant.
307	got := ForeignWithLabel("L", "a\r\n</gno-foreign>\r\nb")
308	if strings.Contains(got, "\r") {
309		t.Errorf("CRLF not normalized in labeled variant: %q", got)
310	}
311	if !strings.Contains(got, "&lt;/gno-foreign>") {
312		t.Errorf("sentinel not escaped in labeled variant: %q", got)
313	}
314}
315
316func TestMaxBlocksPerRender(t *testing.T) {
317	// Surfaces gnoweb's per-render foreign-block cap to realms via the
318	// chain/markdown native. Pin the contract value; bump here if the
319	// renderer cap changes.
320	if got := MaxBlocksPerRender(); got != 256 {
321		t.Errorf("MaxBlocksPerRender() = %d, want 256", got)
322	}
323}