29b762908259f51b7c62c5ddf1cd9ffe5276d2fa
[pyi2ncommon] / src / cnfvar_old.py
1 #!/usr/bin/env python
2 #
3 # The software in this package is distributed under the GNU General
4 # Public License version 2 (with a special exception described below).
5 #
6 # A copy of GNU General Public License (GPL) is included in this distribution,
7 # in the file COPYING.GPL.
8 #
9 # As a special exception, if other files instantiate templates or use macros
10 # or inline functions from this file, or you compile this file and link it
11 # with other works to produce a work based on this file, this file
12 # does not by itself cause the resulting work to be covered
13 # by the GNU General Public License.
14 #
15 # However the source code for this file must still be made available
16 # in accordance with section (3) of the GNU General Public License.
17 #
18 # This exception does not invalidate any other reasons why a work based
19 # on this file might be covered by the GNU General Public License.
20 #
21 # Copyright (c) 2016-2018 Intra2net AG <info@intra2net.com>
22
23 """
24
25 summary
26 -------------------------------------------------------------------------------
27 Represent CNF_VARs as recursive structures.
28
29 DEPRECATED! Please do not extend this or add new uses of this module, use
30 :py:mod:`pyi2ncommon.arnied_api` or :py:mod:`pyi2ncommon.cnfvar` instead!
31
32 Copyright: 2014-2017 Intra2net AG
33 License:   GPLv2+
34
35
36 contents
37 -------------------------------------------------------------------------------
38
39 This module provides read and write functionality for the Intra2net *CNF*
40 format. Two different syntaxes are available: classical *CNF* and a JSON
41 representation. Both versions are commonly understood by Intra2net software.
42
43 On the command line, raw CNF is accepted if the option ``-`` is given: ::
44
45     $ get_cnf routing 2 |python3 cnfvar.py - <<ENOUGH
46     1 ROUTING,2: "192.168.55.0"
47     2    (1) ROUTING_COMMENT,0: ""
48     3    (1) ROUTING_DNS_RELAYING_ALLOWED,0: "1"
49     4    (1) ROUTING_EMAIL_RELAYING_ALLOWED,0: "1"
50     5    (1) ROUTING_FIREWALL_RULESET_REF,0: "9"
51     6    (1) ROUTING_GATEWAY,0: "10.0.254.1"
52     7    (1) ROUTING_NAT_INTO,0: "0"
53     8    (1) ROUTING_NETMASK,0: "255.255.255.0"
54     9    (1) ROUTING_PROXY_PROFILE_REF,0: "2"
55     ENOUGH
56
57     1 ROUTING,2: "192.168.55.0"
58     2   (1) ROUTING_COMMENT,0: ""
59     3   (1) ROUTING_DNS_RELAYING_ALLOWED,0: "1"
60     4   (1) ROUTING_EMAIL_RELAYING_ALLOWED,0: "1"
61     5   (1) ROUTING_FIREWALL_RULESET_REF,0: "9"
62     6   (1) ROUTING_GATEWAY,0: "10.0.254.1"
63     7   (1) ROUTING_NAT_INTO,0: "0"
64     8   (1) ROUTING_NETMASK,0: "255.255.255.0"
65     9   (1) ROUTING_PROXY_PROFILE_REF,0: "2"
66
67 The input takes one round-trip through the parsers and will error out on
68 problematic lines. Thus, ``cnfvar.py`` can be used to syntax-check CNF data.
69
70 Note that line numbers may be arbitrarily reassigned in the process. Of course,
71 parent references and the relative ordering of lines will be preserved in this
72 case.
73
74 .. todo::
75     Decide on some facility for automatic fixup of line number values. The
76     internal representation is recursive so line numbers are not needed to
77     establish a variable hierarchy. They might as well be omitted from the json
78     input and only added when writing cnf. For the time being a lack of the
79     "number" field is interpreted as an error. Though it should at least
80     optionally be possible to omit the numbers entirely and have a function add
81     them after the fact. This might as well be added to :py:func:`is_cnf`
82     though it would be counterintuitive to have a predicate mutate its
83     argument. So maybe it could return a second argument to indicate a valid
84     structure that needs fixup or something like that.
85
86 .. note::
87     The variable values of get_cnf seems to be encoded in latin1, and set_cnf
88     seems to assume latin1-encoded values (not var names). Function
89     :py:func:`read_cnf` converts this to unicode and functions
90     :py:func:`dump_cnf_string`, :py:func:`print_cnf`, and :py:func:`write_cnf`
91     convert unicode back to latin1.
92
93
94 notes on Python 3 conversion
95 -------------------------------------------------------------------------------
96
97 Since the original *CNF* format assumes latin-1 encoded data pretty much
98 exclusively, we preserve the original encoding while parsing the file.
99 When assembling the data structures returned to the user, values are then
100 converted to strings so they can be used naturally at the Python end.
101
102 implementation
103 -------------------------------------------------------------------------------
104 """
105
106 import functools
107 import sys
108 import json
109 import re
110 import io
111
112
113 ###############################################################################
114 # CONSTANTS
115 ###############################################################################
116
117
118 CNF_FIELD_MANDATORY = set ([ "varname", "data", "instance" ])
119 CNF_FIELD_OPTIONAL  = set ([ "parent", "children", "comment", "number" ])
120 CNF_FIELD_KNOWN     = CNF_FIELD_MANDATORY | CNF_FIELD_OPTIONAL
121
122 grab_parent_pattern = re.compile(b"""
123                                     ^            # match from start
124                                     \s*          # optional spaces
125                                     \d+          # line number
126                                     \s+          # spaces
127                                     \((\d+)\)    # parent
128                                  """,
129                                  re.VERBOSE)
130
131 base_line_pattern = re.compile(b"""
132                                     ^                    # match from start
133                                     \s*                  # optional spaces
134                                     (\d+)                # line number
135                                     \s+                  # spaces
136                                     ([A-Z][A-Z0-9_]*)    # varname
137                                     \s*                  # optional spaces
138                                     ,                    # delimiter
139                                     \s*                  # optional spaces
140                                     (-1|\d+)             # instance
141                                     \s*                  # optional spaces
142                                     :                    # delimiter
143                                     \s*                  # optional spaces
144                                     \"(                  # quoted string (data)
145                                        (?:  \\\"         #  (of escaped dquote
146                                            |[^\"])*      #   or anything not a
147                                       )\"                #   literal quote)
148                                     \s*                  # optional spaces
149                                     (                    # bgroup
150                                      \#                  # comment leader
151                                      \s*                 # optional spaces
152                                      .*                  # string (comment)
153                                     )?                   # egroup, optional
154                                     $                    # eol
155                                """,
156                                re.VERBOSE)
157
158 child_line_pattern = re.compile(b"""
159                                      ^                    # match from start
160                                      \s*                  # optional spaces
161                                      (\d+)                # line number
162                                      \s+                  # spaces
163                                      \((\d+)\)            # parent
164                                      \s+                  # spaces
165                                      ([A-Z][A-Z0-9_]*)    # varname
166                                      \s*                  # optional spaces
167                                      ,                    # delimiter
168                                      \s*                  # optional spaces
169                                      (-1|\d+)             # instance
170                                      \s*                  # optional spaces
171                                      :                    # delimiter
172                                      \s*                  # optional spaces
173                                      \"([^\"]*)\"         # quoted string (data)
174                                      \s*                  # optional spaces
175                                      (                    # bgroup
176                                       \#                  # comment leader
177                                       \s*                 # optional spaces
178                                       .*                  # string (comment)
179                                      )?                   # egroup, optional
180                                      $                    # eol
181                                 """,
182                                 re.VERBOSE)
183
184
185 ###############################################################################
186 # HELPERS
187 ###############################################################################
188
189
190 #
191 # Sadly, the Intranator is still stuck with one leg in the 90s.
192 #
193 def to_latin1(s):
194     """Take given unicode str and convert it to a latin1-encoded `bytes`."""
195     return s.encode("latin-1")
196
197
198 def from_latin1(s):
199     """Take given latin1-encoded `bytes` value and convert it to `str`."""
200     return s.decode("latin-1")
201
202
203 #
204 # Conversion functions
205 #
206
207 def marshal_in_number(number):
208     return int(number)
209
210
211 def marshal_in_parent(parent):
212     return int(parent)
213
214
215 def marshal_in_instance(instance):
216     return int(instance)
217
218
219 def marshal_in_varname(varname):
220     return from_latin1(varname).lower()
221
222
223 def marshal_in_data(data):
224     return from_latin1(data) if data is not None else ""
225
226
227 def marshal_in_comment(comment):
228     return comment and from_latin1(comment[1:].strip()) or None
229
230
231 #
232 # Type checking
233 #
234
235 def is_string(s):
236     return isinstance(s, str)
237
238
239 ###############################################################################
240 # EXCEPTIONS
241 ###############################################################################
242
243
244 class InvalidCNF(Exception):
245
246     def __init__(self, msg):
247         self.msg = msg
248
249     def __str__(self):
250         return "Malformed CNF_VAR: \"%s\"" % self.msg
251
252
253 class MalformedCNF(Exception):
254
255     def __init__(self, msg):
256         self.msg = msg
257
258     def __str__(self):
259         return "Malformed CNF file: \"%s\"" % self.msg
260
261
262 ###############################################################################
263 # VALIDATION
264 ###############################################################################
265
266
267 def is_valid(acc,
268              nested,
269              comment,
270              data,
271              instance,
272              number,
273              parent,
274              varname):
275     if varname is None:
276         raise InvalidCNF("CNF_VAR lacks a name.")
277     elif not is_string(varname):
278         raise InvalidCNF("Varname field of CNF_VAR \"%s\" is not a string."
279                          % varname)
280     elif varname == "":
281         raise InvalidCNF("Varname field of CNF_VAR is the empty string.")
282
283     if comment is not None:
284         if not is_string(comment):
285             raise InvalidCNF("Comment field of CNF_VAR \"%s\" is not a string."
286                              % varname)
287
288     if data is None:
289         raise InvalidCNF("Data field of CNF_VAR \"%s\" is empty."
290                          % varname)
291     elif not is_string(data):
292         raise InvalidCNF("Data field of CNF_VAR \"%s\" is not a string."
293                          % varname)
294
295     if instance is None:
296         raise InvalidCNF("Instance field of CNF_VAR \"%s\" is empty."
297                          % varname)
298     elif not isinstance(instance, int):
299         raise InvalidCNF("Instance field of CNF_VAR \"%s\" is not an integer."
300                          % varname)
301
302     if number is None:
303         raise InvalidCNF("Number field of CNF_VAR \"%s\" is empty."
304                          % varname)
305     elif not isinstance(number, int):
306         raise InvalidCNF("Number field of CNF_VAR \"%s\" is not an integer."
307                          % varname)
308     elif number < 1:
309         raise InvalidCNF("Number field of CNF_VAR \"%s\" must be positive, not %d."
310                          % (varname, number))
311     else:
312         other = acc.get(number, None)
313         if other is not None:  # already in use
314             raise InvalidCNF("Number field of CNF_VAR \"%s\" already used by variable %s."
315                              % (varname, other))
316         acc[number] = varname
317
318     if nested is True:
319         if parent is None:
320             raise InvalidCNF("Parent field of nested CNF_VAR \"%s\" is empty."
321                              % varname)
322         elif not isinstance(parent, int):
323             raise InvalidCNF("Parent field of CNF_VAR \"%s\" is not an integer."
324                              % varname)
325     else:
326         if parent is not None:
327             raise InvalidCNF("Flat CNF_VAR \"%s\" has nonsensical parent field \"%s\"."
328                              % (varname, parent))
329     return acc
330
331
332 def is_cnf(root):
333     """
334     is_cnf -- Predicate testing "CNF_VAR-ness". Folds the :py:func:`is_valid`
335     predicate over the argument which can be either a well-formed CNF
336     dictionary or a list of CNF_VARs.
337
338     :type root: cnfvar or cnf list
339     :rtype: bool
340
341     Not that if it returns at all, ``is_cnf()`` returns ``True``. Any non
342     well-formed member of the argument will cause the predicate to bail out
343     with an exception during traversal.
344     """
345     cnf = cnf_root(root)
346     if cnf is None:
347         raise InvalidCNF(root)
348     return walk_cnf(cnf, False, is_valid, {}) is not None
349
350
351 def is_cnf_var(obj):
352     """
353     Check whether a dictionary is a valid CNF.
354
355     :param dict obj: dictionary to check
356     :returns: True if the dictionary has all the mandatory fields and no
357               unknown fields, False otherwise
358     :rtype: bool
359     """
360     assert isinstance (obj, dict)
361
362     for f in CNF_FIELD_MANDATORY:
363         if obj.get(f, None) is None:
364             return False
365
366     for f in obj:
367         if f not in CNF_FIELD_KNOWN:
368             return False
369
370     return True
371
372
373 ###############################################################################
374 # DESERIALIZATION
375 ###############################################################################
376
377
378 #
379 # JSON reader for get_cnf -j (the easy part)
380 #
381
382 def make_varname_lowercase(cnfvar):
383     """
384     Custom hook for json decoder: convert variable name to lowercase.
385
386     Since variable names are case insensitive, :py:func:`read_cnf` converts
387     them all to lower case. Downstream users of :py:func:`read_cnf_json` (e.g.
388     :py:class:`simple_cnf.SimpleCnf`) rely on lowercase variable names.
389
390     :param dict cnfvar: JSON "object" converted into a dict
391     :returns: same as input but if field `varname` is present, its value is
392               converted to lower case
393     :rtype: dict with str keys
394     """
395     try:
396         cnfvar['varname'] = cnfvar['varname'].lower()
397     except KeyError:      # there is no "varname" field
398         pass
399     except AttributeError:   # cnfvar['varname'] is not a string
400         pass
401     return cnfvar
402
403
404 def read_cnf_json(cnfdata):
405     """
406     Read json data from cnf data bytes.
407
408     :param bytes cnfdata: config data
409     :return: the parsed json data
410     :rtype: str
411
412     .. note:: The JSON module does not decode data for all versions
413         of Python 3 so we handle the decoding ourselves.
414     """
415     if isinstance (cnfdata, bytes) is True:
416         cnfdata = from_latin1 (cnfdata)
417     cnf_json = json.loads(cnfdata, object_hook=make_varname_lowercase)
418     if is_cnf(cnf_json) is False:
419         raise TypeError("Invalid CNF_VAR.")
420     return cnf_json
421
422
423 #
424 # CNF reader (the moderately more complicated part)
425 #
426 # Parsing usually starts from the `read_cnf`, which accepts a string containing
427 # the variables to parse in the same structure as returned by `get_cnf`.
428 #
429 # In the `prepare` function the string is split into lines, and a 3-element
430 # tuple is built. The first (named `current`) and second (named `next`)
431 # elements of this tuple are respectively the first and second non-empty lines
432 # of the input, while the third is a list of the remaining lines. This tuple is
433 # named `state` in the implementation below, and it is passed around during
434 # parsing. The `get` and `peek` functions are used to easily retrieve the
435 # `current` and `next` items from the "state".
436 #
437 # When we "advance" the state, we actually drop the "current" element,
438 # replacing it with the "next", while a new "next" is popped from the list of
439 # remaining lines. Parsing is done this way because we need to look ahead at
440 # the next line -- if it is a child it needs to be appended to the `children`
441 # property of the current line.
442 #
443 # Regular expressions are used to extract important information from the CNF
444 # lines. Finally, once parsing is completed, a dictionary is returned. The dict
445 # has the same structure as the serialized JSON output returned by
446 # `get_cnf -j`.
447 #
448
449
450 def read_cnf(data):
451     """
452     Read cnf data from data bytes.
453
454     :param data: raw data
455     :type data: str or bytes
456     :return: the parsed cnf data
457     :rtype: {str, {str, str or int}}
458     """
459     if isinstance(data, str):
460         data = to_latin1(data)
461     state = prepare(data)
462     if state is None:
463         raise InvalidCNF("Empty input string.")
464
465     cnf = parse_cnf_root(state)
466     if is_cnf(cnf) is False:
467         raise TypeError("Invalid CNF_VAR.")
468     return {"cnf": cnf}
469
470
471 def prepare(raw):
472     """
473     Build 3-element iterable from a CNF string dump.
474
475     :param raw: string content as returned by `get_cnf`
476     :type raw: bytes
477     :returns: 3-element tuple, where the first two elements are the first two
478               lines of the output and the third is a list containing the rest
479               of the lines in reverse.
480     :rtype: (str * str option * str list) option
481     """
482     lines = raw.splitlines()
483     lines.reverse()
484     try:
485         first = lines.pop()
486     except IndexError:
487         return None
488
489     try:
490         second = lines.pop()
491     except IndexError:
492         second = None
493
494     first = first.strip()
495     if first == b"":
496         return advance((first, second, lines))
497
498     return (first, second, lines)
499
500
501 def advance(cns):
502     """
503     Pop the next line from the stream, advancing the tuple.
504
505     :param cns: a 3-element tuple containing two CNF lines and a list of the
506                 remaining lines
507     :type cnd: (str, str, [str])
508     :returns: a new tuple with a new item popped from the list of lines
509     :rtype cnd: (str, str, [str])
510     """
511     current, next, stream = cns
512     if next is None:  # reached end of stream
513         return None
514     current = next
515
516     try:
517         next = stream.pop()
518         next = next.strip()
519     except IndexError:
520         next = None
521
522     if current == "":  # skip blank lines
523         return advance((current, next, stream))
524     return (current, next, stream)
525
526
527 def get(cns):
528     """
529     Get the current line from the state without advancing it.
530
531     :param cns: a 3-element tuple containing two CNF lines and a list of the
532                 remaining lines
533     :type cnd: (str, str, [str])
534     :returns: the CNF line stored as `current`
535     :rtype: str
536     """
537     current, _, _ = cns
538     return current
539
540
541 def peek(cns):
542     """
543     Get the next line from the state without advancing it.
544
545     :param cns: a 3-element tuple containing two CNF lines and a list of the
546                 remaining lines
547     :type cnd: (str, str, [str])
548     :returns: the CNF line stored as `next`
549     :rtype: str
550     """
551     _, next, _ = cns
552     return next
553
554
555 def parse_cnf_root(state):
556     """
557     Iterate over and parse a list of CNF lines.
558
559     :param state: a 3-element tuple containing two lines and a list of the
560                   remaining lines
561     :type state: (str, str, [str])
562     :returns: a list of parsed CNF variables
563     :rtype: [dict]
564
565     The function will parse the first element from the `state` tuple, then read
566     the next line to see if it is a child variable. If it is, it will be
567     appended to the last parsed CNF, otherwise top-level parsing is done
568     normally.
569     """
570     lines = []
571     current = get(state)
572     while state:
573         cnf_line = read_base_line(current)
574         if cnf_line is not None:
575             lines.append(cnf_line)
576             state = advance(state)
577             if state is None:  # -> nothing left to do
578                 break
579             current = get(state)
580             parent = get_parent(current)  # peek at next line
581             if parent is not None:  # -> recurse into children
582                 (state, children, _parent) = parse_cnf_children(state, parent)
583                 cnf_line["children"] = children
584                 if state is None:
585                     break
586                 current = get(state)
587         else:
588             state = advance(state)
589             if state is None:
590                 break
591             current = get(state)
592     return lines
593
594
595 def parse_cnf_children(state, parent):
596     """
597     Read and parse child CNFs of a given parent until there is none left.
598
599     :param state: a 3-element tuple containing two lines and a list of the
600                   remaining lines
601     :type state: (str, str, [str])
602     :param parent: id of the parent whose children we are looking for
603     :type parent: int
604     :returns: a 3-element tuple with the current state, a list of children of
605               the given parent and the parent ID
606     :rtype: (tuple, [str], int)
607
608     The function will recursively parse child lines from the `state` tuple
609     until one of these conditions is satisfied:
610
611     1. the input is exhausted
612     2. the next CNF line
613         2.1. is a toplevel line
614         2.2. is a child line whose parent has a lower parent number
615
616     Conceptually, 2.1 is a very similar to 2.2 but due to the special status of
617     toplevel lines in CNF we need to handle them separately.
618
619     Note that since nesting of CNF vars is achieved via parent line numbers,
620     lines with different parents could appear out of order. libcnffile will
621     happily parse those and still assign children to the specified parent:
622
623     ::
624         # set_cnf <<THATSALL
625         1 USER,1337: "l33t_h4x0r"
626         2    (1) USER_GROUP_MEMBER_REF,0: "2"
627         4 USER,1701: "picard"
628         5    (4) USER_GROUP_MEMBER_REF,0: "2"
629         6    (4) USER_PASSWORD,0: "engage"
630         3    (1) USER_PASSWORD,0: "hacktheplanet"
631         THATSALL
632         # get_cnf user 1337
633         1 USER,1337: "l33t_h4x0r"
634         2    (1) USER_GROUP_MEMBER_REF,0: "2"
635         3    (1) USER_PASSWORD,0: "hacktheplanet"
636         # get_cnf user 1701
637         1 USER,1701: "picard"
638         2    (1) USER_GROUP_MEMBER_REF,0: "2"
639         3    (1) USER_PASSWORD,0: "engage"
640
641     It is a limitation of ``cnfvar.py`` that it cannot parse CNF data
642     structured like the above example: child lists are only populated from
643     subsequent CNF vars using the parent number solely to track nesting levels.
644     The parser does not keep track of line numbers while traversing the input
645     so it doesn’t support retroactively assigning a child to anything else but
646     the immediate parent.
647     """
648     lines = []
649     current = get(state)
650     while True:
651         cnf_line = read_child_line(current)
652         if cnf_line is not None:
653             lines.append(cnf_line)
654             state = advance(state)
655             if state is None:
656                 break
657             current = get(state)
658             new_parent = get_parent(current)
659             if new_parent is None:
660                 # drop stack
661                 return (state, lines, None)
662             if new_parent > parent:
663                 # parent is further down in hierarchy -> new level
664                 (state, children, new_parent) = \
665                     parse_cnf_children (state, new_parent)
666                 if state is None:
667                     break
668                 cnf_line["children"] = children
669                 current = get(state)
670                 new_parent = get_parent(current)
671                 if new_parent is None:
672                     # drop stack
673                     return (state, lines, None)
674             if new_parent < parent:
675                 # parent is further up in hierarchy -> pop level
676                 return (state, lines, new_parent)
677             # new_parent == parent -> continue parsing on same level
678     return (state, lines, parent)
679
680
681 def get_parent(line):
682     """
683     Extract the ID of the parent for a given CNF line.
684
685     :param str line: CNF line
686     :returns: parent ID or None if no parent is found
687     :rtype: int or None
688     """
689     match = re.match(grab_parent_pattern, line)
690     if match is None:  # -> no parent
691         return None
692     return int(match.groups()[0])
693
694
695 def read_base_line(line):
696     """
697     Turn one top-level CNF line into a dictionary.
698
699     :param str line: CNF line
700     :rtype: {str: Any}
701
702     This performs the necessary decoding on values to obtain proper Python
703     strings from 8-bit encoded CNF data.
704
705     The function only operates on individual lines. Argument strings that
706     contain data for multiple lines â€“ this includes child lines of the current
707     CNF var! â€“ will trigger a parsing exception.
708     """
709     if len(line.strip()) == 0:
710         return None  # ignore empty lines
711     if line[0] == b"#":
712         return None  # ignore comments
713
714     match = re.match(base_line_pattern, line)
715     if match is None:
716         raise MalformedCNF("Syntax error in line \"\"\"%s\"\"\"" % line)
717     number, varname, instance, data, comment = match.groups()
718     return {
719         "number"   : marshal_in_number   (number),
720         "varname"  : marshal_in_varname  (varname),
721         "instance" : marshal_in_instance (instance),
722         "data"     : marshal_in_data     (data),
723         "comment"  : marshal_in_comment  (comment),
724     }
725
726
727 def read_child_line(line):
728     """
729     Turn one child CNF line into a dictionary.
730
731     :param str line: CNF line
732     :rtype: {str: Any}
733
734     This function only operates on individual lines. If the argument string is
735     syntactically valid but contains input representing multiple CNF vars, a
736     parse error will be thrown.
737     """
738     if len(line.strip()) == 0:
739         return None  # ignore empty lines
740     if line[0] == "#":
741         return None  # ignore comments
742
743     match = re.match(child_line_pattern, line)
744     if match is None:
745         raise MalformedCNF("Syntax error in child line \"\"\"%s\"\"\""
746                            % from_latin1 (line))
747     number, parent, varname, instance, data, comment = match.groups()
748     return {
749         "number"   : marshal_in_number   (number),
750         "parent"   : marshal_in_parent   (parent),
751         "varname"  : marshal_in_varname  (varname),
752         "instance" : marshal_in_instance (instance),
753         "data"     : marshal_in_data     (data),
754         "comment"  : marshal_in_comment  (comment),
755     }
756
757
758 ###############################################################################
759 # SERIALIZATION
760 ###############################################################################
761
762
763 cnf_line_nest_indent = "  "
764 cnf_line_base_fmt    = "%d %s,%d: \"%s\""
765 cnf_line_child_fmt   = "%d %s(%d) %s,%d: \"%s\""
766
767
768 def format_cnf_vars(da, var):
769     """
770     Return a list of formatted cnf_line byte strings.
771
772     :param da: a tuple where the first element is the depth (0 = top-level,
773                >1 = child CNF) and the second is the string being built.
774     :type da: (int, str)
775     :param var: the CNF element to convert to string in the current iteration
776     :type var: dict
777     :returns: a tuple like `da`, where the second element should contain all
778               converted CNFs
779     :rtype: (int, str)
780
781     This function is meant to be passed to the :py:func:`functools.reduce`
782     function.
783
784     The variable names are uppercased unconditionally because while ``get_cnf``
785     is case-indifferent for variable names, ``set_cnf`` isn’t.
786     """
787     depth, acc = da
788     line = None
789     if depth > 0:
790         line = cnf_line_child_fmt \
791             % (var["number"],
792                cnf_line_nest_indent * depth,
793                var["parent"],
794                var["varname"].upper(),
795                var["instance"],
796                var["data"])
797     else:
798         line = cnf_line_base_fmt \
799             % (var["number"],
800                var["varname"].upper(),
801                var["instance"],
802                var["data"])
803
804     comment = var.get("comment", None)
805     if comment and len(comment) != 0:
806         line = line + (" # %s" % comment)
807
808     acc.append(to_latin1(line))
809
810     children = var.get("children", None)
811     if children is not None:
812         (_, acc) = functools.reduce(format_cnf_vars, children, (depth + 1, acc))
813
814     return (depth, acc)
815
816
817 def cnf_root(root):
818     """
819     Extract a list of CNFs from a given structure.
820
821     :param root: list of CNFs or a CNF dictionary
822     :type root: [dict] or dict
823     :raises: :py:class:`TypeError` if no CNFs can be extracted
824     :returns: list with one or more CNF objects
825     :rtype: [dict]
826
827     Output varies depending on a few conditions:
828     - If `root` is a list, return it right away
829     - If `root` is a dict corresponding to a valid CNF value, return it wrapped
830       in a list
831     - If `root` is a dict with a `cnf` key containg a list (as the JSON
832       returned by `get_cnf -j`), return the value
833     - Otherwise, raise an error
834     """
835     if isinstance(root, list):
836         return root
837     if not isinstance(root, dict):
838         raise TypeError(
839             "Expected dictionary of CNF_VARs, got %s." % type(root))
840     if is_cnf_var(root):
841         return [root]
842     cnf = root.get("cnf", None)
843     if not isinstance(cnf, list):
844         raise TypeError("Expected list of CNF_VARs, got %s." % type(cnf))
845     return cnf
846
847
848 def normalize_cnf(cnf):
849     """
850     Ensure the output conforms to set_cnf()’s expectations.
851
852     :param cnf: list of CNF objects to normalize
853     :type cnf: [dict]
854     :returns: normalized list
855     :rtype: [list]
856     """
857     if isinstance(cnf, list) is False:
858         raise MalformedCNF("expected list of CNF_VARs, got [%s]" % type(cnf))
859     def norm(var):
860         vvar = \
861             { "number"   : var ["number"]
862             , "varname"  : var ["varname"].upper()
863             , "instance" : var ["instance"]
864             , "data"     : var ["data"]
865             }
866
867         children = var.get("children", None)
868         if children is not None:
869             vvar ["children"] = normalize_cnf(children)
870
871         parent = var.get("parent", None)
872         if parent is not None:
873             vvar ["parent"] = var["parent"]
874
875         comment = var.get("comment", None)
876         if comment is not None:
877             vvar ["comment"] = var["comment"]
878
879         return vvar
880
881     return [norm(var) for var in cnf]
882
883
884 ###############################################################################
885 # TRAVERSAL
886 ###############################################################################
887
888
889 def walk_cnf(cnf, nested, fun, acc):
890     """
891     Depth-first traversal of a CNF tree.
892
893     :type cnf: cnf list
894     :type nested: bool
895     :type fun: 'a -> bool -> (cnf stuff) -> 'a
896     :type acc: 'a
897     :rtype: 'a
898
899     Executes ``fun`` recursively for each node in the tree. The function
900     receives the accumulator ``acc`` which can be of an arbitrary type as first
901     argument. The second argument is a flag indicating whether the current
902     CNF var is a child (if ``True``) or a parent var. CNF member fields are
903     passed via named optional arguments.
904     """
905     for var in cnf:
906         acc = fun(acc,
907                   nested,
908                   comment=var.get("comment", None),
909                   data=var.get("data", None),
910                   instance=var.get("instance", None),
911                   number=var.get("number", None),
912                   parent=var.get("parent", None),
913                   varname=var.get("varname", None))
914         children = var.get("children", None)
915         if children is not None:
916             acc = walk_cnf(children, True, fun, acc)
917     return acc
918
919
920 def renumber_vars(root, parent=None, toplevel=False):
921     """
922     Number cnfvars linearly.
923
924     If *parent* is specified, numbering will start at this offset. Also, the
925     VAR *root* will be assigned this number as a parent lineno unless
926     *toplevel* is set (the root var in a CNF tree obviously can’t have a
927     parent).
928
929     The *toplevel* parameter is useful when renumbering an existing variable
930     starting at a given offset without at the same time having that offset
931     assigned as a parent.
932     """
933     root = cnf_root (root)
934     i = parent or 0
935     for var in root:
936         i += 1
937         var["number"] = i
938         if toplevel is False and parent is not None:
939             var["parent"] = parent
940         children = var.get("children", None)
941         if children is not None:
942             i = renumber_vars(children, parent=i, toplevel=False)
943     return i
944
945
946 def count_vars(root):
947     """
948     Traverse the cnf structure recursively, counting VAR objects (CNF lines).
949     """
950     cnf = cnf_root(root)
951     if cnf is None:
952         raise InvalidCNF(root)
953     return walk_cnf(cnf, True, lambda n, _nested, **_kwa: n + 1, 0)
954
955 #
956 # querying
957 #
958
959
960 def get_vars(cnf, data=None, instance=None):
961     """
962     get_vars -- Query a CNF_VAR structure. Skims the *toplevel* of the CNF_VAR
963     structure for entries with a matching `data` or `instance` field.
964
965     :type cnf: CNF_VAR
966     :type data: str
967     :type instance: int
968     :rtype: CNF_VAR
969     :returns: The structure containing only references to the
970              matching variables. Containing an empty list of
971              variables in case there is no match.
972
973     Values are compared literally. If both ``instance`` and ``data`` are
974     specified, vars will be compared against both.
975     """
976     cnf = cnf["cnf"]
977     if cnf:
978         criterion = lambda _var: False
979         if data:
980             if instance:
981                 criterion = lambda var: var[
982                     "data"] == data and var["instance"] == instance
983             else:
984                 criterion = lambda var: var["data"] == data
985         elif instance:
986             criterion = lambda var: var["instance"] == instance
987
988         return {"cnf": [var for var in cnf if criterion(var) is True]}
989
990     return {"cnf": []}
991
992
993 ###############################################################################
994 # PRINTING/DUMPING
995 ###############################################################################
996
997
998 #
999 # Print/dump raw CNF values
1000 #
1001
1002 def output_cnf(root, out, renumber=False):
1003     """
1004     Dump a textual representation of given CNF VAR structure to given stream.
1005
1006     Runs :py:func:`format_cnf_vars` on the input (`root`) and then writes that
1007     to the given file-like object (out).
1008
1009     :param root: a CNF_VAR structure
1010     :type root: dict or list or anything that :py:func:`cnf_root` accepts
1011     :param out: file-like object or something with a `write(str)` function
1012     :param bool renumber: Whether to renumber cnfvars first
1013
1014     Files are converted to the 8-bit format expected by CNF so they can be fed
1015     directly into libcnffile.
1016     """
1017     cnf = cnf_root(root)
1018     if renumber is True:
1019         _count = renumber_vars(root)
1020     if is_cnf(cnf) is True:
1021         (_, lines) = functools.reduce(format_cnf_vars, cnf, (0, []))
1022         if isinstance(out, (io.RawIOBase, io.BufferedIOBase)):
1023             out.write (b"\n".join (lines))
1024             out.write (b"\n")
1025         else:   # either subclass of io.TextIOBase or unknown
1026             out.write ("\n".join (map (from_latin1, lines)))
1027             out.write ("\n")
1028
1029
1030 def dump_cnf_bytes (root, renumber=False):
1031     """
1032     Serialize CNF var structure, returning the result as a byte sequence.
1033     """
1034     cnf = cnf_root(root)
1035     out = io.BytesIO()
1036     output_cnf(root, out, renumber=renumber)
1037     res = out.getvalue()
1038     out.close()
1039     return res
1040
1041
1042 def dump_cnf_string(root, renumber=False):
1043     """
1044     Serialize CNF var structure, returning a latin1-encode byte string.
1045
1046     .. todo::this is identical to :py:func:`dump_cnf_bytes`!
1047     """
1048     cnf = cnf_root(root)
1049     out = io.BytesIO()
1050     output_cnf(root, out, renumber=renumber)
1051     res = out.getvalue()
1052     out.close()
1053     return res
1054
1055
1056 def print_cnf(root, out=None, renumber=False):
1057     """
1058     Print given CNF_VAR structure to stdout (or other file-like object).
1059
1060     Note that per default the config is printed to sys.stdout using the shell's
1061     preferred encoding. If the shell cannot handle unicode this might raise
1062     `UnicodeError`.
1063
1064     All params forwarded to :py:func:`output_cnf`. See args there.
1065     """
1066     if root is not None:
1067         output_cnf(root, out or sys.stdout, renumber=renumber)
1068
1069
1070 def write_cnf(*argv, **kw_argv):
1071     """Alias for :py:func:`print_cnf`."""
1072     print_cnf(*argv, **kw_argv)
1073
1074
1075 def print_cnf_raw(root, out=None):
1076     """`if root is not None: out.write(root)`."""
1077     if root is not None:
1078         out.write(root)
1079
1080
1081 def write_cnf_raw(*argv, **kw_argv):
1082     """Alias for :py:func:`print_cnf_raw`."""
1083     print_cnf_raw(*argv, **kw_argv)
1084
1085
1086 #
1087 # Print/dump CNF values in JSON format
1088 #
1089
1090
1091 def output_json(root, out, renumber=False):
1092     """
1093     Dump CNF_VAR structure to file-like object in json format.
1094
1095     :param root: CNF_VAR structure
1096     :type root: dict or list or anything that :py:func:`cnf_root` accepts
1097     :param out: file-like object, used as argument to :py:func:`json.dumps` so
1098                 probably has to accept `str` (as opposed to `bytes`).
1099     :param bool renumber: whether to renumber variables before dupming.
1100     """
1101     # if not isinstance(out, file):
1102         #raise TypeError("%s (%s) is not a stream." % (out, type(out)))
1103     if renumber is True:
1104         _count = renumber_vars(root)
1105     if is_cnf(root) is True:
1106         root ["cnf"] = normalize_cnf(cnf_root (root))
1107         data = json.dumps(root)
1108         out.write(data)
1109         out.write("\n")
1110     # TODO: else raise value error?
1111
1112
1113 def dump_json_string(root, renumber=False):
1114     """
1115     Serialize CNF var structure as JSON, returning the result as a string.
1116     """
1117     out = io.StringIO()
1118     output_json(root, out, renumber=renumber)
1119     res = out.getvalue()
1120     out.close()
1121     return res
1122
1123
1124 def print_cnf_json(root, out=None, renumber=False):
1125     """
1126     Print CNF_VAR structure in json format to stdout.
1127
1128     Calls :py:func:`output_json` with `sys.stdout` if `out` is not given or
1129     `None`.
1130     """
1131     if root is not None:
1132         output_json(root, out or sys.stdout, renumber=renumber)
1133
1134
1135 def write_cnf_json(*argv, **kw_argv):
1136     """Alias for :py:func:`print_cnf_json`."""
1137     print_cnf_json(*argv, **kw_argv)
1138
1139
1140
1141 ###############################################################################
1142 # ENTRY POINT FOR DEVELOPMENT
1143 ###############################################################################
1144
1145
1146 def usage():
1147     print("usage: cnfvar.py -"      , file=sys.stderr)
1148     print(""                        , file=sys.stderr)
1149     print("    Read CNF from stdin.", file=sys.stderr)
1150     print(""                        , file=sys.stderr)
1151
1152
1153 def main(argv):
1154     if len(argv) > 1:
1155         first = argv[1]
1156         if first == "-":
1157             cnf = read_cnf(sys.stdin.buffer.read())
1158             print_cnf(cnf)
1159             return 0
1160         elif first == "test":
1161             cnf = read_cnf(sys.stdin.buffer.read())
1162             cnff = get_vars(cnf, instance=2, data="FAX")
1163             print_cnf(cnff)
1164             return 0
1165     usage()
1166     return -1
1167
1168
1169 if __name__ == "__main__":
1170     sys.exit(main(sys.argv))