Note where the first valued mark's allowlist has to go
mark_names returns names and drops every value, which is why an unknown
mark carried through by name is safe today. A valued mark changes that,
and the tempting allowlist — link is the one with an href — is wrong on
arrival: this function collects marks it has never heard of straight off
the wire, and POST /d/{sqid}/updates accepts bytes that never met the
schema, so an unknown mark can carry a value too.
It is also the right place: the single point the HTML renderer and every
JSON consumer agree on what marks a run carries, so a check here cannot
be re-implemented differently by markdown.go and pdf.go.
Found by the session that owns Plan 7, reading this file after the
point-ladder work restructured around it. Recorded here rather than only
in that plan, because the plan may land long after someone else touches
this function.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+29
−0
core/src/fragment.rs+29 −0
Intent
Attributed to this commit: 526 g CO₂e (60804241 tokens). This is an attribution, not a measurement — usage counted from this commit's own marker forward to the next one on diagnose, never backward from it. A turn that produced more than one commit books all of its tokens to the last one, and a concurrent session on this branch can add to the same figure. Some of it was priced using the shared default rate, because no model-specific factor row exists yet for at least one model involved. The Carbon view has the repo total, which is never derived from this attribution.
diff --git a/core/src/fragment.rs b/core/src/fragment.rs| index 5074ad3..3e18e97 100644 |
| --- a/core/src/fragment.rs |
| +++ b/core/src/fragment.rs |
| @@ -517,6 +517,35 @@ fn escape_json(s: &str, out: &mut String) { |
| /// children (see the module comment): a consumer that ignores a mark it |
| /// does not recognise still has the text, and one that grows to |
| /// understand it needs it to have been there. |
| +/// WHOEVER ADDS THE FIRST VALUED MARK: THIS FUNCTION IS WHERE THE |
| +/// ALLOWLIST GOES, AND IT MUST NOT ASSUME ONLY THAT MARK HAS A VALUE. |
| +/// |
| +/// This returns NAMES. Every mark's value — including an unknown mark's |
| +/// — is dropped here and reaches no consumer, which is why an unknown |
| +/// mark carried through by name is safe today: `fragment_html` finds no |
| +/// MARKS row and emits no wrapper, and `text_json` writes the name |
| +/// through `escape_json`. |
| +/// |
| +/// A valued mark changes that. The moment one value is allowed past |
| +/// this point, the shape of the question becomes "which marks may carry |
| +/// a value, and what may that value be" — and the tempting answer, that |
| +/// `link` is the one with an `href`, is wrong on arrival. This function |
| +/// collects marks it has never heard of, straight off the wire, and |
| +/// `POST /d/{sqid}/updates` accepts bytes that never went through |
| +/// ProseMirror's schema. So an unknown mark can carry an arbitrary |
| +/// value too, and an allowlist written around the known one would let |
| +/// it past. |
| +/// |
| +/// It is also the right place, and that is the other half of the note: |
| +/// this is the single point the HTML renderer and every JSON consumer |
| +/// agree on what marks a run carries, so a check here cannot be |
| +/// re-implemented differently by `markdown.go` and `pdf.go` the way one |
| +/// in `node_html` would have to be. Plan 7's D1 names it. |
| +/// |
| +/// (Found by the session that owns Plan 7, reading this file after the |
| +/// point-ladder work restructured around it — recorded here rather than |
| +/// only in that plan, because the plan may land long after someone else |
| +/// touches this function.) |
| fn mark_names(attrs: &yrs::types::Attrs) -> Vec<String> { |
| let mut names: Vec<String> = MARKS |
| .iter() |