#! /usr/bin/env python3
# -*- coding: utf-8 -*-

# Tea4CUPS : Tee for CUPS
#
# (c) 2005-2018 Jerome Alet <alet@librelogiciel.com>
# (c) 2005 Peter Stuge <stuge-tea4cups@cdy.org>
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# $Id: tea4cups 3575 2018-08-23 00:18:54Z jerome $
#
#

"""Tea4CUPS is the Swiss Army's knife of the CUPS Administrator.


Licensing terms :

  (c) 2005-2018 Jerome Alet <alet@librelogiciel.com>
  (c) 2005 Peter Stuge <stuge-tea4cups@cdy.org>
  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.


Setup :

  Copy the different files where they belong :

    $ cp tea4cups /usr/lib/cups/backend/
    $ chown root.root /usr/lib/cups/backend/tea4cups
    $ chmod 700 /usr/lib/cups/backend/tea4cups
    $ cp tea4cups.conf /etc/cupsd/

  Now edit the configuration file to suit your needs :

    $ vi /etc/cupsd/tea4cups.conf

    NB : you can use emacs as well :-)

  Finally restart CUPS :

    $ /etc/init.d/cupsys restart

  You can now create "Tea4CUPS Managed" print queues from
  CUPS' web interface, or using lpadmin.

Send bug reports to : alet@librelogiciel.com
"""

import sys
import os
import time
import pwd
import errno
import random
# following doesn't work, see https://github.com/VitaliyRodnenko/geeknote/issues/299
# from hashlib import md5
import hashlib
import io
import shlex
import tempfile
import configparser
import signal
import fcntl
import logging
import requests

from struct import pack, unpack

__version__ = "3.15alpha_unofficial"

# http debug
logging.basicConfig(level=logging.DEBUG)
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True

class TeeError(Exception):
    """Base exception for Tea4CUPS related stuff."""
    def __init__(self, message = ""):
        self.message = message
        Exception.__init__(self, message)
    def __repr__(self):
        return self.message
    __str__ = __repr__

class ConfigError(TeeError) :
    """Configuration related exceptions."""
    pass

class IPPError(TeeError) :
    """IPP related exceptions."""
    pass

IPP_VERSION = "1.1"     # default version number

IPP_PORT = 631

IPP_MAX_NAME = 256
IPP_MAX_VALUES = 8

IPP_TAG_ZERO = 0x00
IPP_TAG_OPERATION = 0x01
IPP_TAG_JOB = 0x02
IPP_TAG_END = 0x03
IPP_TAG_PRINTER = 0x04
IPP_TAG_UNSUPPORTED_GROUP = 0x05
IPP_TAG_SUBSCRIPTION = 0x06
IPP_TAG_EVENT_NOTIFICATION = 0x07
IPP_TAG_UNSUPPORTED_VALUE = 0x10
IPP_TAG_DEFAULT = 0x11
IPP_TAG_UNKNOWN = 0x12
IPP_TAG_NOVALUE = 0x13
IPP_TAG_NOTSETTABLE = 0x15
IPP_TAG_DELETEATTR = 0x16
IPP_TAG_ADMINDEFINE = 0x17
IPP_TAG_INTEGER = 0x21
IPP_TAG_BOOLEAN = 0x22
IPP_TAG_ENUM = 0x23
IPP_TAG_STRING = 0x30
IPP_TAG_DATE = 0x31
IPP_TAG_RESOLUTION = 0x32
IPP_TAG_RANGE = 0x33
IPP_TAG_BEGIN_COLLECTION = 0x34
IPP_TAG_TEXTLANG = 0x35
IPP_TAG_NAMELANG = 0x36
IPP_TAG_END_COLLECTION = 0x37
IPP_TAG_TEXT = 0x41
IPP_TAG_NAME = 0x42
IPP_TAG_KEYWORD = 0x44
IPP_TAG_URI = 0x45
IPP_TAG_URISCHEME = 0x46
IPP_TAG_CHARSET = 0x47
IPP_TAG_LANGUAGE = 0x48
IPP_TAG_MIMETYPE = 0x49
IPP_TAG_MEMBERNAME = 0x4a
IPP_TAG_MASK = 0x7fffffff
IPP_TAG_COPY = -0x7fffffff-1

IPP_RES_PER_INCH = 3
IPP_RES_PER_CM = 4

IPP_FINISHINGS_NONE = 3
IPP_FINISHINGS_STAPLE = 4
IPP_FINISHINGS_PUNCH = 5
IPP_FINISHINGS_COVER = 6
IPP_FINISHINGS_BIND = 7
IPP_FINISHINGS_SADDLE_STITCH = 8
IPP_FINISHINGS_EDGE_STITCH = 9
IPP_FINISHINGS_FOLD = 10
IPP_FINISHINGS_TRIM = 11
IPP_FINISHINGS_BALE = 12
IPP_FINISHINGS_BOOKLET_MAKER = 13
IPP_FINISHINGS_JOB_OFFSET = 14
IPP_FINISHINGS_STAPLE_TOP_LEFT = 20
IPP_FINISHINGS_STAPLE_BOTTOM_LEFT = 21
IPP_FINISHINGS_STAPLE_TOP_RIGHT = 22
IPP_FINISHINGS_STAPLE_BOTTOM_RIGHT = 23
IPP_FINISHINGS_EDGE_STITCH_LEFT = 24
IPP_FINISHINGS_EDGE_STITCH_TOP = 25
IPP_FINISHINGS_EDGE_STITCH_RIGHT = 26
IPP_FINISHINGS_EDGE_STITCH_BOTTOM = 27
IPP_FINISHINGS_STAPLE_DUAL_LEFT = 28
IPP_FINISHINGS_STAPLE_DUAL_TOP = 29
IPP_FINISHINGS_STAPLE_DUAL_RIGHT = 30
IPP_FINISHINGS_STAPLE_DUAL_BOTTOM = 31
IPP_FINISHINGS_BIND_LEFT = 50
IPP_FINISHINGS_BIND_TOP = 51
IPP_FINISHINGS_BIND_RIGHT = 52
IPP_FINISHINGS_BIND_BOTTO = 53

IPP_PORTRAIT = 3
IPP_LANDSCAPE = 4
IPP_REVERSE_LANDSCAPE = 5
IPP_REVERSE_PORTRAIT = 6

IPP_QUALITY_DRAFT = 3
IPP_QUALITY_NORMAL = 4
IPP_QUALITY_HIGH = 5

IPP_JOB_PENDING = 3
IPP_JOB_HELD = 4
IPP_JOB_PROCESSING = 5
IPP_JOB_STOPPED = 6
IPP_JOB_CANCELLED = 7
IPP_JOB_ABORTED = 8
IPP_JOB_COMPLETE = 9

IPP_PRINTER_IDLE = 3
IPP_PRINTER_PROCESSING = 4
IPP_PRINTER_STOPPED = 5

IPP_ERROR = -1
IPP_IDLE = 0
IPP_HEADER = 1
IPP_ATTRIBUTE = 2
IPP_DATA = 3

