Source code for univention.udm.binary_props
# SPDX-FileCopyrightText: 2018-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only
"""Classes for holding binary UDM object properties."""
from __future__ import annotations
import base64
import bz2
import codecs
from io import BytesIO
from typing import BinaryIO, NamedTuple, cast
import magic
FileType = NamedTuple('FileType', [('mime_type', str), ('encoding', str), ('text', str)])
[docs]
def get_file_type(filename_or_file: str | BinaryIO) -> FileType:
"""
Get mime_type and encoding of file `filename_or_file`.
Handles both magic libraries.
:param filename_or_file: filename or open file
:return: mime_type and encoding of `filename_or_file`
"""
if hasattr(filename_or_file, 'seek'):
f = cast(BinaryIO, filename_or_file)
old_pos = f.tell()
txt = f.read()
f.seek(old_pos)
elif isinstance(filename_or_file, str):
with open(filename_or_file, 'rb') as fp:
txt = fp.read()
else:
raise ValueError(f'Argument "filename_or_file" has unknown type {type(filename_or_file)!r}.')
if hasattr(magic, 'from_file'):
mime = magic.Magic(mime=True, mime_encoding=True).from_buffer(txt)
mime_type, charset = mime.split(';')
encoding = charset.split('=')[-1]
text = magic.Magic().from_buffer(txt)
elif hasattr(magic, 'detect_from_content'):
fm = magic.detect_from_content(txt)
mime_type = fm.mime_type
encoding = fm.encoding
text = fm.name
else:
raise RuntimeError('Unknown version or type of "magic" library.')
# auto detect utf-8 with BOM
if encoding == 'utf-8' and txt.startswith(codecs.BOM_UTF8):
encoding = 'utf-8-sig'
return FileType(mime_type, encoding, text)
[docs]
class BaseBinaryProperty:
"""
Container for a binary UDM property.
Data can be set and retrieved in both its raw form or encoded for LDAP.
Internally data is held in the encoded state (the form in which it will be
saved to LDAP).
"""
def __init__(self, name: str, encoded_value: bytes | None = None, raw_value: bytes | None = None) -> None:
assert not (encoded_value and raw_value), 'Only one of "encoded_value" and "raw_value" must be set.'
assert (encoded_value or raw_value), 'One of "encoded_value" or "raw_value" must be set.'
self._name = name
self._value = b""
if encoded_value:
self.encoded = encoded_value
elif raw_value:
self.raw = raw_value
def __repr__(self) -> str:
return f'{self.__class__.__name__}({self._name})'
@property
def encoded(self) -> bytes:
return self._value
@encoded.setter
def encoded(self, value: bytes) -> None:
self._value = value
@property
def raw(self) -> bytes:
raise NotImplementedError()
@raw.setter
def raw(self, value: bytes) -> None:
raise NotImplementedError()
@property
def content_type(self) -> FileType:
return get_file_type(BytesIO(self.raw))
[docs]
class Base64BinaryProperty(BaseBinaryProperty):
"""
Container for a binary UDM property encoded using base64.
obj.props.<prop>.encoded == base64.b64encode(obj.props.<prop>.decoded)
>>> binprop = Base64BinaryProperty('example', raw_value=b'raw value')
>>> Base64BinaryProperty('example', encoded_value=binprop.encoded).raw == b'raw value'
True
>>> import base64
>>> binprop.encoded == base64.b64encode(binprop.raw)
True
"""
@property
def raw(self) -> bytes:
return base64.b64decode(self._value)
@raw.setter
def raw(self, value: bytes) -> None:
self._value = base64.b64encode(value)
[docs]
class Base64Bzip2BinaryProperty(BaseBinaryProperty):
"""
Container for a binary UDM property encoded using base64 after using bzip2.
obj.props.<prop>.encoded == base64.b64encode(obj.props.<prop>.decoded)
>>> binprop = Base64Bzip2BinaryProperty('example', raw_value=b'raw value')
>>> Base64Bzip2BinaryProperty('example', encoded_value=binprop.encoded).raw == b'raw value'
True
>>> import bz2, base64
>>> binprop.encoded == base64.b64encode(bz2.compress(binprop.raw))
True
"""
@property
def raw(self) -> bytes:
return bz2.decompress(base64.b64decode(self._value))
@raw.setter
def raw(self, value: bytes) -> None:
self._value = base64.b64encode(bz2.compress(value))