From 6f22cbf98b43fd37c3e5adfe6951fd05f48aee1e Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 1 Jun 2026 17:38:14 +1000 Subject: [PATCH 1/5] Fixes #64. Parsing of raw data would fail if the packet boundary happened to fall between the last byte of raw data and the SOH field separator. Change to ensure the SOH exists, and the field doesn't complete parsing until it has been processed. Added a check that it's actually SOH too. --- simplefix/errors.py | 2 ++ simplefix/parser.py | 13 +++++++++---- test/test_parser.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/simplefix/errors.py b/simplefix/errors.py index 5a42fe5..1d16445 100644 --- a/simplefix/errors.py +++ b/simplefix/errors.py @@ -41,6 +41,8 @@ class TagNotNumberError(ParsingError, ValueError): 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..969a91e 100644 --- a/simplefix/parser.py +++ b/simplefix/parser.py @@ -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..cc8e6b1 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -553,6 +553,36 @@ 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 " From 86cbfb6db64b1861cc973bfda9dae0d8f5419c0c Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 1 Jun 2026 17:48:01 +1000 Subject: [PATCH 2/5] Fix style issues. --- simplefix/errors.py | 2 ++ test/test_parser.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/simplefix/errors.py b/simplefix/errors.py index 1d16445..7bfc54b 100644 --- a/simplefix/errors.py +++ b/simplefix/errors.py @@ -41,9 +41,11 @@ class TagNotNumberError(ParsingError, ValueError): 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/test/test_parser.py b/test/test_parser.py index cc8e6b1..72ed142 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -555,7 +555,6 @@ def test_begin_string_config(self): 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 + \ From 8eb105fbee0e4cb8994e0527a327d138e4d3e49b Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 1 Jun 2026 17:57:06 +1000 Subject: [PATCH 3/5] Fix more style issues from Deep Source. --- test/test_parser.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/test/test_parser.py b/test/test_parser.py index 72ed142..6f5ce84 100644 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -29,27 +29,22 @@ 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"): From 61d2b994f897fc806817a978353e79e6fc11332a Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 1 Jun 2026 18:00:08 +1000 Subject: [PATCH 4/5] Update copyrights. More style issues. --- simplefix/parser.py | 2 +- test/test_parser.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/simplefix/parser.py b/simplefix/parser.py index 969a91e..8bd18c8 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 diff --git a/test/test_parser.py b/test/test_parser.py index 6f5ce84..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,7 +23,6 @@ # ######################################################################## -import sys import unittest from simplefix import FixMessage, FixParser, SOH_STR, errors @@ -43,6 +42,7 @@ def test_not_none(_, other): # skipcq: PYL-R1719 class ParserTests(unittest.TestCase): """Tests for FIX tag-value parser.""" + def setUp(self): """Initialize the test suite.""" if not hasattr(self, "assertIsNotNone"): From dad205fc10dec47ebdf944a7dd5f7cd53a7c648f Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 1 Jun 2026 18:06:09 +1000 Subject: [PATCH 5/5] Ignore high cyclomatic complexity in get_message() for now. --- simplefix/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simplefix/parser.py b/simplefix/parser.py index 8bd18c8..d23aa16 100644 --- a/simplefix/parser.py +++ b/simplefix/parser.py @@ -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