diff --git a/fiftyone_pipeline_did/readme.md b/fiftyone_pipeline_did/readme.md index d0a80d9..c0ea2b9 100644 --- a/fiftyone_pipeline_did/readme.md +++ b/fiftyone_pipeline_did/readme.md @@ -154,7 +154,7 @@ same whichever language parsed the bytes. | `UNEXPECTED_END` | The data stopped in the middle of an envelope field | | `INVALID_DOMAIN_ENCODING` | The creator domain is not terminated or is longer than the OWID maximum | | `BYTE_COUNT_MISMATCH` | The declared payload length disagrees with the bytes present | -| `IMPLEMENTATION_CAPACITY_EXCEEDED` | The envelope is consistent but larger than this runtime can hold | +| `IMPLEMENTATION_CAPACITY_EXCEEDED` | The envelope is consistent but larger than this runtime can hold, or dated past the end of the year 9999 where `datetime` stops. The four byte minute count runs to 15 February 10186, and the read answers with this status rather than raising on such a count | | `ABSENT_NODE` | The version 0 marker, which stands for an absent envelope | | `MALFORMED_ENVELOPE` | Malformed in a way none of the above describes | | `PAYLOAD_TOO_SHORT` | The envelope was read but the payload is shorter than the 5 byte header, so the type cannot be read | diff --git a/fiftyone_pipeline_did/src/fiftyone_pipeline_did/fod_id.py b/fiftyone_pipeline_did/src/fiftyone_pipeline_did/fod_id.py index 57190df..4232d49 100644 --- a/fiftyone_pipeline_did/src/fiftyone_pipeline_did/fod_id.py +++ b/fiftyone_pipeline_did/src/fiftyone_pipeline_did/fod_id.py @@ -79,7 +79,13 @@ class FodIdParseStatus(Enum): INVALID_DOMAIN_ENCODING = "InvalidDomainEncoding" #: The declared payload byte count disagrees with the bytes present. BYTE_COUNT_MISMATCH = "ByteCountMismatch" - #: The envelope is consistent but larger than this runtime can hold. + #: The envelope is consistent but larger than this runtime can hold, or + #: dated past the end of the year 9999 where ``datetime`` stops. The + #: four byte minute count of OWID versions 2 and 3 runs to 15 February + #: 10186, and the OWID reader judges the count before the arithmetic, + #: so a read answers with this status rather than raising. The same + #: bytes read fine where the date type is wider, so the status is not a + #: fault in the data. IMPLEMENTATION_CAPACITY_EXCEEDED = "ImplementationCapacityExceeded" #: The version 0 marker, which stands for an absent envelope and never #: produces a value. diff --git a/fiftyone_pipeline_did/tests/test_fodid.py b/fiftyone_pipeline_did/tests/test_fodid.py index 048c3be..d2c840b 100644 --- a/fiftyone_pipeline_did/tests/test_fodid.py +++ b/fiftyone_pipeline_did/tests/test_fodid.py @@ -605,6 +605,51 @@ def test_wrong_input_type_reports_invalid_input_type(self): self.assert_failed(FodId.try_from_byte_array("AwB="), FodIdParseStatus.INVALID_INPUT_TYPE) + # ----- A date the runtime cannot hold ----- + + def _dated_past_the_year_9999(self): + """A signed envelope whose four byte minute count is 0xFFFFFFFF, + which the wire format allows and which lands on 15 February 10186, + past the end of the year 9999 where ``datetime`` stops. The bytes + are changed after signing, which is fine because the read refuses + the date before any signature is looked at.""" + raw = bytearray(self.factory.signed_bytes(canonical_payload())) + # The four little endian date bytes sit after the version byte, the + # domain and its terminator. + at = 1 + raw.index(0, 1) + 1 + raw[at:at + 4] = bytes([0xFF] * 4) + return bytes(raw) + + def test_date_past_the_year_9999_is_implementation_capacity_exceeded( + self): + # The OWID reader judges the count before the arithmetic, so the + # read answers with a status instead of raising OverflowError, and + # the 51Did surface carries that status through unchanged on both + # the byte and the base64 surface. The same bytes read fine where + # the date type is wider, so the status is the runtime's limit and + # not a fault in the data. + raw = self._dated_past_the_year_9999() + self.assertIs(ParseStatus.IMPLEMENTATION_CAPACITY_EXCEEDED, + Owid.parse_bytes(raw).status) + self.assert_failed( + FodId.try_from_byte_array(raw), + FodIdParseStatus.IMPLEMENTATION_CAPACITY_EXCEEDED) + self.assert_failed( + FodId.try_from_base64(base64.b64encode(raw).decode()), + FodIdParseStatus.IMPLEMENTATION_CAPACITY_EXCEEDED) + + def test_raising_readers_name_the_date_the_runtime_cannot_hold(self): + # The raising readers run the same walk, so the date is the + # documented OwidError with the status in the message, never the + # OverflowError the arithmetic would have raised. + raw = self._dated_past_the_year_9999() + with self.assertRaises(OwidError) as raised: + FodId.from_base64(base64.b64encode(raw).decode()) + self.assertIn("ImplementationCapacityExceeded", str(raised.exception)) + with self.assertRaises(OwidError) as raised: + FodId.from_byte_array(raw) + self.assertIn("ImplementationCapacityExceeded", str(raised.exception)) + # ----- Parsing and verifying are separate ----- def test_tampered_signature_parses_then_verifies_as_invalid(self): diff --git a/owid-python b/owid-python index 0944322..0a77e39 160000 --- a/owid-python +++ b/owid-python @@ -1 +1 @@ -Subproject commit 09443229f98f81bd1cedc624eec74f9486b9042d +Subproject commit 0a77e39e02151ebc477b19b67c6a6c0f4f317a1b