IPP_PRINT_JOB = 0x0002
IPP_PRINT_URI = 0x0003
IPP_VALIDATE_JOB = 0x0004
IPP_CREATE_JOB = 0x0005
IPP_SEND_DOCUMENT = 0x0006
IPP_SEND_URI = 0x0007
IPP_CANCEL_JOB = 0x0008
IPP_GET_JOB_ATTRIBUTES = 0x0009
IPP_GET_JOBS = 0x000a
IPP_GET_PRINTER_ATTRIBUTES = 0x000b
IPP_HOLD_JOB = 0x000c
IPP_RELEASE_JOB = 0x000d
IPP_RESTART_JOB = 0x000e
IPP_PAUSE_PRINTER = 0x0010
IPP_RESUME_PRINTER = 0x0011
IPP_PURGE_JOBS = 0x0012
IPP_SET_PRINTER_ATTRIBUTES = 0x0013
IPP_SET_JOB_ATTRIBUTES = 0x0014
IPP_GET_PRINTER_SUPPORTED_VALUES = 0x0015
IPP_CREATE_PRINTER_SUBSCRIPTION = 0x0016
IPP_CREATE_JOB_SUBSCRIPTION = 0x0017
IPP_GET_SUBSCRIPTION_ATTRIBUTES = 0x0018
IPP_GET_SUBSCRIPTIONS = 0x0019
IPP_RENEW_SUBSCRIPTION = 0x001a
IPP_CANCEL_SUBSCRIPTION = 0x001b
IPP_GET_NOTIFICATIONS = 0x001c
IPP_SEND_NOTIFICATIONS = 0x001d
IPP_GET_PRINT_SUPPORT_FILES = 0x0021
IPP_ENABLE_PRINTER = 0x0022
IPP_DISABLE_PRINTER = 0x0023
IPP_PAUSE_PRINTER_AFTER_CURRENT_JOB = 0x0024
IPP_HOLD_NEW_JOBS = 0x0025
IPP_RELEASE_HELD_NEW_JOBS = 0x0026
IPP_DEACTIVATE_PRINTER = 0x0027
IPP_ACTIVATE_PRINTER = 0x0028
IPP_RESTART_PRINTER = 0x0029
IPP_SHUTDOWN_PRINTER = 0x002a
IPP_STARTUP_PRINTER = 0x002b
IPP_REPROCESS_JOB = 0x002c
IPP_CANCEL_CURRENT_JOB = 0x002d
IPP_SUSPEND_CURRENT_JOB = 0x002e
IPP_RESUME_JOB = 0x002f
IPP_PROMOTE_JOB = 0x0030
IPP_SCHEDULE_JOB_AFTER = 0x0031
IPP_PRIVATE = 0x4000
CUPS_GET_DEFAULT = 0x4001
CUPS_GET_PRINTERS = 0x4002
CUPS_ADD_PRINTER = 0x4003
CUPS_DELETE_PRINTER = 0x4004
CUPS_GET_CLASSES = 0x4005
CUPS_ADD_CLASS = 0x4006
CUPS_DELETE_CLASS = 0x4007
CUPS_ACCEPT_JOBS = 0x4008
CUPS_REJECT_JOBS = 0x4009
CUPS_SET_DEFAULT = 0x400a
CUPS_GET_DEVICES = 0x400b
CUPS_GET_PPDS = 0x400c
CUPS_MOVE_JOB = 0x400d
CUPS_AUTHENTICATE_JOB = 0x400e

IPP_OK = 0x0000
IPP_OK_SUBST = 0x0001
IPP_OK_CONFLICT = 0x0002
IPP_OK_IGNORED_SUBSCRIPTIONS = 0x0003
IPP_OK_IGNORED_NOTIFICATIONS = 0x0004
IPP_OK_TOO_MANY_EVENTS = 0x0005
IPP_OK_BUT_CANCEL_SUBSCRIPTION = 0x0006
IPP_REDIRECTION_OTHER_SITE = 0x0300
IPP_BAD_REQUEST = 0x0400
IPP_FORBIDDEN = 0x0401
IPP_NOT_AUTHENTICATED = 0x0402
IPP_NOT_AUTHORIZED = 0x0403
IPP_NOT_POSSIBLE = 0x0404
IPP_TIMEOUT = 0x0405
IPP_NOT_FOUND = 0x0406
IPP_GONE = 0x0407
IPP_REQUEST_ENTITY = 0x0408
IPP_REQUEST_VALUE = 0x0409
IPP_DOCUMENT_FORMAT = 0x040a
IPP_ATTRIBUTES = 0x040b
IPP_URI_SCHEME = 0x040c
IPP_CHARSET = 0x040d
IPP_CONFLICT = 0x040e
IPP_COMPRESSION_NOT_SUPPORTED = 0x040f
IPP_COMPRESSION_ERROR = 0x0410
IPP_DOCUMENT_FORMAT_ERROR = 0x0411
IPP_DOCUMENT_ACCESS_ERROR = 0x0412
IPP_ATTRIBUTES_NOT_SETTABLE = 0x0413
IPP_IGNORED_ALL_SUBSCRIPTIONS = 0x0414
IPP_TOO_MANY_SUBSCRIPTIONS = 0x0415
IPP_IGNORED_ALL_NOTIFICATIONS = 0x0416
IPP_PRINT_SUPPORT_FILE_NOT_FOUND = 0x0417

IPP_INTERNAL_ERROR = 0x0500
IPP_OPERATION_NOT_SUPPORTED = 0x0501
IPP_SERVICE_UNAVAILABLE = 0x0502
IPP_VERSION_NOT_SUPPORTED = 0x0503
IPP_DEVICE_ERROR = 0x0504
IPP_TEMPORARY_ERROR = 0x0505
IPP_NOT_ACCEPTING = 0x0506
IPP_PRINTER_BUSY = 0x0507
IPP_ERROR_JOB_CANCELLED = 0x0508
IPP_MULTIPLE_JOBS_NOT_SUPPORTED = 0x0509
IPP_PRINTER_IS_DEACTIVATED = 0x50a

CUPS_PRINTER_LOCAL = 0x0000
CUPS_PRINTER_CLASS = 0x0001
CUPS_PRINTER_REMOTE = 0x0002
CUPS_PRINTER_BW = 0x0004
CUPS_PRINTER_COLOR = 0x0008
CUPS_PRINTER_DUPLEX = 0x0010
CUPS_PRINTER_STAPLE = 0x0020
CUPS_PRINTER_COPIES = 0x0040
CUPS_PRINTER_COLLATE = 0x0080
CUPS_PRINTER_PUNCH = 0x0100
CUPS_PRINTER_COVER = 0x0200
CUPS_PRINTER_BIND = 0x0400
CUPS_PRINTER_SORT = 0x0800
CUPS_PRINTER_SMALL = 0x1000
CUPS_PRINTER_MEDIUM = 0x2000
CUPS_PRINTER_LARGE = 0x4000
CUPS_PRINTER_VARIABLE = 0x8000
CUPS_PRINTER_IMPLICIT = 0x1000
CUPS_PRINTER_DEFAULT = 0x2000
CUPS_PRINTER_FAX = 0x4000
CUPS_PRINTER_REJECTING = 0x8000
CUPS_PRINTER_DELETE = 0x1000
CUPS_PRINTER_NOT_SHARED = 0x2000
CUPS_PRINTER_AUTHENTICATED = 0x4000
CUPS_PRINTER_COMMANDS = 0x8000
CUPS_PRINTER_OPTIONS = 0xe6ff

CUPS_BACKEND_OK = 0
CUPS_BACKEND_FAILED = 1
CUPS_BACKEND_AUTH_REQUIRED = 2
CUPS_BACKEND_HOLD = 3
CUPS_BACKEND_STOP = 4
CUPS_BACKEND_CANCEL = 5

