Drop previous simple cnf module and class throughout the codebase
[pyi2ncommon] / src / cnfvar_old.py
CommitLineData
3e9105b3 1#!/usr/bin/env python
f0173798
PG
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.
f365f614
CH
20#
21# Copyright (c) 2016-2018 Intra2net AG <info@intra2net.com>
3e9105b3
PG
22
23"""
24
2c924c9e
PG
25summary
26-------------------------------------------------------------------------------
27Represent CNF_VARs as recursive structures.
3e9105b3 28
b7e04a3e
CH
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!
d7cdea2c 32
966adbbf 33Copyright: 2014-2017 Intra2net AG
f0173798 34License: GPLv2+
3e9105b3
PG
35
36
2c924c9e
PG
37contents
38-------------------------------------------------------------------------------
39
40This module provides read and write functionality for the Intra2net *CNF*
41format. Two different syntaxes are available: classical *CNF* and a JSON
42representation. Both versions are commonly understood by Intra2net software.
2c924c9e 43
bf80448a
PG
44On the command line, raw CNF is accepted if the option ``-`` is given: ::
45
46 $ get_cnf routing 2 |python3 cnfvar.py - <<ENOUGH
2c924c9e
PG
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"
bf80448a
PG
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
68The input takes one round-trip through the parsers and will error out on
69problematic lines. Thus, ``cnfvar.py`` can be used to syntax-check CNF data.
70
71Note that line numbers may be arbitrarily reassigned in the process. Of course,
72parent references and the relative ordering of lines will be preserved in this
73case.
3e9105b3 74
2c924c9e
PG
75.. todo::
76 Decide on some facility for automatic fixup of line number values. The
3e9105b3 77 internal representation is recursive so line numbers are not needed to
2c924c9e
PG
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
f2eaac7a
PG
95notes on Python 3 conversion
96-------------------------------------------------------------------------------
97
98Since the original *CNF* format assumes latin-1 encoded data pretty much
99exclusively, we preserve the original encoding while parsing the file.
100When assembling the data structures returned to the user, values are then
101converted to strings so they can be used naturally at the Python end.
102
2c924c9e
PG
103implementation
104-------------------------------------------------------------------------------
3e9105b3
PG
105"""
106
107import functools
108import sys
109import json
110import re
111import io
112
3e9105b3 113
59b9c10e
SA
114###############################################################################
115# CONSTANTS
116###############################################################################
117
3e9105b3 118
59b9c10e
SA
119CNF_FIELD_MANDATORY = set ([ "varname", "data", "instance" ])
120CNF_FIELD_OPTIONAL = set ([ "parent", "children", "comment", "number" ])
121CNF_FIELD_KNOWN = CNF_FIELD_MANDATORY | CNF_FIELD_OPTIONAL
122
123grab_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)
3e9105b3 131
59b9c10e
SA
132base_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
159child_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#
3e9105b3 194def to_latin1(s):
02fe6179 195 """Take given unicode str and convert it to a latin1-encoded `bytes`."""
3e9105b3
PG
196 return s.encode("latin-1")
197
198
199def from_latin1(s):
02fe6179 200 """Take given latin1-encoded `bytes` value and convert it to `str`."""
3e9105b3
PG
201 return s.decode("latin-1")
202
59b9c10e
SA
203
204#
205# Conversion functions
206#
207
208def marshal_in_number(number):
209 return int(number)
210
211
212def marshal_in_parent(parent):
213 return int(parent)
214
215
216def marshal_in_instance(instance):
217 return int(instance)
218
219
220def marshal_in_varname(varname):
221 return from_latin1(varname).lower()
222
223
224def marshal_in_data(data):
225 return from_latin1(data) if data is not None else ""
226
227
228def marshal_in_comment(comment):
229 return comment and from_latin1(comment[1:].strip()) or None
230
231
3e9105b3 232#
59b9c10e 233# Type checking
3e9105b3
PG
234#
235
59b9c10e
SA
236def is_string(s):
237 return isinstance(s, str)
238
239
240###############################################################################
241# EXCEPTIONS
242###############################################################################
243
3e9105b3
PG
244
245class 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
59b9c10e 254class MalformedCNF(Exception):
3e9105b3 255
59b9c10e
SA
256 def __init__(self, msg):
257 self.msg = msg
3e9105b3 258
59b9c10e
SA
259 def __str__(self):
260 return "Malformed CNF file: \"%s\"" % self.msg
3e9105b3 261
59b9c10e
SA
262
263###############################################################################
264# VALIDATION
265###############################################################################
3e9105b3
PG
266
267
268def 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
333def 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 """
59b9c10e 346 cnf = cnf_root(root)
0936b2f2 347 if cnf is None:
59b9c10e 348 raise InvalidCNF(root)
3e9105b3
PG
349 return walk_cnf(cnf, False, is_valid, {}) is not None
350
a922e13c 351
59b9c10e 352def is_cnf_var(obj):
a922e13c 353 """
59b9c10e
SA
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
a922e13c 360 """
59b9c10e
SA
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
a922e13c 378
3e9105b3 379#
59b9c10e 380# JSON reader for get_cnf -j (the easy part)
3e9105b3
PG
381#
382
b05d27ab
CH
383def 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
3e9105b3
PG
404
405def read_cnf_json(cnfdata):
ff89b6f6
PD
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 """
96eb61d7
PG
416 if isinstance (cnfdata, bytes) is True:
417 cnfdata = from_latin1 (cnfdata)
b05d27ab 418 cnf_json = json.loads(cnfdata, object_hook=make_varname_lowercase)
3e9105b3
PG
419 if is_cnf(cnf_json) is False:
420 raise TypeError("Invalid CNF_VAR.")
421 return cnf_json
422
3e9105b3 423
59b9c10e
SA
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#
3e9105b3 449
3e9105b3 450
59b9c10e
SA
451def read_cnf(data):
452 """
453 Read cnf data from data bytes.
3e9105b3 454
7f66ff3e
CH
455 :param data: raw data
456 :type data: str or bytes
59b9c10e
SA
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}
3e9105b3
PG
470
471
472def prepare(raw):
473 """
59b9c10e
SA
474 Build 3-element iterable from a CNF string dump.
475
476 :param raw: string content as returned by `get_cnf`
ff89b6f6 477 :type raw: bytes
59b9c10e
SA
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.
3e9105b3
PG
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()
ff89b6f6 496 if first == b"":
3e9105b3
PG
497 return advance((first, second, lines))
498
499 return (first, second, lines)
500
501
59b9c10e
SA
502def 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)
3e9105b3
PG
526
527
f2eaac7a 528def get(cns):
59b9c10e
SA
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 """
3e9105b3
PG
538 current, _, _ = cns
539 return current
540
541
59b9c10e
SA
542def peek(cns):
543 """
544 Get the next line from the state without advancing it.
3e9105b3 545
59b9c10e
SA
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
3e9105b3 554
3e9105b3 555
59b9c10e
SA
556def 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
596def 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)
3e9105b3
PG
680
681
682def get_parent(line):
59b9c10e
SA
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 """
3e9105b3
PG
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
f2eaac7a 695
59b9c10e
SA
696def read_base_line(line):
697 """
698 Turn one top-level CNF line into a dictionary.
f2eaac7a 699
59b9c10e
SA
700 :param str line: CNF line
701 :rtype: {str: Any}
f2eaac7a 702
59b9c10e
SA
703 This performs the necessary decoding on values to obtain proper Python
704 strings from 8-bit encoded CNF data.
f2eaac7a 705
59b9c10e
SA
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 """
3e9105b3
PG
710 if len(line.strip()) == 0:
711 return None # ignore empty lines
ff89b6f6 712 if line[0] == b"#":
3e9105b3
PG
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 {
f2eaac7a
PG
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),
3e9105b3
PG
725 }
726
3e9105b3
PG
727
728def read_child_line(line):
59b9c10e
SA
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 """
3e9105b3
PG
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:
5af101c6
PG
746 raise MalformedCNF("Syntax error in child line \"\"\"%s\"\"\""
747 % from_latin1 (line))
3e9105b3
PG
748 number, parent, varname, instance, data, comment = match.groups()
749 return {
f2eaac7a
PG
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),
3e9105b3
PG
756 }
757
758
59b9c10e
SA
759###############################################################################
760# SERIALIZATION
761###############################################################################
02fe6179 762
3e9105b3 763
16453ac5
PG
764cnf_line_nest_indent = " "
765cnf_line_base_fmt = "%d %s,%d: \"%s\""
766cnf_line_child_fmt = "%d %s(%d) %s,%d: \"%s\""
3e9105b3 767
59b9c10e 768
3e9105b3
PG
769def format_cnf_vars(da, var):
770 """
87751ec4 771 Return a list of formatted cnf_line byte strings.
5226025f 772
59b9c10e
SA
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
5226025f
PG
785 The variable names are uppercased unconditionally because while ``get_cnf``
786 is case-indifferent for variable names, ``set_cnf`` isn’t.
3e9105b3 787 """
3e9105b3
PG
788 depth, acc = da
789 line = None
790 if depth > 0:
791 line = cnf_line_child_fmt \
792 % (var["number"],
5226025f
PG
793 cnf_line_nest_indent * depth,
794 var["parent"],
59b9c10e 795 var["varname"].upper(),
5226025f
PG
796 var["instance"],
797 var["data"])
3e9105b3
PG
798 else:
799 line = cnf_line_base_fmt \
800 % (var["number"],
59b9c10e 801 var["varname"].upper(),
5226025f
PG
802 var["instance"],
803 var["data"])
3e9105b3
PG
804
805 comment = var.get("comment", None)
59b9c10e 806 if comment and len(comment) != 0:
16453ac5 807 line = line + (" # %s" % comment)
3e9105b3 808
59b9c10e 809 acc.append(to_latin1(line))
3e9105b3
PG
810
811 children = var.get("children", None)
812 if children is not None:
813 (_, acc) = functools.reduce(format_cnf_vars, children, (depth + 1, acc))
16453ac5 814
3e9105b3
PG
815 return (depth, acc)
816
817
59b9c10e 818def cnf_root(root):
0936b2f2 819 """
59b9c10e
SA
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
0936b2f2 835 """
59b9c10e 836 if isinstance(root, list):
a922e13c 837 return root
3e9105b3
PG
838 if not isinstance(root, dict):
839 raise TypeError(
840 "Expected dictionary of CNF_VARs, got %s." % type(root))
59b9c10e
SA
841 if is_cnf_var(root):
842 return [root]
3e9105b3
PG
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
59b9c10e 849def normalize_cnf(cnf):
32eeaf11
PG
850 """
851 Ensure the output conforms to set_cnf()’s expectations.
59b9c10e
SA
852
853 :param cnf: list of CNF objects to normalize
854 :type cnf: [dict]
855 :returns: normalized list
856 :rtype: [list]
32eeaf11 857 """
59b9c10e
SA
858 if isinstance(cnf, list) is False:
859 raise MalformedCNF("expected list of CNF_VARs, got [%s]" % type(cnf))
860 def norm(var):
32eeaf11
PG
861 vvar = \
862 { "number" : var ["number"]
59b9c10e 863 , "varname" : var ["varname"].upper()
32eeaf11
PG
864 , "instance" : var ["instance"]
865 , "data" : var ["data"]
866 }
867
59b9c10e 868 children = var.get("children", None)
32eeaf11 869 if children is not None:
59b9c10e
SA
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"]
32eeaf11
PG
879
880 return vvar
881
59b9c10e 882 return [norm(var) for var in cnf]
32eeaf11
PG
883
884
59b9c10e
SA
885###############################################################################
886# TRAVERSAL
887###############################################################################
3e9105b3
PG
888
889
59b9c10e
SA
890def 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
3e9105b3
PG
919
920
59b9c10e
SA
921def 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
947def 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
961def 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
c24fd52c
CH
1003def 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
59b9c10e
SA
1014
1015 Files are converted to the 8-bit format expected by CNF so they can be fed
1016 directly into libcnffile.
c24fd52c 1017 """
3e9105b3 1018 cnf = cnf_root(root)
3e9105b3 1019 if renumber is True:
fb356a82 1020 _count = renumber_vars(root)
3e9105b3
PG
1021 if is_cnf(cnf) is True:
1022 (_, lines) = functools.reduce(format_cnf_vars, cnf, (0, []))
c24fd52c 1023 if isinstance(out, (io.RawIOBase, io.BufferedIOBase)):
87751ec4
PG
1024 out.write (b"\n".join (lines))
1025 out.write (b"\n")
c24fd52c
CH
1026 else: # either subclass of io.TextIOBase or unknown
1027 out.write ("\n".join (map (from_latin1, lines)))
1028 out.write ("\n")
87751ec4
PG
1029
1030
1031def dump_cnf_bytes (root, renumber=False):
1032 """
02fe6179 1033 Serialize CNF var structure, returning the result as a byte sequence.
87751ec4
PG
1034 """
1035 cnf = cnf_root(root)
1036 out = io.BytesIO()
c24fd52c 1037 output_cnf(root, out, renumber=renumber)
87751ec4
PG
1038 res = out.getvalue()
1039 out.close()
1040 return res
3e9105b3
PG
1041
1042
1043def dump_cnf_string(root, renumber=False):
1044 """
02fe6179
CH
1045 Serialize CNF var structure, returning a latin1-encode byte string.
1046
1047 .. todo::this is identical to :py:func:`dump_cnf_bytes`!
3e9105b3
PG
1048 """
1049 cnf = cnf_root(root)
ce9ef23c 1050 out = io.BytesIO()
c24fd52c 1051 output_cnf(root, out, renumber=renumber)
3e9105b3
PG
1052 res = out.getvalue()
1053 out.close()
1054 return res
1055
02fe6179 1056
3e9105b3 1057def print_cnf(root, out=None, renumber=False):
02fe6179
CH
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 """
3e9105b3 1067 if root is not None:
c24fd52c 1068 output_cnf(root, out or sys.stdout, renumber=renumber)
3e9105b3
PG
1069
1070
1071def write_cnf(*argv, **kw_argv):
02fe6179 1072 """Alias for :py:func:`print_cnf`."""
3e9105b3
PG
1073 print_cnf(*argv, **kw_argv)
1074
1075
59b9c10e
SA
1076def 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
1082def 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
3e9105b3 1092def output_json(root, out, renumber=False):
02fe6179
CH
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 """
3e9105b3
PG
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:
32eeaf11 1107 root ["cnf"] = normalize_cnf(cnf_root (root))
3e9105b3
PG
1108 data = json.dumps(root)
1109 out.write(data)
1110 out.write("\n")
02fe6179 1111 # TODO: else raise value error?
3e9105b3
PG
1112
1113
1114def dump_json_string(root, renumber=False):
1115 """
02fe6179 1116 Serialize CNF var structure as JSON, returning the result as a string.
3e9105b3
PG
1117 """
1118 out = io.StringIO()
1119 output_json(root, out, renumber=renumber)
1120 res = out.getvalue()
1121 out.close()
1122 return res
1123
1124
1125def print_cnf_json(root, out=None, renumber=False):
02fe6179
CH
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 """
3e9105b3
PG
1132 if root is not None:
1133 output_json(root, out or sys.stdout, renumber=renumber)
1134
1135
1136def write_cnf_json(*argv, **kw_argv):
02fe6179 1137 """Alias for :py:func:`print_cnf_json`."""
3e9105b3
PG
1138 print_cnf_json(*argv, **kw_argv)
1139
3e9105b3
PG
1140
1141
59b9c10e
SA
1142###############################################################################
1143# ENTRY POINT FOR DEVELOPMENT
1144###############################################################################
3e9105b3 1145
3e9105b3 1146
59b9c10e
SA
1147def 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)
3e9105b3
PG
1152
1153
1154def main(argv):
1155 if len(argv) > 1:
1156 first = argv[1]
1157 if first == "-":
f2eaac7a 1158 cnf = read_cnf(sys.stdin.buffer.read())
3e9105b3 1159 print_cnf(cnf)
f2eaac7a 1160 return 0
3e9105b3 1161 elif first == "test":
f2eaac7a 1162 cnf = read_cnf(sys.stdin.buffer.read())
3e9105b3
PG
1163 cnff = get_vars(cnf, instance=2, data="FAX")
1164 print_cnf(cnff)
f2eaac7a 1165 return 0
59b9c10e 1166 usage()
f2eaac7a 1167 return -1
3e9105b3 1168
7f66ff3e 1169
3e9105b3 1170if __name__ == "__main__":
59b9c10e 1171 sys.exit(main(sys.argv))