From 89b3650c4c5b8d8c79678b446b53c8e7f206b7b5 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 8 May 2024 15:02:39 -0700 Subject: [PATCH] Improve _ChunkedValue's handling of split chunks with unexpected whitespace --- CHANGELOG.md | 2 + octodns/record/chunked.py | 5 +- tests/test_octodns_record_chunked.py | 96 ++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09aeeae..6bdcdda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## v1.?.? - 2024-??-?? - ??? * Fix CAA rdata parsing to allow values with tags +* Improve TXT (and SPF) record handling of unexpected whitespace + before/after/in-between quotes. ## v1.7.0 - 2024-04-29 - All the knobs and dials diff --git a/octodns/record/chunked.py b/octodns/record/chunked.py index 3f088e3..ea49782 100644 --- a/octodns/record/chunked.py +++ b/octodns/record/chunked.py @@ -34,6 +34,7 @@ class _ChunkedValuesMixin(ValuesMixin): class _ChunkedValue(str): _unescaped_semicolon_re = re.compile(r'\w;') + _chunk_sep_re = re.compile(r'"\s+"') @classmethod def parse_rdata_text(cls, value): @@ -62,9 +63,11 @@ class _ChunkedValue(str): def process(cls, values): ret = [] for v in values: + # remove leading/trailing whitespace + v = v.strip() if v and v[0] == '"': v = v[1:-1] - ret.append(cls(v.replace('" "', ''))) + ret.append(cls(cls._chunk_sep_re.sub('', v))) return ret @property diff --git a/tests/test_octodns_record_chunked.py b/tests/test_octodns_record_chunked.py index 30592cc..fb6f991 100644 --- a/tests/test_octodns_record_chunked.py +++ b/tests/test_octodns_record_chunked.py @@ -67,3 +67,99 @@ class TestChunkedValue(TestCase): ['non ASCII character in "Déjà vu"'], _ChunkedValue.validate('Déjà vu', 'TXT'), ) + + def test_large_values(self): + # There is additional testing in TXT + + # "standard" format quoted and split value + value = ( + '"Lorem ipsum dolor sit amet, consectetur ' + 'adipiscing elit, sed do eiusmod tempor incididunt ut ' + 'labore et dolore magna aliqua. Ut enim ad minim veniam, ' + 'quis nostrud exercitation ullamco laboris nisi ut aliquip ' + 'ex" " ea commodo consequat. Duis aute irure dolor in ' + 'reprehenderit in voluptate velit esse cillum dolore eu ' + 'fugiat nulla pariatur. Excepteur sint occaecat cupidatat ' + 'non proident, sunt in culpa qui officia deserunt mollit ' + 'anim id est laborum."' + ) + chunked = _ChunkedValue.process([value]) + self.assertEqual(1, len(chunked)) + chunked = chunked[0] + self.assertIsInstance(chunked, _ChunkedValue) + dechunked_value = ( + 'Lorem ipsum dolor sit amet, consectetur ' + 'adipiscing elit, sed do eiusmod tempor incididunt ut ' + 'labore et dolore magna aliqua. Ut enim ad minim veniam, ' + 'quis nostrud exercitation ullamco laboris nisi ut aliquip ' + 'ex ea commodo consequat. Duis aute irure dolor in ' + 'reprehenderit in voluptate velit esse cillum dolore eu ' + 'fugiat nulla pariatur. Excepteur sint occaecat cupidatat ' + 'non proident, sunt in culpa qui officia deserunt mollit ' + 'anim id est laborum.' + ) + self.assertEqual(dechunked_value, chunked) + + # already dechunked, noop + chunked = _ChunkedValue.process([dechunked_value])[0] + self.assertEqual(dechunked_value, chunked) + + # leading whitespace + chunked = _ChunkedValue.process([f' {value}'])[0] + self.assertEqual(dechunked_value, chunked) + chunked = _ChunkedValue.process([f' {value}'])[0] + self.assertEqual(dechunked_value, chunked) + chunked = _ChunkedValue.process([f'\t{value}'])[0] + self.assertEqual(dechunked_value, chunked) + chunked = _ChunkedValue.process([f'\t\t{value}'])[0] + self.assertEqual(dechunked_value, chunked) + chunked = _ChunkedValue.process([f' \t{value}'])[0] + self.assertEqual(dechunked_value, chunked) + + # trailing whitespace + chunked = _ChunkedValue.process([f'{value} '])[0] + self.assertEqual(dechunked_value, chunked) + chunked = _ChunkedValue.process([f'{value} '])[0] + self.assertEqual(dechunked_value, chunked) + chunked = _ChunkedValue.process([f'{value}\t'])[0] + self.assertEqual(dechunked_value, chunked) + chunked = _ChunkedValue.process([f'{value}\t\t'])[0] + self.assertEqual(dechunked_value, chunked) + chunked = _ChunkedValue.process([f'{value} \t'])[0] + self.assertEqual(dechunked_value, chunked) + + # both + chunked = _ChunkedValue.process([f' {value} '])[0] + self.assertEqual(dechunked_value, chunked) + chunked = _ChunkedValue.process([f'\t{value} '])[0] + self.assertEqual(dechunked_value, chunked) + chunked = _ChunkedValue.process([f' {value}\t'])[0] + self.assertEqual(dechunked_value, chunked) + + # variations of whitepsace in the chunk seperator + multi = value.replace('" "', '" "') + chunked = _ChunkedValue.process([multi])[0] + self.assertEqual(dechunked_value, chunked) + multi = value.replace('" "', '"\t"') + chunked = _ChunkedValue.process([multi])[0] + self.assertEqual(dechunked_value, chunked) + multi = value.replace('" "', '"\t\t"') + chunked = _ChunkedValue.process([multi])[0] + self.assertEqual(dechunked_value, chunked) + multi = value.replace('" "', '" \t"') + chunked = _ChunkedValue.process([multi])[0] + self.assertEqual(dechunked_value, chunked) + + # ~real world test case + values = [ + 'before', + ' "v=DKIM1\\; h=sha256\\; k=rsa\\; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx78E7PtJvr8vpoNgHdIAe+llFKoy8WuTXDd6Z5mm3D4AUva9MBt5fFetxg/kcRy3KMDnMw6kDybwbpS/oPw1ylk6DL1xit7Cr5xeYYSWKukxXURAlHwT2K72oUsFKRUvN1X9lVysAeo+H8H/22Z9fJ0P30sOuRIRqCaiz+OiUYicxy4x" "rpfH2s9a+o3yRwX3zhlp8GjRmmmyK5mf7CkQTCfjnKVsYtB7mabXXmClH9tlcymnBMoN9PeXxaS5JRRysVV8RBCC9/wmfp9y//cck8nvE/MavFpSUHvv+TfTTdVKDlsXPjKX8iZQv0nO3xhspgkqFquKjydiR8nf4meHhwIDAQAB" ', + 'z after', + ] + chunked = _ChunkedValue.process(values) + expected = [ + 'before', + 'v=DKIM1\\; h=sha256\\; k=rsa\\; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx78E7PtJvr8vpoNgHdIAe+llFKoy8WuTXDd6Z5mm3D4AUva9MBt5fFetxg/kcRy3KMDnMw6kDybwbpS/oPw1ylk6DL1xit7Cr5xeYYSWKukxXURAlHwT2K72oUsFKRUvN1X9lVysAeo+H8H/22Z9fJ0P30sOuRIRqCaiz+OiUYicxy4xrpfH2s9a+o3yRwX3zhlp8GjRmmmyK5mf7CkQTCfjnKVsYtB7mabXXmClH9tlcymnBMoN9PeXxaS5JRRysVV8RBCC9/wmfp9y//cck8nvE/MavFpSUHvv+TfTTdVKDlsXPjKX8iZQv0nO3xhspgkqFquKjydiR8nf4meHhwIDAQAB', + 'z after', + ] + self.assertEqual(expected, chunked)