class FakeAttribute :
    """Fakes an IPPRequest attribute to simplify usage syntax."""
    def __init__(self, request, name) :
        """Initializes the fake attribute."""
        self.request = request
        self.name = name

    def __setitem__(self, key, value) :
        """Appends the value to the real attribute."""
        attributeslist = getattr(self.request, "_%s_attributes" % self.name)
        for i in range(len(attributeslist)) :
            attribute = attributeslist[i]
            for j in range(len(attribute)) :
                (attrname, attrvalue) = attribute[j]
                if attrname == key :
                    attribute[j][1].append(value)
                    return
            attribute.append((key, [value]))

    def __getitem__(self, key) :
        """Returns an attribute's value."""
        answer = []
        attributeslist = getattr(self.request, "_%s_attributes" % self.name)
        for i in range(len(attributeslist)) :
            attribute = attributeslist[i]
            for j in range(len(attribute)) :
                (attrname, attrvalue) = attribute[j]
                if attrname == key :
                    answer.extend(attrvalue)
        if answer :
            return answer
        raise KeyError(key)

class IPPRequest :
    """A class for IPP requests."""
    attributes_types = ("operation", "job", "printer", "unsupported", \
                                     "subscription", "event_notification")
    def __init__(self, data="", version=IPP_VERSION,
                                operation_id=None, \
                                request_id=None, \
                                debug=False) :
        """Initializes an IPP Message object.

           Parameters :

             data : the complete IPP Message's content.
             debug : a boolean value to output debug info on stderr.
        """
        self.debug = debug
        self._data = data
        self.parsed = False

        # Initializes message
        self.setVersion(version)
        self.setOperationId(operation_id)
        self.setRequestId(request_id)
        self.data = ""

        for attrtype in self.attributes_types :
            setattr(self, "_%s_attributes" % attrtype, [[]])

        # Initialize tags
        self.tags = [ None ] * 256 # by default all tags reserved

        # Delimiter tags
        self.tags[0x01] = "operation-attributes-tag"
        self.tags[0x02] = "job-attributes-tag"
        self.tags[0x03] = "end-of-attributes-tag"
        self.tags[0x04] = "printer-attributes-tag"
        self.tags[0x05] = "unsupported-attributes-tag"
        self.tags[0x06] = "subscription-attributes-tag"
        self.tags[0x07] = "event_notification-attributes-tag"

        # out of band values
        self.tags[0x10] = "unsupported"
        self.tags[0x11] = "reserved-for-future-default"
        self.tags[0x12] = "unknown"
        self.tags[0x13] = "no-value"
        self.tags[0x15] = "not-settable"
        self.tags[0x16] = "delete-attribute"
        self.tags[0x17] = "admin-define"

        # integer values
        self.tags[0x20] = "generic-integer"
        self.tags[0x21] = "integer"
        self.tags[0x22] = "boolean"
        self.tags[0x23] = "enum"

        # octetString
        self.tags[0x30] = "octetString-with-an-unspecified-format"
        self.tags[0x31] = "dateTime"
        self.tags[0x32] = "resolution"
        self.tags[0x33] = "rangeOfInteger"
        self.tags[0x34] = "begCollection" # TODO : find sample files for testing
        self.tags[0x35] = "textWithLanguage"
        self.tags[0x36] = "nameWithLanguage"
        self.tags[0x37] = "endCollection"

        # character strings
        self.tags[0x40] = "generic-character-string"
        self.tags[0x41] = "textWithoutLanguage"
        self.tags[0x42] = "nameWithoutLanguage"
        self.tags[0x44] = "keyword"
        self.tags[0x45] = "uri"
        self.tags[0x46] = "uriScheme"
        self.tags[0x47] = "charset"
        self.tags[0x48] = "naturalLanguage"
        self.tags[0x49] = "mimeMediaType"
        self.tags[0x4a] = "memberAttrName"

        # Reverse mapping to generate IPP messages
        self.tagvalues = {}
        for i in range(len(self.tags)) :
            value = self.tags[i]
            if value is not None :
                self.tagvalues[value] = i

    def __getattr__(self, name) :
        """Fakes attribute access."""
        if name in self.attributes_types :
            return FakeAttribute(self, name)
        else :
            raise AttributeError(name)

    def __str__(self) :
        """Returns the parsed IPP message in a readable form."""
        if not self.parsed :
            return ""
        mybuffer = []
        mybuffer.append("IPP version : %s.%s" % self.version)
        mybuffer.append("IPP operation Id : 0x%04x" % self.operation_id)
        mybuffer.append("IPP request Id : 0x%08x" % self.request_id)
        for attrtype in self.attributes_types :
            for attribute in getattr(self, "_%s_attributes" % attrtype) :
                if attribute :
                    mybuffer.append("%s attributes :" % attrtype.title())
                for (name, value) in attribute :
                    mybuffer.append("  %s : %s" % (name, value))
        if self.data :
            mybuffer.append("IPP datas : %s" % repr(self.data))
        return "\n".join(str(mybuffer))

    def logDebug(self, msg) :
        """Prints a debug message."""
        if self.debug :
            logging.debug(msg)

    def setVersion(self, version) :
        """Sets the request's operation id."""
        if version is not None :
            try :
                self.version = [int(p) for p in version.split(".")]
            except AttributeError :
                if len(version) == 2 : # 2-tuple
                    self.version = version
                else :
                    try :
                        self.version = [int(p) for p in str(float(version)).split(".")]
                    except :
                        self.version = [int(p) for p in IPP_VERSION.split(".")]

    def setOperationId(self, opid) :
        """Sets the request's operation id."""
        self.operation_id = opid

    def setRequestId(self, reqid) :
        """Sets the request's request id."""
        self.request_id = reqid

    def dump(self) :
        """Generates an IPP Message.

           Returns the message as a string of text.
        """

        mybuffer = []
        if None not in (self.version, self.operation_id) :
            mybuffer.append(chr(self.version[0]) + chr(self.version[1]))
            mybuffer.append(pack(">H", self.operation_id))
            mybuffer.append(pack(">I", self.request_id or 1))
            for attrtype in self.attributes_types :
                for attribute in getattr(self, "_%s_attributes" % attrtype) :
                    if attribute :
                        mybuffer.append(chr(self.tagvalues["%s-attributes-tag" % attrtype]))
                    for (attrname, value) in attribute :
                        nameprinted = 0
                        for (vtype, val) in value :
                            mybuffer.append(chr(self.tagvalues[vtype]))
                            if not nameprinted :
                                mybuffer.append(pack(">H", len(attrname)))
                                mybuffer.append(attrname)
                                nameprinted = 1
                            else :
                                mybuffer.append(pack(">H", 0))
                            if vtype in ("integer", "enum") :
                                mybuffer.append(pack(">H", 4))
                                mybuffer.append(pack(">I", val))
                            elif vtype == "boolean" :
                                mybuffer.append(pack(">H", 1))
                                mybuffer.append(chr(val))
                            else :
                                mybuffer.append(pack(">H", len(val)))
                                mybuffer.append(val)
            mybuffer.append(chr(self.tagvalues["end-of-attributes-tag"]))
        mybuffer.append(self.data)
        

        self.logDebug("mybuffer to str is %s" % str(mybuffer))
        
        cleanBuffer=[]
        for x in mybuffer:
            if isinstance(x, bytes):
                self.logDebug("x is bytes: %s" % str(x))
                cleanBuffer.append(str(x))
            elif isinstance(x, str):
                cleanBuffer.append(x)
            else:
                self.logDebug("uncatched type of %s " % str(x))
            #elif isinstance(x, char)
        self.logDebug("cleanBuffer to str is %s" % str(cleanBuffer))
        #return "".join([chr(x) for x in mybuffer])
