From c71c416b37991df18df0321598b6b54d86738202 Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Mon, 2 Sep 2013 16:53:42 +0200 Subject: [PATCH 01/33] Move the cursor to a datafile *this adds: support for cursor datafiles *a script to generate the cursor data file (automagically done by setup.py *rgb support for the cursor *the default cursor is removed from the source --- lib/__init__.py | 5 +++ lib/xcb.py | 68 ++++++++++++++++++++++++++++--- make_default_lock.py | 51 ++++++++++++++++++++++++ pyxtrlock | 95 ++++++++++++++++++++------------------------ setup.py | 19 +++++++++ 5 files changed, 181 insertions(+), 57 deletions(-) create mode 100644 make_default_lock.py diff --git a/lib/__init__.py b/lib/__init__.py index e69de29..6fbab0c 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -0,0 +1,5 @@ + +import sys +import os + +data_dir = os.path.join(sys.prefix, "share/pyxtrlock") diff --git a/lib/xcb.py b/lib/xcb.py index 37cf126..e650d58 100644 --- a/lib/xcb.py +++ b/lib/xcb.py @@ -81,6 +81,7 @@ class Cookie(Structure): VoidCookie = Cookie AllocNamedColorCookie = Cookie +AllocColorCookie = Cookie GrabKeyboardCookie = Cookie GrabPointerCookie = Cookie @@ -109,6 +110,19 @@ class AllocNamedColorReply(Structure): ("visual_blue", c_uint16) ] +class AllocColorReply(Structure): + _fields_ = [ + ("response_type", c_uint8), + ("pad0", c_uint8), + ("sequence", c_uint16), + ("length", c_uint32), + ("red", c_uint16), + ("green", c_uint16), + ("blue", c_uint16), + ("pad1", c_uint8 * 2), + ("pixel", c_uint32), + ] + class GenericError(Structure): _fields_ = [ @@ -250,6 +264,16 @@ class KeyPressEvent(Structure): ] alloc_named_color.restype = AllocNamedColorCookie +alloc_color = libxcb.xcb_alloc_color +alloc_color.argtypes = [ + POINTER(Connection), # connection + Colormap, # cmap + c_uint16, # r + c_uint16, # g + c_uint16 # b +] +alloc_color.restype = AllocColorCookie + alloc_named_color_reply = libxcb.xcb_alloc_named_color_reply alloc_named_color_reply.argtypes = [ POINTER(Connection), # connection @@ -258,6 +282,14 @@ class KeyPressEvent(Structure): ] alloc_named_color_reply.restype = POINTER(AllocNamedColorReply) +alloc_color_reply = libxcb.xcb_alloc_color_reply +alloc_color_reply.argtypes = [ + POINTER(Connection), # connection + AllocColorCookie, # cookie + POINTER(POINTER(GenericError)) # e +] +alloc_color_reply.restype = POINTER(AllocColorReply) + def alloc_named_color_sync(conn, colormap, color_string): """Synchronously allocate a named color @@ -272,11 +304,37 @@ def alloc_named_color_sync(conn, colormap, color_string): cookie = alloc_named_color(conn, colormap, len(color_string), color_string) error_p = POINTER(GenericError)() - res = alloc_named_color_reply(conn, cookie, byref(error_p)) + res = alloc_named_color_reply(conn, cookie, byref(error_p)).contents if error_p: raise XCBError(error_p.contents) - return res + return (res.visual_red, res.visual_green, res.visual_blue) + +def alloc_color_sync(conn, colormap, r, g, b): + """Synchronously allocate a color + + Wrapper function for xcb_alloc_color and alloc_color_reply. + + The (r, g, b) triple is in the range 0 to 255 (as opposed to + the X protocol using the 0 to 2^16-1 range). + + Raises ``XCBError`` on xcb errors and value errors for invalid + values of r, g, b. + """ + if r < 0 or b < 0 or g < 0: + raise ValueError + if r > 255 or b > 255 or g > 255: + raise ValueError + + r <<= 8; g <<= 8; b <<= 8 + + cookie = alloc_color(conn, colormap, r, g, b) + error_p = POINTER(GenericError)() + res = alloc_color_reply(conn, cookie, byref(error_p)).contents + if error_p: + raise XCBERror(error_p.contents) + + return (res.red, res.blue, res.green) request_check = libxcb.xcb_request_check request_check.argtypes = [POINTER(Connection), VoidCookie] @@ -308,10 +366,8 @@ def create_cursor_sync(conn, source, mask, fg, bg, x, y): """ cursor = generate_id(conn) cookie = create_cursor_checked(conn, cursor, source, mask, - fg.visual_red, fg.visual_green, - fg.visual_blue, bg.visual_red, - bg.visual_green, bg.visual_blue, - x, y) + fg[0], fg[1], fg[2], bg[0], + bg[1], bg[2], x, y) error = request_check(conn, cookie) if error: raise XCBError(error.contents) diff --git a/make_default_lock.py b/make_default_lock.py new file mode 100644 index 0000000..549fdb6 --- /dev/null +++ b/make_default_lock.py @@ -0,0 +1,51 @@ +#!/usr/bin/python3 + +import pickle +import sys + +fg_bitmap = bytes([ + 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0xf8, 0xff, 0x7f, 0x00, 0xe0, 0xff, + 0x3f, 0x00, 0xc0, 0xff, 0x1f, 0x00, 0x80, 0xff, 0x0f, 0xfc, 0x03, 0xff, + 0x0f, 0xfe, 0x07, 0xff, 0x0f, 0xff, 0x0f, 0xff, 0x07, 0xff, 0x0f, 0xfe, + 0x87, 0xff, 0x1f, 0xfe, 0x87, 0xff, 0x1f, 0xfe, 0x87, 0xff, 0x1f, 0xfe, + 0x87, 0xff, 0x1f, 0xfe, 0x87, 0xff, 0x1f, 0xfe, 0x87, 0xff, 0x1f, 0xfe, + 0x87, 0xff, 0x1f, 0xfe, 0x87, 0xff, 0x1f, 0xfe, 0x87, 0xff, 0x1f, 0xfe, + 0x87, 0xff, 0x1f, 0xfe, 0x01, 0x00, 0x00, 0xf8, 0x01, 0x00, 0x00, 0xf8, + 0x01, 0x00, 0x00, 0xf8, 0x01, 0x00, 0x00, 0xf8, 0x01, 0xf0, 0x00, 0xf8, + 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xf8, + 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xf0, 0x00, 0xf8, 0x01, 0x60, 0x00, 0xf8, + 0x01, 0x60, 0x00, 0xf8, 0x01, 0x60, 0x00, 0xf8, 0x01, 0x60, 0x00, 0xf8, + 0x01, 0x60, 0x00, 0xf8, 0x01, 0x60, 0x00, 0xf8, 0x01, 0x00, 0x00, 0xf8, + 0x01, 0x00, 0x00, 0xf8, 0x01, 0x00, 0x00, 0xf8, 0x01, 0x00, 0x00, 0xf8, + 0xff, 0xff, 0xff, 0xff +]) + +bg_bitmap = bytes([ + 0x00, 0xfe, 0x07, 0x00, 0x80, 0xff, 0x1f, 0x00, 0xc0, 0xff, 0x3f, 0x00, + 0xe0, 0xff, 0x7f, 0x00, 0xf0, 0xff, 0xff, 0x00, 0xf8, 0xff, 0xff, 0x01, + 0xf8, 0x03, 0xfc, 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xfc, 0x01, 0xf8, 0x03, + 0xfc, 0x00, 0xf0, 0x03, 0xfc, 0x00, 0xf0, 0x03, 0xfc, 0x00, 0xf0, 0x03, + 0xfc, 0x00, 0xf0, 0x03, 0xfc, 0x00, 0xf0, 0x03, 0xfc, 0x00, 0xf0, 0x03, + 0xfc, 0x00, 0xf0, 0x03, 0xfc, 0x00, 0xf0, 0x03, 0xfc, 0x00, 0xf0, 0x03, + 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, + 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, + 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, + 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, + 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, + 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, + 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, + 0xff, 0xff, 0xff, 0x0f +]) + +with open("lock.pickle", "wb") as f: + pickle.dump({ + "width": 28, + "height": 40, + "x_hot": 14, + "y_hot": 21, + "fg_bitmap": fg_bitmap, + "bg_bitmap": bg_bitmap, + "color_mode": "named", + "bg_color": "steelblue3", + "fg_color": "grey25" + }, f) diff --git a/pyxtrlock b/pyxtrlock index fe0574e..f6c68b2 100755 --- a/pyxtrlock +++ b/pyxtrlock @@ -1,59 +1,20 @@ #!/usr/bin/env python3 # emacs this is -*-python-*- +import os import sys import time +import pickle import getpass from ctypes import byref, cast, sizeof from ctypes import POINTER, c_int, c_uint32, c_char import simplepam as pam +import pyxtrlock import pyxtrlock.xcb as xcb import pyxtrlock.X as X -lock_width = 28 -lock_height = 40 -lock_x_hot = 14 -lock_y_hot = 21 -lock_bits = bytes([ - 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0xf8, 0xff, 0x7f, 0x00, 0xe0, 0xff, - 0x3f, 0x00, 0xc0, 0xff, 0x1f, 0x00, 0x80, 0xff, 0x0f, 0xfc, 0x03, 0xff, - 0x0f, 0xfe, 0x07, 0xff, 0x0f, 0xff, 0x0f, 0xff, 0x07, 0xff, 0x0f, 0xfe, - 0x87, 0xff, 0x1f, 0xfe, 0x87, 0xff, 0x1f, 0xfe, 0x87, 0xff, 0x1f, 0xfe, - 0x87, 0xff, 0x1f, 0xfe, 0x87, 0xff, 0x1f, 0xfe, 0x87, 0xff, 0x1f, 0xfe, - 0x87, 0xff, 0x1f, 0xfe, 0x87, 0xff, 0x1f, 0xfe, 0x87, 0xff, 0x1f, 0xfe, - 0x87, 0xff, 0x1f, 0xfe, 0x01, 0x00, 0x00, 0xf8, 0x01, 0x00, 0x00, 0xf8, - 0x01, 0x00, 0x00, 0xf8, 0x01, 0x00, 0x00, 0xf8, 0x01, 0xf0, 0x00, 0xf8, - 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xf8, - 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xf0, 0x00, 0xf8, 0x01, 0x60, 0x00, 0xf8, - 0x01, 0x60, 0x00, 0xf8, 0x01, 0x60, 0x00, 0xf8, 0x01, 0x60, 0x00, 0xf8, - 0x01, 0x60, 0x00, 0xf8, 0x01, 0x60, 0x00, 0xf8, 0x01, 0x00, 0x00, 0xf8, - 0x01, 0x00, 0x00, 0xf8, 0x01, 0x00, 0x00, 0xf8, 0x01, 0x00, 0x00, 0xf8, - 0xff, 0xff, 0xff, 0xff -]) - -mask_width = 28 -mask_height = 40 -mask_x_hot = 14 -mask_y_hot = 21 -mask_bits = bytes([ - 0x00, 0xfe, 0x07, 0x00, 0x80, 0xff, 0x1f, 0x00, 0xc0, 0xff, 0x3f, 0x00, - 0xe0, 0xff, 0x7f, 0x00, 0xf0, 0xff, 0xff, 0x00, 0xf8, 0xff, 0xff, 0x01, - 0xf8, 0x03, 0xfc, 0x01, 0xf8, 0x01, 0xf8, 0x01, 0xfc, 0x01, 0xf8, 0x03, - 0xfc, 0x00, 0xf0, 0x03, 0xfc, 0x00, 0xf0, 0x03, 0xfc, 0x00, 0xf0, 0x03, - 0xfc, 0x00, 0xf0, 0x03, 0xfc, 0x00, 0xf0, 0x03, 0xfc, 0x00, 0xf0, 0x03, - 0xfc, 0x00, 0xf0, 0x03, 0xfc, 0x00, 0xf0, 0x03, 0xfc, 0x00, 0xf0, 0x03, - 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, - 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, - 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, - 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, - 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, - 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, - 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0x0f, - 0xff, 0xff, 0xff, 0x0f -]) - if getpass.getuser() == 'root' and sys.argv[1:] != ['-f']: msg = ( "pyxtrlock: refusing to run as root. Use -f to force. Warning: You " @@ -62,6 +23,23 @@ if getpass.getuser() == 'root' and sys.argv[1:] != ['-f']: print(msg, file=sys.stderr) sys.exit(1) +# load cursor data file +try: + f_name = os.path.expanduser("~/.config/pyxtrlock/lock.pickle") + if os.path.exists(f_name): + f = open(f_name, "rb") + else: + f = open(os.path.join(pyxtrlock.data_dir, "lock.pickle"), "rb") + cursor = pickle.load(f) +except OSError as e: + print(e.strerror, file=sys.stderr) + sys.exit(1) +except pickle.UnpicklingError as e: + print(e.args, file=sys.stderr) + sys.exit(1) +finally: + f.close() + display = X.create_window(None) conn = X.get_xcb_connection(display) @@ -92,21 +70,36 @@ ret = xcb.create_window(conn, xcb.COPY_FROM_PARENT, window, screen.root, cast(byref(attribs), POINTER(c_uint32))) # create cursor -csr_map = xcb.image_create_pixmap_from_bitmap_data(conn, window, lock_bits, - lock_width, lock_height, +csr_map = xcb.image_create_pixmap_from_bitmap_data(conn, window, + cursor["fg_bitmap"], + cursor["width"], + cursor["height"], 1, 0, 0, None) -csr_mask = xcb.image_create_pixmap_from_bitmap_data(conn, window, mask_bits, - mask_width, mask_height, +csr_mask = xcb.image_create_pixmap_from_bitmap_data(conn, window, + cursor["bg_bitmap"], + cursor["width"], + cursor["height"], 1, 0, 0, None) -csr_bg = xcb.alloc_named_color_sync(conn, screen.default_colormap, - "steelblue3").contents -csr_fg = xcb.alloc_named_color_sync(conn, screen.default_colormap, - "grey25").contents +if cursor["color_mode"] == "named": + csr_bg = xcb.alloc_named_color_sync(conn, screen.default_colormap, + cursor["bg_color"]) + csr_fg = xcb.alloc_named_color_sync(conn, screen.default_colormap, + cursor["fg_color"]) +elif cursor["color_mode"] == "rgb": + r, g, b = cursor["bg_color"] + csr_bg = xcb.alloc_color_sync(conn, screen.default_colormap, + r, g, b) + r, g, b = cursor["fg_color"] + csr_fg = xcb.alloc_color_sync(conn, screen.default_colormap, + r, g, b) +else: + print("Invalid color mode", file=sys.stderr) + sys.exit(1) try: cursor = xcb.create_cursor_sync(conn, csr_map, csr_mask, csr_fg, csr_bg, - lock_x_hot, lock_y_hot) + cursor["x_hot"], cursor["y_hot"]) except xcb.XCBError as e: print("pyxtrlock: Could not create cursor", file=sys.stderr) sys.exit(1) diff --git a/setup.py b/setup.py index 92dfe79..de9fca9 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,21 @@ from distutils.core import setup +from distutils.command.install import install + +import os +import stat +import subprocess + +class my_install(install): + def run(self): + stat_make_lock = os.stat("make_default_lock.py") + try: + stat_lock = os.stat("lock.pickle") + except OSError: + stat_lock = None + if stat_lock is None \ + or stat_lock[stat.ST_MTIME] < stat_make_lock[stat.ST_MTIME]: + subprocess.call(["python3", "./make_default_lock.py"]) + super().run() authors = ( 'Leon Weber , ' @@ -38,8 +55,10 @@ author_email='leon@leonweber.de', requires=['simplepam'], package_dir={'pyxtrlock': 'lib'}, + data_files=[('share/pyxtrlock/', ['lock.pickle'])], packages=['pyxtrlock'], scripts=['pyxtrlock'], + cmdclass={'install': my_install}, license='GPLv3+', url='https://zombofant.net/hacking/pyxtrlock', description=desc, From 4e27f08a1389db5db3922090667bf2845c8d665c Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Mon, 2 Sep 2013 16:56:22 +0200 Subject: [PATCH 02/33] fix .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 192d347..075d014 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.py[cod] +*.pickle # C extensions *.so @@ -31,3 +32,6 @@ nosetests.xml .mr.developer.cfg .project .pydevproject + +# Unix editor backup files +*~ From caebadcb7dd9998f1f86cf56ddd5bb7163123984 Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Mon, 2 Sep 2013 16:56:57 +0200 Subject: [PATCH 03/33] tools for creating cursor data files --- tools/make_lock.py | 514 +++++++++++++++++++++++++++++++++++++++++++++ tools/repickle.py | 14 ++ 2 files changed, 528 insertions(+) create mode 100644 tools/make_lock.py create mode 100644 tools/repickle.py diff --git a/tools/make_lock.py b/tools/make_lock.py new file mode 100644 index 0000000..2c96a5d --- /dev/null +++ b/tools/make_lock.py @@ -0,0 +1,514 @@ +#!/usr/bin/python2 + +from __future__ import division, print_function + +import sys +import argparse +import pickle +import re +from abc import ABCMeta, abstractmethod + +import Image + +ap = argparse.ArgumentParser() +ap.add_argument('bg_bitmap') +ap.add_argument('fg_bitmap', nargs='?') + +ap.add_argument('--width', '-W', type=int, default=None) +ap.add_argument('--height', '-H', type=int, default=None) + +ap.add_argument('--x-hit', '-x', type=int, default=None) +ap.add_argument('--y-hit', '-y', type=int, default=None) + +ap.add_argument('--fg-color', '-f', default=None) +ap.add_argument('--bg-color', '-b', default=None) + +ap.add_argument('--output', '-o', type=argparse.FileType('wb'), + default=sys.stdout) +ap.add_argument('--debug', action='store_true', default=False) + +class Bitmap(object): + def __init__(self, width, height, buf=None): + self.width = width + self.height = height + self.pitch = ((width + 7) // 8) + + if buf is not None: + if len(buf) != self.height * self.pitch: + raise ValueError + self.buffer = buf + else: + self.wipe() + + def __str__(self): + lines = [] + for i in range(self.height): + lines.append(''.join( + 'o' if bit else '.' + for byte in self.buffer[i*self.pitch:(i+1)*self.pitch] + for bit in ((byte >> j) & 0x1 for j in range(8)) + )[:self.width]) + return '\n'.join(lines) + + def wipe(self): + self.buffer = bytearray(b'\0' * (self.pitch * self.height)) + + def __getitem__(self, pos): + i, j = pos + if i >= self.width or j >= self.height: + raise IndexError + h_byte = j * self.pitch + w_byte, bit = divmod(i, 8) + return (self.buffer[h_byte + w_byte] >> bit) & 0x1 + + def __setitem__(self, pos, value): + i, j = pos + if i >= self.width or j >= self.height: + raise IndexError + h_byte = j * self.pitch + w_byte, bit = divmod(i, 8) + if value: + self.buffer[h_byte + w_byte] |= 0x1 << bit + else: + self.buffer[h_byte + w_byte] &= ~(0x1 << bit) + + def __hash__(self): + raise TypeError + + def __eq__(self, other): + return isinstance(other, Bitmap) and self.width == other.width \ + and self.height == other.height and self.buffer == other.buffer + + def __invert__(self): + return bytearray(~i for i in self.buffer) + + def _copy(self): + return self.__class__(self.width, self.height, self.buffer) + + def __iand__(self, other): + if not isinstance(other, Bitmap): + raise TypeError + + for i, v in enumerate(other.buffer): + self.buffer[i] &= v + + return self + + def __ior__(self, other): + if not isinstance(other, Bitmap): + raise TypeError + + for i, v in enumerate(other.buffer): + self.buffer[i] |= v + + return self + + def __ixor__(self, other): + if not isinstance(other, Bitmap): + raise TypeError + + for i, v in enumerate(other.buffer): + self.buffer[i] ^= v + + return self + + def __and__(self, other): + cpy = self._copy() + cpy &= other + return cpy + + def __or__(self, other): + cpy = self._copy() + cpy |= other + return cpy + + def __xor__(self, other): + cpy = self._copy() + cpy |= other + return cpy + +class ColorHandlerMeta(ABCMeta): + + def __new__(cls, name, bases, dict): + res = super(ColorHandlerMeta, cls).__new__(cls, name, bases, dict) + res._register_recurse(res, set()) + return res + + def _register_recurse(cls, sub_class, marked): + marked.add(cls) + for base in cls.__bases__: + if isinstance(base, ColorHandlerMeta) and base not in marked: + base._register_subclass(sub_class, marked) + + def _register_subclass(cls, sub_class, marked): + if hasattr(cls, 'MODES'): + for mode in sub_class.MODE: + cls.MODES[mode] = sub_class + else: + cls._register_recurse(sub_class, marked) + +class ColorHandler(object): + __metaclass__ = ColorHandlerMeta + MODES = {} + + def __new__(cls, PIL_image, **kwargs): + return super(ColorHandler, cls).__new__(cls.MODES[PIL_image.mode], PIL_image) + + def __init__(self, PIL_image, thresh=127): + self._image = PIL_image + self._threshold = thresh + + # RATIONALE: why factories of filters instead of a filter method? + # Because the filter may be run on all the pixels of the image + # therfore being potentially a bottleneck, a short lambda can be + # faster than the entire method + @abstractmethod + def make_transparency_filter(self): + pass + + +class RGBColorHandler(ColorHandler): + MODE = ['RGB'] + + def make_transparency_filter(self): + if 'transparency' in self._image.info: + transparent_color = self._image.info['transparency'] + return lambda x: x == transparent_color + else: + return lambda x: False + + +class RGBAColorHandler(ColorHandler): + MODE = ['RGBA', 'RGBa'] + + def make_transparency_filter(self): + threshold = self._threshold + return lambda x: x[3] < threshold + + +class LColorHandler(ColorHandler): + MODE = ['L'] + + def make_transparency_filter(self): + if 'transparency' in self._image.info: + transparent_color = self._image.info['transparency'] + return lambda x: x == transparent_color + else: + return lambda x: False + +class PColorHander(ColorHandler): + MODE = ['P'] + + def make_transparency_filter(self): + if 'transparency' in self._image.info: + transparent_colors = self._image.info['transparency'] + threshold = self._threshold + return lambda x: transparent_colors[x] < threshold + else: + return lambda x: False + +class OneColorHandler(ColorHandler): + MODE = ['1'] + + def make_transparency_filter(self): + return lambda x: False + + +class LockMaker(object): + RGB_TRIPLE_RE = \ + r'\s*rgb\s*\(\s*([0-9\.]+)\s*,\s*\([0-9\.])\s*,\s*\([0-9\.])\s*\)\s*' + + def __init__(self, args): + self.args = args + self.color_mode = None + self.width = None + self.height = None + + self.stroke_border = False + self._fg_filter = None + self._bg_filter = None + + self._bg_bitmap_raw = Image.open(args.bg_bitmap, "r") + self._fg_bitmap_raw = None + self.uni_image = False + if args.fg_bitmap is not None: + self._fg_bitmap_raw = Image.open(args.fg_bitmap, "r") + else: + self.uni_image = True + + + self._guess_size() + self._guess_hotspot() + self._guess_colors() + + self._fg_bitmap = Bitmap(self.width, self.height) + self._bg_bitmap = Bitmap(self.width, self.height) + + if self.uni_image: + self._stroke(self._bg_bitmap_raw, self._bg_bitmap, self._bg_filter) + + if self.stroke_border: + self._stroke_border() + else: + self._stroke(self._bg_bitmap_raw, self._fg_bitmap, + self._fg_filter) + else: + self._stroke(self._fg_bitmap_raw, self._fg_bitmap, self._fg_filter) + self._stroke(self._bg_bitmap_raw, self._bg_bitmap, self._bg_filter) + self._bg_bitmap_raw |= self._fg_bitmap_raw + + if self.args.debug: + print(str(self._bg_bitmap)) + print(str(self._fg_bitmap)) + + assert self._bg_bitmap & self._fg_bitmap == self._fg_bitmap + assert self._bg_bitmap | self._fg_bitmap == self._bg_bitmap + + def _guess_size(self): + bg_width, bg_height = self._bg_bitmap_raw.size + fg_width, fg_height = self._bg_bitmap_raw.size + + if self.args.width is not None: + self.width = self.args.width + else: + self.width = bg_width + if self.args.height is not None: + self.height = self.args.height + else: + self.height = bg_height + + if not self.height == bg_height == fg_height and \ + not self.width == bg_width == fg_width: + print("The sizes of the images do not match", file=sys.stderr) + sys.exit(1) + + def _guess_hotspot(self): + # TODO: add support for hotspot from xbm (this should be + # provided in the PIL image info) + if args.x_hit is not None: + self.x_hot = args.x_hot + else: + self.x_hot = self.width // 2 + 1 + + if args.y_hit is not None: + self.y_hot = args.y_hot + else: + self.y_hot = self.height // 2 + 1 + + def _guess_colors(self): + image_has_colors = False + bg_hist = self._histogram(self._bg_bitmap_raw) + if not self.uni_image: + fg_hist = self._histogram(self._fg_bitmap_raw) + + if self.uni_image: + mode = self._bg_bitmap_raw.mode + info = self._bg_bitmap_raw.info + + bg_color_handler = ColorHandler(self._bg_bitmap_raw) + tr_filter = bg_color_handler.make_transparency_filter() + effective_colors = {} + for color, num in bg_hist.items(): + if not tr_filter(color): + effective_colors[color] = num + + n_effective_colors = len(effective_colors) + + if mode in ('RGB', 'RGBA', 'RGBa', 'P'): + if n_effective_colors == 1: + self.stroke_border = True + self._bg_filter = lambda x: not tr_filter(x) + elif n_effective_colors == 2: + image_has_colors = True + f, b = effective_colors + if mode == 'RGB': + image_fg, image_bg = f, b + elif mode == 'RGBA' or mode == 'RGBa': + image_fg, image_bg = f[:3], b[:3] + elif mode == 'P': + plte = self._bg_bitmap_raw.palette + image_fg, image_bg = plte[f], plte[b] + else: + raise Exception("Can't happen") + self._bg_filter = lambda x: x == f or x == b + self._fg_filter = lambda x: x == f + else: + print("Too many colors in image", file=sys.stderr) + sys.exit(1) + + elif mode == 'L': + if n_effective_colors == 1: + self.stroke_border = True + self._bg_filter = lambda x: not tr_filter(x) + elif n_effective_colors == 2: + image_has_colors = True + f, b = effective_colors + image_fg = (f, f, f) + image_bg = (b, b, b) + self.fg_filter = lambda x: f + self._bg_filter = lambda x: not tr_filter(x) + + elif mode == '1': + self._bg_filter = lambda x: bool(x) + self.stroke_border = True + else: + print("Unsopported image mode", file=sys.stderr) + sys.exit(1) + else: + if mode in ('RGB', 'RGBA', 'RGBa', 'P', 'L'): + print("Unsupported image mode for dual-image", file=sys.stderr) + sys.exit(1) + elif mode == '1': + self.fg_filter = lambda x: bool(x) + self._bg_filter = lambda x: bool(x) + else: + print("Unsopported image mode", file=sys.stderr) + sys.exit(1) + + if self.args.fg_color is not None: + if self.args.bg_color is None: + print("Inconsistent color specification", file=sys.stderr) + sys.exit(1) + self.fg_color = self._parse_color(self.args.fg_color) + self.bg_color = self._parse_color(self.args.bg_color) + elif image_has_colors: + self._check_color_mode('rgb') + self.fg_color = image_fg + self.bg_color = image_bg + else: + if self.uni_image: + self.stroke_border = True + self.fg_color = 'white' + self.bg_color = 'black' + self.color_mode = 'named' + + def _check_color_mode(self, color_mode): + if self.color_mode is None: + self.color_mode = color_mode + elif self.color_mode != color_mode: + print("Color mode mismatch", file=sys.stderr) + sys.exit(1) + + def _histogram(self, PIL_img): + hist = {} + data = PIL_img.load() + width, height = PIL_img.size + for i in range(width): + for j in range(height): + pixel = data[i, j] + if pixel not in hist: + hist[pixel] = 0 + hist[pixel] += 1 + return hist + + def _stroke(self, PIL_img, bitmap, filter, wipe=True): + if wipe: + bitmap.wipe() + data = PIL_img.load() + for i in range(self.width): + for j in range(self.height): + if filter(data[i, j]): + bitmap[i,j] = 1 + + def _stroke_border(self): + def action(i, j, di, dj, in_img): + if self._bg_bitmap[i,j]: + if not in_img: + self._fg_bitmap[i,j] = 1 + return True + else: + if in_img: + self._fg_bitmap[i-di, j-dj] = 1 + return False + + def finish(i, j, in_img): + if in_img: + self._fg_bitmap[i-di, j-dj] = 1 + + + # stroke vertically + for i in self.width: + in_img = False + for j in self.height: + in_img = action(i, j, 1, 0, in_img) + finish(i-1, j, in_img) + + # stroke horizontally + for j in self.height: + in_img = False + for i in self.width: + in_img = action(i, j, 0, 1, in_img) + finish(i, j-1, in_img) + + def _parse_color(self, color_string): + """Parse a string representing a color the formats + * rgb(255, 127, 0) + * rgb(1.0, 0.5, 0.0) + * #f70 + * #ff7f00 + * named color + """ + args = self.args + if args.fg_color.startswith('#'): + self._check_color_mode(rgb) + + if len(color_string) == 4: + return tuple(17*int(color_string[i], base=16) + for i in range(1,4)) + elif len(args.fg_color) == 7: + return tuple(int(color_string[i:i+1], base=16) + for i in range(1,6,2)) + else: + print("Invalid color format", file=sys.stderr) + sys.exit(1) + else: + match = re.match(RGB_TRIPLE_RE, args.fg_color) + if match is not None: + self._check_color_mode('rgb') + + try: + r, g, b = map(int, (match.group(i) for i in range(0, 3))) + except ValueError: + try: + r, g, b = map(lambda x: int(float(x)*255), + (match.group(i) for i in range(0, 3))) + except ValueError: + print("Invalid color format", file=sys.stderr) + sys.exit(1) + + # note: no check for negative values is required as + # the regex does not allow - + if r > 255 or g > 255 or b > 255: + print("Invalid color format", file=sys.stderr) + sys.exit(1) + return (r, g, b) + + self._check_color_mode('named') + return color_string + + @property + def fg_bitmap(self): + return bytes(self._fg_bitmap.buffer) + + @property + def bg_bitmap(self): + return bytes(self._bg_bitmap.buffer) + + +args = ap.parse_args() + +lock_maker = LockMaker(args) + + +with args.output as f: + pickle.dump({ + "width": lock_maker.width, + "height": lock_maker.height, + "x_hot": lock_maker.x_hot, + "y_hot": lock_maker.y_hot, + "fg_bitmap": lock_maker.fg_bitmap, + "bg_bitmap": lock_maker.bg_bitmap, + "color_mode": lock_maker.color_mode, + "bg_color": lock_maker.bg_color, + "fg_color": lock_maker.fg_color + }, f, protocol=2) diff --git a/tools/repickle.py b/tools/repickle.py new file mode 100644 index 0000000..ce564a4 --- /dev/null +++ b/tools/repickle.py @@ -0,0 +1,14 @@ +#!/usr/bin/python3 + +import pickle +import sys + +data = None +for arg in sys.argv[1:]: + with open(arg, "rb") as f: + data = pickle.load(f, encoding='latin1') + if data is not None: + data["fg_bitmap"] = bytes(data["fg_bitmap"], encoding='latin1') + data["bg_bitmap"] = bytes(data["bg_bitmap"], encoding='latin1') + with open(arg, "wb") as f: + pickle.dump(data, f) From 8e4682f8b3f3dff4a84befbac542c2f7cdbcc9ec Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Mon, 2 Sep 2013 17:32:09 +0200 Subject: [PATCH 04/33] Add documentation for the new features --- README.md | 18 ++++++++ tools/README | 103 +++++++++++++++++++++++++++++++++++++++++++++ tools/make_lock.py | 34 +++++++++------ tools/repickle.py | 2 + 4 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 tools/README mode change 100644 => 100755 tools/make_lock.py mode change 100644 => 100755 tools/repickle.py diff --git a/README.md b/README.md index d14e9de..df8dd9c 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,24 @@ terminals locked. Please report any new bugs you may find to our [Github issue tracker](https://github.com/leonnnn/pyxtrlock/issues). +Configuration +------------- + +The padlock icon can be changed. It is stored as a +[pickle](http://docs.python.org/3/library/pickle.html) of a +dictionary, and the ``tools`` directory contains a tool for generating +cursors from image files. + +The default cursor file is placed at +``PREFIX/share/pyxtrlock/lock.pickle`` while the cursor file at +``~/.config/pyxtrlock/lock.pickle`` takes precedence if present. + +*PLEASE NOTE:* The ``pickle`` file format is not designed to be +resistant against maliciously crafted files. Therfore do not open +``pickle`` files from untrusted sources as they may compromise your +system. The default padlock file is created on install (by +``make_default_lock.py``). + Requirements ------------ * [python3-simplepam](https://github.com/leonnnn/python3-simplepam) diff --git a/tools/README b/tools/README new file mode 100644 index 0000000..11b13a2 --- /dev/null +++ b/tools/README @@ -0,0 +1,103 @@ +make_lock.py +============ + +PLEASE NOTE: make_lock.py requires python2 as the PIL is not packaged +for python3 on most distris. + +Therefore another tool – repickle.py – must be used to postprocess the +generated files. + +usage: make_lock.py [-h] [--x-hit X_HIT] [--y-hit Y_HIT] [--fg-color FG_COLOR] + [--bg-color BG_COLOR] [--output OUTPUT] [--debug] + bg_bitmap [fg_bitmap] + +positional arguments: + bg_bitmap The single image or the 1-bit mask + fg_bitmap If given, the 1-bit foreground pixels + +optional arguments: + -h, --help show this help message and exit + --x-hit X_HIT, -x X_HIT + x-coordinate of the cursor hotspot + --y-hit Y_HIT, -y Y_HIT + x-coordinate of the cursor hotspot + --fg-color FG_COLOR, -f FG_COLOR + The foreground colour (necessary only if the + colourscannot be guessed from the image file). + Accepted formats:colour name, rgb(255, 50, 0), + rgb(1.0, 0.2, 0.0), #ff7700, #f70 + --bg-color BG_COLOR, -b BG_COLOR + The background colour. + --output OUTPUT, -o OUTPUT + The output file, by default stdout + --debug Check for consistency and printthe bitmaps to the + stdout + + +This tools allows you to easily make cursor files for pyxtrlock from +various image file types (basically: anything with 1-3 discrete colors, +various forms of transparency, that can be opened by python imaging). + +The recommended file type is PNG. + +There are several modes of operation which are guessed from the +supplied file: + +*a singe colour image with 2 colours and transparency is compiled to + the appropriate cursor (transparency may either be an alpha threshold + or single colour transparency) +*a single image with 1 colour will have its border stroked the colours + should be given on the commandline +*two (one bit!) bitmaps may be given, on is the mask and the other the + foreground of the cursor. The colours should be given on the commandline. +*colours may be given on the commandline or the default colours black + and white apply, colours given on the commandline override colours from + the file, but note that the assignment will be random in that case + +Additionally the cursor hotspot can be given otherwhise it is the +center of the image (this is more or less irrelevant, as the all +cursor events are blocked by pyxtrlock). + +Typical usage +------------- + +To create a cursor from a PNG with two colors (foreground and +background) and transparenct pixels and then install it for your user +do the following: + + $ ./make_lock.py lock.png -o lock.pickle + $ ./repickle lock.pickle + $ mkdir ~/.config/pyxtrlock/ + $ cp lock.pickle ~/.config/pyxtrlock + +Requirements +------------ +*Python 2.7 +*python-imaging (PIL) + +Authors +------- +Sebastian Riese + +Liense +------ + +Copyright 2013 Sebastian Riese + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/tools/make_lock.py b/tools/make_lock.py old mode 100644 new mode 100755 index 2c96a5d..2b0e860 --- a/tools/make_lock.py +++ b/tools/make_lock.py @@ -11,21 +11,29 @@ import Image ap = argparse.ArgumentParser() -ap.add_argument('bg_bitmap') -ap.add_argument('fg_bitmap', nargs='?') - -ap.add_argument('--width', '-W', type=int, default=None) -ap.add_argument('--height', '-H', type=int, default=None) - -ap.add_argument('--x-hit', '-x', type=int, default=None) -ap.add_argument('--y-hit', '-y', type=int, default=None) - -ap.add_argument('--fg-color', '-f', default=None) -ap.add_argument('--bg-color', '-b', default=None) +ap.add_argument('bg_bitmap', help="The single image or the 1-bit mask") +ap.add_argument('fg_bitmap', nargs='?', + help="If given, the 1-bit foreground pixels") + +ap.add_argument('--x-hit', '-x', type=int, default=None, + help="x-coordinate of the cursor hotspot") +ap.add_argument('--y-hit', '-y', type=int, default=None, + help="x-coordinate of the cursor hotspot") + +ap.add_argument('--fg-color', '-f', default=None, + help="The foreground colour (necessary only if the colours" + "cannot be guessed from the image file). Accepted formats:" + "colour name, rgb(255, 50, 0), rgb(1.0, 0.2, 0.0), " + "#ff7700, #f70") +ap.add_argument('--bg-color', '-b', default=None, + help="The background colour.") ap.add_argument('--output', '-o', type=argparse.FileType('wb'), - default=sys.stdout) -ap.add_argument('--debug', action='store_true', default=False) + default=sys.stdout, + help="The output file, by default stdout") +ap.add_argument('--debug', action='store_true', default=False, + help="Check for consistency and print" + "the bitmaps to the stdout") class Bitmap(object): def __init__(self, width, height, buf=None): diff --git a/tools/repickle.py b/tools/repickle.py old mode 100644 new mode 100755 index ce564a4..f154a28 --- a/tools/repickle.py +++ b/tools/repickle.py @@ -6,6 +6,8 @@ data = None for arg in sys.argv[1:]: with open(arg, "rb") as f: + # any single byte, 8-bit encoding will work here + # as long as it is consistent data = pickle.load(f, encoding='latin1') if data is not None: data["fg_bitmap"] = bytes(data["fg_bitmap"], encoding='latin1') From bef8658211daccecdd1150eb23f9383c1db67663 Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Mon, 2 Sep 2013 17:36:37 +0200 Subject: [PATCH 05/33] bugfix --- tools/make_lock.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tools/make_lock.py b/tools/make_lock.py index 2b0e860..2925eec 100755 --- a/tools/make_lock.py +++ b/tools/make_lock.py @@ -276,20 +276,13 @@ def _guess_size(self): bg_width, bg_height = self._bg_bitmap_raw.size fg_width, fg_height = self._bg_bitmap_raw.size - if self.args.width is not None: - self.width = self.args.width - else: - self.width = bg_width - if self.args.height is not None: - self.height = self.args.height - else: - self.height = bg_height - - if not self.height == bg_height == fg_height and \ - not self.width == bg_width == fg_width: + if not bg_height == fg_height and not bg_width == fg_width: print("The sizes of the images do not match", file=sys.stderr) sys.exit(1) + self.height = bg_height + self.widht = bg_width + def _guess_hotspot(self): # TODO: add support for hotspot from xbm (this should be # provided in the PIL image info) From 08587e0100c2fcc8a2d136add74f16540207a2f6 Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Mon, 2 Sep 2013 17:38:39 +0200 Subject: [PATCH 06/33] bugfix --- tools/make_lock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/make_lock.py b/tools/make_lock.py index 2925eec..c88dfb0 100755 --- a/tools/make_lock.py +++ b/tools/make_lock.py @@ -281,7 +281,7 @@ def _guess_size(self): sys.exit(1) self.height = bg_height - self.widht = bg_width + self.width = bg_width def _guess_hotspot(self): # TODO: add support for hotspot from xbm (this should be From 15e4bbf2da8ea98d2b19570329fdb5b50608ee3d Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Mon, 2 Sep 2013 17:39:28 +0200 Subject: [PATCH 07/33] bugfix --- tools/make_lock.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/make_lock.py b/tools/make_lock.py index c88dfb0..4195236 100755 --- a/tools/make_lock.py +++ b/tools/make_lock.py @@ -428,16 +428,16 @@ def finish(i, j, in_img): # stroke vertically - for i in self.width: + for i in range(self.width): in_img = False - for j in self.height: + for j in range(self.height): in_img = action(i, j, 1, 0, in_img) finish(i-1, j, in_img) # stroke horizontally - for j in self.height: + for j in range(self.height): in_img = False - for i in self.width: + for i in range(self.width): in_img = action(i, j, 0, 1, in_img) finish(i, j-1, in_img) From 52b4cbb2d817b0a3350792c610e6a447e552cb1d Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Mon, 2 Sep 2013 18:00:54 +0200 Subject: [PATCH 08/33] fixed color parsing in the make_lock.py tool --- tools/make_lock.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tools/make_lock.py b/tools/make_lock.py index 4195236..185571f 100755 --- a/tools/make_lock.py +++ b/tools/make_lock.py @@ -224,7 +224,7 @@ def make_transparency_filter(self): class LockMaker(object): RGB_TRIPLE_RE = \ - r'\s*rgb\s*\(\s*([0-9\.]+)\s*,\s*\([0-9\.])\s*,\s*\([0-9\.])\s*\)\s*' + r'\s*rgb\s*\(\s*([0-9\.]+)\s*,\s*([0-9\.]+)\s*,\s*([0-9\.]+)\s*\)\s*' def __init__(self, args): self.args = args @@ -449,30 +449,29 @@ def _parse_color(self, color_string): * #ff7f00 * named color """ - args = self.args - if args.fg_color.startswith('#'): - self._check_color_mode(rgb) + if color_string.startswith('#'): + self._check_color_mode('rgb') if len(color_string) == 4: return tuple(17*int(color_string[i], base=16) for i in range(1,4)) - elif len(args.fg_color) == 7: + elif len(color_string) == 7: return tuple(int(color_string[i:i+1], base=16) for i in range(1,6,2)) else: print("Invalid color format", file=sys.stderr) sys.exit(1) else: - match = re.match(RGB_TRIPLE_RE, args.fg_color) + match = re.match(self.RGB_TRIPLE_RE, color_string) if match is not None: self._check_color_mode('rgb') try: - r, g, b = map(int, (match.group(i) for i in range(0, 3))) + r, g, b = map(int, (match.group(i) for i in range(1, 4))) except ValueError: try: r, g, b = map(lambda x: int(float(x)*255), - (match.group(i) for i in range(0, 3))) + (match.group(i) for i in range(1, 4))) except ValueError: print("Invalid color format", file=sys.stderr) sys.exit(1) From 11e3aeb9ee7758cada4d7a7b854ef86fc36b9a9c Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Mon, 2 Sep 2013 18:01:15 +0200 Subject: [PATCH 09/33] fix border stroking in make_lock.py --- tools/make_lock.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/make_lock.py b/tools/make_lock.py index 185571f..3a286ab 100755 --- a/tools/make_lock.py +++ b/tools/make_lock.py @@ -424,22 +424,22 @@ def action(i, j, di, dj, in_img): def finish(i, j, in_img): if in_img: - self._fg_bitmap[i-di, j-dj] = 1 + self._fg_bitmap[i, j] = 1 # stroke vertically for i in range(self.width): in_img = False for j in range(self.height): - in_img = action(i, j, 1, 0, in_img) - finish(i-1, j, in_img) + in_img = action(i, j, 0, 1, in_img) + finish(i, j, in_img) # stroke horizontally for j in range(self.height): in_img = False for i in range(self.width): - in_img = action(i, j, 0, 1, in_img) - finish(i, j-1, in_img) + in_img = action(i, j, 1, 0, in_img) + finish(i, j, in_img) def _parse_color(self, color_string): """Parse a string representing a color the formats From 13411e2894abf997d1c619df92d2a2603bfea1fc Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Mon, 2 Sep 2013 18:47:56 +0200 Subject: [PATCH 10/33] fix palette and dual image mode --- tools/make_lock.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/tools/make_lock.py b/tools/make_lock.py index 3a286ab..5bf78b9 100755 --- a/tools/make_lock.py +++ b/tools/make_lock.py @@ -209,9 +209,8 @@ class PColorHander(ColorHandler): def make_transparency_filter(self): if 'transparency' in self._image.info: - transparent_colors = self._image.info['transparency'] - threshold = self._threshold - return lambda x: transparent_colors[x] < threshold + transparent_color = self._image.info['transparency'] + return lambda x: transparent_color == x else: return lambda x: False @@ -222,6 +221,17 @@ def make_transparency_filter(self): return lambda x: False +class FixedPalette(object): + """Read-access wrapper around ImagingPalettes as the latter is + entirely borken""" + + def __init__(self, palette): + self._palette = bytearray(palette.palette) + + def __getitem__(self, item): + return tuple(self._palette[i] for i in range(3*item, 3*item+3)) + + class LockMaker(object): RGB_TRIPLE_RE = \ r'\s*rgb\s*\(\s*([0-9\.]+)\s*,\s*([0-9\.]+)\s*,\s*([0-9\.]+)\s*\)\s*' @@ -263,7 +273,7 @@ def __init__(self, args): else: self._stroke(self._fg_bitmap_raw, self._fg_bitmap, self._fg_filter) self._stroke(self._bg_bitmap_raw, self._bg_bitmap, self._bg_filter) - self._bg_bitmap_raw |= self._fg_bitmap_raw + self._bg_bitmap |= self._fg_bitmap if self.args.debug: print(str(self._bg_bitmap)) @@ -327,7 +337,7 @@ def _guess_colors(self): elif mode == 'RGBA' or mode == 'RGBa': image_fg, image_bg = f[:3], b[:3] elif mode == 'P': - plte = self._bg_bitmap_raw.palette + plte = FixedPalette(self._bg_bitmap_raw.palette) image_fg, image_bg = plte[f], plte[b] else: raise Exception("Can't happen") @@ -356,11 +366,21 @@ def _guess_colors(self): print("Unsopported image mode", file=sys.stderr) sys.exit(1) else: + mode = self._bg_bitmap_raw.mode + info = self._bg_bitmap_raw.info + + mode_fg = self._fg_bitmap_raw.mode + if mode_fg != mode: + print("Mode mismatch. Only 1-bit bitmaps supported" + " for dual-image mode", file=sys.stderr) + sys.exit(1) + if mode in ('RGB', 'RGBA', 'RGBa', 'P', 'L'): - print("Unsupported image mode for dual-image", file=sys.stderr) + print("Unsupported image mode for dual-image" + " (obviously pointless)", file=sys.stderr) sys.exit(1) elif mode == '1': - self.fg_filter = lambda x: bool(x) + self._fg_filter = lambda x: bool(x) self._bg_filter = lambda x: bool(x) else: print("Unsopported image mode", file=sys.stderr) From 5cd7cddcf1fa9e6a8c9980b5d8127f1c7aa27ef9 Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Mon, 2 Sep 2013 18:48:58 +0200 Subject: [PATCH 11/33] add support for taking the hotspot from an xmb --- tools/make_lock.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/make_lock.py b/tools/make_lock.py index 5bf78b9..97fcd51 100755 --- a/tools/make_lock.py +++ b/tools/make_lock.py @@ -294,15 +294,21 @@ def _guess_size(self): self.width = bg_width def _guess_hotspot(self): - # TODO: add support for hotspot from xbm (this should be - # provided in the PIL image info) if args.x_hit is not None: self.x_hot = args.x_hot + elif 'hotspot' in self._bg_bitmap_raw.info: + self.x_hot = self._bg_bitmap_raw.info['hotspot'][0] + elif not self.uni_image and 'hotspot' in self._fg_bitmap_raw.info: + self.x_hot = self._fg_bitmap_raw.info['hotspot'][0] else: self.x_hot = self.width // 2 + 1 if args.y_hit is not None: self.y_hot = args.y_hot + elif 'hotspot' in self._bg_bitmap_raw.info: + self.y_hot = self._bg_bitmap_raw.info['hotspot'][1] + elif not self.uni_image and 'hotspot' in self._fg_bitmap_raw.info: + self.y_hot = self._fg_bitmap_raw.info['hotspot'][1] else: self.y_hot = self.height // 2 + 1 From 4233ac029739c76be6e5c2e338b7d32b74b18afd Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Mon, 2 Sep 2013 19:14:06 +0200 Subject: [PATCH 12/33] extend README for the tools --- tools/README | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tools/README b/tools/README index 11b13a2..87ad880 100644 --- a/tools/README +++ b/tools/README @@ -70,11 +70,26 @@ do the following: $ mkdir ~/.config/pyxtrlock/ $ cp lock.pickle ~/.config/pyxtrlock +If the tool fails to grok an image file it may help to use ImageMagick +to convert it to PNG: + + $ convert fnord.{ico,bmp,....} fnord.png + Requirements ------------ *Python 2.7 *python-imaging (PIL) +Bugs +---- + +Probably some, not all code paths have been tested. (And all the +tested ones contained bugs ;) ). + +BMPs with Alpha, .ico with transparence, and tif are not correctly +handled by the PIL and therefore are neither handled correctly by this +tool. + Authors ------- Sebastian Riese From 63c691fb3dbb69b0c7787d3afb07fa6921e5f6c8 Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Mon, 2 Sep 2013 19:32:17 +0200 Subject: [PATCH 13/33] fix README --- tools/README | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/README b/tools/README index 87ad880..cb91298 100644 --- a/tools/README +++ b/tools/README @@ -83,10 +83,10 @@ Requirements Bugs ---- -Probably some, not all code paths have been tested. (And all the +Probably some. Not all code paths have been tested. (And all the tested ones contained bugs ;) ). -BMPs with Alpha, .ico with transparence, and tif are not correctly +BMPs with Alpha, .ico with transparence, and .tif are not correctly handled by the PIL and therefore are neither handled correctly by this tool. From 6a72a50aa93a51cc6bedf9ebb705ac001eaaadc8 Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Mon, 2 Sep 2013 19:36:25 +0200 Subject: [PATCH 14/33] update email address --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df8dd9c..675600f 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ These requirements are met at least on Authors ------- * Leon Weber -* Sebastian Riese +* Sebastian Riese pyxtrlock has been inspired by [Ian Jacksons](http://www.chiark.greenend.org.uk/~ijackson/)'s brilliant From bb40323bfd8326639b2e3e6694b4ce53868b0ef8 Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Mon, 2 Sep 2013 19:39:23 +0200 Subject: [PATCH 15/33] update email address --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index de9fca9..3152c56 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ def run(self): authors = ( 'Leon Weber , ' - 'Sebastian Riese ' + 'Sebastian Riese ' ) desc = ( From d22df66f808ba0c8ee78d22fa20c1a568e05a1e5 Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Mon, 2 Sep 2013 21:07:39 +0200 Subject: [PATCH 16/33] fix memleaks *valgrind still reports errors, but does not show stack traces --- lib/xcb.py | 39 ++++++++++++++--- pyxtrlock | 125 ++++++++++++++++++++++++++++------------------------- 2 files changed, 97 insertions(+), 67 deletions(-) diff --git a/lib/xcb.py b/lib/xcb.py index e650d58..143c0a8 100644 --- a/lib/xcb.py +++ b/lib/xcb.py @@ -216,6 +216,7 @@ class KeyPressEvent(Structure): libxcb = cdll.LoadLibrary(find_library('xcb')) libxcb_image = cdll.LoadLibrary(find_library('xcb-image')) +libc = cdll.LoadLibrary(find_library('c')) connect = libxcb.xcb_connect connect.argtypes = [c_char_p, POINTER(c_int)] @@ -304,11 +305,14 @@ def alloc_named_color_sync(conn, colormap, color_string): cookie = alloc_named_color(conn, colormap, len(color_string), color_string) error_p = POINTER(GenericError)() - res = alloc_named_color_reply(conn, cookie, byref(error_p)).contents + res = alloc_named_color_reply(conn, cookie, byref(error_p)) if error_p: raise XCBError(error_p.contents) - return (res.visual_red, res.visual_green, res.visual_blue) + ret = (res.contents.visual_red, res.contents.visual_green, + res.contents.visual_blue) + free(res) + return ret def alloc_color_sync(conn, colormap, r, g, b): """Synchronously allocate a color @@ -330,11 +334,13 @@ def alloc_color_sync(conn, colormap, r, g, b): cookie = alloc_color(conn, colormap, r, g, b) error_p = POINTER(GenericError)() - res = alloc_color_reply(conn, cookie, byref(error_p)).contents + res = alloc_color_reply(conn, cookie, byref(error_p)) if error_p: raise XCBERror(error_p.contents) - return (res.red, res.blue, res.green) + ret = (res.contents.red, res.contents.blue, res.contents.green) + free(res) + return ret request_check = libxcb.xcb_request_check request_check.argtypes = [POINTER(Connection), VoidCookie] @@ -463,9 +469,28 @@ def grab_pointer_sync(conn, owner_events, window, event_mask, ptr_mode, raise XCBError(error_p.contents) return ptr_grab -wait_for_event = libxcb.xcb_wait_for_event -wait_for_event.argtypes = [POINTER(Connection)] -wait_for_event.restype = POINTER(GenericEvent) +wait_for_event_ = libxcb.xcb_wait_for_event +wait_for_event_.argtypes = [POINTER(Connection)] +wait_for_event_.restype = POINTER(GenericEvent) + +free = libc.free +free.argtypes = [c_void_p] +free.restype = None + +class FreeWrapper(object): + + def __init__(self, pointer): + self.pointer = pointer + + def __enter__(self): + return self.pointer + + def __exit__(self, etype, evalue, traceback): + free(self.pointer) + + +def wait_for_event(conn): + return FreeWrapper(wait_for_event_(conn)) # xcb_image image_create_pixmap_from_bitmap_data = \ diff --git a/pyxtrlock b/pyxtrlock index f6c68b2..92baec2 100755 --- a/pyxtrlock +++ b/pyxtrlock @@ -111,6 +111,7 @@ xcb.map_window(conn, window) try: kbd_grab = xcb.grab_keyboard_sync(conn, 0, window, xcb.CURRENT_TIME, xcb.GRAB_MODE_ASYNC, xcb.GRAB_MODE_ASYNC) + xcb.free(kbd_grab) except xcb.XCBError as e: print("pyxtrlock: Could not get grab keyboard", file=sys.stderr) sys.exit(1) @@ -129,6 +130,7 @@ for i in range(100): xcb.GRAB_MODE_ASYNC, xcb.WINDOW_NONE, cursor, xcb.CURRENT_TIME) + xcb.free(ptr_grab) break except xcb.XCBError as e: time.sleep(0.01) @@ -163,68 +165,71 @@ pwd = [] timeout = 0 goodwill = INITIALGOODWILL while True: - event = xcb.wait_for_event(conn) - if event.contents.response_type == xcb.KEY_PRESS: - xcb_key_press_event = cast(event, POINTER(xcb.KeyPressEvent)).contents - time_stamp = xcb_key_press_event.time - if time_stamp < timeout: - continue - - x_key_press_event = X.KeyEvent() - x_key_press_event.type = xcb_key_press_event.response_type - x_key_press_event.serial = xcb_key_press_event.sequence - x_key_press_event.send_event = 0 - x_key_press_event.display = display - x_key_press_event.window = xcb_key_press_event.event - x_key_press_event.root = xcb_key_press_event.root - x_key_press_event.subwindow = xcb_key_press_event.child - x_key_press_event.time = xcb_key_press_event.time - x_key_press_event.x = xcb_key_press_event.event_x - x_key_press_event.y = xcb_key_press_event.event_y - x_key_press_event.y_root = xcb_key_press_event.root_y - x_key_press_event.state = xcb_key_press_event.state - x_key_press_event.same_screen = xcb_key_press_event.same_screen - x_key_press_event.keycode = xcb_key_press_event.detail - - status = X.Status() - keysym = X.Keysym() - size = 0 - buf = bytearray(size) - - length = X.utf8_lookup_string(ic, byref(x_key_press_event), - None, size, byref(keysym), byref(status)) - if status.value == X.BUFFER_OVERFLOW: - buf = bytearray(length) - buf_p = cast((c_char * length).from_buffer(buf), POINTER(c_char)) - length = X.utf8_lookup_string(ic, byref(x_key_press_event), buf_p, - length, byref(keysym), byref(status)) - - status = status.value - keysym = keysym.value - if status == X.LOOKUP_BOTH or status == X.LOOKUP_KEYSYM: - if keysym == X.K_Escape or keysym == X.K_Clear: - pwd = [] + with xcb.wait_for_event(conn) as event: + if event.contents.response_type == xcb.KEY_PRESS: + xcb_key_press_event = cast(event, + POINTER(xcb.KeyPressEvent)).contents + time_stamp = xcb_key_press_event.time + if time_stamp < timeout: continue - elif keysym == X.K_Delete or keysym == X.K_BackSpace: - if pwd: - pwd.pop() - continue - elif keysym == X.K_LineFeed or keysym == X.K_Return: - if pam.authenticate(getpass.getuser(), b''.join(pwd)): - break - else: + + x_key_press_event = X.KeyEvent() + x_key_press_event.type = xcb_key_press_event.response_type + x_key_press_event.serial = xcb_key_press_event.sequence + x_key_press_event.send_event = 0 + x_key_press_event.display = display + x_key_press_event.window = xcb_key_press_event.event + x_key_press_event.root = xcb_key_press_event.root + x_key_press_event.subwindow = xcb_key_press_event.child + x_key_press_event.time = xcb_key_press_event.time + x_key_press_event.x = xcb_key_press_event.event_x + x_key_press_event.y = xcb_key_press_event.event_y + x_key_press_event.y_root = xcb_key_press_event.root_y + x_key_press_event.state = xcb_key_press_event.state + x_key_press_event.same_screen = xcb_key_press_event.same_screen + x_key_press_event.keycode = xcb_key_press_event.detail + + status = X.Status() + keysym = X.Keysym() + size = 0 + buf = bytearray(size) + + length = X.utf8_lookup_string(ic, byref(x_key_press_event), None, + size, byref(keysym), byref(status)) + if status.value == X.BUFFER_OVERFLOW: + buf = bytearray(length) + buf_p = cast((c_char * length).from_buffer(buf), + POINTER(c_char)) + length = X.utf8_lookup_string(ic, byref(x_key_press_event), + buf_p, length, byref(keysym), + byref(status)) + + status = status.value + keysym = keysym.value + if status == X.LOOKUP_BOTH or status == X.LOOKUP_KEYSYM: + if keysym == X.K_Escape or keysym == X.K_Clear: pwd = [] - if timeout: - goodwill += time_stamp - timeout - if goodwill > MAXGOODWILL: - goodwill = MAXGOODWILL - timeout = -int(goodwill * GOODWILLPORTION) - goodwill += timeout - timeout += time_stamp + TIMEOUTPERATTEMPT continue - - if status == X.LOOKUP_BOTH or status == X.LOOKUP_CHARS: - if length: - pwd.append(bytes(buf[:length])) + elif keysym == X.K_Delete or keysym == X.K_BackSpace: + if pwd: + pwd.pop() + continue + elif keysym == X.K_LineFeed or keysym == X.K_Return: + if pam.authenticate(getpass.getuser(), b''.join(pwd)): + break + else: + pwd = [] + if timeout: + goodwill += time_stamp - timeout + if goodwill > MAXGOODWILL: + goodwill = MAXGOODWILL + timeout = -int(goodwill * GOODWILLPORTION) + goodwill += timeout + timeout += time_stamp + TIMEOUTPERATTEMPT + continue + + if status == X.LOOKUP_BOTH or status == X.LOOKUP_CHARS: + if length: + pwd.append(bytes(buf[:length])) X.close_window(display) From ba5c1153c5bf38f5cb2ff27164c53f1844b49595 Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Thu, 5 Sep 2013 13:27:55 +0200 Subject: [PATCH 17/33] fix dependency list in README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 675600f..d585515 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ Requirements * [python3-simplepam](https://github.com/leonnnn/python3-simplepam) * Python ≥ 3.0 * libxcb +* libxcb-image * libX11 ≥ 1.4, or libX11 ≥ 1.2 compiled with XCB backend These requirements are met at least on From 8c1d675851707e0494b4f2c49bfb4eb7fb63c18f Mon Sep 17 00:00:00 2001 From: Jonas Wielicki Date: Thu, 5 Sep 2013 13:32:58 +0200 Subject: [PATCH 18/33] Handle None result from find_library Wrap all library loads into a separate function which handles concerning return values from the involved ctypes functions. --- lib/X.py | 6 +++--- lib/utils.py | 9 +++++++++ lib/xcb.py | 9 ++++----- 3 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 lib/utils.py diff --git a/lib/X.py b/lib/X.py index b977827..d763586 100644 --- a/lib/X.py +++ b/lib/X.py @@ -1,10 +1,10 @@ from ctypes import * -from ctypes.util import find_library +from pyxtrlock.utils import check_and_load_library import pyxtrlock.xcb as xcb -libx_xcb = cdll.LoadLibrary(find_library('X11-xcb')) -libx = cdll.LoadLibrary(find_library('X11')) +libx_xcb = check_and_load_library('X11-xcb') +libx = check_and_load_library('X11') class Display(Structure): diff --git a/lib/utils.py b/lib/utils.py new file mode 100644 index 0000000..a352a09 --- /dev/null +++ b/lib/utils.py @@ -0,0 +1,9 @@ +from ctypes import cdll +from ctypes.util import find_library + +def check_and_load_library(libname): + handle = find_library(libname) + if handle is None: + raise ImportError("unable to find system library: {}".format( + libname)) + return cdll.LoadLibrary(handle) diff --git a/lib/xcb.py b/lib/xcb.py index 143c0a8..af8c14b 100644 --- a/lib/xcb.py +++ b/lib/xcb.py @@ -1,6 +1,5 @@ from ctypes import * -from ctypes.util import find_library - +from pyxtrlock.utils import check_and_load_library class XCBError(Exception): """ @@ -214,9 +213,9 @@ class KeyPressEvent(Structure): KEY_PRESS = 2 -libxcb = cdll.LoadLibrary(find_library('xcb')) -libxcb_image = cdll.LoadLibrary(find_library('xcb-image')) -libc = cdll.LoadLibrary(find_library('c')) +libxcb = check_and_load_library('xcb') +libxcb_image = check_and_load_library('xcb-image') +libc = check_and_load_library('c') connect = libxcb.xcb_connect connect.argtypes = [c_char_p, POINTER(c_int)] From 45168b3400c81e3dc61dd62698985814b7c15bcc Mon Sep 17 00:00:00 2001 From: Jonas Wielicki Date: Thu, 5 Sep 2013 13:33:38 +0200 Subject: [PATCH 19/33] Catch ImportErrors from xcb and X utility libraries --- pyxtrlock | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pyxtrlock b/pyxtrlock index 92baec2..fbff3d2 100755 --- a/pyxtrlock +++ b/pyxtrlock @@ -12,8 +12,16 @@ from ctypes import POINTER, c_int, c_uint32, c_char import simplepam as pam import pyxtrlock -import pyxtrlock.xcb as xcb -import pyxtrlock.X as X +try: + import pyxtrlock.xcb as xcb +except ImportError as err: + print(err, file=sys.stderr) + sys.exit(1) +try: + import pyxtrlock.X as X +except ImportError as err: + print(err, file=sys.stderr) + sys.exit(1) if getpass.getuser() == 'root' and sys.argv[1:] != ['-f']: msg = ( From d093e500a6a6bacffb950b552eb28bb60a5a571f Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Thu, 5 Sep 2013 14:40:00 +0200 Subject: [PATCH 20/33] refactoring --- lib/X.py | 21 +++++++++++++++++++++ pyxtrlock | 17 ++--------------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/lib/X.py b/lib/X.py index d763586..3b5a8dd 100644 --- a/lib/X.py +++ b/lib/X.py @@ -35,6 +35,27 @@ class KeyEvent(Structure): ("same_screen", Bool) ] + @classmethod + def from_xcb_event(cls, display, xcb_key_press_event): + x_key_press_event = cls() + x_key_press_event.type = xcb_key_press_event.response_type + x_key_press_event.serial = xcb_key_press_event.sequence + x_key_press_event.send_event = 0 + x_key_press_event.display = display + x_key_press_event.window = xcb_key_press_event.event + x_key_press_event.root = xcb_key_press_event.root + x_key_press_event.subwindow = xcb_key_press_event.child + x_key_press_event.time = xcb_key_press_event.time + x_key_press_event.x = xcb_key_press_event.event_x + x_key_press_event.y = xcb_key_press_event.event_y + x_key_press_event.y_root = xcb_key_press_event.root_y + x_key_press_event.state = xcb_key_press_event.state + x_key_press_event.same_screen = xcb_key_press_event.same_screen + x_key_press_event.keycode = xcb_key_press_event.detail + + return x_key_press_event + + Keysym = c_ulong Status = c_int diff --git a/pyxtrlock b/pyxtrlock index fbff3d2..b6f5962 100755 --- a/pyxtrlock +++ b/pyxtrlock @@ -181,21 +181,8 @@ while True: if time_stamp < timeout: continue - x_key_press_event = X.KeyEvent() - x_key_press_event.type = xcb_key_press_event.response_type - x_key_press_event.serial = xcb_key_press_event.sequence - x_key_press_event.send_event = 0 - x_key_press_event.display = display - x_key_press_event.window = xcb_key_press_event.event - x_key_press_event.root = xcb_key_press_event.root - x_key_press_event.subwindow = xcb_key_press_event.child - x_key_press_event.time = xcb_key_press_event.time - x_key_press_event.x = xcb_key_press_event.event_x - x_key_press_event.y = xcb_key_press_event.event_y - x_key_press_event.y_root = xcb_key_press_event.root_y - x_key_press_event.state = xcb_key_press_event.state - x_key_press_event.same_screen = xcb_key_press_event.same_screen - x_key_press_event.keycode = xcb_key_press_event.detail + x_key_press_event = X.KeyEvent.from_xcb_event(display, + xcb_key_press_event) status = X.Status() keysym = X.Keysym() From 510b888d1330a23059a91d505fc571e0fe772543 Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Thu, 5 Sep 2013 14:42:18 +0200 Subject: [PATCH 21/33] update changelog --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 3b9e7b2..598bdc7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,3 +2,5 @@ Release 0.1: • [#8] Security: Fixed a typo that could in some circumstances lead to a crash after multiple failed authentication attempts. Thanks, Paul Lhussiez. +Development Version: +• Enhancement: Report missing libraries when loading via ctypes From 50a8522392809a5688638d074fb9f84264c8b58d Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Thu, 5 Sep 2013 14:43:28 +0200 Subject: [PATCH 22/33] check the return values of the grab functions correctly --- CHANGELOG | 2 ++ lib/xcb.py | 18 ++++++++++++++---- pyxtrlock | 25 +++++++++++++++---------- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 598bdc7..d9ff56d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,3 +4,5 @@ Release 0.1: Paul Lhussiez. Development Version: • Enhancement: Report missing libraries when loading via ctypes +• Security: Check correctly for the result of the + xcb_grab_{pointer,keyboard} commands diff --git a/lib/xcb.py b/lib/xcb.py index af8c14b..01ee8b0 100644 --- a/lib/xcb.py +++ b/lib/xcb.py @@ -412,8 +412,8 @@ def grab_keyboard_sync(conn, owner_events, grab_window, time, ptr_mode, """ Synchronously grab the keyboard. - Wrapper function for grab_pointer and grab_pointer_reply. - Raises ``XCBError`` on error, otherwise returns ``GrabKeyboardReply``. + Wrapper function for grab_pointer and grab_pointer_reply. Returns + the status field from the reply. Raises ``XCBError`` on error. """ owner_events = 1 if owner_events else 0 @@ -424,7 +424,9 @@ def grab_keyboard_sync(conn, owner_events, grab_window, time, ptr_mode, if error_p: raise XCBError(error_p.contents) - return kbd_grab + status = kbd_grab.contents.status + free(kbd_grab) + return status grab_pointer = libxcb.xcb_grab_pointer @@ -449,6 +451,12 @@ def grab_keyboard_sync(conn, owner_events, grab_window, time, ptr_mode, ] grab_pointer_reply.restype = POINTER(GrabPointerReply) +# constants to interpret grab results +GrabSuccess = 0 +AlreadyGrabbed = 1 +GrabInvalidTime = 2 +GrabNotViewable = 3 +GrabFrozen = 4 def grab_pointer_sync(conn, owner_events, window, event_mask, ptr_mode, kbd_mode, confine_to, cursor, timestamp): @@ -466,7 +474,9 @@ def grab_pointer_sync(conn, owner_events, window, event_mask, ptr_mode, ptr_grab = grab_pointer_reply(conn, cookie, byref(error_p)) if error_p: raise XCBError(error_p.contents) - return ptr_grab + status = ptr_grab.contents.status + free(ptr_grab) + return status wait_for_event_ = libxcb.xcb_wait_for_event wait_for_event_.argtypes = [POINTER(Connection)] diff --git a/pyxtrlock b/pyxtrlock index b6f5962..77fc5ff 100755 --- a/pyxtrlock +++ b/pyxtrlock @@ -117,9 +117,11 @@ xcb.map_window(conn, window) # Grab keyboard try: - kbd_grab = xcb.grab_keyboard_sync(conn, 0, window, xcb.CURRENT_TIME, - xcb.GRAB_MODE_ASYNC, xcb.GRAB_MODE_ASYNC) - xcb.free(kbd_grab) + status = xcb.grab_keyboard_sync(conn, 0, window, xcb.CURRENT_TIME, + xcb.GRAB_MODE_ASYNC, xcb.GRAB_MODE_ASYNC) + + if status != xcb.GrabSuccess: + panic("pyxtrlock: Could not get grab keyboard") except xcb.XCBError as e: print("pyxtrlock: Could not get grab keyboard", file=sys.stderr) sys.exit(1) @@ -133,13 +135,16 @@ except xcb.XCBError as e: # (i.e. after 1s in total), then give up, and emit an error" for i in range(100): try: - ptr_grab = xcb.grab_pointer_sync(conn, False, window, 0, - xcb.GRAB_MODE_ASYNC, - xcb.GRAB_MODE_ASYNC, - xcb.WINDOW_NONE, cursor, - xcb.CURRENT_TIME) - xcb.free(ptr_grab) - break + status = xcb.grab_pointer_sync(conn, False, window, 0, + xcb.GRAB_MODE_ASYNC, + xcb.GRAB_MODE_ASYNC, + xcb.WINDOW_NONE, cursor, + xcb.CURRENT_TIME) + + if status == xcb.GrabSuccess: + break + else: + time.sleep(0.01) except xcb.XCBError as e: time.sleep(0.01) else: From 7e1274bf91b41310310e0ddcab2e451c4db4695c Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Thu, 5 Sep 2013 14:44:11 +0200 Subject: [PATCH 23/33] refactoring --- lib/__init__.py | 5 +++++ pyxtrlock | 38 ++++++++++++++------------------------ 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/lib/__init__.py b/lib/__init__.py index 6fbab0c..9af987c 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -3,3 +3,8 @@ import os data_dir = os.path.join(sys.prefix, "share/pyxtrlock") + +def panic(message, exit_code=1): + """Print an error message to stderr and exit""" + print(message, file=sys.stderr) + sys.exit(exit_code) diff --git a/pyxtrlock b/pyxtrlock index 77fc5ff..825f510 100755 --- a/pyxtrlock +++ b/pyxtrlock @@ -12,24 +12,23 @@ from ctypes import POINTER, c_int, c_uint32, c_char import simplepam as pam import pyxtrlock +from pyxtrlock import panic try: import pyxtrlock.xcb as xcb except ImportError as err: - print(err, file=sys.stderr) - sys.exit(1) + panic(err) + try: import pyxtrlock.X as X except ImportError as err: - print(err, file=sys.stderr) - sys.exit(1) + panic(err) if getpass.getuser() == 'root' and sys.argv[1:] != ['-f']: msg = ( "pyxtrlock: refusing to run as root. Use -f to force. Warning: You " "might not be able to unlock." ) - print(msg, file=sys.stderr) - sys.exit(1) + panic(msg) # load cursor data file try: @@ -40,11 +39,9 @@ try: f = open(os.path.join(pyxtrlock.data_dir, "lock.pickle"), "rb") cursor = pickle.load(f) except OSError as e: - print(e.strerror, file=sys.stderr) - sys.exit(1) + panic(e.strerror) except pickle.UnpicklingError as e: - print(e.args, file=sys.stderr) - sys.exit(1) + panic(e.args) finally: f.close() @@ -52,8 +49,7 @@ display = X.create_window(None) conn = X.get_xcb_connection(display) if not display: - print("pyxtrlock: Could not connect to X server", file=sys.stderr) - sys.exit(1) + panic("pyxtrlock: Could not connect to X server") screen_num = c_int() @@ -102,15 +98,13 @@ elif cursor["color_mode"] == "rgb": csr_fg = xcb.alloc_color_sync(conn, screen.default_colormap, r, g, b) else: - print("Invalid color mode", file=sys.stderr) - sys.exit(1) + panic("Invalid color mode") try: cursor = xcb.create_cursor_sync(conn, csr_map, csr_mask, csr_fg, csr_bg, cursor["x_hot"], cursor["y_hot"]) except xcb.XCBError as e: - print("pyxtrlock: Could not create cursor", file=sys.stderr) - sys.exit(1) + panic("pyxtrlock: Could not create cursor") # map window xcb.map_window(conn, window) @@ -123,8 +117,7 @@ try: if status != xcb.GrabSuccess: panic("pyxtrlock: Could not get grab keyboard") except xcb.XCBError as e: - print("pyxtrlock: Could not get grab keyboard", file=sys.stderr) - sys.exit(1) + panic("pyxtrlock: Could not get grab keyboard") # Grab pointer # Use the method from the original xtrlock code: @@ -148,22 +141,19 @@ for i in range(100): except xcb.XCBError as e: time.sleep(0.01) else: - print("pyxtrlock: Could not grab pointing device", file=sys.stderr) - sys.exit(1) + panic("pyxtrlock: Could not grab pointing device") xcb.flush(conn) # Prepare X Input im = X.open_IM(display, None, None, None) if not im: - print("pyxtrlock: Could not open Input Method", file=sys.stderr) - sys.exit(1) + panic("pyxtrlock: Could not open Input Method") ic = X.create_IC(im, X.N_INPUT_STYLE, X.IM_PRE_EDIT_NOTHING | X.IM_STATUS_NOTHING, None) if not ic: - print("pyxtrlock: Could not open Input Context", file=sys.stderr) - sys.exit(1) + panic("pyxtrlock: Could not open Input Context") X.set_ic_focus(ic) From b328a99cd9c84cda517227d42db9da89d202e6c7 Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Thu, 5 Sep 2013 14:44:19 +0200 Subject: [PATCH 24/33] limit password buffer length to prevent memory exhaustion --- CHANGELOG | 3 +++ pyxtrlock | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d9ff56d..17d8e81 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,3 +6,6 @@ Development Version: • Enhancement: Report missing libraries when loading via ctypes • Security: Check correctly for the result of the xcb_grab_{pointer,keyboard} commands +• Security: Limit length of buffered password to prevent memory exhaustion + (this is a real concern when attacked with custom hardware which + simulates most rapid keystrokes) diff --git a/pyxtrlock b/pyxtrlock index 825f510..d268fdc 100755 --- a/pyxtrlock +++ b/pyxtrlock @@ -157,6 +157,10 @@ if not ic: X.set_ic_focus(ic) +# pwd length limit to prevent memory exhaustion (and therefore +# possible failure due to OOM killing) +PWD_LENGTH_LIMIT = 100 * 1024 + # timeout algorithm constants TIMEOUTPERATTEMPT = 30000 MAXGOODWILL = TIMEOUTPERATTEMPT * 5 @@ -219,7 +223,7 @@ while True: continue if status == X.LOOKUP_BOTH or status == X.LOOKUP_CHARS: - if length: + if length and sum(map(len, pwd)) < PWD_LENGTH_LIMIT: pwd.append(bytes(buf[:length])) X.close_window(display) From 818b312318ae1e261da5ea55fc4bd5986ea27094 Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Thu, 5 Sep 2013 14:52:43 +0200 Subject: [PATCH 25/33] update README.md to reflect recent changes --- README.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d585515..10128b5 100644 --- a/README.md +++ b/README.md @@ -53,24 +53,29 @@ we recommend the ``xautolock`` tool. Just add something like xautolock -locker pyxtrlock -time 5 -to your X autostart file to lock the screen with ``pyxtrlock`` after -5 minutes idle time. ``xautolock`` has many other useful features, see +to your X autostart file to lock the screen with ``pyxtrlock`` after 5 +minutes idle time. ``xautolock`` has many other useful features, see its documentation. Most distributions provide an ``xautolock`` package -with a man page. +with a man page. An alternative to ``xautolock`` is the use of +[autolockd](https://github.com/zombofant/autolockd) which also +monitors for lid close and suspend events. -Bugs ----- +Bugs & Limitations +------------------ Additional input devices other than the keyboard and mouse are not disabled. -Although this is not a bug, please note that pyxtrlock does not prevent a -user from switching to a virtual terminal, so be advised to always leave your -terminals locked. +Although this is not a bug, please note that pyxtrlock does not +prevent a user from switching to a virtual terminal, so be advised to +always log out from your terminals. -Please report any new bugs you may find to our [Github issue tracker](https://github.com/leonnnn/pyxtrlock/issues). +The lenght of the password is limited to 100 KiB to prevent memory +exhaustion attacks. This limit can only be adapted in the source code. + +Please report any new bugs you may find to our +[Github issue tracker](https://github.com/leonnnn/pyxtrlock/issues). Configuration ------------- - The padlock icon can be changed. It is stored as a [pickle](http://docs.python.org/3/library/pickle.html) of a dictionary, and the ``tools`` directory contains a tool for generating From a933ecc334bc897ea42b12f76ce0592a12fe87b6 Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Thu, 5 Sep 2013 15:26:54 +0200 Subject: [PATCH 26/33] update MANIFEST.in --- MANIFEST.in | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 86c7026..9612e95 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,3 @@ -include COPYING README.md +include COPYING CHANGELOG README.md +include tools/*.py tools/README +include make_default_lock.py From 58d651d81ac8c55e5ed616e2dadd309ee77c95a3 Mon Sep 17 00:00:00 2001 From: Sebastian Riese Date: Sat, 7 Sep 2013 01:00:50 +0200 Subject: [PATCH 27/33] fix typos in the documentation of make_lock.py --- tools/README | 5 +++-- tools/make_lock.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tools/README b/tools/README index cb91298..4789860 100644 --- a/tools/README +++ b/tools/README @@ -23,7 +23,7 @@ optional arguments: x-coordinate of the cursor hotspot --fg-color FG_COLOR, -f FG_COLOR The foreground colour (necessary only if the - colourscannot be guessed from the image file). + colours cannot be guessed from the image file). Accepted formats:colour name, rgb(255, 50, 0), rgb(1.0, 0.2, 0.0), #ff7700, #f70 --bg-color BG_COLOR, -b BG_COLOR @@ -62,7 +62,7 @@ Typical usage ------------- To create a cursor from a PNG with two colors (foreground and -background) and transparenct pixels and then install it for your user +background) and transparent pixels and then install it for your user do the following: $ ./make_lock.py lock.png -o lock.pickle @@ -78,6 +78,7 @@ to convert it to PNG: Requirements ------------ *Python 2.7 +*Python 3 for repickle.py *python-imaging (PIL) Bugs diff --git a/tools/make_lock.py b/tools/make_lock.py index 97fcd51..0e6e773 100755 --- a/tools/make_lock.py +++ b/tools/make_lock.py @@ -21,7 +21,7 @@ help="x-coordinate of the cursor hotspot") ap.add_argument('--fg-color', '-f', default=None, - help="The foreground colour (necessary only if the colours" + help="The foreground colour (necessary only if the colours " "cannot be guessed from the image file). Accepted formats:" "colour name, rgb(255, 50, 0), rgb(1.0, 0.2, 0.0), " "#ff7700, #f70") From 00fff193ec9f17824def937effad216ea4d0225b Mon Sep 17 00:00:00 2001 From: Leon Weber Date: Mon, 9 Sep 2013 08:49:45 +0200 Subject: [PATCH 28/33] Fix typo in README --- tools/README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/README b/tools/README index 4789860..86100fa 100644 --- a/tools/README +++ b/tools/README @@ -43,7 +43,7 @@ The recommended file type is PNG. There are several modes of operation which are guessed from the supplied file: -*a singe colour image with 2 colours and transparency is compiled to +*a single colour image with 2 colours and transparency is compiled to the appropriate cursor (transparency may either be an alpha threshold or single colour transparency) *a single image with 1 colour will have its border stroked the colours From 7cf8fe0e4b317ce0df1b7886b2e4e6ac4531e4f8 Mon Sep 17 00:00:00 2001 From: Leon Weber Date: Mon, 9 Sep 2013 09:44:10 +0200 Subject: [PATCH 29/33] Make error message when started as root more clear --- pyxtrlock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyxtrlock b/pyxtrlock index d268fdc..c35f5ed 100755 --- a/pyxtrlock +++ b/pyxtrlock @@ -25,8 +25,8 @@ except ImportError as err: if getpass.getuser() == 'root' and sys.argv[1:] != ['-f']: msg = ( - "pyxtrlock: refusing to run as root. Use -f to force. Warning: You " - "might not be able to unlock." + "pyxtrlock: refusing to run as root. Use -f to force. Warning: " + "Your PAM configuration may deny unlocking as root." ) panic(msg) From 4409b6824760ed622ed6680ac2e3bcb10f8cfd84 Mon Sep 17 00:00:00 2001 From: Leon Weber Date: Mon, 9 Sep 2013 09:44:56 +0200 Subject: [PATCH 30/33] Fix wording in error messages --- pyxtrlock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyxtrlock b/pyxtrlock index c35f5ed..913b133 100755 --- a/pyxtrlock +++ b/pyxtrlock @@ -115,9 +115,9 @@ try: xcb.GRAB_MODE_ASYNC, xcb.GRAB_MODE_ASYNC) if status != xcb.GrabSuccess: - panic("pyxtrlock: Could not get grab keyboard") + panic("pyxtrlock: Could not grab keyboard") except xcb.XCBError as e: - panic("pyxtrlock: Could not get grab keyboard") + panic("pyxtrlock: Could not grab keyboard") # Grab pointer # Use the method from the original xtrlock code: From a2ee85f9bc325deec3ed809fab9af0ab271e295a Mon Sep 17 00:00:00 2001 From: Leon Weber Date: Mon, 9 Sep 2013 09:45:44 +0200 Subject: [PATCH 31/33] Fix typos --- tools/README | 31 ++++++++++++++++--------------- tools/make_lock.py | 4 ++-- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/tools/README b/tools/README index 86100fa..d39cf21 100644 --- a/tools/README +++ b/tools/README @@ -43,18 +43,18 @@ The recommended file type is PNG. There are several modes of operation which are guessed from the supplied file: -*a single colour image with 2 colours and transparency is compiled to +*A single colour image with 2 colours and transparency is compiled to the appropriate cursor (transparency may either be an alpha threshold - or single colour transparency) -*a single image with 1 colour will have its border stroked the colours - should be given on the commandline -*two (one bit!) bitmaps may be given, on is the mask and the other the - foreground of the cursor. The colours should be given on the commandline. -*colours may be given on the commandline or the default colours black - and white apply, colours given on the commandline override colours from - the file, but note that the assignment will be random in that case - -Additionally the cursor hotspot can be given otherwhise it is the + or single colour transparency). +*A single image with 1 colour will have its border stroked. The colours + should be given on the command line. +*Two (one bit!) bitmaps may be given, one is the mask and the other the + foreground of the cursor. The colours should be given on the command line. +*Colours may be given on the command line or the default colours black + and white apply. Colours given on the command line override colours from + the file, but note that the assignment will be random in that case. + +Additionally, the cursor hotspot can be given. If not specified, it is the center of the image (this is more or less irrelevant, as the all cursor events are blocked by pyxtrlock). @@ -85,9 +85,10 @@ Bugs ---- Probably some. Not all code paths have been tested. (And all the -tested ones contained bugs ;) ). +tested ones contained bugs ;) ). Please report any bugs you may find +to our [Github issue tracker](https://github.com/leonnnn/pyxtrlock/issues). -BMPs with Alpha, .ico with transparence, and .tif are not correctly +BMPs with Alpha, .ico with transparency, and .tif are not correctly handled by the PIL and therefore are neither handled correctly by this tool. @@ -95,8 +96,8 @@ Authors ------- Sebastian Riese -Liense ------- +License +------- Copyright 2013 Sebastian Riese diff --git a/tools/make_lock.py b/tools/make_lock.py index 0e6e773..8bc73c5 100755 --- a/tools/make_lock.py +++ b/tools/make_lock.py @@ -18,7 +18,7 @@ ap.add_argument('--x-hit', '-x', type=int, default=None, help="x-coordinate of the cursor hotspot") ap.add_argument('--y-hit', '-y', type=int, default=None, - help="x-coordinate of the cursor hotspot") + help="y-coordinate of the cursor hotspot") ap.add_argument('--fg-color', '-f', default=None, help="The foreground colour (necessary only if the colours " @@ -33,7 +33,7 @@ help="The output file, by default stdout") ap.add_argument('--debug', action='store_true', default=False, help="Check for consistency and print" - "the bitmaps to the stdout") + "the bitmaps to stdout") class Bitmap(object): def __init__(self, width, height, buf=None): From b4118b6f4e555ae01ab25c16d619a712fd2679ea Mon Sep 17 00:00:00 2001 From: Leon Weber Date: Mon, 9 Sep 2013 09:51:29 +0200 Subject: [PATCH 32/33] Reorganise and extend CHANGELOG --- CHANGELOG | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 17d8e81..838cb2d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,11 +1,14 @@ -Release 0.1: -• [#8] Security: Fixed a typo that could in some circumstances lead to a - crash after multiple failed authentication attempts. Thanks, - Paul Lhussiez. -Development Version: -• Enhancement: Report missing libraries when loading via ctypes +Release 0.2 : • Security: Check correctly for the result of the xcb_grab_{pointer,keyboard} commands • Security: Limit length of buffered password to prevent memory exhaustion (this is a real concern when attacked with custom hardware which simulates most rapid keystrokes) +• Security: Fix several memory leaks +• Enhancement: Report missing libraries when loading via ctypes. +• Enhancement: Provide ability and tools to use custom lock images as cursor + +Release 0.1: +• [#8] Security: Fixed a typo that could in some circumstances lead to a + crash after multiple failed authentication attempts. Thanks, + Paul Lhussiez. From 77d490a8bf96bc0be8c378117ce690f464f46727 Mon Sep 17 00:00:00 2001 From: Leon Weber Date: Mon, 9 Sep 2013 10:01:20 +0200 Subject: [PATCH 33/33] Bump version number to 0.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3152c56..0a50e0f 100644 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ def run(self): ] setup(name='pyxtrlock', - version='0.1', + version='0.2', author=authors, author_email='leon@leonweber.de', requires=['simplepam'],