--- /dev/null
+#!/usr/bin/env python
+
+"""
+
+SUMMARY
+------------------------------------------------------
+cnfvar
+
+Copyright: Intra2net AG
+
+
+CONTENTS
+------------------------------------------------------
+Represent CNF_VARs as recursive structures.
+
+.. todo:: Decide on some facility for automatic fixup of line number values. The
+ internal representation is recursive so line numbers are not needed to
+ establish a variable hierarchy. They might as well be omitted from the
+ json input and only added when writing cnf. For the time being a lack
+ of the "number" field is interpreted as an error. Though it should at
+ least optionally be possible to omit the numbers entirely and have a
+ function add them after the fact. This might as well be added to
+ :py:func:`is_cnf` though it would be counterintuitive to have a predicate
+ mutate its argument. So maybe it could return a second argument to
+ indicate a valid structure that needs fixup or something like that.
+
+.. note:: The variable values of get_cnf seems to be encoded in latin1, and
+ set_cnf seems to require latin1-encoded values (not var names).
+ Function :py:func:`read_cnf` converts this to unicode and functions
+ :py:func:`dump_cnf_string`, :py:func:`print_cnf`, and
+ :py:func:`write_cnf` convert unicode back to latin1.
+
+
+INTERFACE
+------------------------------------------------------
+"""
+
+import functools
+import sys
+import json
+import re
+import io
+
+#
+# helpers
+#
+
+# Sadly, the Intranator is still stuck with one leg in the 90s.
+#
+
+
+def to_latin1(s):
+ """ take given unicode value and convert it to a latin1-encoded string """
+ return s.encode("latin-1")
+
+
+def from_latin1(s):
+ """ take given latin1-encoded string value and convert it to unicode """
+ return s.decode("latin-1")
+
+#
+# traversal
+#
+
+
+class InvalidCNF(Exception):
+
+ def __init__(self, msg):
+ self.msg = msg
+
+ def __str__(self):
+ return "Malformed CNF_VAR: \"%s\"" % self.msg
+
+
+def walk_cnf(cnf, nested, fun, acc):
+ """
+ :type cnf: cnf list
+ :type nested: bool
+ :type fun: ('a -> bool -> (cnf stuff) -> 'a)
+ :type acc: 'a
+ :rtype: 'a
+ """
+ for var in cnf:
+ acc = fun(acc,
+ nested,
+ comment=var.get("comment", None),
+ data=var.get("data", None),
+ instance=var.get("instance", None),
+ number=var.get("number", None),
+ parent=var.get("parent", None),
+ varname=var.get("varname", None))
+ children = var.get("children", None)
+ if children is not None:
+ acc = walk_cnf(children, True, fun, acc)
+ return acc
+
+
+#
+# validation
+#
+
+def is_string(s):
+ return isinstance(s, str) or isinstance(s, unicode)
+
+
+def is_valid(acc,
+ nested,
+ comment,
+ data,
+ instance,
+ number,
+ parent,
+ varname):
+ if varname is None:
+ raise InvalidCNF("CNF_VAR lacks a name.")
+ elif not is_string(varname):
+ raise InvalidCNF("Varname field of CNF_VAR \"%s\" is not a string."
+ % varname)
+ elif varname == "":
+ raise InvalidCNF("Varname field of CNF_VAR is the empty string.")
+
+ if comment is not None:
+ if not is_string(comment):
+ raise InvalidCNF("Comment field of CNF_VAR \"%s\" is not a string."
+ % varname)
+
+ if data is None:
+ raise InvalidCNF("Data field of CNF_VAR \"%s\" is empty."
+ % varname)
+ elif not is_string(data):
+ raise InvalidCNF("Data field of CNF_VAR \"%s\" is not a string."
+ % varname)
+
+ if instance is None:
+ raise InvalidCNF("Instance field of CNF_VAR \"%s\" is empty."
+ % varname)
+ elif not isinstance(instance, int):
+ raise InvalidCNF("Instance field of CNF_VAR \"%s\" is not an integer."
+ % varname)
+
+ if number is None:
+ raise InvalidCNF("Number field of CNF_VAR \"%s\" is empty."
+ % varname)
+ elif not isinstance(number, int):
+ raise InvalidCNF("Number field of CNF_VAR \"%s\" is not an integer."
+ % varname)
+ elif number < 1:
+ raise InvalidCNF("Number field of CNF_VAR \"%s\" must be positive, not %d."
+ % (varname, number))
+ else:
+ other = acc.get(number, None)
+ if other is not None: # already in use
+ raise InvalidCNF("Number field of CNF_VAR \"%s\" already used by variable %s."
+ % (varname, other))
+ acc[number] = varname
+
+ if nested is True:
+ if parent is None:
+ raise InvalidCNF("Parent field of nested CNF_VAR \"%s\" is empty."
+ % varname)
+ elif not isinstance(parent, int):
+ raise InvalidCNF("Parent field of CNF_VAR \"%s\" is not an integer."
+ % varname)
+ else:
+ if parent is not None:
+ raise InvalidCNF("Flat CNF_VAR \"%s\" has nonsensical parent field \"%s\"."
+ % (varname, parent))
+ return acc
+
+
+def is_cnf(root):
+ """
+ is_cnf -- Predicate testing "CNF_VAR-ness". Folds the :py:func:`is_valid`
+ predicate over the argument which can be either a well-formed CNF
+ dictionary or a list of CNF_VARs.
+
+ :type root: cnfvar or cnf list
+ :rtype: bool
+
+ Not that if it returns at all, ``is_cnf()`` returns ``True``. Any non
+ well-formed member of the argument will cause the predicate to bail out
+ with an exception during traversal.
+ """
+ t_root = type(root)
+ if t_root is list:
+ cnf = root
+ elif t_root is dict:
+ cnf = root["cnf"]
+ else:
+ raise InvalidCNF(root)
+ return walk_cnf(cnf, False, is_valid, {}) is not None
+
+#
+# deserialization
+#
+
+# the easy part: JSON reader for get_cnf -j
+
+
+def read_cnf_json(cnfdata):
+ cnf_json = json.loads(cnfdata)
+ if is_cnf(cnf_json) is False:
+ raise TypeError("Invalid CNF_VAR.")
+ return cnf_json
+
+# the moderately more complicated part: CNF reader
+
+
+def advance(cns):
+ current, next, stream = cns
+ if next is None: # reached end of stream
+ return None
+ current = next
+
+ try:
+ next = stream.pop()
+ next = next.strip()
+ except IndexError:
+ next = None
+
+ if current == "": # skip blank lines
+ return advance((current, next, stream))
+ return (current, next, stream)
+
+
+def prepare(raw):
+ """
+ :type raw: str
+ :rtype: (str * str option * str list) option
+ """
+ lines = raw.splitlines()
+ lines.reverse()
+ try:
+ first = lines.pop()
+ except IndexError:
+ return None
+
+ try:
+ second = lines.pop()
+ except IndexError:
+ second = None
+
+ first = first.strip()
+ if first == "":
+ return advance((first, second, lines))
+
+ return (first, second, lines)
+
+
+def peek(cns):
+ _, next, _ = cns
+ return next
+
+
+def get(cnf):
+ current, _, _ = cns
+ return current
+
+
+class MalformedCNF(Exception):
+
+ def __init__(self, msg):
+ self.msg = msg
+
+ def __str__(self):
+ return "Malformed CNF file: \"%s\"" % self.msg
+
+grab_parent_pattern = re.compile("""
+ ^ # match from start
+ \d+ # line number
+ \s+ # spaces
+ \((\d+)\) # parent
+ """,
+ re.VERBOSE)
+
+
+def get_parent(line):
+ match = re.match(grab_parent_pattern, line)
+ if match is None: # -> no parent
+ return None
+ return int(match.groups()[0])
+
+base_line_pattern = re.compile("""
+ ^ # match from start
+ \s* # optional spaces
+ (\d+) # line number
+ \s+ # spaces
+ ([A-Z][A-Z0-9_]*) # varname
+ \s* # optional spaces
+ , # delimiter
+ \s* # optional spaces
+ (-1|\d+) # instance
+ \s* # optional spaces
+ : # delimiter
+ \s* # optional spaces
+ \"([^\"]*)\" # quoted string (data)
+ \s* # optional spaces
+ ( # bgroup
+ \# # comment leader
+ \s* # optional spaces
+ .* # string (comment)
+ )? # egroup, optional
+ $ # eol
+ """,
+ re.VERBOSE)
+
+
+def read_base_line(line):
+ if len(line.strip()) == 0:
+ return None # ignore empty lines
+ if line[0] == "#":
+ return None # ignore comments
+
+ match = re.match(base_line_pattern, line)
+ if match is None:
+ raise MalformedCNF("Syntax error in line \"\"\"%s\"\"\"" % line)
+ number, varname, instance, data, comment = match.groups()
+ return {
+ "number": int(number),
+ "varname": varname,
+ "instance": int(instance),
+ "data": data and from_latin1(data) or "",
+ "comment": comment and from_latin1(comment[1:].strip()) or None,
+ }
+
+child_line_pattern = re.compile("""
+ ^ # match from start
+ \s* # optional spaces
+ (\d+) # line number
+ \s+ # spaces
+ \((\d+)\) # parent
+ \s+ # spaces
+ ([A-Z][A-Z0-9_]*) # varname
+ \s* # optional spaces
+ , # delimiter
+ \s* # optional spaces
+ (-1|\d+) # instance
+ \s* # optional spaces
+ : # delimiter
+ \s* # optional spaces
+ \"([^\"]*)\" # quoted string (data)
+ \s* # optional spaces
+ ( # bgroup
+ \# # comment leader
+ \s* # optional spaces
+ .* # string (comment)
+ )? # egroup, optional
+ $ # eol
+ """,
+ re.VERBOSE)
+
+
+def read_child_line(line):
+ if len(line.strip()) == 0:
+ return None # ignore empty lines
+ if line[0] == "#":
+ return None # ignore comments
+
+ match = re.match(child_line_pattern, line)
+ if match is None:
+ raise MalformedCNF("Syntax error in child line \"\"\"%s\"\"\"" % line)
+ number, parent, varname, instance, data, comment = match.groups()
+ return {
+ "number": int(number),
+ "parent": int(parent),
+ "varname": varname,
+ "instance": int(instance),
+ "data": data and from_latin1(data) or "",
+ "comment": comment and from_latin1(comment[1:].strip()) or None,
+ }
+
+
+def parse_cnf_children(state, parent):
+ lines = []
+ current = get(state)
+ while True:
+ cnf_line = read_child_line(current)
+ if cnf_line is not None:
+ lines.append(cnf_line)
+ state = advance(state)
+ if state is None:
+ break
+ current = get(state)
+ new_parent = get_parent(current)
+ if new_parent is None:
+ # drop stack
+ return (state, lines, None)
+ if new_parent > parent:
+ # parent is further down in hierarchy -> new level
+ (state, children, new_parent) = parse_cnf_children(
+ state, new_parent)
+ cnf_line["children"] = children
+ current = get(state)
+ if new_parent < parent:
+ # parent is further up in hierarchy -> pop level
+ return (state, lines, new_parent)
+ # new_parent == parent -> continue parsing on same level
+ return (state, lines, parent)
+
+
+def parse_cnf_root(state):
+ lines = []
+ current = get(state)
+ while state:
+ cnf_line = read_base_line(current)
+ if cnf_line is not None:
+ lines.append(cnf_line)
+ state = advance(state)
+ if state is None: # -> nothing left to do
+ break
+ current = get(state)
+ parent = get_parent(current) # peek at next line
+ if parent is not None: # -> recurse into children
+ (state, children, _parent) = parse_cnf_children(state, parent)
+ cnf_line["children"] = children
+ if state is None:
+ break
+ current = get(state)
+ return lines
+
+
+def read_cnf(data):
+ state = prepare(data)
+ if state is None:
+ raise InvalidCNF("Empty input string.")
+
+ cnf = parse_cnf_root(state)
+ if is_cnf(cnf) is False:
+ raise TypeError("Invalid CNF_VAR.")
+ return {"cnf": cnf}
+
+
+def renumber_vars(root, parent=None):
+ """
+ renumber_vars -- Number cnfvars linearly.
+ """
+ if isinstance(root, dict):
+ root = root["cnf"]
+ i = parent or 0
+ for var in root:
+ i += 1
+ var["number"] = i
+ if parent is not None:
+ var["parent"] = parent
+ children = var.get("children", None)
+ if children is not None:
+ i = renumber_vars(children, i)
+ return i
+
+#
+# serialization
+#
+
+cnf_line_nest_indent = " "
+cnf_line_base_fmt = "%d %s,%d: \"%s\""
+cnf_line_child_fmt = "%d %s(%d) %s,%d: \"%s\""
+
+
+def format_cnf_vars(da, var):
+ """
+ Return a list of formatted cnf_line strings.
+ """
+
+ depth, acc = da
+ line = None
+ if depth > 0:
+ line = cnf_line_child_fmt \
+ % (var["number"],
+ cnf_line_nest_indent * depth,
+ var["parent"],
+ to_latin1(var["varname"]).decode (),
+ var["instance"],
+ to_latin1(var["data"]).decode ())
+ else:
+ line = cnf_line_base_fmt \
+ % (var["number"],
+ to_latin1(var["varname"]).decode (),
+ var["instance"],
+ to_latin1(var["data"]).decode ())
+
+ comment = var.get("comment", None)
+ if comment and comment is not "":
+ line = line + (" # %s" % to_latin1(comment))
+
+ acc.append(line)
+
+ children = var.get("children", None)
+ if children is not None:
+ (_, acc) = functools.reduce(format_cnf_vars, children, (depth + 1, acc))
+ return (depth, acc)
+
+
+def cnf_root(root):
+ if not isinstance(root, dict):
+ raise TypeError(
+ "Expected dictionary of CNF_VARs, got %s." % type(root))
+ cnf = root.get("cnf", None)
+ if not isinstance(cnf, list):
+ raise TypeError("Expected list of CNF_VARs, got %s." % type(cnf))
+ return cnf
+
+
+def print_cnf_raw(root, out=None):
+ if root is not None:
+ out.write(root)
+
+
+def write_cnf_raw(*argv, **kw_argv):
+ print_cnf_raw(*argv, **kw_argv)
+
+
+def output_cnf(root, out, renumber=False):
+ cnf = cnf_root(root)
+ # Cthulhu (a.k.a Autotest) hates us so much he replaced the innocuous
+ # stdout with some chimera that isn't derived from ``file``, rendering
+ # the following sanity check moot.
+ # if not isinstance(out, file):
+ # raise TypeError("%s (%s) is not a stream." % (out, type(out)))
+ if renumber is True:
+ _count = renumber(root)
+ if is_cnf(cnf) is True:
+ (_, lines) = functools.reduce(format_cnf_vars, cnf, (0, []))
+ out.write("\n".join(lines))
+ out.write("\n")
+
+
+def dump_cnf_string(root, renumber=False):
+ """
+ dump_cnf_string -- Serialize CNF var structure, returning the result as a
+ string.
+ """
+ cnf = cnf_root(root)
+ out = io.StringIO()
+ output_cnf(root, out, renumber=renumber)
+ res = out.getvalue()
+ out.close()
+ return res
+
+
+def print_cnf(root, out=None, renumber=False):
+ if root is not None:
+ output_cnf(root, out or sys.stdout, renumber=renumber)
+
+
+def write_cnf(*argv, **kw_argv):
+ print_cnf(*argv, **kw_argv)
+
+
+def output_json(root, out, renumber=False):
+ # Sanity check incompatible with Autotest. Who needs a seatbelt anyways?
+ # if not isinstance(out, file):
+ #raise TypeError("%s (%s) is not a stream." % (out, type(out)))
+ if renumber is True:
+ _count = renumber_vars(root)
+ if is_cnf(root) is True:
+ data = json.dumps(root)
+ out.write(data)
+ out.write("\n")
+
+
+def dump_json_string(root, renumber=False):
+ """
+ dump_json_string -- Serialize CNF var structure as JSON, returning the
+ result as a string.
+ """
+ out = io.StringIO()
+ output_json(root, out, renumber=renumber)
+ res = out.getvalue()
+ out.close()
+ return res
+
+
+def print_cnf_json(root, out=None, renumber=False):
+ if root is not None:
+ output_json(root, out or sys.stdout, renumber=renumber)
+
+
+def write_cnf_json(*argv, **kw_argv):
+ print_cnf_json(*argv, **kw_argv)
+
+#
+# querying
+#
+
+
+def get_vars(cnf, data=None, instance=None):
+ """
+ get_vars -- Query a CNF_VAR structure. Skims the *toplevel* of the CNF_VAR
+ structure for entries with a matching `data` or `instance` field.
+
+ :type cnf: CNF_VAR
+ :type data: str
+ :type instance: int
+ :rtype: CNF_VAR
+ :returns: The structure containing only references to the
+ matching variables. Containing an empty list of
+ variables in case there is no match.
+ """
+ cnf = cnf["cnf"]
+ if cnf:
+ criterion = lambda _var: False
+ if data:
+ if instance:
+ criterion = lambda var: var[
+ "data"] == data and var["instance"] == instance
+ else:
+ criterion = lambda var: var["data"] == data
+ elif instance:
+ criterion = lambda var: var["instance"] == instance
+
+ return {"cnf": [var for var in cnf if criterion(var) is True]}
+
+ return {"cnf": []}
+
+#
+# entry point for development
+#
+
+
+def main(argv):
+ if len(argv) > 1:
+ first = argv[1]
+ if first == "-":
+ cnf = read_cnf(sys.stdin.read())
+ print_cnf(cnf)
+ elif first == "test":
+ cnf = read_cnf(sys.stdin.read())
+ cnff = get_vars(cnf, instance=2, data="FAX")
+ print_cnf(cnff)
+
+if __name__ == "__main__":
+ main(sys.argv)
--- /dev/null
+#!/usr/bin/env python
+# This Python file uses the following encoding: utf-8
+
+import unittest
+import cnfvar
+
+#
+# test data
+#
+
+# model cnf tree
+demo_cnfvar = {"cnf": [
+ {
+ "varname": "MY_FAVORITE_CNF_VAR",
+ "instance": 1337,
+ "number": 42,
+ "data": "string conf content",
+ "comment": ""
+ },
+ {
+ "varname": "SOME_NESTED_CNF_VAR",
+ "instance": 0,
+ "number": 23,
+ "data": "999",
+ "comment": "",
+ "children": [
+ {
+ "varname": "SOME_CHILD_VAR",
+ "instance": 1,
+ "number": 24,
+ "data": "2014",
+ "parent": 23,
+ "comment": ""
+ }
+ ]
+ },
+]}
+
+# duplicate line number
+demo_invalid_cnfvar = {"cnf": [
+ {
+ "varname": "SOME_PARTICULARLY_TASTY_CNF_VAR",
+ "instance": 1337,
+ "number": 23,
+ "data": "classic wingers",
+ "comment": ""
+ },
+ {
+ "varname": "EXTRAORDINARILY_FANCY_CNF_VAR",
+ "instance": 1,
+ "number": 42,
+ "data": "ab mentions",
+ "comment": ""
+ },
+ {
+ "varname": "ANOTHER_POPULAR_CNF_VAR",
+ "instance": 0,
+ "number": 42,
+ "data": "notches",
+ "comment": ""
+ }
+]}
+
+demo_jsoncnf = """
+{
+ "cnf" : [
+ {
+ "children" : [
+ {
+ "comment" : "",
+ "data" : "1",
+ "instance" : 0,
+ "number" : 2,
+ "parent" : 1,
+ "varname" : "FIREWALL_SERVICEGROUP_PREDEFINED_ID"
+ },
+ {
+ "children" : [
+ {
+ "comment" : "",
+ "data" : "",
+ "instance" : 0,
+ "number" : 4,
+ "parent" : 3,
+ "varname" : "FIREWALL_SERVICEGROUP_TYPE_COMMENT"
+ },
+ {
+ "comment" : "",
+ "data" : "80",
+ "instance" : 0,
+ "number" : 5,
+ "parent" : 3,
+ "varname" : "FIREWALL_SERVICEGROUP_TYPE_TCPUDP_DST_PORT"
+ }
+ ],
+ "comment" : "",
+ "data" : "TCP",
+ "instance" : 0,
+ "number" : 3,
+ "parent" : 1,
+ "varname" : "FIREWALL_SERVICEGROUP_TYPE"
+ }
+ ],
+ "comment" : "",
+ "data" : "http",
+ "instance" : 1,
+ "number" : 1,
+ "varname" : "FIREWALL_SERVICEGROUP"
+ },
+ {
+ "children" : [
+ {
+ "comment" : "",
+ "data" : "2",
+ "instance" : 0,
+ "number" : 7,
+ "parent" : 6,
+ "varname" : "FIREWALL_SERVICEGROUP_PREDEFINED_ID"
+ },
+ {
+ "children" : [
+ {
+ "comment" : "",
+ "data" : "",
+ "instance" : 0,
+ "number" : 9,
+ "parent" : 8,
+ "varname" : "FIREWALL_SERVICEGROUP_TYPE_COMMENT"
+ },
+ {
+ "comment" : "",
+ "data" : "443",
+ "instance" : 0,
+ "number" : 10,
+ "parent" : 8,
+ "varname" : "FIREWALL_SERVICEGROUP_TYPE_TCPUDP_DST_PORT"
+ }
+ ],
+ "comment" : "",
+ "data" : "TCP",
+ "instance" : 0,
+ "number" : 8,
+ "parent" : 6,
+ "varname" : "FIREWALL_SERVICEGROUP_TYPE"
+ }
+ ],
+ "comment" : "",
+ "data" : "https",
+ "instance" : 2,
+ "number" : 6,
+ "varname" : "FIREWALL_SERVICEGROUP"
+ }
+ ]
+}
+"""
+
+demo_latin1crap = r"""
+{ "cnf" : [
+ {
+ "children" : [
+ {
+ "comment" : "",
+ "data" : "0",
+ "instance" : 0,
+ "number" : 2,
+ "parent" : 1,
+ "varname" : "USER_DISABLED"
+ },
+ {
+ "comment" : "",
+ "data" : "Administrator",
+ "instance" : 0,
+ "number" : 3,
+ "parent" : 1,
+ "varname" : "USER_FULLNAME"
+ },
+ {
+ "comment" : "",
+ "data" : "INBOX/Kalender",
+ "instance" : 0,
+ "number" : 4,
+ "parent" : 1,
+ "varname" : "USER_GROUPWARE_FOLDER_CALENDAR"
+ },
+ {
+ "comment" : "",
+ "data" : "INBOX/Kontakte",
+ "instance" : 0,
+ "number" : 5,
+ "parent" : 1,
+ "varname" : "USER_GROUPWARE_FOLDER_CONTACTS"
+ },
+ {
+ "comment" : "",
+ "data" : "INBOX/Entwürfe",
+ "instance" : 0,
+ "number" : 6,
+ "parent" : 1,
+ "varname" : "USER_GROUPWARE_FOLDER_DRAFTS"
+ },
+ {
+ "comment" : "",
+ "data" : "INBOX/Notizen",
+ "instance" : 0,
+ "number" : 7,
+ "parent" : 1,
+ "varname" : "USER_GROUPWARE_FOLDER_NOTES"
+ },
+ {
+ "comment" : "",
+ "data" : "INBOX/Gesendete Elemente",
+ "instance" : 0,
+ "number" : 8,
+ "parent" : 1,
+ "varname" : "USER_GROUPWARE_FOLDER_OUTBOX"
+ },
+ {
+ "comment" : "",
+ "data" : "INBOX/Aufgaben",
+ "instance" : 0,
+ "number" : 9,
+ "parent" : 1,
+ "varname" : "USER_GROUPWARE_FOLDER_TASKS"
+ },
+
+ {
+ "comment" : "",
+ "data" : "INBOX/Gelöschte Elemente",
+ "instance" : 0,
+ "number" : 10,
+ "parent" : 1,
+ "varname" : "USER_GROUPWARE_FOLDER_TRASH"
+ },
+ {
+ "comment" : "",
+ "data" : "1",
+ "instance" : 0,
+ "number" : 11,
+ "parent" : 1,
+ "varname" : "USER_GROUP_MEMBER_REF"
+ },
+ {
+ "comment" : "",
+ "data" : "2",
+ "instance" : 1,
+ "number" : 12,
+ "parent" : 1,
+ "varname" : "USER_GROUP_MEMBER_REF"
+ },
+ {
+ "comment" : "",
+ "data" : "",
+ "instance" : 0,
+ "number" : 13,
+ "parent" : 1,
+ "varname" : "USER_LOCALE"
+ },
+ {
+ "comment" : "",
+ "data" : "idkfa",
+ "instance" : 0,
+ "number" : 14,
+ "parent" : 1,
+ "varname" : "USER_PASSWORD"
+ },
+ {
+ "comment" : "",
+ "data" : "30",
+ "instance" : 0,
+ "number" : 15,
+ "parent" : 1,
+ "varname" : "USER_TRASH_DELETEDAYS"
+ }
+ ],
+ "comment" : "",
+ "data" : "admin",
+ "instance" : 1,
+ "number" : 1,
+ "varname" : "USER"
+ }
+]}
+"""
+
+demo_cnf_group = """
+1 GROUP,1: "Administratoren"
+2 (1) GROUP_ACCESS_GO_ONLINE_ALLOWED,0: "1"
+3 (1) GROUP_EMAILFILTER_BAN_FILTERLIST_REF,0: "-1"
+4 (1) GROUP_EMAIL_RELAY_RIGHTS,0: "RELAY_FROM_INTRANET"
+5 (1) GROUP_PROXY_PROFILE_REF,0: "1"
+"""
+
+demo_cnf_filter = """
+1 EMAILFILTER_BAN_FILTERLIST,1: "Vordefiniert: Alles verboten"
+2 (1) EMAILFILTER_BAN_FILTERLIST_ENCRYPTED,0: "BLOCK"
+3 (1) EMAILFILTER_BAN_FILTERLIST_EXTENSIONS,0: ""
+4 (1) EMAILFILTER_BAN_FILTERLIST_MIMETYPES,0: ""
+5 (4) EMAILFILTER_BAN_FILTERLIST_MIMETYPES_NAME,0: "text/plain"
+6 (4) EMAILFILTER_BAN_FILTERLIST_MIMETYPES_NAME,1: "text/html"
+7 (1) EMAILFILTER_BAN_FILTERLIST_MODE,0: "ALLOW"
+8 (1) EMAILFILTER_BAN_FILTERLIST_PREDEFINED_ID,0: "1"
+"""
+
+demo_cnf_comments = """
+1 EMAILFILTER_BAN_FILTERLIST,1: "Vordefiniert: Alles verboten"
+2 (1) EMAILFILTER_BAN_FILTERLIST_ENCRYPTED,0: "BLOCK"
+3 (1) EMAILFILTER_BAN_FILTERLIST_EXTENSIONS,0: ""
+4 (1) EMAILFILTER_BAN_FILTERLIST_MIMETYPES,0: "" # foo
+5 (4) EMAILFILTER_BAN_FILTERLIST_MIMETYPES_NAME,0: "text/plain"#bar
+6 (4) EMAILFILTER_BAN_FILTERLIST_MIMETYPES_NAME,1: "text/html"
+7 (1) EMAILFILTER_BAN_FILTERLIST_MODE,0: "ALLOW" # baz
+8 (1) EMAILFILTER_BAN_FILTERLIST_PREDEFINED_ID,0: "1"
+"""
+
+
+#
+# test class
+#
+
+class CnfVarUnittest(unittest.TestCase):
+
+ def test_print_cnf(self):
+ with open("/dev/null", "w") as devnull:
+ cnfvar.print_cnf(demo_cnfvar, out=devnull)
+
+ def test_parse_cnf_simple(self):
+ cnf = cnfvar.read_cnf(demo_cnf_group)
+ with open("/dev/null", "w") as devnull:
+ cnfvar.print_cnf_json(cnf, out=devnull)
+
+ def test_parse_cnf_nested(self):
+ cnf = cnfvar.read_cnf(demo_cnf_filter)
+ with open("/dev/null", "w") as devnull:
+ cnfvar.print_cnf_json(cnf, out=devnull)
+
+ def test_parse_cnf_comments(self):
+ cnf = cnfvar.read_cnf(demo_cnf_comments)
+ with open("/dev/null", "w") as devnull:
+ cnfvar.print_cnf_json(cnf, out=devnull)
+
+ def test_print_cnf_garbage(self):
+ try:
+ with open("/dev/null", "w") as devnull:
+ cnfvar.print_cnf(demo_invalid_cnfvar, out=devnull)
+ except cnfvar.InvalidCNF, exn:
+ print "Caught the duplicate line, bravo!"
+
+ def test_read_json(self):
+ cnf = cnfvar.read_cnf_json(demo_jsoncnf)
+ with open("/dev/null", "w") as devnull:
+ cnfvar.print_cnf(cnf, out=devnull)
+
+ def test_read_json_nonascii(self):
+ cnf = cnfvar.read_cnf_json(demo_latin1crap)
+ with open("/dev/null", "w") as devnull:
+ cnfvar.print_cnf(cnf, out=devnull)
+
+
+if __name__ == '__main__':
+ unittest.main()