Deprecate cnfvar_old and arnied_wrapper
[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
d7cdea2c
CH
29DEPRECATED! 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
966adbbf 32Copyright: 2014-2017 Intra2net AG
f0173798 33License: GPLv2+
3e9105b3
PG
34
35
2c924c9e
PG
36contents
37-------------------------------------------------------------------------------
38
39This module provides read and write functionality for the Intra2net *CNF*
40format. Two different syntaxes are available: classical *CNF* and a JSON
41representation. Both versions are commonly understood by Intra2net software.
2c924c9e 42
bf80448a
PG
43On the command line, raw CNF is accepted if the option ``-`` is given: ::
44
45 $ get_cnf routing 2 |python3 cnfvar.py - <<ENOUGH
2c924c9e
PG
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"
bf80448a
PG
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
67The input takes one round-trip through the parsers and will error out on
68problematic lines. Thus, ``cnfvar.py`` can be used to syntax-check CNF data.
69
70Note that line numbers may be arbitrarily reassigned in the process. Of course,
71parent references and the relative ordering of lines will be preserved in this
72case.
3e9105b3 73
2c924c9e
PG
74.. todo::
75 Decide on some facility for automatic fixup of line number values. The
3e9105b3 76 internal representation is recursive so line numbers are not needed to
2c924c9e
PG
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
f2eaac7a
PG
94notes on Python 3 conversion
95-------------------------------------------------------------------------------
96
97Since the original *CNF* format assumes latin-1 encoded data pretty much
98exclusively, we preserve the original encoding while parsing the file.
99When assembling the data structures returned to the user, values are then
100converted to strings so they can be used naturally at the Python end.
101
2c924c9e
PG
102implementation
103-------------------------------------------------------------------------------
3e9105b3
PG
104"""
105
106import functools
107import sys
108import json
109import re
110import io
111
3e9105b3 112
59b9c10e
SA
113###############################################################################
114# CONSTANTS
115###############################################################################
116
3e9105b3 117
59b9c10e
SA
118CNF_FIELD_MANDATORY = set ([ "varname", "data", "instance" ])
119CNF_FIELD_OPTIONAL = set ([ "parent", "children", "comment", "number" ])
120CNF_FIELD_KNOWN = CNF_FIELD_MANDATORY | CNF_FIELD_OPTIONAL
121
122grab_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)
3e9105b3 130
59b9c10e
SA
131base_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
158child_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#
3e9105b3 193def to_latin1(s):
02fe6179 194 """Take given unicode str and convert it to a latin1-encoded `bytes`."""
3e9105b3
PG
195 return s.encode("latin-1")
196
197
198def from_latin1(s):
02fe6179 199 """Take given latin1-encoded `bytes` value and convert it to `str`."""
3e9105b3
PG
200 return s.decode("latin-1")
201
59b9c10e
SA
202
203#
204# Conversion functions
205#
206
207def marshal_in_number(number):
208 return int(number)
209
210
211def marshal_in_parent(parent):
212 return int(parent)
213
214
215def marshal_in_instance(instance):
216 return int(instance)
217
218
219def marshal_in_varname(varname):
220 return from_latin1(varname).lower()
221
222
223def marshal_in_data(data):
224 return from_latin1(data) if data is not None else ""
225
226
227def marshal_in_comment(comment):
228 return comment and from_latin1(comment[1:].strip()) or None
229
230
3e9105b3 231#
59b9c10e 232# Type checking
3e9105b3
PG
233#
234
59b9c10e
SA
235def is_string(s):
236 return isinstance(s, str)
237
238
239###############################################################################
240# EXCEPTIONS
241###############################################################################
242
3e9105b3
PG
243
244class 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
59b9c10e 253class MalformedCNF(Exception):
3e9105b3 254
59b9c10e
SA
255 def __init__(self, msg):
256 self.msg = msg
3e9105b3 257
59b9c10e
SA
258 def __str__(self):
259 return "Malformed CNF file: \"%s\"" % self.msg
3e9105b3 260
59b9c10e
SA
261
262###############################################################################
263# VALIDATION
264###############################################################################
3e9105b3
PG
265
266
267def 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
332def 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 """
59b9c10e 345 cnf = cnf_root(root)
0936b2f2 346 if cnf is None:
59b9c10e 347 raise InvalidCNF(root)
3e9105b3
PG
348 return walk_cnf(cnf, False, is_valid, {}) is not None
349
a922e13c 350
59b9c10e 351def is_cnf_var(obj):
a922e13c 352 """
59b9c10e
SA
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
a922e13c 359 """
59b9c10e
SA
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
a922e13c 377
3e9105b3 378#
59b9c10e 379# JSON reader for get_cnf -j (the easy part)
3e9105b3
PG
380#
381
b05d27ab
CH
382def 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
3e9105b3
PG
403
404def read_cnf_json(cnfdata):
ff89b6f6
PD
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 """
96eb61d7
PG
415 if isinstance (cnfdata, bytes) is True:
416 cnfdata = from_latin1 (cnfdata)
b05d27ab 417 cnf_json = json.loads(cnfdata, object_hook=make_varname_lowercase)
3e9105b3
PG
418 if is_cnf(cnf_json) is False:
419 raise TypeError("Invalid CNF_VAR.")
420 return cnf_json
421
3e9105b3 422
59b9c10e
SA
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#
3e9105b3 448
3e9105b3 449
59b9c10e
SA
450def read_cnf(data):
451 """
452 Read cnf data from data bytes.
3e9105b3 453
59b9c10e
SA
454 :param bytes data: raw data
455 :return: the parsed cnf data
456 :rtype: {str, {str, str or int}}
457 """
458 if isinstance(data, str):
459 data = to_latin1(data)
460 state = prepare(data)
461 if state is None:
462 raise InvalidCNF("Empty input string.")
463
464 cnf = parse_cnf_root(state)
465 if is_cnf(cnf) is False:
466 raise TypeError("Invalid CNF_VAR.")
467 return {"cnf": cnf}
3e9105b3
PG
468
469
470def prepare(raw):
471 """
59b9c10e
SA
472 Build 3-element iterable from a CNF string dump.
473
474 :param raw: string content as returned by `get_cnf`
ff89b6f6 475 :type raw: bytes
59b9c10e
SA
476 :returns: 3-element tuple, where the first two elements are the first two
477 lines of the output and the third is a list containing the rest
478 of the lines in reverse.
3e9105b3
PG
479 :rtype: (str * str option * str list) option
480 """
481 lines = raw.splitlines()
482 lines.reverse()
483 try:
484 first = lines.pop()
485 except IndexError:
486 return None
487
488 try:
489 second = lines.pop()
490 except IndexError:
491 second = None
492
493 first = first.strip()
ff89b6f6 494 if first == b"":
3e9105b3
PG
495 return advance((first, second, lines))
496
497 return (first, second, lines)
498
499
59b9c10e
SA
500def advance(cns):
501 """
502 Pop the next line from the stream, advancing the tuple.
503
504 :param cns: a 3-element tuple containing two CNF lines and a list of the
505 remaining lines
506 :type cnd: (str, str, [str])
507 :returns: a new tuple with a new item popped from the list of lines
508 :rtype cnd: (str, str, [str])
509 """
510 current, next, stream = cns
511 if next is None: # reached end of stream
512 return None
513 current = next
514
515 try:
516 next = stream.pop()
517 next = next.strip()
518 except IndexError:
519 next = None
520
521 if current == "": # skip blank lines
522 return advance((current, next, stream))
523 return (current, next, stream)
3e9105b3
PG
524
525
f2eaac7a 526def get(cns):
59b9c10e
SA
527 """
528 Get the current line from the state without advancing it.
529
530 :param cns: a 3-element tuple containing two CNF lines and a list of the
531 remaining lines
532 :type cnd: (str, str, [str])
533 :returns: the CNF line stored as `current`
534 :rtype: str
535 """
3e9105b3
PG
536 current, _, _ = cns
537 return current
538
539
59b9c10e
SA
540def peek(cns):
541 """
542 Get the next line from the state without advancing it.
3e9105b3 543
59b9c10e
SA
544 :param cns: a 3-element tuple containing two CNF lines and a list of the
545 remaining lines
546 :type cnd: (str, str, [str])
547 :returns: the CNF line stored as `next`
548 :rtype: str
549 """
550 _, next, _ = cns
551 return next
3e9105b3 552
3e9105b3 553
59b9c10e
SA
554def parse_cnf_root(state):
555 """
556 Iterate over and parse a list of CNF lines.
557
558 :param state: a 3-element tuple containing two lines and a list of the
559 remaining lines
560 :type state: (str, str, [str])
561 :returns: a list of parsed CNF variables
562 :rtype: [dict]
563
564 The function will parse the first element from the `state` tuple, then read
565 the next line to see if it is a child variable. If it is, it will be
566 appended to the last parsed CNF, otherwise top-level parsing is done
567 normally.
568 """
569 lines = []
570 current = get(state)
571 while state:
572 cnf_line = read_base_line(current)
573 if cnf_line is not None:
574 lines.append(cnf_line)
575 state = advance(state)
576 if state is None: # -> nothing left to do
577 break
578 current = get(state)
579 parent = get_parent(current) # peek at next line
580 if parent is not None: # -> recurse into children
581 (state, children, _parent) = parse_cnf_children(state, parent)
582 cnf_line["children"] = children
583 if state is None:
584 break
585 current = get(state)
586 else:
587 state = advance(state)
588 if state is None:
589 break
590 current = get(state)
591 return lines
592
593
594def parse_cnf_children(state, parent):
595 """
596 Read and parse child CNFs of a given parent until there is none left.
597
598 :param state: a 3-element tuple containing two lines and a list of the
599 remaining lines
600 :type state: (str, str, [str])
601 :param parent: id of the parent whose children we are looking for
602 :type parent: int
603 :returns: a 3-element tuple with the current state, a list of children of
604 the given parent and the parent ID
605 :rtype: (tuple, [str], int)
606
607 The function will recursively parse child lines from the `state` tuple
608 until one of these conditions is satisfied:
609
610 1. the input is exhausted
611 2. the next CNF line
612 2.1. is a toplevel line
613 2.2. is a child line whose parent has a lower parent number
614
615 Conceptually, 2.1 is a very similar to 2.2 but due to the special status of
616 toplevel lines in CNF we need to handle them separately.
617
618 Note that since nesting of CNF vars is achieved via parent line numbers,
619 lines with different parents could appear out of order. libcnffile will
620 happily parse those and still assign children to the specified parent:
621
622 ::
623 # set_cnf <<THATSALL
624 1 USER,1337: "l33t_h4x0r"
625 2 (1) USER_GROUP_MEMBER_REF,0: "2"
626 4 USER,1701: "picard"
627 5 (4) USER_GROUP_MEMBER_REF,0: "2"
628 6 (4) USER_PASSWORD,0: "engage"
629 3 (1) USER_PASSWORD,0: "hacktheplanet"
630 THATSALL
631 # get_cnf user 1337
632 1 USER,1337: "l33t_h4x0r"
633 2 (1) USER_GROUP_MEMBER_REF,0: "2"
634 3 (1) USER_PASSWORD,0: "hacktheplanet"
635 # get_cnf user 1701
636 1 USER,1701: "picard"
637 2 (1) USER_GROUP_MEMBER_REF,0: "2"
638 3 (1) USER_PASSWORD,0: "engage"
639
640 It is a limitation of ``cnfvar.py`` that it cannot parse CNF data
641 structured like the above example: child lists are only populated from
642 subsequent CNF vars using the parent number solely to track nesting levels.
643 The parser does not keep track of line numbers while traversing the input
644 so it doesn’t support retroactively assigning a child to anything else but
645 the immediate parent.
646 """
647 lines = []
648 current = get(state)
649 while True:
650 cnf_line = read_child_line(current)
651 if cnf_line is not None:
652 lines.append(cnf_line)
653 state = advance(state)
654 if state is None:
655 break
656 current = get(state)
657 new_parent = get_parent(current)
658 if new_parent is None:
659 # drop stack
660 return (state, lines, None)
661 if new_parent > parent:
662 # parent is further down in hierarchy -> new level
663 (state, children, new_parent) = \
664 parse_cnf_children (state, new_parent)
665 if state is None:
666 break
667 cnf_line["children"] = children
668 current = get(state)
669 new_parent = get_parent(current)
670 if new_parent is None:
671 # drop stack
672 return (state, lines, None)
673 if new_parent < parent:
674 # parent is further up in hierarchy -> pop level
675 return (state, lines, new_parent)
676 # new_parent == parent -> continue parsing on same level
677 return (state, lines, parent)
3e9105b3
PG
678
679
680def get_parent(line):
59b9c10e
SA
681 """
682 Extract the ID of the parent for a given CNF line.
683
684 :param str line: CNF line
685 :returns: parent ID or None if no parent is found
686 :rtype: int or None
687 """
3e9105b3
PG
688 match = re.match(grab_parent_pattern, line)
689 if match is None: # -> no parent
690 return None
691 return int(match.groups()[0])
692
f2eaac7a 693
59b9c10e
SA
694def read_base_line(line):
695 """
696 Turn one top-level CNF line into a dictionary.
f2eaac7a 697
59b9c10e
SA
698 :param str line: CNF line
699 :rtype: {str: Any}
f2eaac7a 700
59b9c10e
SA
701 This performs the necessary decoding on values to obtain proper Python
702 strings from 8-bit encoded CNF data.
f2eaac7a 703
59b9c10e
SA
704 The function only operates on individual lines. Argument strings that
705 contain data for multiple lines – this includes child lines of the current
706 CNF var! – will trigger a parsing exception.
707 """
3e9105b3
PG
708 if len(line.strip()) == 0:
709 return None # ignore empty lines
ff89b6f6 710 if line[0] == b"#":
3e9105b3
PG
711 return None # ignore comments
712
713 match = re.match(base_line_pattern, line)
714 if match is None:
715 raise MalformedCNF("Syntax error in line \"\"\"%s\"\"\"" % line)
716 number, varname, instance, data, comment = match.groups()
717 return {
f2eaac7a
PG
718 "number" : marshal_in_number (number),
719 "varname" : marshal_in_varname (varname),
720 "instance" : marshal_in_instance (instance),
721 "data" : marshal_in_data (data),
722 "comment" : marshal_in_comment (comment),
3e9105b3
PG
723 }
724
3e9105b3
PG
725
726def read_child_line(line):
59b9c10e
SA
727 """
728 Turn one child CNF line into a dictionary.
729
730 :param str line: CNF line
731 :rtype: {str: Any}
732
733 This function only operates on individual lines. If the argument string is
734 syntactically valid but contains input representing multiple CNF vars, a
735 parse error will be thrown.
736 """
3e9105b3
PG
737 if len(line.strip()) == 0:
738 return None # ignore empty lines
739 if line[0] == "#":
740 return None # ignore comments
741
742 match = re.match(child_line_pattern, line)
743 if match is None:
5af101c6
PG
744 raise MalformedCNF("Syntax error in child line \"\"\"%s\"\"\""
745 % from_latin1 (line))
3e9105b3
PG
746 number, parent, varname, instance, data, comment = match.groups()
747 return {
f2eaac7a
PG
748 "number" : marshal_in_number (number),
749 "parent" : marshal_in_parent (parent),
750 "varname" : marshal_in_varname (varname),
751 "instance" : marshal_in_instance (instance),
752 "data" : marshal_in_data (data),
753 "comment" : marshal_in_comment (comment),
3e9105b3
PG
754 }
755
756
59b9c10e
SA
757###############################################################################
758# SERIALIZATION
759###############################################################################
02fe6179 760
3e9105b3 761
16453ac5
PG
762cnf_line_nest_indent = " "
763cnf_line_base_fmt = "%d %s,%d: \"%s\""
764cnf_line_child_fmt = "%d %s(%d) %s,%d: \"%s\""
3e9105b3 765
59b9c10e 766
3e9105b3
PG
767def format_cnf_vars(da, var):
768 """
87751ec4 769 Return a list of formatted cnf_line byte strings.
5226025f 770
59b9c10e
SA
771 :param da: a tuple where the first element is the depth (0 = top-level,
772 >1 = child CNF) and the second is the string being built.
773 :type da: (int, str)
774 :param var: the CNF element to convert to string in the current iteration
775 :type var: dict
776 :returns: a tuple like `da`, where the second element should contain all
777 converted CNFs
778 :rtype: (int, str)
779
780 This function is meant to be passed to the :py:func:`functools.reduce`
781 function.
782
5226025f
PG
783 The variable names are uppercased unconditionally because while ``get_cnf``
784 is case-indifferent for variable names, ``set_cnf`` isn’t.
3e9105b3 785 """
3e9105b3
PG
786 depth, acc = da
787 line = None
788 if depth > 0:
789 line = cnf_line_child_fmt \
790 % (var["number"],
5226025f
PG
791 cnf_line_nest_indent * depth,
792 var["parent"],
59b9c10e 793 var["varname"].upper(),
5226025f
PG
794 var["instance"],
795 var["data"])
3e9105b3
PG
796 else:
797 line = cnf_line_base_fmt \
798 % (var["number"],
59b9c10e 799 var["varname"].upper(),
5226025f
PG
800 var["instance"],
801 var["data"])
3e9105b3
PG
802
803 comment = var.get("comment", None)
59b9c10e 804 if comment and len(comment) != 0:
16453ac5 805 line = line + (" # %s" % comment)
3e9105b3 806
59b9c10e 807 acc.append(to_latin1(line))
3e9105b3
PG
808
809 children = var.get("children", None)
810 if children is not None:
811 (_, acc) = functools.reduce(format_cnf_vars, children, (depth + 1, acc))
16453ac5 812
3e9105b3
PG
813 return (depth, acc)
814
815
59b9c10e 816def cnf_root(root):
0936b2f2 817 """
59b9c10e
SA
818 Extract a list of CNFs from a given structure.
819
820 :param root: list of CNFs or a CNF dictionary
821 :type root: [dict] or dict
822 :raises: :py:class:`TypeError` if no CNFs can be extracted
823 :returns: list with one or more CNF objects
824 :rtype: [dict]
825
826 Output varies depending on a few conditions:
827 - If `root` is a list, return it right away
828 - If `root` is a dict corresponding to a valid CNF value, return it wrapped
829 in a list
830 - If `root` is a dict with a `cnf` key containg a list (as the JSON
831 returned by `get_cnf -j`), return the value
832 - Otherwise, raise an error
0936b2f2 833 """
59b9c10e 834 if isinstance(root, list):
a922e13c 835 return root
3e9105b3
PG
836 if not isinstance(root, dict):
837 raise TypeError(
838 "Expected dictionary of CNF_VARs, got %s." % type(root))
59b9c10e
SA
839 if is_cnf_var(root):
840 return [root]
3e9105b3
PG
841 cnf = root.get("cnf", None)
842 if not isinstance(cnf, list):
843 raise TypeError("Expected list of CNF_VARs, got %s." % type(cnf))
844 return cnf
845
846
59b9c10e 847def normalize_cnf(cnf):
32eeaf11
PG
848 """
849 Ensure the output conforms to set_cnf()’s expectations.
59b9c10e
SA
850
851 :param cnf: list of CNF objects to normalize
852 :type cnf: [dict]
853 :returns: normalized list
854 :rtype: [list]
32eeaf11 855 """
59b9c10e
SA
856 if isinstance(cnf, list) is False:
857 raise MalformedCNF("expected list of CNF_VARs, got [%s]" % type(cnf))
858 def norm(var):
32eeaf11
PG
859 vvar = \
860 { "number" : var ["number"]
59b9c10e 861 , "varname" : var ["varname"].upper()
32eeaf11
PG
862 , "instance" : var ["instance"]
863 , "data" : var ["data"]
864 }
865
59b9c10e 866 children = var.get("children", None)
32eeaf11 867 if children is not None:
59b9c10e
SA
868 vvar ["children"] = normalize_cnf(children)
869
870 parent = var.get("parent", None)
871 if parent is not None:
872 vvar ["parent"] = var["parent"]
873
874 comment = var.get("comment", None)
875 if comment is not None:
876 vvar ["comment"] = var["comment"]
32eeaf11
PG
877
878 return vvar
879
59b9c10e 880 return [norm(var) for var in cnf]
32eeaf11
PG
881
882
59b9c10e
SA
883###############################################################################
884# TRAVERSAL
885###############################################################################
3e9105b3
PG
886
887
59b9c10e
SA
888def walk_cnf(cnf, nested, fun, acc):
889 """
890 Depth-first traversal of a CNF tree.
891
892 :type cnf: cnf list
893 :type nested: bool
894 :type fun: 'a -> bool -> (cnf stuff) -> 'a
895 :type acc: 'a
896 :rtype: 'a
897
898 Executes ``fun`` recursively for each node in the tree. The function
899 receives the accumulator ``acc`` which can be of an arbitrary type as first
900 argument. The second argument is a flag indicating whether the current
901 CNF var is a child (if ``True``) or a parent var. CNF member fields are
902 passed via named optional arguments.
903 """
904 for var in cnf:
905 acc = fun(acc,
906 nested,
907 comment=var.get("comment", None),
908 data=var.get("data", None),
909 instance=var.get("instance", None),
910 number=var.get("number", None),
911 parent=var.get("parent", None),
912 varname=var.get("varname", None))
913 children = var.get("children", None)
914 if children is not None:
915 acc = walk_cnf(children, True, fun, acc)
916 return acc
3e9105b3
PG
917
918
59b9c10e
SA
919def renumber_vars(root, parent=None, toplevel=False):
920 """
921 Number cnfvars linearly.
922
923 If *parent* is specified, numbering will start at this offset. Also, the
924 VAR *root* will be assigned this number as a parent lineno unless
925 *toplevel* is set (the root var in a CNF tree obviously can’t have a
926 parent).
927
928 The *toplevel* parameter is useful when renumbering an existing variable
929 starting at a given offset without at the same time having that offset
930 assigned as a parent.
931 """
932 root = cnf_root (root)
933 i = parent or 0
934 for var in root:
935 i += 1
936 var["number"] = i
937 if toplevel is False and parent is not None:
938 var["parent"] = parent
939 children = var.get("children", None)
940 if children is not None:
941 i = renumber_vars(children, parent=i, toplevel=False)
942 return i
943
944
945def count_vars(root):
946 """
947 Traverse the cnf structure recursively, counting VAR objects (CNF lines).
948 """
949 cnf = cnf_root(root)
950 if cnf is None:
951 raise InvalidCNF(root)
952 return walk_cnf(cnf, True, lambda n, _nested, **_kwa: n + 1, 0)
953
954#
955# querying
956#
957
958
959def get_vars(cnf, data=None, instance=None):
960 """
961 get_vars -- Query a CNF_VAR structure. Skims the *toplevel* of the CNF_VAR
962 structure for entries with a matching `data` or `instance` field.
963
964 :type cnf: CNF_VAR
965 :type data: str
966 :type instance: int
967 :rtype: CNF_VAR
968 :returns: The structure containing only references to the
969 matching variables. Containing an empty list of
970 variables in case there is no match.
971
972 Values are compared literally. If both ``instance`` and ``data`` are
973 specified, vars will be compared against both.
974 """
975 cnf = cnf["cnf"]
976 if cnf:
977 criterion = lambda _var: False
978 if data:
979 if instance:
980 criterion = lambda var: var[
981 "data"] == data and var["instance"] == instance
982 else:
983 criterion = lambda var: var["data"] == data
984 elif instance:
985 criterion = lambda var: var["instance"] == instance
986
987 return {"cnf": [var for var in cnf if criterion(var) is True]}
988
989 return {"cnf": []}
990
991
992###############################################################################
993# PRINTING/DUMPING
994###############################################################################
995
996
997#
998# Print/dump raw CNF values
999#
1000
c24fd52c
CH
1001def output_cnf(root, out, renumber=False):
1002 """
1003 Dump a textual representation of given CNF VAR structure to given stream.
1004
1005 Runs :py:func:`format_cnf_vars` on the input (`root`) and then writes that
1006 to the given file-like object (out).
1007
1008 :param root: a CNF_VAR structure
1009 :type root: dict or list or anything that :py:func:`cnf_root` accepts
1010 :param out: file-like object or something with a `write(str)` function
1011 :param bool renumber: Whether to renumber cnfvars first
59b9c10e
SA
1012
1013 Files are converted to the 8-bit format expected by CNF so they can be fed
1014 directly into libcnffile.
c24fd52c 1015 """
3e9105b3 1016 cnf = cnf_root(root)
3e9105b3 1017 if renumber is True:
fb356a82 1018 _count = renumber_vars(root)
3e9105b3
PG
1019 if is_cnf(cnf) is True:
1020 (_, lines) = functools.reduce(format_cnf_vars, cnf, (0, []))
c24fd52c 1021 if isinstance(out, (io.RawIOBase, io.BufferedIOBase)):
87751ec4
PG
1022 out.write (b"\n".join (lines))
1023 out.write (b"\n")
c24fd52c
CH
1024 else: # either subclass of io.TextIOBase or unknown
1025 out.write ("\n".join (map (from_latin1, lines)))
1026 out.write ("\n")
87751ec4
PG
1027
1028
1029def dump_cnf_bytes (root, renumber=False):
1030 """
02fe6179 1031 Serialize CNF var structure, returning the result as a byte sequence.
87751ec4
PG
1032 """
1033 cnf = cnf_root(root)
1034 out = io.BytesIO()
c24fd52c 1035 output_cnf(root, out, renumber=renumber)
87751ec4
PG
1036 res = out.getvalue()
1037 out.close()
1038 return res
3e9105b3
PG
1039
1040
1041def dump_cnf_string(root, renumber=False):
1042 """
02fe6179
CH
1043 Serialize CNF var structure, returning a latin1-encode byte string.
1044
1045 .. todo::this is identical to :py:func:`dump_cnf_bytes`!
3e9105b3
PG
1046 """
1047 cnf = cnf_root(root)
ce9ef23c 1048 out = io.BytesIO()
c24fd52c 1049 output_cnf(root, out, renumber=renumber)
3e9105b3
PG
1050 res = out.getvalue()
1051 out.close()
1052 return res
1053
02fe6179 1054
3e9105b3 1055def print_cnf(root, out=None, renumber=False):
02fe6179
CH
1056 """
1057 Print given CNF_VAR structure to stdout (or other file-like object).
1058
1059 Note that per default the config is printed to sys.stdout using the shell's
1060 preferred encoding. If the shell cannot handle unicode this might raise
1061 `UnicodeError`.
1062
1063 All params forwarded to :py:func:`output_cnf`. See args there.
1064 """
3e9105b3 1065 if root is not None:
c24fd52c 1066 output_cnf(root, out or sys.stdout, renumber=renumber)
3e9105b3
PG
1067
1068
1069def write_cnf(*argv, **kw_argv):
02fe6179 1070 """Alias for :py:func:`print_cnf`."""
3e9105b3
PG
1071 print_cnf(*argv, **kw_argv)
1072
1073
59b9c10e
SA
1074def print_cnf_raw(root, out=None):
1075 """`if root is not None: out.write(root)`."""
1076 if root is not None:
1077 out.write(root)
1078
1079
1080def write_cnf_raw(*argv, **kw_argv):
1081 """Alias for :py:func:`print_cnf_raw`."""
1082 print_cnf_raw(*argv, **kw_argv)
1083
1084
1085#
1086# Print/dump CNF values in JSON format
1087#
1088
1089
3e9105b3 1090def output_json(root, out, renumber=False):
02fe6179
CH
1091 """
1092 Dump CNF_VAR structure to file-like object in json format.
1093
1094 :param root: CNF_VAR structure
1095 :type root: dict or list or anything that :py:func:`cnf_root` accepts
1096 :param out: file-like object, used as argument to :py:func:`json.dumps` so
1097 probably has to accept `str` (as opposed to `bytes`).
1098 :param bool renumber: whether to renumber variables before dupming.
1099 """
3e9105b3
PG
1100 # if not isinstance(out, file):
1101 #raise TypeError("%s (%s) is not a stream." % (out, type(out)))
1102 if renumber is True:
1103 _count = renumber_vars(root)
1104 if is_cnf(root) is True:
32eeaf11 1105 root ["cnf"] = normalize_cnf(cnf_root (root))
3e9105b3
PG
1106 data = json.dumps(root)
1107 out.write(data)
1108 out.write("\n")
02fe6179 1109 # TODO: else raise value error?
3e9105b3
PG
1110
1111
1112def dump_json_string(root, renumber=False):
1113 """
02fe6179 1114 Serialize CNF var structure as JSON, returning the result as a string.
3e9105b3
PG
1115 """
1116 out = io.StringIO()
1117 output_json(root, out, renumber=renumber)
1118 res = out.getvalue()
1119 out.close()
1120 return res
1121
1122
1123def print_cnf_json(root, out=None, renumber=False):
02fe6179
CH
1124 """
1125 Print CNF_VAR structure in json format to stdout.
1126
1127 Calls :py:func:`output_json` with `sys.stdout` if `out` is not given or
1128 `None`.
1129 """
3e9105b3
PG
1130 if root is not None:
1131 output_json(root, out or sys.stdout, renumber=renumber)
1132
1133
1134def write_cnf_json(*argv, **kw_argv):
02fe6179 1135 """Alias for :py:func:`print_cnf_json`."""
3e9105b3
PG
1136 print_cnf_json(*argv, **kw_argv)
1137
3e9105b3
PG
1138
1139
59b9c10e
SA
1140###############################################################################
1141# ENTRY POINT FOR DEVELOPMENT
1142###############################################################################
3e9105b3 1143
3e9105b3 1144
59b9c10e
SA
1145def usage():
1146 print("usage: cnfvar.py -" , file=sys.stderr)
1147 print("" , file=sys.stderr)
1148 print(" Read CNF from stdin.", file=sys.stderr)
1149 print("" , file=sys.stderr)
3e9105b3
PG
1150
1151
1152def main(argv):
1153 if len(argv) > 1:
1154 first = argv[1]
1155 if first == "-":
f2eaac7a 1156 cnf = read_cnf(sys.stdin.buffer.read())
3e9105b3 1157 print_cnf(cnf)
f2eaac7a 1158 return 0
3e9105b3 1159 elif first == "test":
f2eaac7a 1160 cnf = read_cnf(sys.stdin.buffer.read())
3e9105b3
PG
1161 cnff = get_vars(cnf, instance=2, data="FAX")
1162 print_cnf(cnff)
f2eaac7a 1163 return 0
59b9c10e 1164 usage()
f2eaac7a 1165 return -1
3e9105b3
PG
1166
1167if __name__ == "__main__":
59b9c10e 1168 sys.exit(main(sys.argv))