#note: cleanbuffer should be string but must not be parsed to string otherwise \'HTTPResponse\' object is not subscriptable
        return "\n".join(cleanBuffer)

    def parse(self) :
        """Parses an IPP Request.

           NB : Only a subset of RFC2910 is implemented.
        """
        self._curname = None
        self._curattributes = None
        self.logDebug("data is %s" % str(self._data))
        self.setVersion((ord(self._data[0]), ord(self._data[1])))
        self.setOperationId(unpack(">H", self._data[2:4])[0])
        self.setRequestId(unpack(">I", self._data[4:8])[0])
        self.position = 8
        endofattributes = self.tagvalues["end-of-attributes-tag"]
        maxdelimiter = self.tagvalues["event_notification-attributes-tag"]
        nulloffset = lambda : 0
        try :
            tag = ord(self._data[self.position])
            while tag != endofattributes :
                self.position += 1
                name = self.tags[tag]
                if name is not None :
                    func = getattr(self, name.replace("-", "_"), nulloffset)
                    self.position += func()
                    if ord(self._data[self.position]) > maxdelimiter :
                        self.position -= 1
                        continue
                oldtag = tag
                tag = ord(self._data[self.position])
                if tag == oldtag :
                    self._curattributes.append([])
        except IndexError :
            raise IPPError("Unexpected end of IPP message.")

        self.data = self._data[self.position+1:]
        self.parsed = True

    def parseTag(self) :
        """Extracts information from an IPP tag."""
        pos = self.position
        tagtype = self.tags[ord(self._data[pos])]
        pos += 1
        posend = pos2 = pos + 2
        namelength = unpack(">H", self._data[pos:pos2])[0]
        if not namelength :
            name = self._curname
        else :
            posend += namelength
            self._curname = name = self._data[pos2:posend]
        pos2 = posend + 2
        valuelength = unpack(">H", self._data[posend:pos2])[0]
        posend = pos2 + valuelength
        value = self._data[pos2:posend]
        if tagtype in ("integer", "enum") :
            value = unpack(">I", value)[0]
        elif tagtype == "boolean" :
            value = ord(value)
        try :
            (oldname, oldval) = self._curattributes[-1][-1]
            if oldname == name :
                oldval.append((tagtype, value))
            else :
                raise IndexError
        except IndexError :
            self._curattributes[-1].append((name, [(tagtype, value)]))
        self.logDebug("%s(%s) : %s" % (name, tagtype, value))
        return posend - self.position

    def operation_attributes_tag(self) :
        """Indicates that the parser enters into an operation-attributes-tag group."""
        self._curattributes = self._operation_attributes
        return self.parseTag()

    def job_attributes_tag(self) :
        """Indicates that the parser enters into a job-attributes-tag group."""
        self._curattributes = self._job_attributes
        return self.parseTag()

    def printer_attributes_tag(self) :
        """Indicates that the parser enters into a printer-attributes-tag group."""
        self._curattributes = self._printer_attributes
        return self.parseTag()

    def unsupported_attributes_tag(self) :
        """Indicates that the parser enters into an unsupported-attributes-tag group."""
        self._curattributes = self._unsupported_attributes
        return self.parseTag()

    def subscription_attributes_tag(self) :
        """Indicates that the parser enters into a subscription-attributes-tag group."""
        self._curattributes = self._subscription_attributes
        return self.parseTag()

    def event_notification_attributes_tag(self) :
        """Indicates that the parser enters into an event-notification-attributes-tag group."""
        self._curattributes = self._event_notification_attributes
        return self.parseTag()


class CUPS :
    """A class for a CUPS instance."""
    def __init__(self, url=None, username=None, password=None, charset="utf-8", language="en-us", debug=False) :
        """Initializes the CUPS instance."""
        if url is not None :
            self.url = url.replace("ipp://", "http://")
            if self.url.endswith("/") :
                self.url = self.url[:-1]
        else :
            self.url = self.getDefaultURL()
        self.username = username
        self.password = password
        self.charset = charset
        self.language = language
        self.debug = debug
        self.lastError = None
        self.lastErrorMessage = None
        self.requestId = None

    def getDefaultURL(self):
        """Builds a default URL."""
        # TODO : encryption methods.
        server = os.environ.get("CUPS_SERVER") or "localhost"
        port = os.environ.get("IPP_PORT") or 631
        if server.startswith("/"):
            # it seems it's a unix domain socket.
            # TODO
            # we can't handle this right now, so we use the default instead.
            return "http://localhost:%s" % port
        else:
            return "http://%s:%s" % (server, port)

    def identifierToURI(self, service, ident):
        """Transforms an identifier into a particular URI depending on requested service."""
        return "%s/%s/%s" % (self.url.replace("http://", "ipp://"),
                             service,
                             ident)

    def nextRequestId(self):
        """Increments the current request id and returns the new value."""
        try :
            self.requestId += 1
        except TypeError :
            self.requestId = 1
        return self.requestId

    def newRequest(self, operationid=None) :
        """Generates a new empty request."""
        if operationid is not None :
            req = IPPRequest(operation_id=operationid, \
                             request_id=self.nextRequestId(), \
                             debug=self.debug)
            req.operation["attributes-charset"] = ("charset", self.charset)
            req.operation["attributes-natural-language"] = ("naturalLanguage", self.language)
            wrapper.logDebug("charset is set to %s" % req.operation["attributes-charset"])
            wrapper.logDebug("language is set to %s" % req.operation["attributes-natural-language"])
            return req

    def doRequest(self, req, url=None) :
        """Sends a request to the CUPS server.
           returns a new IPPRequest object, containing the parsed answer.
        """
        data = req.dump()
        headers = {'Connection':'close','Content-Type': 'application/ipp','Accept-Encoding':'identity'}
        auth=None
        if self.username :
            auth=(self.username, self.password or "")

        # TODO proper error handling
        #self.lastError = None
        #self.lastErrorMessage = None

        r = requests.post(url=url or self.url, auth=auth, data=data, headers=headers, stream=True)
        r.raw.decode_content=True

        datas = r.raw

        wrapper.logDebug("data to parse: %s" % str(datas))
        wrapper.logDebug("request content: %s"%str(r.raw.content))
        ippresponse = IPPRequest(datas)

        ippresponse.parse()
        return ippresponse

    def getPPD(self, queuename) :
        """Retrieves the PPD for a particular queuename."""
        req = self.newRequest(IPP_GET_PRINTER_ATTRIBUTES)
        req.operation["printer-uri"] = ("uri", self.identifierToURI("printers", queuename))
        for attrib in ("printer-uri-supported", "printer-type", "member-uris") :
            req.operation["requested-attributes"] = ("nameWithoutLanguage", attrib)
        return self.doRequest(req)  # TODO : get the PPD from the actual print server

    def getDefault(self) :
        """Retrieves CUPS' default printer."""
        return self.doRequest(self.newRequest(CUPS_GET_DEFAULT))

    def getJobAttributes(self, jobid) :
        """Retrieves a print job's attributes."""
        req = self.newRequest(IPP_GET_JOB_ATTRIBUTES)
        req.operation["job-uri"] = ("uri", self.identifierToURI("jobs", jobid))
        return self.doRequest(req)

    def getPrinters(self) :
        """Returns the list of print queues names."""
        req = self.newRequest(CUPS_GET_PRINTERS)
        req.operation["requested-attributes"] = ("keyword", "printer-name")
        req.operation["printer-type"] = ("enum", 0)
        req.operation["printer-type-mask"] = ("enum", CUPS_PRINTER_CLASS)
        return [printer[1] for printer in self.doRequest(req).printer["printer-name"]]

    def getDevices(self) :
        """Returns a list of devices as (deviceclass, deviceinfo, devicemakeandmodel, deviceuri) tuples."""
        answer = self.doRequest(self.newRequest(CUPS_GET_DEVICES))
        return list(zip([d[1] for d in answer.printer["device-class"]], \
                   [d[1] for d in answer.printer["device-info"]], \
                   [d[1] for d in answer.printer["device-make-and-model"]], \
                   [d[1] for d in answer.printer["device-uri"]]))

    def getPPDs(self) :
        """Returns a list of PPDs as (ppdnaturallanguage, ppdmake, ppdmakeandmodel, ppdname) tuples."""
        answer = self.doRequest(self.newRequest(CUPS_GET_PPDS))
        return list(zip([d[1] for d in answer.printer["ppd-natural-language"]], \
                   [d[1] for d in answer.printer["ppd-make"]], \
                   [d[1] for d in answer.printer["ppd-make-and-model"]], \
                   [d[1] for d in answer.printer["ppd-name"]]))

    def createSubscription(self, uri, events=["all"],
                                      userdata=None,
                                      recipient=None,
                                      pullmethod=None,
                                      charset=None,
                                      naturallanguage=None,
                                      leaseduration=None,
                                      timeinterval=None,
                                      jobid=None) :
        """Creates a job, printer or server subscription.

           uri : the subscription's uri, e.g. ipp://server
           events : a list of events to subscribe to, e.g. ["printer-added", "printer-deleted"]
           recipient : the notifier's uri
           pullmethod : the pull method to use
           charset : the charset to use when sending notifications
           naturallanguage : the language to use when sending notifications
           leaseduration : the duration of the lease in seconds
           timeinterval : the interval of time during notifications
           jobid : the optional job id in case of a job subscription
        """
        if jobid is not None :
            opid = IPP_CREATE_JOB_SUBSCRIPTION
            uritype = "job-uri"
        else :
            opid = IPP_CREATE_PRINTER_SUBSCRIPTION
            uritype = "printer-uri"
        req = self.newRequest(opid)
        req.operation[uritype] = ("uri", uri)
        for event in events :
            req.subscription["notify-events"] = ("keyword", event)
        if userdata is not None :
            req.subscription["notify-user-data"] = ("octetString-with-an-unspecified-format", userdata)
        if recipient is not None :
            req.subscription["notify-recipient"] = ("uri", recipient)
        if pullmethod is not None :
            req.subscription["notify-pull-method"] = ("keyword", pullmethod)
        if charset is not None :
            req.subscription["notify-charset"] = ("charset", charset)
        if naturallanguage is not None :
            req.subscription["notify-natural-language"] = ("naturalLanguage", naturallanguage)
        if leaseduration is not None :
            req.subscription["notify-lease-duration"] = ("integer", leaseduration)
        if timeinterval is not None :
            req.subscription["notify-time-interval"] = ("integer", timeinterval)
        if jobid is not None :
            req.subscription["notify-job-id"] = ("integer", jobid)
        return self.doRequest(req)

    def cancelSubscription(self, uri, subscriptionid, jobid=None) :
        """Cancels a subscription.

           uri : the subscription's uri.
           subscriptionid : the subscription's id.
           jobid : the optional job's id.
        """
        req = self.newRequest(IPP_CANCEL_SUBSCRIPTION)
        if jobid is not None :
            uritype = "job-uri"
        else :
            uritype = "printer-uri"
        req.operation[uritype] = ("uri", uri)
        req.event_notification["notify-subscription-id"] = ("integer", subscriptionid)
        return self.doRequest(req)


