Commit | Line | Data |
---|---|---|
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 |
25 | summary |
26 | ------------------------------------------------------------------------------- | |
27 | Represent CNF_VARs as recursive structures. | |
3e9105b3 | 28 | |
d7cdea2c CH |
29 | DEPRECATED! Please do not extend this or add new uses of this module, use |
30 | :py:mod:`pyi2ncommon.arnied_api` or :py:mod:`pyi2ncommon.cnfvar` instead! | |
31 | ||
966adbbf | 32 | Copyright: 2014-2017 Intra2net AG |
f0173798 | 33 | License: GPLv2+ |
3e9105b3 PG |
34 | |
35 | ||
2c924c9e PG |
36 | contents |
37 | ------------------------------------------------------------------------------- | |
38 | ||
39 | This module provides read and write functionality for the Intra2net *CNF* | |
40 | format. Two different syntaxes are available: classical *CNF* and a JSON | |
41 | representation. Both versions are commonly understood by Intra2net software. | |
2c924c9e | 42 | |
bf80448a PG |
43 | On 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 | ||
67 | The input takes one round-trip through the parsers and will error out on | |
68 | problematic lines. Thus, ``cnfvar.py`` can be used to syntax-check CNF data. | |
69 | ||
70 | Note that line numbers may be arbitrarily reassigned in the process. Of course, | |
71 | parent references and the relative ordering of lines will be preserved in this | |
72 | case. | |
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 |
94 | notes on Python 3 conversion |
95 | ------------------------------------------------------------------------------- | |
96 | ||
97 | Since the original *CNF* format assumes latin-1 encoded data pretty much | |
98 | exclusively, we preserve the original encoding while parsing the file. | |
99 | When assembling the data structures returned to the user, values are then | |
100 | converted to strings so they can be used naturally at the Python end. | |
101 | ||
2c924c9e PG |
102 | implementation |
103 | ------------------------------------------------------------------------------- | |
3e9105b3 PG |
104 | """ |
105 | ||
106 | import functools | |
107 | import sys | |
108 | import json | |
109 | import re | |
110 | import io | |
111 | ||
3e9105b3 | 112 | |
59b9c10e SA |
113 | ############################################################################### |
114 | # CONSTANTS | |
115 | ############################################################################### | |
116 | ||
3e9105b3 | 117 | |
59b9c10e SA |
118 | CNF_FIELD_MANDATORY = set ([ "varname", "data", "instance" ]) |
119 | CNF_FIELD_OPTIONAL = set ([ "parent", "children", "comment", "number" ]) | |
120 | CNF_FIELD_KNOWN = CNF_FIELD_MANDATORY | CNF_FIELD_OPTIONAL | |
121 | ||
122 | grab_parent_pattern = re.compile(b""" | |
123 | ^ # match from start | |
124 | \s* # optional spaces | |
125 | \d+ # line number | |
126 | \s+ # spaces | |
127 | \((\d+)\) # parent | |
128 | """, | |
129 | re.VERBOSE) | |
3e9105b3 | 130 | |
59b9c10e SA |
131 | base_line_pattern = re.compile(b""" |
132 | ^ # match from start | |
133 | \s* # optional spaces | |
134 | (\d+) # line number | |
135 | \s+ # spaces | |
136 | ([A-Z][A-Z0-9_]*) # varname | |
137 | \s* # optional spaces | |
138 | , # delimiter | |
139 | \s* # optional spaces | |
140 | (-1|\d+) # instance | |
141 | \s* # optional spaces | |
142 | : # delimiter | |
143 | \s* # optional spaces | |
144 | \"( # quoted string (data) | |
145 | (?: \\\" # (of escaped dquote | |
146 | |[^\"])* # or anything not a | |
147 | )\" # literal quote) | |
148 | \s* # optional spaces | |
149 | ( # bgroup | |
150 | \# # comment leader | |
151 | \s* # optional spaces | |
152 | .* # string (comment) | |
153 | )? # egroup, optional | |
154 | $ # eol | |
155 | """, | |
156 | re.VERBOSE) | |
157 | ||
158 | child_line_pattern = re.compile(b""" | |
159 | ^ # match from start | |
160 | \s* # optional spaces | |
161 | (\d+) # line number | |
162 | \s+ # spaces | |
163 | \((\d+)\) # parent | |
164 | \s+ # spaces | |
165 | ([A-Z][A-Z0-9_]*) # varname | |
166 | \s* # optional spaces | |
167 | , # delimiter | |
168 | \s* # optional spaces | |
169 | (-1|\d+) # instance | |
170 | \s* # optional spaces | |
171 | : # delimiter | |
172 | \s* # optional spaces | |
173 | \"([^\"]*)\" # quoted string (data) | |
174 | \s* # optional spaces | |
175 | ( # bgroup | |
176 | \# # comment leader | |
177 | \s* # optional spaces | |
178 | .* # string (comment) | |
179 | )? # egroup, optional | |
180 | $ # eol | |
181 | """, | |
182 | re.VERBOSE) | |
183 | ||
184 | ||
185 | ############################################################################### | |
186 | # HELPERS | |
187 | ############################################################################### | |
188 | ||
189 | ||
190 | # | |
191 | # Sadly, the Intranator is still stuck with one leg in the 90s. | |
192 | # | |
3e9105b3 | 193 | def 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 | ||
198 | def 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 | ||
207 | def marshal_in_number(number): | |
208 | return int(number) | |
209 | ||
210 | ||
211 | def marshal_in_parent(parent): | |
212 | return int(parent) | |
213 | ||
214 | ||
215 | def marshal_in_instance(instance): | |
216 | return int(instance) | |
217 | ||
218 | ||
219 | def marshal_in_varname(varname): | |
220 | return from_latin1(varname).lower() | |
221 | ||
222 | ||
223 | def marshal_in_data(data): | |
224 | return from_latin1(data) if data is not None else "" | |
225 | ||
226 | ||
227 | def marshal_in_comment(comment): | |
228 | return comment and from_latin1(comment[1:].strip()) or None | |
229 | ||
230 | ||
3e9105b3 | 231 | # |
59b9c10e | 232 | # Type checking |
3e9105b3 PG |
233 | # |
234 | ||
59b9c10e SA |
235 | def is_string(s): |
236 | return isinstance(s, str) | |
237 | ||
238 | ||
239 | ############################################################################### | |
240 | # EXCEPTIONS | |
241 | ############################################################################### | |
242 | ||
3e9105b3 PG |
243 | |
244 | class InvalidCNF(Exception): | |
245 | ||
246 | def __init__(self, msg): | |
247 | self.msg = msg | |
248 | ||
249 | def __str__(self): | |
250 | return "Malformed CNF_VAR: \"%s\"" % self.msg | |
251 | ||
252 | ||
59b9c10e | 253 | class 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 | ||
267 | def is_valid(acc, | |
268 | nested, | |
269 | comment, | |
270 | data, | |
271 | instance, | |
272 | number, | |
273 | parent, | |
274 | varname): | |
275 | if varname is None: | |
276 | raise InvalidCNF("CNF_VAR lacks a name.") | |
277 | elif not is_string(varname): | |
278 | raise InvalidCNF("Varname field of CNF_VAR \"%s\" is not a string." | |
279 | % varname) | |
280 | elif varname == "": | |
281 | raise InvalidCNF("Varname field of CNF_VAR is the empty string.") | |
282 | ||
283 | if comment is not None: | |
284 | if not is_string(comment): | |
285 | raise InvalidCNF("Comment field of CNF_VAR \"%s\" is not a string." | |
286 | % varname) | |
287 | ||
288 | if data is None: | |
289 | raise InvalidCNF("Data field of CNF_VAR \"%s\" is empty." | |
290 | % varname) | |
291 | elif not is_string(data): | |
292 | raise InvalidCNF("Data field of CNF_VAR \"%s\" is not a string." | |
293 | % varname) | |
294 | ||
295 | if instance is None: | |
296 | raise InvalidCNF("Instance field of CNF_VAR \"%s\" is empty." | |
297 | % varname) | |
298 | elif not isinstance(instance, int): | |
299 | raise InvalidCNF("Instance field of CNF_VAR \"%s\" is not an integer." | |
300 | % varname) | |
301 | ||
302 | if number is None: | |
303 | raise InvalidCNF("Number field of CNF_VAR \"%s\" is empty." | |
304 | % varname) | |
305 | elif not isinstance(number, int): | |
306 | raise InvalidCNF("Number field of CNF_VAR \"%s\" is not an integer." | |
307 | % varname) | |
308 | elif number < 1: | |
309 | raise InvalidCNF("Number field of CNF_VAR \"%s\" must be positive, not %d." | |
310 | % (varname, number)) | |
311 | else: | |
312 | other = acc.get(number, None) | |
313 | if other is not None: # already in use | |
314 | raise InvalidCNF("Number field of CNF_VAR \"%s\" already used by variable %s." | |
315 | % (varname, other)) | |
316 | acc[number] = varname | |
317 | ||
318 | if nested is True: | |
319 | if parent is None: | |
320 | raise InvalidCNF("Parent field of nested CNF_VAR \"%s\" is empty." | |
321 | % varname) | |
322 | elif not isinstance(parent, int): | |
323 | raise InvalidCNF("Parent field of CNF_VAR \"%s\" is not an integer." | |
324 | % varname) | |
325 | else: | |
326 | if parent is not None: | |
327 | raise InvalidCNF("Flat CNF_VAR \"%s\" has nonsensical parent field \"%s\"." | |
328 | % (varname, parent)) | |
329 | return acc | |
330 | ||
331 | ||
332 | def is_cnf(root): | |
333 | """ | |
334 | is_cnf -- Predicate testing "CNF_VAR-ness". Folds the :py:func:`is_valid` | |
335 | predicate over the argument which can be either a well-formed CNF | |
336 | dictionary or a list of CNF_VARs. | |
337 | ||
338 | :type root: cnfvar or cnf list | |
339 | :rtype: bool | |
340 | ||
341 | Not that if it returns at all, ``is_cnf()`` returns ``True``. Any non | |
342 | well-formed member of the argument will cause the predicate to bail out | |
343 | with an exception during traversal. | |
344 | """ | |
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 | 351 | def 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 |
382 | def make_varname_lowercase(cnfvar): |
383 | """ | |
384 | Custom hook for json decoder: convert variable name to lowercase. | |
385 | ||
386 | Since variable names are case insensitive, :py:func:`read_cnf` converts | |
387 | them all to lower case. Downstream users of :py:func:`read_cnf_json` (e.g. | |
388 | :py:class:`simple_cnf.SimpleCnf`) rely on lowercase variable names. | |
389 | ||
390 | :param dict cnfvar: JSON "object" converted into a dict | |
391 | :returns: same as input but if field `varname` is present, its value is | |
392 | converted to lower case | |
393 | :rtype: dict with str keys | |
394 | """ | |
395 | try: | |
396 | cnfvar['varname'] = cnfvar['varname'].lower() | |
397 | except KeyError: # there is no "varname" field | |
398 | pass | |
399 | except AttributeError: # cnfvar['varname'] is not a string | |
400 | pass | |
401 | return cnfvar | |
402 | ||
3e9105b3 PG |
403 | |
404 | def 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 |
450 | def 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 | ||
470 | def 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 |
500 | def 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 | 526 | def 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 |
540 | def 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 |
554 | def 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 | ||
594 | def 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 | ||
680 | def 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 |
694 | def 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 | |
726 | def 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 |
762 | cnf_line_nest_indent = " " |
763 | cnf_line_base_fmt = "%d %s,%d: \"%s\"" | |
764 | cnf_line_child_fmt = "%d %s(%d) %s,%d: \"%s\"" | |
3e9105b3 | 765 | |
59b9c10e | 766 | |
3e9105b3 PG |
767 | def 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 | 816 | def 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 | 847 | def 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 |
888 | def 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 |
919 | def 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 | ||
945 | def 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 | ||
959 | def 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 |
1001 | def 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 | ||
1029 | def 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 | ||
1041 | def 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 | 1055 | def 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 | ||
1069 | def 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 |
1074 | def 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 | ||
1080 | def 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 | 1090 | def 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 | ||
1112 | def 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 | ||
1123 | def 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 | ||
1134 | def 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 |
1145 | def 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 | ||
1152 | def 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 | |
1167 | if __name__ == "__main__": | |
59b9c10e | 1168 | sys.exit(main(sys.argv)) |