Repository: RDFLib/rdflib

Issue:
Literals with xsd:base64Binary datatype serialize incorrectly
Looking at `rdflib.term` source this appears to be because the lexical is taken as the decoded value, because no base64 encoder is defined. This is in contrast to `xsd:hexBinary` where the lexical is the encoded value.

Example:

```
>>> byts = b'foo'
>>> enc = hexlify(byts)
>>> lit = Literal(enc, datatype=XSD.hexBinary)
>>> print(lit.toPython(), lit)
b'foo', 666f6f   # expected
```
vs
```
>>> enc = b64encode(byts)
>>> lit = Literal(enc, datatype=XSD.base64Binary)
>>> print(lit.toPython(), lit)
b'foo', foo  # unexpected
```

This means for instance that Graph containing this literal in the o position actually serializes like:
```
@prefix ns1: <http://example.com/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ns1:foo ns1:bar "foo"^^xsd:base64Binary,
```

when I expect:
```
@prefix ns1: <http://example.com/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

ns1:foo ns1:bar "Zm9v"^^xsd:base64Binary,
```

I can work around by using `rdflib.term.bind` to handle this datatype in a custom fashion, but I believe the fix is easy enough

Discussion:

Patch:
diff --git a/rdflib/term.py b/rdflib/term.py
index 3a822158..0f627e69 100644
--- a/rdflib/term.py
+++ b/rdflib/term.py
@@ -38,7 +38,6 @@ import logging
 import warnings
 import math
 
-import base64
 import xml.dom.minidom
 
 from datetime import date, time, datetime, timedelta
@@ -53,6 +52,7 @@ from isodate import (
     parse_duration,
     duration_isoformat,
 )
+from base64 import b64decode, b64encode
 from binascii import hexlify, unhexlify
 
 import rdflib
@@ -1435,6 +1435,7 @@ _XSD_DAYTIMEDURATION = URIRef(_XSD_PFX + "dayTimeDuration")
 _XSD_YEARMONTHDURATION = URIRef(_XSD_PFX + "yearMonthDuration")
 
 _OWL_RATIONAL = URIRef("http://www.w3.org/2002/07/owl#rational")
+_XSD_B64BINARY = URIRef(_XSD_PFX + "base64Binary")
 _XSD_HEXBINARY = URIRef(_XSD_PFX + "hexBinary")
 # TODO: gYearMonth, gYear, gMonthDay, gDay, gMonth
 
@@ -1559,6 +1560,8 @@ _GenericPythonToXSDRules = [
 _SpecificPythonToXSDRules = [
     ((str, _XSD_HEXBINARY), hexlify),
     ((bytes, _XSD_HEXBINARY), hexlify),
+    ((str, _XSD_B64BINARY), b64encode),
+    ((bytes, _XSD_B64BINARY), b64encode),
 ]
 
 XSDToPython = {
@@ -1593,7 +1596,7 @@ XSDToPython = {
     URIRef(_XSD_PFX + "unsignedByte"): int,
     URIRef(_XSD_PFX + "float"): float,
     URIRef(_XSD_PFX + "double"): float,
-    URIRef(_XSD_PFX + "base64Binary"): lambda s: base64.b64decode(s),
+    URIRef(_XSD_PFX + "base64Binary"): b64decode,
     URIRef(_XSD_PFX + "anyURI"): None,
     _RDF_XMLLITERAL: _parseXML,
     _RDF_HTMLLITERAL: _parseHTML,


Test patch:
diff --git a/test/test_b64_binary.py b/test/test_b64_binary.py
new file mode 100644
index 00000000..b512f29d
--- /dev/null
+++ b/test/test_b64_binary.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+
+import unittest
+import base64 
+from rdflib import Literal, XSD
+
+
+class B64BinaryTestCase(unittest.TestCase):
+
+    def test_unicode(self):
+        str1 = "Test utf-8 string éàë"
+        # u b64string
+        b64_str1 = base64.b64encode(str1.encode("utf-8")).decode()
+        l1 = Literal(b64_str1, datatype=XSD.base64Binary)
+        b_str1 = l1.toPython()
+        self.assertEqual(b_str1.decode("utf-8"), str1)
+        self.assertEqual(str(l1), b64_str1)
+
+        # b b64string
+        b64_str1b = base64.b64encode(str1.encode("utf-8"))
+        l1b = Literal(b64_str1b, datatype=XSD.base64Binary)
+        b_str1b = l1b.toPython()
+        self.assertEqual(b_str1, b_str1b)
+        self.assertEqual(b_str1b.decode("utf-8"), str1)
+        self.assertEqual(str(l1b), b64_str1)
+
+
+if __name__ == "__main__":
+    unittest.main()