]> Cypherpunks repositories - keks.git/commitdiff
Revised and corrected work with TAI64
authorSergey Matveev <stargrave@stargrave.org>
Wed, 20 Nov 2024 12:39:34 +0000 (15:39 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Wed, 20 Nov 2024 12:55:39 +0000 (15:55 +0300)
pyac/pyac.py
pyac/test_leapsecs.py [new file with mode: 0644]

index 49414519e90c6abb5d3023ce89d545f70c601e45dd17c9b9215f0e187c4b08d3..a3a2db26bb39c895794d9439f040288054fda6faccf1036910c04aff236fd43f 100755 (executable)
@@ -89,36 +89,38 @@ def LenFirstSort(v):
 TAI64Base = 0x4000000000000000
 Leapsecs1972 = 10
 Leapsecs = tuple(
-    int(datetime(y, m, 1, 0, 0, 0, 0, tzinfo=timezone.utc).timestamp())
-    for y, m in (
-        (2017, 1),
-        (2015, 7),
-        (2012, 7),
-        (2009, 1),
-        (2006, 1),
-        (1999, 1),
-        (1997, 7),
-        (1996, 1),
-        (1994, 7),
-        (1993, 7),
-        (1992, 7),
-        (1991, 1),
-        (1990, 1),
-        (1988, 1),
-        (1985, 7),
-        (1983, 7),
-        (1982, 7),
-        (1981, 7),
-        (1980, 1),
-        (1979, 1),
-        (1978, 1),
-        (1977, 1),
-        (1976, 1),
-        (1975, 1),
-        (1974, 1),
-        (1973, 1),
-        (1972, 7),
+    Leapsecs1972 + i + int(
+        datetime(y, m, 1, 0, 0, 0, 0, tzinfo=timezone.utc).timestamp()
     )
+    for i, (y, m) in enumerate((
+        (1972, 7),
+        (1973, 1),
+        (1974, 1),
+        (1975, 1),
+        (1976, 1),
+        (1977, 1),
+        (1978, 1),
+        (1979, 1),
+        (1980, 1),
+        (1981, 7),
+        (1982, 7),
+        (1983, 7),
+        (1985, 7),
+        (1988, 1),
+        (1990, 1),
+        (1991, 1),
+        (1992, 7),
+        (1993, 7),
+        (1994, 7),
+        (1996, 1),
+        (1997, 7),
+        (1999, 1),
+        (2006, 1),
+        (2009, 1),
+        (2012, 7),
+        (2015, 7),
+        (2017, 1),
+    ))
 )
 
 
@@ -162,12 +164,7 @@ def dumps(v):
         raise NotImplementedError("no FLOAT* support")
     if isinstance(v, datetime):
         secs = int(v.replace(tzinfo=timezone.utc).timestamp())
-        diff = Leapsecs1972
-        for n, leapsec in enumerate(Leapsecs):
-            if secs > leapsec:
-                diff += len(Leapsecs) - n
-                break
-        secs += TAI64Base + diff
+        secs = utc2tai(secs)
         if v.microsecond == 0:
             return _byte(TagTAI64) + secs.to_bytes(8, "big")
         return (
@@ -233,7 +230,46 @@ _floats = {TagFloat16: 2, TagFloat32: 4, TagFloat64: 8, TagFloat128: 16, TagFloa
 _tais = {TagTAI64: 8, TagTAI64N: 12, TagTAI64NA: 16}
 
 
-def loads(v, sets=False):
+def tai2utc(secs, leapsecUTCAllow=False):
+    """Subtract leapseconds from TAI
+
+    :returns: UTC seconds, or None of TAI equals to leap second
+    """
+    secs -= TAI64Base
+    diff = 0
+    for leapsec in Leapsecs:
+        if secs < leapsec:
+            break
+        diff += 1
+        if secs == leapsec:
+            if leapsecUTCAllow:
+                break
+            return None
+    return secs - (diff + Leapsecs1972)
+
+
+def utc2tai(secs):
+    """Add leapseconds to UTC
+    """
+    for diff, leapsec in enumerate(Leapsecs):
+        if secs < leapsec:
+            break
+    secs += Leapsecs1972 + diff
+    if secs in Leapsecs:
+        secs += 1
+    return secs + TAI64Base
+
+
+def loads(v, sets=False, leapsecUTCAllow=False):
+    """Decode YAC-encoded data.
+
+    :param bool sets: transform maps with NIL-only values to set()s
+    :param bool leapsecUTCAllow: allow TAI64 values equal to leap seconds,
+                                 to be decoded as datetime UTC value. Raw()
+                                 value will be returned instead
+    :returns: decoded data and the undecoded tail
+    :rtype: (any, bytes)
+    """
     if len(v) == 0:
         raise NotEnoughData(1)
     if v[0] == TagEOC:
@@ -260,13 +296,6 @@ def loads(v, sets=False):
         secs = int.from_bytes(v[1:1+8], "big")
         if secs >= (1 << 63):
             raise DecodeError("reserved TAI64 value is in use")
-        secs -= TAI64Base
-        diff = 0
-        for n, leapsec in enumerate(Leapsecs):
-            if secs > (leapsec + len(Leapsecs) - n):
-                diff = 10 + len(Leapsecs) - n
-                break
-        secs -= diff
         nsecs = 0
         if l > 8:
             nsecs = int.from_bytes(v[1+8:1+8+4], "big")
@@ -277,6 +306,9 @@ def loads(v, sets=False):
             asecs = int.from_bytes(v[1+8+4:1+8+4+4], "big")
             if asecs > 999999999:
                 raise DecodeError("too many attoseconds")
+        secs = tai2utc(secs, leapsecUTCAllow)
+        if secs is None:
+            return Raw(v[0], v[1:1+l]), v[1+l:]
         if (abs(secs) > (1 << 60)) or (asecs > 0) or ((nsecs % 1000) > 0):
             # Python can represent neither big values, nor nanoseconds
             return Raw(v[0], v[1:1+l]), v[1+l:]
diff --git a/pyac/test_leapsecs.py b/pyac/test_leapsecs.py
new file mode 100644 (file)
index 0000000..eff9435
--- /dev/null
@@ -0,0 +1,45 @@
+from unittest import TestCase
+
+from pyac import Leapsecs
+from pyac import TAI64Base
+
+
+class TestLeapsecs(TestCase):
+    """Check that our pyac.Leapsecs is equally calculated to DJB's
+        ``curl http://cr.yp.to/libtai/leapsecs.dat | xxd -c 8 -p``
+    """
+
+    def runTest(self):
+        for our, their in zip(Leapsecs, (
+            "4000000004b2580a",
+            "4000000005a4ec0b",
+            "4000000007861f8c",
+            "400000000967530d",
+            "400000000b48868e",
+            "400000000d2b0b8f",
+            "400000000f0c3f10",
+            "4000000010ed7291",
+            "4000000012cea612",
+            "40000000159fca93",
+            "400000001780fe14",
+            "4000000019623195",
+            "400000001d25ea16",
+            "4000000021dae517",
+            "40000000259e9d98",
+            "40000000277fd119",
+            "400000002a50f59a",
+            "400000002c32291b",
+            "400000002e135c9c",
+            "4000000030e7241d",
+            "4000000033b8489e",
+            "40000000368c101f",
+            "4000000043b71ba0",
+            "40000000495c07a1",
+            "400000004fef9322",
+            "4000000055932da3",
+            "40000000586846a4",
+        )):
+            self.assertEqual(
+                our,
+                int.from_bytes(bytes.fromhex(their), "big") - TAI64Base,
+            )