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