Terminplaner: ICS-Faltung prueft Surrogatpaare statt wirkungslosem UTF-8-Decode
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
181445a69d
commit
171b5086b1
1 file changed
+36
-5
+36
-5
@@ -28,8 +28,17 @@ const termin = (extra = {}) => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const enc = new TextEncoder()
|
const enc = new TextEncoder()
|
||||||
const dec = new TextDecoder('utf-8', { fatal: true })
|
|
||||||
const octets = (s) => enc.encode(s).length
|
const octets = (s) => enc.encode(s).length
|
||||||
|
/**
|
||||||
|
* Jede physische Zeile hoechstens 75 Oktette und wohlgeformtes UTF-16 (kein halbes Surrogatpaar). Ein Decode-Test
|
||||||
|
* (TextDecoder) taugt dafuer nicht: JS-Strings kodieren immer, einzelne Surrogate werden still zu U+FFFD.
|
||||||
|
*/
|
||||||
|
const pruefeZeilen = (text) => {
|
||||||
|
for (const zeile of text.split('\r\n')) {
|
||||||
|
assert.ok(octets(zeile) <= 75, `Zeile zu lang (${octets(zeile)}): ${zeile}`)
|
||||||
|
assert.ok(zeile.isWellFormed(), `Zeile mit halbem Surrogatpaar: ${JSON.stringify(zeile)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
/** Zeilenfaltung rueckgaengig machen: CRLF + Leerzeichen faellt weg. */
|
/** Zeilenfaltung rueckgaengig machen: CRLF + Leerzeichen faellt weg. */
|
||||||
const entfalte = (text) => text.replace(/\r\n /g, '')
|
const entfalte = (text) => text.replace(/\r\n /g, '')
|
||||||
/** Logische Zeilen (entfaltet, ohne das abschliessende CRLF). */
|
/** Logische Zeilen (entfaltet, ohne das abschliessende CRLF). */
|
||||||
@@ -80,16 +89,38 @@ test('Zeilenlaenge: jede Zeile hoechstens 75 Oktette, nie mitten in einem UTF-8-
|
|||||||
for (let n = 40; n < 130; n += 1) {
|
for (let n = 40; n < 130; n += 1) {
|
||||||
const titel = baustein.repeat(n)
|
const titel = baustein.repeat(n)
|
||||||
const ics = icsTermin(termin({ titel, ort: titel, beschreibung: titel, organisator: { name: titel, email: 'info@salon-muster.de' }, teilnehmer: { name: titel, email: 'anna@example.com' } }))
|
const ics = icsTermin(termin({ titel, ort: titel, beschreibung: titel, organisator: { name: titel, email: 'info@salon-muster.de' }, teilnehmer: { name: titel, email: 'anna@example.com' } }))
|
||||||
for (const zeile of ics.split('\r\n')) {
|
pruefeZeilen(ics)
|
||||||
assert.ok(octets(zeile) <= 75, `Zeile zu lang (${octets(zeile)}): ${zeile}`)
|
|
||||||
assert.doesNotThrow(() => dec.decode(enc.encode(zeile)))
|
|
||||||
}
|
|
||||||
for (const folge of ics.split('\r\n').filter((l) => l.startsWith(' '))) assert.ok(folge.length > 1, 'Folgezeile nicht leer')
|
for (const folge of ics.split('\r\n').filter((l) => l.startsWith(' '))) assert.ok(folge.length > 1, 'Folgezeile nicht leer')
|
||||||
assert.equal(entescape(wert(ics, 'SUMMARY').slice('SUMMARY:'.length)), titel, 'Entfalten ergibt den Titel wieder')
|
assert.equal(entescape(wert(ics, 'SUMMARY').slice('SUMMARY:'.length)), titel, 'Entfalten ergibt den Titel wieder')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Zeilenlaenge: Emoji (Surrogatpaare) werden nie zwischen ihren Haelften gefaltet, in Termin und Abo', () => {
|
||||||
|
let gefaltet = 0
|
||||||
|
for (const vorsatz of ['', 'a', 'ab', 'abc']) {
|
||||||
|
for (let n = 1; n < 60; n += 1) {
|
||||||
|
const titel = vorsatz + '😀'.repeat(n)
|
||||||
|
const felder = { titel, ort: titel, beschreibung: titel, organisator: { name: titel, email: 'info@salon-muster.de' }, teilnehmer: { name: titel, email: 'anna@example.com' } }
|
||||||
|
const ics = icsTermin(termin(felder))
|
||||||
|
pruefeZeilen(ics)
|
||||||
|
gefaltet += ics.split('\r\n').filter((l) => l.startsWith(' ')).length
|
||||||
|
assert.equal(entescape(wert(ics, 'SUMMARY').slice('SUMMARY:'.length)), titel)
|
||||||
|
assert.equal(entescape(wert(ics, 'DESCRIPTION').slice('DESCRIPTION:'.length)), titel)
|
||||||
|
const feed = icsKalender([termin(felder)], { name: titel, jetzt: JETZT })
|
||||||
|
pruefeZeilen(feed)
|
||||||
|
assert.equal(entescape(wert(feed, 'X-WR-CALNAME').slice('X-WR-CALNAME:'.length)), titel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.ok(gefaltet > 500, `der Test faltet wirklich (${gefaltet} Folgezeilen)`)
|
||||||
|
// Beispiel: DESCRIPTION: ist 12 Oktette lang, danach passen 15 Emoji (60 Oktette) in die erste Zeile, das 16. beginnt die Folgezeile
|
||||||
|
const beschreibung = '😀'.repeat(16)
|
||||||
|
const zeilen = icsTermin(termin({ beschreibung })).split('\r\n')
|
||||||
|
const i = zeilen.findIndex((l) => l.startsWith('DESCRIPTION:'))
|
||||||
|
assert.equal(zeilen[i], `DESCRIPTION:${'😀'.repeat(15)}`)
|
||||||
|
assert.equal(zeilen[i + 1], ' 😀')
|
||||||
|
})
|
||||||
|
|
||||||
test('Zeilenlaenge: eine Zeile von genau 75 Oktetten bleibt ungefaltet, 76 wird gefaltet', () => {
|
test('Zeilenlaenge: eine Zeile von genau 75 Oktetten bleibt ungefaltet, 76 wird gefaltet', () => {
|
||||||
const zeileMit = (n) => icsTermin(termin({ titel: 'x'.repeat(n - 'SUMMARY:'.length) })).split('\r\n').find((l) => l.startsWith('SUMMARY:'))
|
const zeileMit = (n) => icsTermin(termin({ titel: 'x'.repeat(n - 'SUMMARY:'.length) })).split('\r\n').find((l) => l.startsWith('SUMMARY:'))
|
||||||
assert.equal(octets(zeileMit(75)), 75)
|
assert.equal(octets(zeileMit(75)), 75)
|
||||||
|
|||||||
Reference in new issue
Block a user