class FakeConfig :
    """Fakes a configuration file parser."""
    def get(self, section, option, raw=0) :
        """Fakes the retrieval of an option."""
        raise ConfigError("Invalid configuration file : no option %s in section [%s]" % (option, section))

def isTrue(option) :
    """Returns 1 if option is set to true, else 0."""
    if (option is not None) and (option.upper().strip() in ['Y', 'YES', '1', 'ON', 'T', 'TRUE']) :
        return 1
    else :
        return 0

def getCupsConfigDirectives(directives=[]) :
    """Retrieves some CUPS directives from its configuration file.

       Returns a mapping with lowercased directives as keys and
       their setting as values.
    """
    dirvalues = {}
    cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups")
    cupsdconf = os.path.join(cupsroot, "cupsd.conf")
    try :
        conffile = open(cupsdconf, "r")
    except IOError :
        raise TeeError("Unable to open %s" % cupsdconf)
    else :
        for line in conffile.readlines() :
            linecopy = line.strip().lower()
            for di in [d.lower() for d in directives] :
                if linecopy.startswith("%s " % di) :
                    try :
                        val = line.split()[1]
                    except :
                        pass # ignore errors, we take the last value in any case.
                    else :
                        dirvalues[di] = val
        conffile.close()
    return dirvalues

class CupsBackend :
    """Base class for tools with no database access."""
    def __init__(self) :
        """Initializes the CUPS backend wrapper."""
        signal.signal(signal.SIGTERM, signal.SIG_IGN)
        signal.signal(signal.SIGPIPE, signal.SIG_IGN)
        self.MyName = "Tea4CUPS"
        self.myname = "tea4cups"
        self.pid = os.getpid()
        self.debug = True
        self.isCancelled = False
        self.Title = None
        self.InputFile = None
        self.RealBackend = None
        self.ControlFile = None
        self.ClientHost = None
        self.PrinterName = None
        self.JobBilling = None
        self.UserName = None
        self.Copies = None
        self.Options = None
        self.DataFile = None
        self.JobMD5Sum = None
        self.JobSize = None
        self.DeviceURI = None
        self.JobId = None
        self.Directory = None
        self.pipes = {}
        self.config = None
        self.conffile = None
        self.LockFile = None

    def waitForLock(self) :
        """Waits until we can acquire the lock file."""
        random.seed()
        lockfilename = self.DeviceURI.replace("/", ".")
        lockfilename = lockfilename.replace(":", ".")
        lockfilename = lockfilename.replace("?", ".")
        lockfilename = lockfilename.replace("&", ".")
        lockfilename = lockfilename.replace("@", ".")
        lockfilename = os.path.join(self.Directory, "%s-%s..LCK" % (self.myname, lockfilename))
        self.logDebug("Waiting for lock %s to become available..." % lockfilename)
        haslock = False
        while not haslock :
            try :
                # open the lock file, optionally creating it if needed.
                self.LockFile = None
                if not os.path.isfile(lockfilename):
                    open(lockfilename, 'w+').close()
                self.LockFile = open(lockfilename, "a+")

                # we wait indefinitely for the lock to become available.
                # works over NFS too.
                fcntl.lockf(self.LockFile, fcntl.LOCK_EX)
                haslock = True

                self.logDebug("Lock %s acquired." % lockfilename)

                # Here we save the PID in the lock file, but we don't use
                # it, because the lock file may be in a directory shared
                # over NFS between two (or more) print servers, so the PID
                # has no meaning in this case.
                self.LockFile.truncate(0)
                self.LockFile.seek(0, 0)
                self.LockFile.write(str(self.pid))
                self.LockFile.flush()
            except IOError :
                self.logDebug("I/O Error while waiting for lock %s" % lockfilename)
                if self.LockFile is not None :
                    self.LockFile.close()
                time.sleep(0.25 + random.random())

    def readConfig(self) :
        """Reads the configuration file."""
        confdir = os.environ.get("CUPS_SERVERROOT", ".")
        self.conffile = os.path.join(confdir, "%s.conf" % self.myname)
        if os.path.isfile(self.conffile) :
            self.config = configparser.ConfigParser()
            self.config.read([self.conffile])
            self.debug = True
            #isTrue(self.getGlobalOption("debug", ignore=1))
        else :
            self.config = FakeConfig()
            self.debug = True      # no config, so force debug mode !
            logging.warning("no config found")

    def logInfo(self, message, level="info") :
        """Logs a message to CUPS' error_log file."""
        try :
            logging.debug(message)
            sys.stderr.write("[TEA4CUPS]_%s: %s v%s (PID %i) : %s\n" % (level.upper(), self.MyName, __version__, os.getpid(), message))
            sys.stderr.flush()
        except IOError :
            pass

