diff --git a/CHANGELOG.md b/CHANGELOG.md index 53bb57e..82a298d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ * MetaProcessor.include_extra to add support for arbitrary extra values to be set on the meta record. * Correctly handled quoted svcparams when parsing SVCB/HTTPS rdata text +* Fix handling of chunked values (TXT, SPF) when escaped characters land at the + split boundaries, don't split escapes from their following chars ## v1.9.1 - 2024-06-21 - What's in a name diff --git a/octodns/record/chunked.py b/octodns/record/chunked.py index 3f088e3..a07acfd 100644 --- a/octodns/record/chunked.py +++ b/octodns/record/chunked.py @@ -13,10 +13,20 @@ class _ChunkedValuesMixin(ValuesMixin): def chunked_value(self, value): value = value.replace('"', '\\"') - vs = [ - value[i : i + self.CHUNK_SIZE] - for i in range(0, len(value), self.CHUNK_SIZE) - ] + vs = [] + i = 0 + n = len(value) + # until we've processed the whole string + while i < n: + # start with a full chunk size + c = min(self.CHUNK_SIZE, n - i) + # make sure that we don't break on escape chars + while value[i + c - 1] == '\\': + c -= 1 + # we have our chunk now + vs.append(value[i : i + c]) + # and can step over if + i += c vs = '" "'.join(vs) return self._value_type(f'"{vs}"') diff --git a/tests/test_octodns_record_chunked.py b/tests/test_octodns_record_chunked.py index 30592cc..4d7bbc0 100644 --- a/tests/test_octodns_record_chunked.py +++ b/tests/test_octodns_record_chunked.py @@ -4,8 +4,9 @@ from unittest import TestCase -from octodns.record.chunked import _ChunkedValue +from octodns.record.chunked import _ChunkedValue, _ChunkedValuesMixin from octodns.record.spf import SpfRecord +from octodns.record.txt import TxtValue from octodns.zone import Zone @@ -67,3 +68,50 @@ class TestChunkedValue(TestCase): ['non ASCII character in "Déjà vu"'], _ChunkedValue.validate('Déjà vu', 'TXT'), ) + + zone = Zone('unit.tests.', []) + + # some hacks to let us work with smaller sizes + class Base: + def __init__(self, *args, **kwargs): + pass + + class SmallerChunkedMixin(_ChunkedValuesMixin, Base): + CHUNK_SIZE = 8 + _type = 'TXT' + _value_type = TxtValue + + def __init__(self, values): + super().__init__(None, None, {'values': values}) + + def test_splitting(self): + + for value, expected in ( + # shorter + ('0123', '"0123"'), + # exact + ('01234567', '"01234567"'), + # simple + ('0123456789', '"01234567" "89"'), + # 1 extra + ('012345678', '"01234567" "8"'), + # escape in the middle + ('01234\\;56789', '"01234\\;5" "6789"'), + # escape before the boundary + ('012345\\;6789', '"012345\\;" "6789"'), + # escape after the boundary + ('01234567\\;89', '"01234567" "\\;89"'), + # escape spanning the boundary + ('0123456\\;789', '"0123456" "\\;789"'), + # multiple escapes at the boundary + ('012345\\\\;6789', '"012345" "\\\\;6789"'), + # exact size escape + ('012345\\;', '"012345\\;"'), + # spanning ending + ('0123456\\;', '"0123456" "\\;"'), + ): + sc = self.SmallerChunkedMixin(value) + self.assertEqual([expected], sc.chunked_values) + + sc = self.SmallerChunkedMixin(['0123456789']) + self.assertEqual(['"01234567" "89"'], sc.chunked_values)