diff --git a/simplefix/errors.py b/simplefix/errors.py index 5a42fe5..7bfc54b 100644 --- a/simplefix/errors.py +++ b/simplefix/errors.py @@ -42,6 +42,10 @@ class RawLengthNotNumberError(ParsingError, ValueError): """Raw length value could not be converted to integer.""" +class RawDataNotFollowedByFieldSeparator(ParsingError): + """Raw data field isn't followed by SOH field separator.""" + + class FieldOrderError(ParsingError): """Field not found where required by standard.""" diff --git a/simplefix/parser.py b/simplefix/parser.py index 8de3849..d23aa16 100644 --- a/simplefix/parser.py +++ b/simplefix/parser.py @@ -1,7 +1,7 @@ #! /usr/bin/env python ######################################################################## # SimpleFIX -# Copyright (C) 2016-2023, David Arnold. +# Copyright (C) 2016-2026, David Arnold. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -262,7 +262,7 @@ def get_buffer(self): """Return a reference to the internal buffer.""" return self.buf - def get_message(self): + def get_message(self): # skipcq: PY-R1000 """Process the accumulated buffer and return the first message. If the buffer starts with FIX fields other than BeginString @@ -299,12 +299,17 @@ def get_message(self): raise errors.TagNotNumberError(*e.args) if tag in self.raw_data_tags and self.raw_len > 0: - # Try to extract the data value. - if self.raw_len > len(self.buf) - point: - # The buffer doesn't yet contain all the raw data, - # so wait for more to be added. + # Try to extract the data value (and its SOH, + # although that's then ignored) + if self.raw_len + 1 > len(self.buf) - point: + # The buffer doesn't yet contain all the raw data + # plus the following SOH, so wait for more to be added. break + # Check for SOH after raw data. + if self.buf[point + self.raw_len] != SOH_BYTE: + raise errors.RawDataNotFollowedByFieldSeparator(tag_string) + # We've got enough buffer to extract the raw data value. value = self.buf[point:point + self.raw_len] self.buf = self.buf[point + self.raw_len + 1:] diff --git a/test/test_parser.py b/test/test_parser.py index 787b3e3..641f990 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -1,7 +1,7 @@ #! /usr/bin/env python ######################################################################## # SimpleFIX -# Copyright (C) 2016-2023, David Arnold. +# Copyright (C) 2016-2026, David Arnold. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -23,33 +23,28 @@ # ######################################################################## -import sys import unittest from simplefix import FixMessage, FixParser, SOH_STR, errors -def make_str(s): - if sys.version_info.major == 2: - return bytes(s) - - return bytes(s, 'ASCII') - - # Python 2.6's unittest.TestCase doesn't have assertIsNone() def test_none(_, other): # skipcq: PYL-R1719 + """Check for None.""" return other is None # Python 2.6's unittest.TestCase doesn't have assertIsNotNone() def test_not_none(_, other): # skipcq: PYL-R1719 + """Check is not None.""" return other is not None class ParserTests(unittest.TestCase): - + """Tests for FIX tag-value parser.""" def setUp(self): + """Initialize the test suite.""" if not hasattr(self, "assertIsNotNone"): ParserTests.assertIsNotNone = test_not_none if not hasattr(self, "assertIsNone"): @@ -553,6 +548,35 @@ def test_begin_string_config(self): else: self.fail("These keywords together should fail validation.") + def test_raw_data_ending_on_packet_boundary(self): + """Check parsing when raw data field ends on a packet boundary.""" + b1 = b"8=FIX.4.2" + SOH_STR + \ + b"9=169" + SOH_STR + \ + b"35=A" + SOH_STR + \ + b"52=20171213-01:41:08.063" + SOH_STR + \ + b"49=HelloWorld" + SOH_STR + \ + b"56=1234" + SOH_STR + \ + b"34=1" + SOH_STR + \ + b"95=6" + SOH_STR + \ + b"96=ABC=DE" + + # Packet boundary between end of data and SOH, as in + # https://github.com/da4089/simplefix/issues/64 + + b2 = SOH_STR + \ + b"98=0" + SOH_STR + \ + b"108=30" + SOH_STR + \ + b"10=166" + SOH_STR + + parser = FixParser() + parser.append_buffer(b1) + msg = parser.get_message() + self.assertIsNone(msg) + + parser.append_buffer(b2) + msg = parser.get_message() + self.assertIsNotNone(msg) + # b"2018-05-06 12:34:56.789 RECV "