# TODO are there two logDebug?
    def logDebug(self, message) :
        """Logs something to debug output if debug is enabled."""
        if self.debug :
            self.logInfo(message, level="debug")

    def getGlobalOption(self, option, ignore=0) :
        """Returns an option from the global section, or raises a ConfigError if ignore is not set, else returns None."""
        try :
            return self.config.get("global", option, raw=1)
        except (configparser.NoSectionError, configparser.NoOptionError) :
            if not ignore :
                raise ConfigError("Option %s not found in section global of %s" % (option, self.conffile))

    def getPrintQueueOption(self, printqueuename, option, ignore=0) :
        """Returns an option from the printer section, or the global section, or raises a ConfigError."""
        globaloption = self.getGlobalOption(option, ignore=1)
        try :
            return self.config.get(printqueuename, option, raw=1)
        except (configparser.NoSectionError, configparser.NoOptionError) :
            if globaloption is not None :
                return globaloption
            elif not ignore :
                raise ConfigError("Option %s not found in section [%s] of %s" % (option, printqueuename, self.conffile))

    def enumBranches(self, printqueuename, branchtype="tee") :
        """Returns the list of branchtypes branches for a particular section's."""
        branchbasename = "%s_" % branchtype.lower()
        try :
            globalbranches = [ (k, self.config.get("global", k)) for k in self.config.options("global") if k.startswith(branchbasename) ]
        except configparser.NoSectionError as msg :
            raise ConfigError("Invalid configuration file : %s" % msg)
        try :
            sectionbranches = [ (k, self.config.get(printqueuename, k)) for k in self.config.options(printqueuename) if k.startswith(branchbasename) ]
        except configparser.NoSectionError as msg :
            self.logInfo("No section for print queue %s : %s" % (printqueuename, msg))
            sectionbranches = []
        branches = {}
        for (k, v) in globalbranches :
            value = v.strip()
            if value :
                branches[k] = value
        for (k, v) in sectionbranches :
            value = v.strip()
            if value :
                branches[k] = value # overwrite any global option or set a new value
            else :
                del branches[k] # empty value disables a global option
        return branches

    def discoverOtherBackends(self) :
        """Discovers the other CUPS backends.

           Executes each existing backend in turn in device enumeration mode.
           Returns the list of available backends.
        """
        # Unfortunately this method can't output any debug information
        # to stdout or stderr, else CUPS considers that the device is
        # not available.
        available = []
        (directory, myname) = os.path.split(sys.argv[0])
        if not directory :
            directory = "./"
        tmpdir = tempfile.gettempdir()
        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
        if os.path.exists(lockfilename) :
            lockfile = open(lockfilename, "r")
            pid = int(lockfile.read())
            lockfile.close()
            try :
                # see if the pid contained in the lock file is still running
                os.kill(pid, 0)
            except OSError as error :
                if error.errno != errno.EPERM :
                    # process doesn't exist anymore
                    os.remove(lockfilename)

        if not os.path.exists(lockfilename) :
            lockfile = open(lockfilename, "w")
            lockfile.write("%i" % self.pid)
            lockfile.close()
            allbackends = [ os.path.join(directory, b) \
                                for b in os.listdir(directory)
                                    if os.access(os.path.join(directory, b), os.X_OK) \
                                        and (b != myname)]
            for backend in allbackends :
                answer = os.popen(backend, "r")
                try :
                    devices = [deviceline.strip() for deviceline in answer.readlines()]
                except :
                    devices = []
                status = answer.close()
                if status is None :
                    for d in devices :
                        # each line is of the form :
                        # 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
                        # so we have to decompose it carefully
                        fdevice = io.StringIO(d)
                        tokenizer = shlex.shlex(fdevice)
                        tokenizer.wordchars = tokenizer.wordchars + \
                                                        r".:,?!~/\_$*-+={}[]()#"
                        arguments = []
                        while 1 :
                            token = tokenizer.get_token()
                            if token :
                                arguments.append(token)
                            else :
                                break
                        fdevice.close()
                        try :
                            (devicetype, device, name, fullname) = arguments
                        except ValueError :
                            pass    # ignore this 'bizarre' device
                        else :
                            if name.startswith('"') and name.endswith('"') :
                                name = name[1:-1]
                            if fullname.startswith('"') and fullname.endswith('"') :
                                fullname = fullname[1:-1]
                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \
                                                 % (devicetype, self.myname, device, self.MyName, name, self.MyName, fullname))
            os.remove(lockfilename)
        available.append('direct %s:// "%s+Nothing" "%s managed Virtual Printer"' \
                             % (self.myname, self.MyName, self.MyName))
        return available

    def initBackend(self) :
        """Initializes the backend's attributes."""
        self.JobId = sys.argv[1].strip()
        self.UserName = sys.argv[2].strip() or pwd.getpwuid(os.geteuid())[0] # use CUPS' user when printing test pages from CUPS' web interface
        self.Title = sys.argv[3].strip()
        self.Copies = int(sys.argv[4].strip())
        self.Options = sys.argv[5].strip()
        if len(sys.argv) == 7 :
            self.InputFile = sys.argv[6] # read job's datas from file
        else :
            self.InputFile = None        # read job's datas from stdin
        self.PrinterName = os.environ.get("PRINTER", "")
        self.Directory = self.getPrintQueueOption(self.PrinterName, "directory", ignore=1) or tempfile.gettempdir()
        self.DataFile = os.path.join(self.Directory, "%s-%s-%s-%s" % (self.myname, self.PrinterName, self.UserName, self.JobId))

        # check that the DEVICE_URI environment variable's value is
        # prefixed with self.myname otherwise don't touch it.
        # If this is the case, we have to remove the prefix from
        # the environment before launching the real backend
        muststartwith = "%s:" % self.myname
        device_uri = os.environ.get("DEVICE_URI", "")
        if device_uri.startswith(muststartwith) :
            fulldevice_uri = device_uri[:]
            device_uri = fulldevice_uri[len(muststartwith):]
            for dummy in range(2) :
                if device_uri.startswith("/") :
                    device_uri = device_uri[1:]
        try :
            (backend, dummy) = device_uri.split(":", 1)
        except ValueError :
            if not device_uri :
                self.logDebug("Not attached to an existing print queue.")
                backend = ""
            else :
                raise TeeError("Invalid DEVICE_URI : %s\n" % device_uri)

        self.RealBackend = backend
        self.DeviceURI = device_uri

        try :
            cupsserver = CUPS() # TODO : username and password and/or encryption
            answer = cupsserver.getJobAttributes(self.JobId)
            self.logInfo("answer is %s" % str(answer), "warn")
            if answer is None :  # probably connection refused because we
                raise ValueError # don't hande unix domain sockets yet.
            self.ControlFile = "NotUsedAnymore"
        except :
            (ippfilename, answer) = self.parseIPPRequestFile()
            self.ControlFile = ippfilename

        try :
            john = answer.job["job-originating-host-name"]
        except (KeyError, AttributeError) :
            try :
                john = answer.operation["job-originating-host-name"]
            except (KeyError, AttributeError) :
                john = (None, None)
        if type(john) == type([]) :
            john = john[-1]
        (dummy, self.ClientHost) = john
        try :
            jbing = answer.job["job-billing"]
        except (KeyError, AttributeError) :
            jbing = (None, None)
        if type(jbing) == type([]) :
            jbing = jbing[-1]
        (dummy, self.JobBilling) = jbing
        try :
            self.JobBilling = str(self.JobBilling) # In some cases it seems to be an integer : bug in pkipplib or something else ???
        except :
            pass 

    def parseIPPRequestFile(self) :
        """Parses the IPP message file and returns a tuple (filename, parsedvalue)."""
        requestroot = os.environ.get("CUPS_REQUESTROOT")
        if requestroot is None :
            cupsdconf = getCupsConfigDirectives(["RequestRoot"])
            requestroot = cupsdconf.get("requestroot", "/var/spool/cups")
            
        if (len(self.JobId) < 5) and self.JobId.isdigit() :
            ippmessagefile = "c%05i" % int(self.JobId)
        else :
            ippmessagefile = "c%s" % self.JobId
        ippmessagefile = os.path.join(requestroot, ippmessagefile)
        ippmessage = {}
        try :
            ippdatafile = open(ippmessagefile,"r")
        except TeeError:
            self.logDebug("IOError: Unable to open IPP message file %s" % ippmessagefile)
            self.logDebug("Debug: user: %s"  % (os.getgroups()))
        except FileNotFoundError:
            # TODO I always get "File Not Found" for /var/spool/cups/cxxxx
            self.logDebug("File Not found: %s" % ippmessagefile)
            self.logDebug("Debug: user: %s"  % (os.getgroups()))
        else:
            self.logDebug("Parsing of IPP message file %s begins." % ippmessagefile)
            try :
                ippmessage = IPPRequest(ippdatafile.read())
                ippmessage.parse()
            except IPPError as msg :
                self.logDebug("Error while parsing %s : %s" % (ippmessagefile, msg))
            else :
                self.logDebug("Parsing of IPP message file %s ends." % ippmessagefile)
            ippdatafile.close()
        return (ippmessagefile, ippmessage)

    def exportAttributes(self) :
        """Exports our backend's attributes to the environment."""
        os.environ["DEVICE_URI"] = self.DeviceURI       # WARNING !
        os.environ["TEAPRINTERNAME"] = self.PrinterName
        os.environ["TEADIRECTORY"] = self.Directory
        os.environ["TEADATAFILE"] = self.DataFile
        os.environ["TEAJOBSIZE"] = str(self.JobSize)
        os.environ["TEAMD5SUM"] = self.JobMD5Sum
        os.environ["TEACLIENTHOST"] = self.ClientHost or ""
        os.environ["TEAJOBID"] = self.JobId
        os.environ["TEAUSERNAME"] = self.UserName
        os.environ["TEATITLE"] = self.Title
        os.environ["TEACOPIES"] = str(self.Copies)
        os.environ["TEAOPTIONS"] = self.Options
        os.environ["TEAINPUTFILE"] = self.InputFile or ""
        os.environ["TEABILLING"] = self.JobBilling or ""
        os.environ["TEACONTROLFILE"] = self.ControlFile

    def saveDatasAndCheckSum(self) :
        """Saves the input datas into a static file."""
        self.logDebug("Duplicating data stream into %s" % self.DataFile)
        mustclose = 0
        if self.InputFile is not None :
            infile = open(self.InputFile, "rb")
            mustclose = 1
        else :
            # https://stackoverflow.com/a/32282458
            infile = sys.stdin.buffer

        filtercommand = self.getPrintQueueOption(self.PrinterName, "filter", \
                                                 ignore=1)
        if filtercommand :
            self.logDebug("Data stream will be filtered through [%s]" % filtercommand)
            filteroutput = "%s.filteroutput" % self.DataFile
            outf = open(filteroutput, "wb")
            filterstatus = self.stdioRedirSystem(filtercommand, infile.fileno(), outf.fileno())
            outf.close()
            self.logDebug("Filter's output status : %s" % repr(filterstatus))
            if mustclose :
                infile.close()
            infile = open(filteroutput, "rb")
            mustclose = 1
        else :
            self.logDebug("Data stream will be used as-is (no filter defined)")

        CHUNK = 64*1024         # read 64 Kb at a time
        dummy = 0
        sizeread = 0
        checksum = ""
        outfile = open(self.DataFile, "wb") # was wb
        while 1 :
            data = infile.read(CHUNK)
            if not data :
                break
            sizeread += len(data)
            outfile.write(data)
            checksum = hashlib.md5(data)
            if not (dummy % 32) : # Only display every 2 Mb
                self.logDebug("%s bytes saved..." % sizeread)
            dummy += 1
        outfile.close()

        if filtercommand :
            self.logDebug("Removing filter's output file %s" % filteroutput)
            try :
                os.remove(filteroutput)
            except :
                pass

        if mustclose :
            infile.close()

        self.logDebug("%s bytes saved..." % sizeread)
        self.JobSize = sizeread
        self.JobMD5Sum = checksum.hexdigest()
        self.logDebug("Job %s is %s bytes long." % (self.JobId, self.JobSize))
        self.logDebug("Job %s MD5 sum is %s" % (self.JobId, self.JobMD5Sum))

    def cleanUp(self) :
        """Cleans up the place."""
        self.logDebug("Cleaning up...")
        if (not isTrue(self.getPrintQueueOption(self.PrinterName, "keepfiles", ignore=1))) \
            and os.path.exists(self.DataFile) :
            try :
                os.remove(self.DataFile)
            except OSError as msg :
                self.logInfo("Problem when removing %s : %s" % (self.DataFile, msg), "error")

        if self.LockFile is not None :
            self.logDebug("Removing lock...")
            try :
                fcntl.lockf(self.LockFile, fcntl.LOCK_UN)
                self.LockFile.close()
            except :
                self.logInfo("Problem while unlocking.", "error")
            else :
                self.logDebug("Lock removed.")
        self.logDebug("Clean.")

    def runBranches(self) :
        """Launches each hook defined for the current print queue."""
        self.isCancelled = False    # did a prehook cancel the print job ?
        serialize = isTrue(self.getPrintQueueOption(self.PrinterName, "serialize", ignore=1))
        self.pipes = { 0: (0, 1) }
        branches = self.enumBranches(self.PrinterName, "prehook")
        for b in list(branches.keys()) :
            self.pipes[b.split("_", 1)[1]] = os.pipe()
        retcode = self.runCommands("prehook", branches, serialize)
        for p in [ (k, v) for (k, v) in list(self.pipes.items()) if k != 0 ] :
            os.close(p[1][1])
        if self.isCancelled :
            retcode = CUPS_BACKEND_CANCEL # Job cancelled, for CUPS.
        elif retcode == CUPS_BACKEND_OK :
            if self.RealBackend :
                retcode = self.launchOriginalBackend()
                if retcode != CUPS_BACKEND_OK :
                    onfail = self.getPrintQueueOption(self.PrinterName, \
                                                      "onfail", ignore=1)
                    if onfail :
                        self.logDebug("Launching onfail script %s" % onfail)
                        os.system(onfail)

            os.environ["TEASTATUS"] = str(retcode)
            branches = self.enumBranches(self.PrinterName, "posthook")
            if self.runCommands("posthook", branches, serialize) :
                self.logInfo("An error occured during the execution of posthooks.", "warn")

        for p in [ (k, v) for (k, v) in list(self.pipes.items()) if k != 0 ] :
            os.close(p[1][0])
        if retcode == CUPS_BACKEND_OK :
            self.logInfo("OK")
        else :
            self.logInfo("An error occured, please check CUPS' error_log file.")
        return retcode

    def stdioRedirSystem(self, cmd, stdin=0, stdout=1) :
        """Launches a command with stdio redirected."""
        # Code contributed by Peter Stuge on May 23rd and June 7th 2005
        pid = os.fork()
        if pid == 0 :
            if stdin != 0 :
                os.dup2(stdin, 0)
                os.close(stdin)
            if stdout != 1 :
                os.dup2(stdout, 1)
                os.close(stdout)
            try :
                os.execl("/bin/sh", "sh", "-c", cmd)
            except OSError as msg :
                self.logDebug("execl() failed: %s" % msg)
            os._exit(-1)
        status = os.waitpid(pid, 0)[1]
        if os.WIFEXITED(status) :
            return os.WEXITSTATUS(status)
        return -1

    def runCommand(self, branch, command) :
        """Runs a particular branch command."""
        # Code contributed by Peter Stuge on June 7th 2005
        self.logDebug("Launching %s : %s" % (branch, command))
        btype, bname = branch.split("_", 1)
        if bname not in list(self.pipes.keys()) :
            bname = 0
        if btype == "prehook" :
            return self.stdioRedirSystem(command, 0, self.pipes[bname][1])
        else :
            return self.stdioRedirSystem(command, self.pipes[bname][0])

    def runCommands(self, btype, branches, serialize) :
        """Runs the commands for a particular branch type."""
        exitcode = CUPS_BACKEND_OK
        btype = btype.lower()
        btypetitle = btype.title()
        branchlist = list(branches.keys())
        branchlist.sort()
        if serialize :
            self.logDebug("Begin serialized %ss" % btypetitle)
            for branch in branchlist :
                retcode = self.runCommand(branch, branches[branch])
                self.logDebug("Exit code for %s %s on printer %s is %s" % (btype, branch, self.PrinterName, retcode))
                if retcode :
                    if (btype == "prehook") and (retcode == 255) : # -1
                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
                        self.isCancelled = True
                    elif (btype == "prehook") and (retcode == 254) : # -2
                        self.logInfo("Job %s hook processing stopped by prehook %s" % (self.JobId, branch))
                        exitcode = CUPS_BACKEND_FAILED
                        break
                    else :
                        self.logInfo("%s %s on printer %s didn't exit successfully." % (btypetitle, branch, self.PrinterName), "error")
                        exitcode = CUPS_BACKEND_FAILED
            self.logDebug("End serialized %ss" % btypetitle)
        else :
            self.logDebug("Begin forked %ss" % btypetitle)
            pids = {}
            for branch in branchlist :
                pid = os.fork()
                if pid :
                    pids[branch] = pid
                else :
                    os._exit(self.runCommand(branch, branches[branch]))
            for (branch, pid) in list(pids.items()) :
                retcode = os.waitpid(pid, 0)[1]
                if os.WIFEXITED(retcode) :
                    retcode = os.WEXITSTATUS(retcode)
                else :
                    retcode = -1
                self.logDebug("Exit code for %s %s (PID %s) on printer %s is %s" % (btype, branch, pid, self.PrinterName, retcode))
                if retcode :
                    if (btype == "prehook") and (retcode == 255) : # -1
                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
                        self.isCancelled = True
                    else :
                        self.logInfo("%s %s (PID %s) on printer %s didn't exit successfully." % (btypetitle, branch, pid, self.PrinterName), "error")
                        exitcode = CUPS_BACKEND_FAILED
            self.logDebug("End forked %ss" % btypetitle)
        return exitcode

    def launchOriginalBackend(self) :
        """Launches the original backend, optionally retrying if needed."""
        number = 1
        delay = 0
        retry = self.getPrintQueueOption(self.PrinterName, "retry", ignore=1)
        if retry is not None :
            try :
                (number, delay) = [int(p) for p in retry.strip().split(",")]
            except (ValueError, AttributeError, TypeError) :
                self.logInfo("Invalid value '%s' for the 'retry' directive for printer %s in %s." % (retry, self.PrinterName, self.conffile), "error")
                number = 1
                delay = 0

        loopcount = 1
        while 1 :
            retcode = self.runOriginalBackend()
            if retcode == CUPS_BACKEND_OK :
                break
            else :
                if (not number) or (loopcount < number) :
                    self.logInfo("The real backend produced an error, we will try again in %s seconds." % delay, "warn")
                    time.sleep(delay)
                    loopcount += 1
                else :
                    break
        return retcode

    def runOriginalBackend(self) :
        """Launches the original backend."""
        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
        arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:]
        self.logDebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in arguments])))

        pid = os.fork()
        if pid == 0 :
            if self.InputFile is None :
                f = open(self.DataFile, "rb")
                os.dup2(f.fileno(), 0)
                f.close()
            else :
                arguments[6] = self.DataFile # in case a tea4cups filter was applied
            try :
                os.execve(originalbackend, arguments, os.environ)
            except OSError as msg :
                self.logDebug("originalbackend: %s, arguments: %s, os.environ: %s" %originalbackend %arguments %os.environ)
                self.logDebug("execve() failed: %s" % msg)
                logging.error("could run original backend because execve failed: %s" %msg)
            os._exit(-1)
        killed = 0
        status = -1
        while status == -1 :
            try :
                status = os.waitpid(pid, 0)[1]
                # TODO there seems to be some work to be done
            except OSError as xxx_todo_changeme :
                (err, msg) = xxx_todo_changeme.args
                if err == 4 :
                    killed = 1
        if os.WIFEXITED(status) :
            status = os.WEXITSTATUS(status)
            if status :
                self.logInfo("CUPS backend %s returned %d." % (originalbackend,
                                                               status),
                             "error")
            return status
        elif not killed :
            self.logInfo("CUPS backend %s died abnormally." % originalbackend, \
                                                              "error")
            return -1
        else :
            return CUPS_BACKEND_FAILED

