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 | |
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 | 33 | Copyright: 2014-2017 Intra2net AG |
f0173798 | 34 | License: GPLv2+ |
3e9105b3 PG |
35 | |
36 | ||
2c924c9e PG |
37 | contents |
38 | ------------------------------------------------------------------------------- | |
39 | ||
40 | This module provides read and write functionality for the Intra2net *CNF* | |
41 | format. Two different syntaxes are available: classical *CNF* and a JSON | |
42 | representation. Both versions are commonly understood by Intra2net software. | |
2c924c9e | 43 | |
bf80448a PG |
44 | On 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 | ||
68 | The input takes one round-trip through the parsers and will error out on | |
69 | problematic lines. Thus, ``cnfvar.py`` can be used to syntax-check CNF data. | |
70 | ||
71 | Note that line numbers may be arbitrarily reassigned in the process. Of course, | |
72 | parent references and the relative ordering of lines will be preserved in this | |
73 | case. | |
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 |
95 | notes on Python 3 conversion |
96 | ------------------------------------------------------------------------------- | |
97 | ||
98 | Since the original *CNF* format assumes latin-1 encoded data pretty much | |
99 | exclusively, we preserve the original encoding while parsing the file. | |
100 | When assembling the data structures returned to the user, values are then | |
101 | converted to strings so they can be used naturally at the Python end. | |
102 | ||
2c924c9e PG |
103 | implementation |
104 | ------------------------------------------------------------------------------- | |
3e9105b3 PG |
105 | """ |
106 | ||
107 | import functools | |
108 | import sys | |
109 | import json | |
110 | import re | |
111 | import io | |
112 | ||
3e9105b3 | 113 | |
59b9c10e SA |
114 | ############################################################################### |
115 | # CONSTANTS | |
116 | ############################################################################### | |
117 | ||
3e9105b3 | 118 | |
59b9c10e SA |
119 | CNF_FIELD_MANDATORY = set ([ "varname", "data", "instance" ]) |
120 | CNF_FIELD_OPTIONAL = set ([ "parent", "children", "comment", "number" ]) | |
121 | CNF_FIELD_KNOWN = CNF_FIELD_MANDATORY | CNF_FIELD_OPTIONAL | |
122 | ||
123 | grab_parent_pattern = re.compile(b""" | |
124 | ^ # match from start | |
125 | \s* # optional spaces | |
126 | \d+ # line number | |
127 | \s+ # spaces | |
128 | \((\d+)\) # parent | |
129 | """, | |
130 | re.VERBOSE) | |
3e9105b3 | 131 | |
59b9c10e SA |
132 | base_line_pattern = re.compile(b""" |
133 | ^ # match from start | |
134 | \s* # optional spaces | |
135 | (\d+) # line number | |
136 | \s+ # spaces | |
137 | ([A-Z][A-Z0-9_]*) # varname | |
138 | \s* # optional spaces | |
139 | , # delimiter | |
140 | \s* # optional spaces | |
141 | (-1|\d+) # instance | |
142 | \s* # optional spaces | |
143 | : # delimiter | |
144 | \s* # optional spaces | |
145 | \"( # quoted string (data) | |
146 | (?: \\\" # (of escaped dquote | |
147 | |[^\"])* # or anything not a | |
148 | )\" # literal quote) | |
149 | \s* # optional spaces | |
150 | ( # bgroup | |
151 | \# # comment leader | |
152 | \s* # optional spaces | |
153 | .* # string (comment) | |
154 | )? # egroup, optional | |
155 | $ # eol | |
156 | """, | |
157 | re.VERBOSE) | |
158 | ||
159 | child_line_pattern = re.compile(b""" | |
160 | ^ # match from start | |
161 | \s* # optional spaces | |
162 | (\d+) # line number | |
163 | \s+ # spaces | |
164 | \((\d+)\) # parent | |
165 | \s+ # spaces | |
166 | ([A-Z][A-Z0-9_]*) # varname | |
167 | \s* # optional spaces | |
168 | , # delimiter | |
169 | \s* # optional spaces | |
170 | (-1|\d+) # instance | |
171 | \s* # optional spaces | |
172 | : # delimiter | |
173 | \s* # optional spaces | |
174 | \"([^\"]*)\" # quoted string (data) | |
175 | \s* # optional spaces | |
176 | ( # bgroup | |
177 | \# # comment leader | |
178 | \s* # optional spaces | |
179 | .* # string (comment) | |
180 | )? # egroup, optional | |
181 | $ # eol | |
182 | """, | |
183 | re.VERBOSE) | |
184 | ||
185 | ||
186 | ############################################################################### | |
187 | # HELPERS | |
188 | ############################################################################### | |
189 | ||
190 | ||
191 | # | |
192 | # Sadly, the Intranator is still stuck with one leg in the 90s. | |
193 | # | |
3e9105b3 | 194 | def 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 | ||
199 | def 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 | ||
208 | def marshal_in_number(number): | |
209 | return int(number) | |
210 | ||
211 | ||
212 | def marshal_in_parent(parent): | |
213 | return int(parent) | |
214 | ||
215 | ||
216 | def marshal_in_instance(instance): | |
217 | return int(instance) | |
218 | ||
219 | ||
220 | def marshal_in_varname(varname): | |
221 | return from_latin1(varname).lower() | |
222 | ||
223 | ||
224 | def marshal_in_data(data): | |
225 | return from_latin1(data) if data is not None else "" | |
226 | ||
227 | ||
228 | def marshal_in_comment(comment): | |
229 | return comment and from_latin1(comment[1:].strip()) or None | |
230 | ||
231 | ||
3e9105b3 | 232 | # |
59b9c10e | 233 | # Type checking |
3e9105b3 PG |
234 | # |
235 | ||
59b9c10e SA |
236 | def is_string(s): |
237 | return isinstance(s, str) | |
238 | ||
239 | ||
240 | ############################################################################### | |
241 | # EXCEPTIONS | |
242 | ############################################################################### | |
243 | ||
3e9105b3 PG |
244 | |
245 | class InvalidCNF(Exception): | |
246 | ||
247 | def __init__(self, msg): | |
248 | self.msg = msg | |
249 | ||
250 | def __str__(self): | |
251 | return "Malformed CNF_VAR: \"%s\"" % self.msg | |
252 | ||
253 | ||
59b9c10e | 254 | class 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 | ||
268 | def is_valid(acc, | |
269 | nested, | |
270 | comment, | |
271 | data, | |
272 | instance, | |
273 | number, | |
274 | parent, | |
275 | varname): | |
276 | if varname is None: | |
277 | raise InvalidCNF("CNF_VAR lacks a name.") | |
278 | elif not is_string(varname): | |
279 | raise InvalidCNF("Varname field of CNF_VAR \"%s\" is not a string." | |
280 | % varname) | |
281 | elif varname == "": | |
282 | raise InvalidCNF("Varname field of CNF_VAR is the empty string.") | |
283 | ||
284 | if comment is not None: | |
285 | if not is_string(comment): | |
286 | raise InvalidCNF("Comment field of CNF_VAR \"%s\" is not a string." | |
287 | % varname) | |
288 | ||
289 | if data is None: | |
290 | raise InvalidCNF("Data field of CNF_VAR \"%s\" is empty." | |
291 | % varname) | |
292 | elif not is_string(data): | |
293 | raise InvalidCNF("Data field of CNF_VAR \"%s\" is not a string." | |
294 | % varname) | |
295 | ||
296 | if instance is None: | |
297 | raise InvalidCNF("Instance field of CNF_VAR \"%s\" is empty." | |
298 | % varname) | |
299 | elif not isinstance(instance, int): | |
300 | raise InvalidCNF("Instance field of CNF_VAR \"%s\" is not an integer." | |
301 | % varname) | |
302 | ||
303 | if number is None: | |
304 | raise InvalidCNF("Number field of CNF_VAR \"%s\" is empty." | |
305 | % varname) | |
306 | elif not isinstance(number, int): | |
307 | raise InvalidCNF("Number field of CNF_VAR \"%s\" is not an integer." | |
308 | % varname) | |
309 | elif number < 1: | |
310 | raise InvalidCNF("Number field of CNF_VAR \"%s\" must be positive, not %d." | |
311 | % (varname, number)) | |
312 | else: | |
313 | other = acc.get(number, None) | |
314 | if other is not None: # already in use | |
315 | raise InvalidCNF("Number field of CNF_VAR \"%s\" already used by variable %s." | |
316 | % (varname, other)) | |
317 | acc[number] = varname | |
318 | ||
319 | if nested is True: | |
320 | if parent is None: | |
321 | raise InvalidCNF("Parent field of nested CNF_VAR \"%s\" is empty." | |
322 | % varname) | |
323 | elif not isinstance(parent, int): | |
324 | raise InvalidCNF("Parent field of CNF_VAR \"%s\" is not an integer." | |
325 | % varname) | |
326 | else: | |
327 | if parent is not None: | |
328 | raise InvalidCNF("Flat CNF_VAR \"%s\" has nonsensical parent field \"%s\"." | |
329 | % (varname, parent)) | |
330 | return acc | |
331 | ||
332 | ||
333 | def is_cnf(root): | |
334 | """ | |
335 | is_cnf -- Predicate testing "CNF_VAR-ness". Folds the :py:func:`is_valid` | |
336 | predicate over the argument which can be either a well-formed CNF | |
337 | dictionary or a list of CNF_VARs. | |
338 | ||
339 | :type root: cnfvar or cnf list | |
340 | :rtype: bool | |
341 | ||
342 | Not that if it returns at all, ``is_cnf()`` returns ``True``. Any non | |
343 | well-formed member of the argument will cause the predicate to bail out | |
344 | with an exception during traversal. | |
345 | """ | |
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 | 352 | def 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 |
383 | def make_varname_lowercase(cnfvar): |
384 | """ | |
385 | Custom hook for json decoder: convert variable name to lowercase. | |
386 | ||
387 | Since variable names are case insensitive, :py:func:`read_cnf` converts | |
388 | them all to lower case. Downstream users of :py:func:`read_cnf_json` (e.g. | |
389 | :py:class:`simple_cnf.SimpleCnf`) rely on lowercase variable names. | |
390 | ||
391 | :param dict cnfvar: JSON "object" converted into a dict | |
392 | :returns: same as input but if field `varname` is present, its value is | |
393 | converted to lower case | |
394 | :rtype: dict with str keys | |
395 | """ | |
396 | try: | |
397 | cnfvar['varname'] = cnfvar['varname'].lower() | |
398 | except KeyError: # there is no "varname" field | |
399 | pass | |
400 | except AttributeError: # cnfvar['varname'] is not a string | |
401 | pass | |
402 | return cnfvar | |
403 | ||
3e9105b3 PG |
404 | |
405 | def 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 |
451 | def 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 | ||
472 | def 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 |
502 | def advance(cns): |
503 | """ | |
504 | Pop the next line from the stream, advancing the tuple. | |
505 | ||
506 | :param cns: a 3-element tuple containing two CNF lines and a list of the | |
507 | remaining lines | |
508 | :type cnd: (str, str, [str]) | |
509 | :returns: a new tuple with a new item popped from the list of lines | |
510 | :rtype cnd: (str, str, [str]) | |
511 | """ | |
512 | current, next, stream = cns | |
513 | if next is None: # reached end of stream | |
514 | return None | |
515 | current = next | |
516 | ||
517 | try: | |
518 | next = stream.pop() | |
519 | next = next.strip() | |
520 | except IndexError: | |
521 | next = None | |
522 | ||
523 | if current == "": # skip blank lines | |
524 | return advance((current, next, stream)) | |
525 | return (current, next, stream) | |
3e9105b3 PG |
526 | |
527 | ||
f2eaac7a | 528 | def 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 |
542 | def 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 |
556 | def parse_cnf_root(state): |
557 | """ | |
558 | Iterate over and parse a list of CNF lines. | |
559 | ||
560 | :param state: a 3-element tuple containing two lines and a list of the | |
561 | remaining lines | |
562 | :type state: (str, str, [str]) | |
563 | :returns: a list of parsed CNF variables | |
564 | :rtype: [dict] | |
565 | ||
566 | The function will parse the first element from the `state` tuple, then read | |
567 | the next line to see if it is a child variable. If it is, it will be | |
568 | appended to the last parsed CNF, otherwise top-level parsing is done | |
569 | normally. | |
570 | """ | |
571 | lines = [] | |
572 | current = get(state) | |
573 | while state: | |
574 | cnf_line = read_base_line(current) | |
575 | if cnf_line is not None: | |
576 | lines.append(cnf_line) | |
577 | state = advance(state) | |
578 | if state is None: # -> nothing left to do | |
579 | break | |
580 | current = get(state) | |
581 | parent = get_parent(current) # peek at next line | |
582 | if parent is not None: # -> recurse into children | |
583 | (state, children, _parent) = parse_cnf_children(state, parent) | |
584 | cnf_line["children"] = children | |
585 | if state is None: | |
586 | break | |
587 | current = get(state) | |
588 | else: | |
589 | state = advance(state) | |
590 | if state is None: | |
591 | break | |
592 | current = get(state) | |
593 | return lines | |
594 | ||
595 | ||
596 | def parse_cnf_children(state, parent): | |
597 | """ | |
598 | Read and parse child CNFs of a given parent until there is none left. | |
599 | ||
600 | :param state: a 3-element tuple containing two lines and a list of the | |
601 | remaining lines | |
602 | :type state: (str, str, [str]) | |
603 | :param parent: id of the parent whose children we are looking for | |
604 | :type parent: int | |
605 | :returns: a 3-element tuple with the current state, a list of children of | |
606 | the given parent and the parent ID | |
607 | :rtype: (tuple, [str], int) | |
608 | ||
609 | The function will recursively parse child lines from the `state` tuple | |
610 | until one of these conditions is satisfied: | |
611 | ||
612 | 1. the input is exhausted | |
613 | 2. the next CNF line | |
614 | 2.1. is a toplevel line | |
615 | 2.2. is a child line whose parent has a lower parent number | |
616 | ||
617 | Conceptually, 2.1 is a very similar to 2.2 but due to the special status of | |
618 | toplevel lines in CNF we need to handle them separately. | |
619 | ||
620 | Note that since nesting of CNF vars is achieved via parent line numbers, | |
621 | lines with different parents could appear out of order. libcnffile will | |
622 | happily parse those and still assign children to the specified parent: | |
623 | ||
624 | :: | |
625 | # set_cnf <<THATSALL | |
626 | 1 USER,1337: "l33t_h4x0r" | |
627 | 2 (1) USER_GROUP_MEMBER_REF,0: "2" | |
628 | 4 USER,1701: "picard" | |
629 | 5 (4) USER_GROUP_MEMBER_REF,0: "2" | |
630 | 6 (4) USER_PASSWORD,0: "engage" | |
631 | 3 (1) USER_PASSWORD,0: "hacktheplanet" | |
632 | THATSALL | |
633 | # get_cnf user 1337 | |
634 | 1 USER,1337: "l33t_h4x0r" | |
635 | 2 (1) USER_GROUP_MEMBER_REF,0: "2" | |
636 | 3 (1) USER_PASSWORD,0: "hacktheplanet" | |
637 | # get_cnf user 1701 | |
638 | 1 USER,1701: "picard" | |
639 | 2 (1) USER_GROUP_MEMBER_REF,0: "2" | |
640 | 3 (1) USER_PASSWORD,0: "engage" | |
641 | ||
642 | It is a limitation of ``cnfvar.py`` that it cannot parse CNF data | |
643 | structured like the above example: child lists are only populated from | |
644 | subsequent CNF vars using the parent number solely to track nesting levels. | |
645 | The parser does not keep track of line numbers while traversing the input | |
646 | so it doesn’t support retroactively assigning a child to anything else but | |
647 | the immediate parent. | |
648 | """ | |
649 | lines = [] | |
650 | current = get(state) | |
651 | while True: | |
652 | cnf_line = read_child_line(current) | |
653 | if cnf_line is not None: | |
654 | lines.append(cnf_line) | |
655 | state = advance(state) | |
656 | if state is None: | |
657 | break | |
658 | current = get(state) | |
659 | new_parent = get_parent(current) | |
660 | if new_parent is None: | |
661 | # drop stack | |
662 | return (state, lines, None) | |
663 | if new_parent > parent: | |
664 | # parent is further down in hierarchy -> new level | |
665 | (state, children, new_parent) = \ | |
666 | parse_cnf_children (state, new_parent) | |
667 | if state is None: | |
668 | break | |
669 | cnf_line["children"] = children | |
670 | current = get(state) | |
671 | new_parent = get_parent(current) | |
672 | if new_parent is None: | |
673 | # drop stack | |
674 | return (state, lines, None) | |
675 | if new_parent < parent: | |
676 | # parent is further up in hierarchy -> pop level | |
677 | return (state, lines, new_parent) | |
678 | # new_parent == parent -> continue parsing on same level | |
679 | return (state, lines, parent) | |
3e9105b3 PG |
680 | |
681 | ||
682 | def 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 |
696 | def 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 | |
728 | def 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 |
764 | cnf_line_nest_indent = " " |
765 | cnf_line_base_fmt = "%d %s,%d: \"%s\"" | |
766 | cnf_line_child_fmt = "%d %s(%d) %s,%d: \"%s\"" | |
3e9105b3 | 767 | |
59b9c10e | 768 | |
3e9105b3 PG |
769 | def 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 | 818 | def 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 | 849 | def 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 |
890 | def walk_cnf(cnf, nested, fun, acc): |
891 | """ | |
892 | Depth-first traversal of a CNF tree. | |
893 | ||
894 | :type cnf: cnf list | |
895 | :type nested: bool | |
896 | :type fun: 'a -> bool -> (cnf stuff) -> 'a | |
897 | :type acc: 'a | |
898 | :rtype: 'a | |
899 | ||
900 | Executes ``fun`` recursively for each node in the tree. The function | |
901 | receives the accumulator ``acc`` which can be of an arbitrary type as first | |
902 | argument. The second argument is a flag indicating whether the current | |
903 | CNF var is a child (if ``True``) or a parent var. CNF member fields are | |
904 | passed via named optional arguments. | |
905 | """ | |
906 | for var in cnf: | |
907 | acc = fun(acc, | |
908 | nested, | |
909 | comment=var.get("comment", None), | |
910 | data=var.get("data", None), | |
911 | instance=var.get("instance", None), | |
912 | number=var.get("number", None), | |
913 | parent=var.get("parent", None), | |
914 | varname=var.get("varname", None)) | |
915 | children = var.get("children", None) | |
916 | if children is not None: | |
917 | acc = walk_cnf(children, True, fun, acc) | |
918 | return acc | |
3e9105b3 PG |
919 | |
920 | ||
59b9c10e SA |
921 | def renumber_vars(root, parent=None, toplevel=False): |
922 | """ | |
923 | Number cnfvars linearly. | |
924 | ||
925 | If *parent* is specified, numbering will start at this offset. Also, the | |
926 | VAR *root* will be assigned this number as a parent lineno unless | |
927 | *toplevel* is set (the root var in a CNF tree obviously can’t have a | |
928 | parent). | |
929 | ||
930 | The *toplevel* parameter is useful when renumbering an existing variable | |
931 | starting at a given offset without at the same time having that offset | |
932 | assigned as a parent. | |
933 | """ | |
934 | root = cnf_root (root) | |
935 | i = parent or 0 | |
936 | for var in root: | |
937 | i += 1 | |
938 | var["number"] = i | |
939 | if toplevel is False and parent is not None: | |
940 | var["parent"] = parent | |
941 | children = var.get("children", None) | |
942 | if children is not None: | |
943 | i = renumber_vars(children, parent=i, toplevel=False) | |
944 | return i | |
945 | ||
946 | ||
947 | def count_vars(root): | |
948 | """ | |
949 | Traverse the cnf structure recursively, counting VAR objects (CNF lines). | |
950 | """ | |
951 | cnf = cnf_root(root) | |
952 | if cnf is None: | |
953 | raise InvalidCNF(root) | |
954 | return walk_cnf(cnf, True, lambda n, _nested, **_kwa: n + 1, 0) | |
955 | ||
956 | # | |
957 | # querying | |
958 | # | |
959 | ||
960 | ||
961 | def get_vars(cnf, data=None, instance=None): | |
962 | """ | |
963 | get_vars -- Query a CNF_VAR structure. Skims the *toplevel* of the CNF_VAR | |
964 | structure for entries with a matching `data` or `instance` field. | |
965 | ||
966 | :type cnf: CNF_VAR | |
967 | :type data: str | |
968 | :type instance: int | |
969 | :rtype: CNF_VAR | |
970 | :returns: The structure containing only references to the | |
971 | matching variables. Containing an empty list of | |
972 | variables in case there is no match. | |
973 | ||
974 | Values are compared literally. If both ``instance`` and ``data`` are | |
975 | specified, vars will be compared against both. | |
976 | """ | |
977 | cnf = cnf["cnf"] | |
978 | if cnf: | |
979 | criterion = lambda _var: False | |
980 | if data: | |
981 | if instance: | |
982 | criterion = lambda var: var[ | |
983 | "data"] == data and var["instance"] == instance | |
984 | else: | |
985 | criterion = lambda var: var["data"] == data | |
986 | elif instance: | |
987 | criterion = lambda var: var["instance"] == instance | |
988 | ||
989 | return {"cnf": [var for var in cnf if criterion(var) is True]} | |
990 | ||
991 | return {"cnf": []} | |
992 | ||
993 | ||
994 | ############################################################################### | |
995 | # PRINTING/DUMPING | |
996 | ############################################################################### | |
997 | ||
998 | ||
999 | # | |
1000 | # Print/dump raw CNF values | |
1001 | # | |
1002 | ||
c24fd52c CH |
1003 | def output_cnf(root, out, renumber=False): |
1004 | """ | |
1005 | Dump a textual representation of given CNF VAR structure to given stream. | |
1006 | ||
1007 | Runs :py:func:`format_cnf_vars` on the input (`root`) and then writes that | |
1008 | to the given file-like object (out). | |
1009 | ||
1010 | :param root: a CNF_VAR structure | |
1011 | :type root: dict or list or anything that :py:func:`cnf_root` accepts | |
1012 | :param out: file-like object or something with a `write(str)` function | |
1013 | :param bool renumber: Whether to renumber cnfvars first | |
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 | ||
1031 | def 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 | ||
1043 | def 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 | 1057 | def 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 | ||
1071 | def 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 |
1076 | def print_cnf_raw(root, out=None): |
1077 | """`if root is not None: out.write(root)`.""" | |
1078 | if root is not None: | |
1079 | out.write(root) | |
1080 | ||
1081 | ||
1082 | def write_cnf_raw(*argv, **kw_argv): | |
1083 | """Alias for :py:func:`print_cnf_raw`.""" | |
1084 | print_cnf_raw(*argv, **kw_argv) | |
1085 | ||
1086 | ||
1087 | # | |
1088 | # Print/dump CNF values in JSON format | |
1089 | # | |
1090 | ||
1091 | ||
3e9105b3 | 1092 | def 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 | ||
1114 | def 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 | ||
1125 | def 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 | ||
1136 | def 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 |
1147 | def usage(): |
1148 | print("usage: cnfvar.py -" , file=sys.stderr) | |
1149 | print("" , file=sys.stderr) | |
1150 | print(" Read CNF from stdin.", file=sys.stderr) | |
1151 | print("" , file=sys.stderr) | |
3e9105b3 PG |
1152 | |
1153 | ||
1154 | def 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 | 1170 | if __name__ == "__main__": |
59b9c10e | 1171 | sys.exit(main(sys.argv)) |