Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/net/compression.py : 61%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
2# This file is part of Xpra.
3# Copyright (C) 2011-2020 Antoine Martin <antoine@xpra.org>
4# Copyright (C) 2008, 2009, 2010 Nathaniel Smith <njs@pobox.com>
5# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
6# later version. See the file COPYING for details.
8from collections import namedtuple
10from xpra.util import envbool
11from xpra.net.header import LZ4_FLAG, ZLIB_FLAG, LZO_FLAG, BROTLI_FLAG
14MAX_SIZE = 256*1024*1024
16#all the compressors we know about, in best compatibility order:
17ALL_COMPRESSORS = ("zlib", "lz4", "lzo", "brotli", "none")
18#order for performance:
19PERFORMANCE_ORDER = ("none", "lz4", "lzo", "zlib", "brotli")
22Compression = namedtuple("Compression", ["name", "version", "python_version", "compress", "decompress"])
24COMPRESSION = {}
27def init_lz4():
28 from lz4 import VERSION
29 from lz4.version import version
30 from lz4.block import compress, decompress
31 import struct
32 LZ4_HEADER = struct.Struct(b'<L')
33 def lz4_compress(packet, level):
34 flag = min(15, level) | LZ4_FLAG
35 if level>=7:
36 return flag, compress(packet, mode="high_compression", compression=level)
37 if level<=3:
38 return flag, compress(packet, mode="fast", acceleration=8-level*2)
39 return flag, compress(packet)
40 def lz4_decompress(data):
41 size = LZ4_HEADER.unpack_from(data[:4])[0]
42 #it would be better to use the max_size we have in protocol,
43 #but this hardcoded value will do for now
44 if size>MAX_SIZE:
45 sizemb = size//1024//1024
46 maxmb = MAX_SIZE//1024//1024
47 raise Exception("uncompressed data is too large: %iMB, limit is %iMB" % (sizemb, maxmb))
48 return decompress(data)
49 return Compression("lz4", version, VERSION.encode("latin1"), lz4_compress, lz4_decompress)
51def init_lzo():
52 import lzo #@UnresolvedImport
53 def lzo_compress(packet, level):
54 if isinstance(packet, memoryview):
55 packet = packet.tobytes()
56 return level | LZO_FLAG, lzo.compress(packet)
57 return Compression("lzo", lzo.LZO_VERSION_STRING, lzo.__version__, lzo_compress, lzo.decompress)
59def init_brotli():
60 from brotli import compress, decompress, __version__
61 def brotli_compress(packet, level):
62 if len(packet)>1024*1024:
63 level = min(9, level)
64 else:
65 level = min(11, level)
66 if not isinstance(packet, bytes):
67 packet = bytes(str(packet), 'UTF-8')
68 return level | BROTLI_FLAG, compress(packet, quality=level)
69 return Compression("brotli", None, __version__, brotli_compress, decompress)
71def init_zlib():
72 from zlib import compress, decompress, __version__
73 def zlib_compress(packet, level):
74 level = max(1, level//2)
75 if isinstance(packet, memoryview):
76 packet = packet.tobytes()
77 elif not isinstance(packet, bytes):
78 packet = bytes(str(packet), 'UTF-8')
79 return level + ZLIB_FLAG, compress(packet, level)
80 def zlib_decompress(data):
81 if isinstance(data, memoryview):
82 data = data.tobytes()
83 return decompress(data)
84 return Compression("zlib", None, __version__, zlib_compress, zlib_decompress)
86def init_none():
87 def nocompress(packet, _level):
88 if not isinstance(packet, bytes):
89 packet = bytes(str(packet), 'UTF-8')
90 return 0, packet
91 def decompress(v):
92 return v
93 return Compression("none", None, None, nocompress, decompress)
96def init_all():
97 for x in list(ALL_COMPRESSORS)+["none"]:
98 if not envbool("XPRA_%s" % (x.upper()), True):
99 continue
100 fn = globals().get("init_%s" % x)
101 try:
102 c = fn()
103 assert c
104 COMPRESSION[x] = c
105 except (ImportError, AttributeError):
106 from xpra.log import Logger
107 logger = Logger("network", "protocol")
108 logger.debug("no %s", x, exc_info=True)
109init_all()
112def use(compressor) -> bool:
113 return compressor in COMPRESSION
116def get_compression_caps() -> dict:
117 caps = {}
118 for x in ALL_COMPRESSORS:
119 c = COMPRESSION.get(x)
120 if c is None:
121 continue
122 ccaps = caps.setdefault(x, {})
123 if c.version:
124 ccaps["version"] = c.version
125 if c.python_version:
126 pcaps = ccaps.setdefault("python-%s" % x, {})
127 pcaps[""] = True
128 if c.python_version is not None:
129 pcaps["version"] = c.python_version
130 #legacy format - only used for zlib:
131 if x=="zlib":
132 ccaps[""] = True
133 return caps
135def get_enabled_compressors(order=ALL_COMPRESSORS):
136 return tuple(x for x in order if x in COMPRESSION)
138def get_compressor(name):
139 c = COMPRESSION.get(name)
140 assert c is not None, "'%s' compression is not supported" % name
141 return c.compress
144def sanity_checks():
145 if not use("lzo") and not use("lz4"):
146 from xpra.log import Logger
147 logger = Logger("network", "protocol")
148 if not use("zlib"):
149 logger.warn("Warning: all the compressors are unavailable or disabled,")
150 logger.warn(" performance may suffer in some cases")
151 else:
152 logger.warn("Warning: zlib is the only compressor enabled")
153 logger.warn(" install and enable lz4 support for better performance")
156class Compressed:
157 def __init__(self, datatype, data, can_inline=False):
158 assert data is not None, "compressed data cannot be set to None"
159 self.datatype = datatype
160 self.data = data
161 self.can_inline = can_inline
162 def __len__(self):
163 return len(self.data)
164 def __repr__(self):
165 return "Compressed(%s: %i bytes)" % (self.datatype, len(self.data))
168class LevelCompressed(Compressed):
169 def __init__(self, datatype, data, level, algo, can_inline):
170 super().__init__(datatype, data, can_inline)
171 self.level = level
172 self.algorithm = algo
173 def __repr__(self):
174 return "LevelCompressed(%s: %i bytes as %s/%i)" % (self.datatype, len(self.data), self.algorithm, self.level)
177class LargeStructure:
178 def __init__(self, datatype, data):
179 self.datatype = datatype
180 self.data = data
181 def __len__(self):
182 return len(self.data)
183 def __repr__(self):
184 return "LargeStructure(%s: %i bytes)" % (self.datatype, len(self.data))
186class Compressible(LargeStructure):
187 #wrapper for data that should be compressed at some point,
188 #to use this class, you must override compress()
189 def __repr__(self):
190 return "Compressible(%s: %i bytes)" % (self.datatype, len(self.data))
191 def compress(self):
192 raise Exception("compress() not defined on %s" % self)
195def compressed_wrapper(datatype, data, level=5, zlib=False, lz4=False, lzo=False, brotli=False, none=False, can_inline=True):
196 size = len(data)
197 if size>MAX_SIZE:
198 sizemb = size//1024//1024
199 maxmb = MAX_SIZE//1024//1024
200 raise Exception("uncompressed data is too large: %iMB, limit is %iMB" % (sizemb, maxmb))
201 if lz4 and use("lz4"):
202 algo = "lz4"
203 elif lzo and use("lzo"):
204 algo = "lzo"
205 elif brotli and use("brotli"):
206 algo = "brotli"
207 elif zlib and use("zlib"):
208 algo = "zlib"
209 elif none and use("none"):
210 algo = "none"
211 else:
212 raise InvalidCompressionException("no compressors available")
213 c = COMPRESSION[algo]
214 cl, cdata = c.compress(data, level)
215 return LevelCompressed(datatype, cdata, cl, algo, can_inline=can_inline)
218class InvalidCompressionException(Exception):
219 pass
222def get_compression_type(level) -> str:
223 if level & LZ4_FLAG:
224 return "lz4"
225 if level & LZO_FLAG:
226 return "lzo"
227 if level & BROTLI_FLAG:
228 return "brotli"
229 return "zlib"
232def decompress(data, level):
233 #log.info("decompress(%s bytes, %s) type=%s", len(data), get_compression_type(level))
234 if level & LZ4_FLAG:
235 algo = "lz4"
236 elif level & LZO_FLAG:
237 algo = "lzo"
238 elif level & BROTLI_FLAG:
239 algo = "brotli"
240 else:
241 algo = "zlib"
242 return decompress_by_name(data, algo)
244def decompress_by_name(data, algo):
245 c = COMPRESSION.get(algo)
246 if c is None:
247 raise InvalidCompressionException("%s is not available" % algo)
248 return c.decompress(data)
251def main(): # pragma: no cover
252 from xpra.util import print_nested_dict
253 from xpra.platform import program_context
254 with program_context("Compression", "Compression Info"):
255 print_nested_dict(get_compression_caps())
258if __name__ == "__main__": # pragma: no cover
259 main()