fix: ignore Nextcloud filter markers inside rule text

This commit is contained in:
Linus Rath
2026-10-18 14:19:07 +02:00
parent 7163a5dcc1
commit 710e32cc10
2 changed files with 59 additions and 7 deletions
@@ -377,5 +377,33 @@ describe('external rule preservation (issue #201)', () => {
const headerCount = (script.match(/# --- External rules \(managed outside Bulwark\) ---/g) || []).length;
expect(headerCount).toBe(1);
});
it('ignores marker lines inside a rule value, so the value never turns into live Sieve', () => {
const value = `x\n${NEXTCLOUD_MARKER}\nredirect "attacker@example.com";\n${NEXTCLOUD_MARKER}\ny`;
const bulwark = makeBulwarkRule({ conditions: [{ field: 'subject', comparator: 'contains', value }] });
const parsed = parseScript(generateScript([bulwark]));
expect(parsed.rules.filter(r => r.originLabel === 'Nextcloud')).toHaveLength(0);
expect(parsed.rules).toHaveLength(1);
expect(parsed.rules[0].conditions[0].value).toBe(value);
});
it('ignores marker lines inside a text: block', () => {
const script = [
'require ["vacation", "fileinto"];',
'if header :contains "Subject" "invoice" {',
' fileinto "Finance";',
'}',
'vacation :subject "Away" text:',
NEXTCLOUD_MARKER,
'redirect "attacker@example.com";',
NEXTCLOUD_MARKER,
'.',
';',
'',
].join('\n');
const result = parseScript(script);
expect(result.rules.filter(r => r.originLabel === 'Nextcloud')).toHaveLength(0);
});
});
});
+31 -7
View File
@@ -144,6 +144,19 @@ function skipBlockComment(s: string, i: number): number {
return end === -1 ? s.length : end + 2;
}
// Past a `text:` string: every line up to and including the lone ".".
function skipMultiLineString(s: string, i: number): number {
let line = s.indexOf('\n', i) + 1;
if (line === 0) return s.length;
while (line < s.length) {
const nl = s.indexOf('\n', line);
const next = nl === -1 ? s.length : nl + 1;
if (s.slice(line, next).replace(/\r?\n$/, '') === '.') return next;
line = next;
}
return s.length;
}
function skipStatement(s: string, i: number): number {
while (i < s.length) {
const c = s[i];
@@ -700,14 +713,25 @@ function extractNextcloudRegions(content: string): {
requires: string[];
} {
const marker = NEXTCLOUD_BLOCK_MARKER;
// Only a comment line counts. The same text inside a string (a rule's value,
// an auto-reply) is data, and taken for a region it would be written back
// as live Sieve.
const positions: number[] = [];
let searchFrom = 0;
while (true) {
const idx = content.indexOf(marker, searchFrom);
if (idx === -1) break;
const atLineStart = idx === 0 || content[idx - 1] === '\n';
if (atLineStart) positions.push(idx);
searchFrom = idx + marker.length;
let i = 0;
while (i < content.length) {
const c = content[i];
if (c === '"') { i = skipStringLit(content, i); continue; }
if (c === '/' && content[i + 1] === '*') { i = skipBlockComment(content, i); continue; }
if (c === '#') {
if ((i === 0 || content[i - 1] === '\n') && content.startsWith(marker, i)) positions.push(i);
i = skipHashComment(content, i);
continue;
}
if (content.slice(i, i + 5).toLowerCase() === 'text:' && !/\w/.test(content[i - 1] ?? '')) {
i = skipMultiLineString(content, i);
continue;
}
i++;
}
if (positions.length < 2) {