Refuse to save a script we only partly read

The transport fix stops the truncation that caused #76, but the save path
had no answer for a baseline that arrives incomplete. It is neither
unknown nor empty, so every existing guard passes it through: it parses
into a shorter rule list that looks exactly like a script with fewer
rules, and saving writes that back over the real one.

Check the script against the shape the generator emits instead. Every
rule comment parses, every enabled rule has an if and a closed body under
it, every block ends with a blank line. Structural rather than a
re-serialize-and-compare, so a script written by an older version whose
serializer differed is still editable.

The rule editor reports a short script as unreadable rather than showing
the rules that happened to parse, since a list that looks complete over a
script that is not is the most dangerous thing it could offer.

A cut at the end of a complete rule block is still a valid shorter script
and cannot be told apart from one; that residual is the proxy's to cover.
This commit is contained in:
2026-08-30 13:56:12 -07:00
parent 0277b5b6a8
commit 8d475e2b07
6 changed files with 227 additions and 15 deletions
+14 -1
View File
@@ -45,7 +45,7 @@ const RULE_MIME = "application/x-ihasmail-sieve-rule";
function RulesEditor() {
const sieve = useSieve();
const { script, rules, content } = sieve.rules();
const { script, rules, content, damage } = sieve.rules();
const [local, setLocal] = useState<SieveRule[] | null>(null);
const [editing, setEditing] = useState<SieveRule | null>(null);
const [saving, setSaving] = useState(false);
@@ -80,6 +80,19 @@ function RulesEditor() {
}
};
// Before the hand-written branch: a script that arrived in part is not a
// script someone chose to write themselves, and the way out of it is a reload
// rather than the "start with rules" button below, which would write over it.
if (damage) {
return (
<div className="warn-box">
<div className="row gap-8" style={{ marginBottom: 8 }}><AlertTriangle size={18} /> <b>Only part of your filter script arrived.</b></div>
<p style={{ margin: "0 0 8px" }}>It {damage}, so the rules in it can't be shown or edited — saving what did arrive would write it back over the rest. Reload the page to try again. Your rules are still on the server; nothing here has changed them.</p>
<button className="btn" onClick={() => window.location.reload()}>Reload</button>
</div>
);
}
if (rules === null) {
return (
<div className="warn-box">