if __name__ == "__main__" :
    # This is a CUPS backend, we should act and die like a CUPS backend
    wrapper = CupsBackend()
    if len(sys.argv) == 1 :
        print("\n".join(wrapper.discoverOtherBackends()))
        sys.exit(0)
    elif len(sys.argv) not in (6, 7) :
        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
                              % sys.argv[0])
        sys.exit(1)
    else :
        returncode = 1
        try :
            try :
                wrapper.readConfig()
                wrapper.initBackend()
                wrapper.waitForLock()
                wrapper.saveDatasAndCheckSum()
                wrapper.exportAttributes()
                returncode = wrapper.runBranches()
                wrapper.logDebug("returncode is %i" %returncode )
            except SystemExit as e :
                returncode = e.code
            except KeyboardInterrupt :
                wrapper.logInfo("Job %s interrupted by the administrator !" % wrapper.JobId, "warn")
            except :
                import traceback
                lines = []
                for errline in traceback.format_exception(*sys.exc_info()) :
                    lines.extend([l for l in errline.split("\n") if l])
                errormessage = "ERROR: ".join(["%s (PID %s) : %s\n" % (wrapper.MyName, \
                                                              wrapper.pid, l) \
                            for l in (["ERROR: Tea4CUPS v%s" % __version__] + lines)])
                sys.stderr.write(errormessage)
                wrapper.logDebug(errormessage)
                sys.stderr.flush()
                returncode = 1
        finally :
            wrapper.cleanUp()
        sys.exit(returncode)
