1# This file is part of Xpra.
2# Copyright (C) 2008, 2009 Nathaniel Smith <njs@pobox.com>
3# Copyright (C) 2012-2021 Antoine Martin <antoine@xpra.org>
4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
6
7"""
8Functions for converting to and from X11 properties.
9    prop_encode
10    prop_decode
11"""
12
13import struct
14from io import BytesIO
15
16from xpra.os_util import hexstr
17from xpra.x11.bindings.window_bindings import constants     #@UnresolvedImport
18from xpra.log import Logger
19
20log = Logger("x11", "window")
21
22
23USPosition      = constants["USPosition"]
24PPosition       = constants["PPosition"]
25PMaxSize        = constants["PMaxSize"]
26PMinSize        = constants["PMinSize"]
27PBaseSize       = constants["PBaseSize"]
28PResizeInc      = constants["PResizeInc"]
29PAspect         = constants["PAspect"]
30PWinGravity     = constants["PWinGravity"]
31XUrgencyHint    = constants["XUrgencyHint"]
32WindowGroupHint = constants["WindowGroupHint"]
33StateHint       = constants["StateHint"]
34IconicState     = constants["IconicState"]
35InputHint       = constants["InputHint"]
36
37
38def unsupported(*_args):
39    raise Exception("unsupported")
40
41def _force_length(name, data, length, noerror_length=None):
42    if len(data)==length:
43        return data
44    if len(data)!=noerror_length:
45        log.warn("Odd-lengthed property %s: wanted %s bytes, got %s: %r"
46                 % (name, length, len(data), data))
47    # Zero-pad data
48    data += b"\0" * length
49    return data[:length]
50
51
52class NetWMStrut:
53    def __init__(self, data):
54        # This eats both _NET_WM_STRUT and _NET_WM_STRUT_PARTIAL.  If we are
55        # given a _NET_WM_STRUT instead of a _NET_WM_STRUT_PARTIAL, then it
56        # will be only length 4 instead of 12, we just don't define the other values
57        # and let the client deal with it appropriately
58        if len(data)==16:
59            self.left, self.right, self.top, self.bottom = struct.unpack(b"@LLLL", data)
60        else:
61            data = _force_length("_NET_WM_STRUT or _NET_WM_STRUT_PARTIAL", data, 4 * 12)
62            (
63                self.left, self.right, self.top, self.bottom,
64                self.left_start_y, self.left_end_y,
65                self.right_start_y, self.right_end_y,
66                self.top_start_x, self.top_end_x,
67                self.bottom_start_x, self.bottom_stop_x,
68                ) = struct.unpack(b"@" + b"L" * 12, data)
69
70    def todict(self):
71        return self.__dict__
72
73    def __str__(self):
74        return "NetWMStrut(%s)" % self.todict()
75
76
77class MotifWMHints:
78    __slots__ = ("flags", "functions", "decorations", "input_mode", "status")
79    def __init__(self, data):
80        #some applications use the wrong size (ie: blender uses 16) so pad it:
81        sizeof_long = struct.calcsize(b"@L")
82        pdata = _force_length("_MOTIF_WM_HINTS", data, sizeof_long*5, sizeof_long*4)
83        self.flags, self.functions, self.decorations, self.input_mode, self.status = \
84            struct.unpack(b"@LLLlL", pdata)
85        log("MotifWMHints(%s)=%s", hexstr(data), self)
86
87    #found in mwmh.h:
88    # "flags":
89    FUNCTIONS_BIT   = 0
90    DECORATIONS_BIT = 1
91    INPUT_MODE_BIT  = 2
92    STATUS_BIT      = 3
93    # "functions":
94    ALL_BIT         = 0
95    RESIZE_BIT      = 1
96    MOVE_BIT        = 2      # like _NET_WM_ACTION_MOVE
97    MINIMIZE_BIT    = 3      # like _NET_WM_ACTION_MINIMIZE
98    MAXIMIZE_BIT    = 4      # like _NET_WM_ACTION_(FULLSCREEN|MAXIMIZE_(HORZ|VERT))
99    CLOSE_BIT       = 5      # like _NET_WM_ACTION_CLOSE
100    SHADE_BIT       = 6      # like _NET_WM_ACTION_SHADE
101    STICK_BIT       = 7      # like _NET_WM_ACTION_STICK
102    FULLSCREEN_BIT  = 8      # like _NET_WM_ACTION_FULLSCREEN
103    ABOVE_BIT       = 9      # like _NET_WM_ACTION_ABOVE
104    BELOW_BIT       = 10     # like _NET_WM_ACTION_BELOW
105    MAXIMUS_BIT     = 11     # like _NET_WM_ACTION_MAXIMUS_(LEFT|RIGHT|TOP|BOTTOM)
106    # "decorations":
107    ALL_BIT         = 0
108    BORDER_BIT      = 1
109    RESIZEH_BIT     = 2
110    TITLE_BIT       = 3
111    MENU_BIT        = 4
112    MINIMIZE_BIT    = 5
113    MAXIMIZE_BIT    = 6
114    #CLOSE_BIT                # non-standard close button
115    #RESIZE_BIT               # non-standard resize button
116    #SHADE_BIT,               # non-standard shade button
117    #STICK_BIT,               # non-standard stick button
118    #MAXIMUS_BIT              # non-standard maxim
119    # "input":
120    MODELESS        = 0
121    PRIMARY_APPLICATION_MODAL = 1
122    SYSTEM_MODAL    = 2
123    FULL_APPLICATION_MODAL = 3
124    # "status":
125    TEAROFF_WINDOW  = 0
126
127    FLAGS_STR = {
128        FUNCTIONS_BIT      : "functions",
129        DECORATIONS_BIT    : "decorations",
130        INPUT_MODE_BIT     : "input",
131        STATUS_BIT         : "status",
132        }
133    FUNCTIONS_STR = {
134        ALL_BIT        : "all",
135        RESIZE_BIT     : "resize",
136        MOVE_BIT       : "move",
137        MINIMIZE_BIT   : "minimize",
138        MAXIMIZE_BIT   : "maximize",
139        CLOSE_BIT      : "close",
140        SHADE_BIT      : "shade",
141        STICK_BIT      : "stick",
142        FULLSCREEN_BIT : "fullscreen",
143        ABOVE_BIT      : "above",
144        BELOW_BIT      : "below",
145        MAXIMUS_BIT    : "maximus",
146        }
147    DECORATIONS_STR = {
148        ALL_BIT      : "all",
149        BORDER_BIT   : "border",
150        RESIZEH_BIT  : "resizeh",
151        TITLE_BIT    : "title",
152        MENU_BIT     : "menu",
153        MINIMIZE_BIT : "minimize",
154        MAXIMIZE_BIT : "maximize",
155        }
156    INPUT_STR = {
157        MODELESS                   : "modeless",
158        PRIMARY_APPLICATION_MODAL  : "primary-application-modal",
159        SYSTEM_MODAL               : "system-modal",
160        FULL_APPLICATION_MODAL     : "full-application-modal",
161        }
162
163    STATUS_STR = {
164        TEAROFF_WINDOW : "tearoff",
165        }
166
167    def bits_to_strs(self, int_val, flag_bit, dict_str):
168        if flag_bit and not self.flags & (2**flag_bit):
169            #the bit is not set, ignore this attribute
170            return ()
171        return tuple(v for k,v in dict_str.items() if int_val & (2**k))
172    def flags_strs(self):
173        return self.bits_to_strs(self.flags,
174                                 0,
175                                 MotifWMHints.FLAGS_STR)
176    def functions_strs(self):
177        return self.bits_to_strs(self.functions,
178                                 MotifWMHints.FUNCTIONS_BIT,
179                                 MotifWMHints.FUNCTIONS_STR)
180    def decorations_strs(self):
181        return self.bits_to_strs(self.decorations,
182                                 MotifWMHints.DECORATIONS_BIT,
183                                 MotifWMHints.DECORATIONS_STR)
184    def input_strs(self):
185        if self.flags & (2**MotifWMHints.INPUT_MODE_BIT):
186            return MotifWMHints.INPUT_STR.get(self.input_mode, "unknown mode: %i" % self.input_mode)
187        return "modeless"
188    def status_strs(self):
189        return self.bits_to_strs(self.input_mode,
190                                 MotifWMHints.STATUS_BIT,
191                                 MotifWMHints.STATUS_STR)
192
193    def __str__(self):
194        return "MotifWMHints(%s)" % {
195            "flags"         : self.flags_strs(),
196            "functions"     : self.functions_strs(),
197            "decorations"   : self.decorations_strs(),
198            "input_mode"    : self.input_strs(),
199            "status"        : self.status_strs(),
200            }
201
202
203def _read_image(stream):
204    try:
205        int_size = struct.calcsize(b"@I")
206        long_size = struct.calcsize(b"@L")
207        header = stream.read(long_size*2)
208        if not header:
209            return None
210        width, height = struct.unpack(b"@LL", header)
211        data = stream.read(width * height * long_size)
212        expected = width * height * long_size
213        if len(data) < expected:
214            log.warn("Warning: corrupt _NET_WM_ICON, execpted %i bytes but got %i",
215                     expected, len(data))
216            return None
217        if int_size!=long_size:
218            #long to ints (CARD32):
219            longs = struct.unpack(b"@"+b"l"*(width*height), data)
220            data = struct.pack(b"@"+b"i"*(width*height), *longs)
221    except Exception:
222        log.warn("Weird corruption in _NET_WM_ICON", exc_info=True)
223        return None
224    return width, height, "BGRA", data
225
226# This returns a list of icons from a _NET_WM_ICON property.
227# each icon is a tuple:
228# (width, height, fmt, data)
229def NetWMIcons(data):
230    icons = []
231    stream = BytesIO(data)
232    while True:
233        icon = _read_image(stream)
234        if icon is None:
235            break
236        icons.append(icon)
237    if not icons:
238        return None
239    return icons
240
241
242def _to_latin1(v):
243    return v.encode("latin1")
244
245def _from_latin1(v):
246    return v.decode("latin1")
247
248def _to_utf8(v):
249    return v.encode("UTF-8")
250
251def _from_utf8(v):
252    return v.decode("UTF-8")
253
254
255def _from_long(v):
256    return struct.unpack(b"@L", v)[0]
257
258def _to_long(v):
259    return struct.pack(b"@L", v)
260
261
262PROP_TYPES = {
263    # Python type, X type Atom, formatbits, serializer, deserializer, list
264    # terminator
265    "utf8": (str, "UTF8_STRING", 8, _to_utf8, _from_utf8, b"\0"),
266    # In theory, there should be something clever about COMPOUND_TEXT here.  I
267    # am not sufficiently clever to deal with COMPOUNT_TEXT.  Even knowing
268    # that Xutf8TextPropertyToTextList exists.
269    "latin1": (str, "STRING", 8, _to_latin1, _from_latin1, b"\0"),
270    "state": (int, "WM_STATE", 32, _to_long, _from_long, b""),
271    "u32": (int, "CARDINAL", 32, _to_long, _from_long, b""),
272    "integer": (int, "INTEGER", 32, _to_long, _from_long, b""),
273    "strut": (NetWMStrut, "CARDINAL", 32,
274              unsupported, NetWMStrut, None),
275    "strut-partial": (NetWMStrut, "CARDINAL", 32,
276                      unsupported, NetWMStrut, None),
277    "motif-hints": (MotifWMHints, "_MOTIF_WM_HINTS", 32,
278              unsupported, MotifWMHints, None),
279    "icons": (list, "CARDINAL", 32,
280              unsupported, NetWMIcons, None),
281    }
282
283PROP_SIZES = {
284    "icons" : 4*1024*1024,
285    }
286
287
288def prop_encode(etype, value):
289    if isinstance(etype, (list, tuple)):
290        return _prop_encode_list(etype[0], value)
291    return _prop_encode_scalar(etype, value)
292
293def _prop_encode_scalar(etype, value):
294    pytype, atom, formatbits, serialize = PROP_TYPES[etype][:4]
295    assert isinstance(value, pytype), "value for atom %s is not a %s: %s" % (atom, pytype, type(value))
296    return (atom, formatbits, serialize(value))
297
298def _prop_encode_list(etype, value):
299    (_, atom, formatbits, _, _, terminator) = PROP_TYPES[etype]
300    value = tuple(value)
301    serialized = tuple(_prop_encode_scalar(etype, v)[2] for v in value)
302    # Strings in X really are null-separated, not null-terminated (ICCCM
303    # 2.7.1, see also note in 4.1.2.5)
304    return (atom, formatbits, terminator.join(x for x in serialized if x is not None))
305
306
307def prop_decode(etype, data):
308    if isinstance(etype, (list, tuple)):
309        return _prop_decode_list(etype[0], data)
310    return _prop_decode_scalar(etype, data)
311
312def _prop_decode_scalar(etype, data):
313    (pytype, _, _, _, deserialize, _) = PROP_TYPES[etype]
314    value = deserialize(data)
315    assert value is None or isinstance(value, pytype), "expected a %s but value is a %s" % (pytype, type(value))
316    return value
317
318def _prop_decode_list(etype, data):
319    (_, _, formatbits, _, _, terminator) = PROP_TYPES[etype]
320    if terminator:
321        datums = data.split(terminator)
322    else:
323        datums = []
324        if formatbits==32:
325            nbytes = struct.calcsize("@L")
326        else:
327            nbytes = formatbits // 8
328        while data:
329            datums.append(data[:nbytes])
330            data = data[nbytes:]
331    props = (_prop_decode_scalar(etype, datum) for datum in datums)
332    return [x for x in props if x is not None]
333