1# Purpose: validate DXF tag structures
2# Created: 03.01.2018
3# Copyright (C) 2018-2020, Manfred Moitzi
4# License: MIT License
5import logging
6import io
7import bisect
8import math
9from typing import TextIO, Iterable, List, Optional, Set
10
11from .const import (
12    DXFStructureError, DXFError, DXFValueError, DXFAppDataError, DXFXDataError,
13    APP_DATA_MARKER, HEADER_VAR_MARKER, XDATA_MARKER,
14    INVALID_LAYER_NAME_CHARACTERS, acad_release, VALID_DXF_LINEWEIGHT_VALUES,
15    VALID_DXF_LINEWEIGHTS, LINEWEIGHT_BYLAYER,
16)
17
18from .tagger import ascii_tags_loader
19from .types import is_embedded_object_marker, DXFTag, NONE_TAG
20from ezdxf.tools.codepage import toencoding
21from ezdxf.math import NULLVEC
22
23logger = logging.getLogger('ezdxf')
24
25
26class DXFInfo:
27    def __init__(self):
28        self.release = 'R12'
29        self.version = 'AC1009'
30        self.encoding = 'cp1252'
31        self.handseed = '0'
32
33    def set_header_var(self, name: str, value: str) -> int:
34        if name == '$ACADVER':
35            self.version = value
36            self.release = acad_release.get(value, 'R12')
37        elif name == '$DWGCODEPAGE':
38            self.encoding = toencoding(value)
39        elif name == '$HANDSEED':
40            self.handseed = value
41        else:
42            return 0
43        return 1
44
45
46def dxf_info(stream: TextIO) -> DXFInfo:
47    info = DXFInfo()
48    tagger = ascii_tags_loader(stream)
49    # comments already removed
50    if next(tagger) != (0, 'SECTION'):
51        # maybe a DXF structure error, handled by later processing
52        return info
53    if next(tagger) != (2, 'HEADER'):
54        # no leading HEADER section like DXF R12 with only ENTITIES section
55        return info
56    tag = NONE_TAG
57    found = 0
58    while tag != (0, 'ENDSEC'):  # until end of HEADER section
59        tag = next(tagger)
60        if tag.code != HEADER_VAR_MARKER:
61            continue
62        name = tag.value
63        value = next(tagger).value
64        found += info.set_header_var(name, value)
65        if found > 2:  # all expected values collected
66            break
67    return info
68
69
70def header_validator(tagger: Iterable[DXFTag]) -> Iterable[DXFTag]:
71    """ Checks the tag structure of the content of the header section.
72
73    Do not feed (0, 'SECTION') (2, 'HEADER') and (0, 'ENDSEC') tags!
74
75    Args:
76        tagger: generator/iterator of low level tags or compiled tags
77
78    Raises:
79        DXFStructureError() -> invalid group codes
80        DXFValueError() -> invalid header variable name
81
82    """
83    variable_name_tag = True
84    for tag in tagger:
85        code, value = tag
86        if variable_name_tag:
87            if code != HEADER_VAR_MARKER:
88                raise DXFStructureError(
89                    f'Invalid header variable tag {code}, {value}).'
90                )
91            if not value.startswith('$'):
92                raise DXFValueError(
93                    f'Invalid header variable name "{value}", missing leading "$".'
94                )
95            variable_name_tag = False
96        else:
97            variable_name_tag = True
98        yield tag
99
100
101def entity_structure_validator(tags: List[DXFTag]) -> Iterable[DXFTag]:
102    """ Checks for valid DXF entity tag structure.
103
104    - APP DATA can not be nested and every opening tag (102, '{...') needs a
105      closing tag (102, '}')
106    - extended group codes (>=1000) allowed before XDATA section
107    - XDATA section starts with (1001, APPID) and is always at the end of an
108      entity
109    - XDATA section: only group code >= 1000 is allowed
110    - XDATA control strings (1002, '{') and (1002, '}') have to be balanced
111    - embedded objects may follow XDATA
112
113    XRECORD entities will not be checked.
114
115    Args:
116        tags: list of DXFTag()
117
118    Raises:
119        DXFAppDataError: for invalid APP DATA
120        DXFXDataError: for invalid XDATA
121
122    """
123    assert isinstance(tags, list)
124    dxftype = tags[0].value  # type: str
125    is_xrecord = dxftype == 'XRECORD'
126    handle = '???'
127    app_data = False
128    xdata = False
129    xdata_list_level = 0
130    app_data_closing_tag = '}'
131    embedded_object = False
132    for tag in tags:
133        if tag.code == 5 and handle == '???':
134            handle = tag.value
135
136        if is_embedded_object_marker(tag):
137            embedded_object = True
138
139        if embedded_object:  # no further validation
140            yield tag
141            continue  # with next tag
142
143        if xdata and not embedded_object:
144            if tag.code < 1000:
145                dxftype = tags[0].value
146                raise DXFXDataError(
147                    f'Invalid XDATA structure in entity {dxftype}(#{handle}), '
148                    f'only group code >=1000 allowed in XDATA section'
149                )
150            if tag.code == 1002:
151                value = tag.value
152                if value == '{':
153                    xdata_list_level += 1
154                elif value == '}':
155                    xdata_list_level -= 1
156                else:
157                    raise DXFXDataError(
158                        f'Invalid XDATA control string (1002, "{value}") entity'
159                        f' {dxftype}(#{handle}).'
160                    )
161                if xdata_list_level < 0:  # more closing than opening tags
162                    raise DXFXDataError(
163                        f'Invalid XDATA structure in entity {dxftype}(#{handle}), '
164                        f'unbalanced list markers, missing  (1002, "{{").'
165                    )
166
167        if tag.code == APP_DATA_MARKER and not is_xrecord:
168            # Ignore control tags (102, ...) tags in XRECORD
169            value = tag.value
170            if value.startswith('{'):
171                if app_data:  # already in app data mode
172                    raise DXFAppDataError(
173                        f'Invalid APP DATA structure in entity {dxftype}'
174                        f'(#{handle}), APP DATA can not be nested.'
175                    )
176                app_data = True
177                # 'APPID}' is also a valid closing tag
178                app_data_closing_tag = value[1:] + '}'
179            elif value == '}' or value == app_data_closing_tag:
180                if not app_data:
181                    raise DXFAppDataError(
182                        f'Invalid APP DATA structure in entity {dxftype}'
183                        f'(#{handle}), found (102, "}}") tag without opening tag.'
184                    )
185                app_data = False
186                app_data_closing_tag = '}'
187            else:
188                raise DXFAppDataError(
189                    f'Invalid APP DATA structure tag (102, "{value}") in '
190                    f'entity {dxftype}(#{handle}).'
191                )
192
193        # XDATA section starts with (1001, APPID) and is always at the end of
194        # an entity.
195        if tag.code == XDATA_MARKER and xdata is False:
196            xdata = True
197            if app_data:
198                raise DXFAppDataError(
199                    f'Invalid APP DATA structure in entity {dxftype}'
200                    f'(#{handle}), missing closing tag (102, "}}").'
201                )
202        yield tag
203
204    if app_data:
205        raise DXFAppDataError(
206            f'Invalid APP DATA structure in entity {dxftype}(#{handle}), '
207            f'missing closing tag (102, "}}").'
208        )
209    if xdata:
210        if xdata_list_level < 0:
211            raise DXFXDataError(
212                f'Invalid XDATA structure in entity {dxftype}(#{handle}), '
213                f'unbalanced list markers, missing  (1002, "{{").'
214            )
215        elif xdata_list_level > 0:
216            raise DXFXDataError(
217                f'Invalid XDATA structure in entity {dxftype}(#{handle}), '
218                f'unbalanced list markers, missing  (1002, "}}").')
219
220
221def is_dxf_file(filename: str) -> bool:
222    """ Returns ``True`` if `filename` is an ASCII DXF file. """
223    with io.open(filename, errors='ignore') as fp:
224        return is_dxf_stream(fp)
225
226
227def is_binary_dxf_file(filename: str) -> bool:
228    """ Returns ``True`` if `filename` is a binary DXF file. """
229    with open(filename, 'rb') as fp:
230        sentinel = fp.read(22)
231    return sentinel == b'AutoCAD Binary DXF\r\n\x1a\x00'
232
233
234def is_dwg_file(filename: str) -> bool:
235    """ Returns ``True`` if `filename` is a DWG file. """
236    return dwg_version(filename) is not None
237
238
239def dwg_version(filename: str) -> Optional[str]:
240    """ Returns DWG version of `filename` as string or ``None``. """
241    with open(str(filename), 'rb') as fp:
242        try:
243            version = fp.read(6).decode(errors='ignore')
244        except IOError:
245            return None
246        if version not in acad_release:
247            return None
248        return version
249
250
251def is_dxf_stream(stream: TextIO) -> bool:
252    try:
253        reader = ascii_tags_loader(stream)
254    except DXFError:
255        return False
256    try:
257        for tag in reader:
258            # The common case for well formed DXF files
259            if tag == (0, 'SECTION'):
260                return True
261            # Accept/Ignore tags in front of first SECTION - like AutoCAD and
262            # BricsCAD, but group code should be < 1000, until reality proofs
263            # otherwise.
264            if tag.code > 999:
265                return False
266    except DXFStructureError:
267        pass
268    return False
269
270
271# Names used in symbol table records and in dictionaries must follow these rules:
272#
273# - Names can be any length in ObjectARX, but symbol names entered by users in
274#   AutoCAD are limited to 255 characters.
275# - AutoCAD preserves the case of names but does not use the case in
276#   comparisons. For example, AutoCAD considers "Floor" to be the same symbol
277#   as "FLOOR."
278# - Names can be composed of all characters allowed by Windows or Mac OS for
279#   filenames, except comma (,), backquote (‘), semi-colon (;), and equal
280#   sign (=).
281# http://help.autodesk.com/view/OARX/2018/ENU/?guid=GUID-83ABF20A-57D4-4AB3-8A49-D91E0F70DBFF
282
283def is_valid_table_name(name: str) -> bool:
284    return not bool(INVALID_LAYER_NAME_CHARACTERS.intersection(set(name)))
285
286
287def is_valid_layer_name(name: str) -> bool:
288    if is_adsk_special_layer(name):
289        return True
290    else:
291        return is_valid_table_name(name)
292
293
294def is_adsk_special_layer(name: str) -> bool:
295    if name.startswith('*'):
296        # special Autodesk layers starts with invalid character *
297        name = name.upper()
298        if name.startswith('*ADSK_'):
299            return True
300        if name.startswith('*ACMAP'):
301            return True
302        if name.startswith('*TEMPORARY'):
303            return True
304    return False
305
306
307def is_valid_block_name(name: str) -> bool:
308    if name.startswith('*'):
309        return is_valid_table_name(name[1:])
310    else:
311        return is_valid_table_name(name)
312
313
314def is_valid_vport_name(name: str) -> bool:
315    if name.startswith('*'):
316        return name.upper() == '*ACTIVE'
317    else:
318        return is_valid_table_name(name)
319
320
321def is_valid_lineweight(lineweight: int) -> bool:
322    return lineweight in VALID_DXF_LINEWEIGHT_VALUES
323
324
325def fix_lineweight(lineweight: int) -> int:
326    if lineweight in VALID_DXF_LINEWEIGHT_VALUES:
327        return lineweight
328    if lineweight < -3:
329        return LINEWEIGHT_BYLAYER
330    if lineweight > 211:
331        return 211
332    index = bisect.bisect(VALID_DXF_LINEWEIGHTS, lineweight)
333    return VALID_DXF_LINEWEIGHTS[index]
334
335
336def is_valid_aci_color(aci: int) -> bool:
337    return 0 <= aci <= 257
338
339
340def is_in_integer_range(start: int, end: int):
341    """ Range of integer values, excluding the `end` value. """
342
343    def _validator(value: int) -> bool:
344        return start <= value < end
345
346    return _validator
347
348
349def fit_into_integer_range(start: int, end: int):
350    def _fixer(value: int) -> int:
351        return min(max(value, start), end - 1)
352
353    return _fixer
354
355
356def fit_into_float_range(start: float, end: float):
357    def _fixer(value: float) -> float:
358        return min(max(value, start), end)
359
360    return _fixer
361
362
363def is_in_float_range(start: float, end: float):
364    """ Range of float values, including the `end` value. """
365
366    def _validator(value: float) -> bool:
367        return start <= value <= end
368
369    return _validator
370
371
372def is_not_null_vector(v) -> bool:
373    return not NULLVEC.isclose(v)
374
375
376def is_not_zero(v: float) -> bool:
377    return not math.isclose(v, 0.0, abs_tol=1e-12)
378
379
380def is_not_negative(v) -> bool:
381    return v >= 0
382
383
384is_greater_or_equal_zero = is_not_negative
385
386
387def is_positive(v) -> bool:
388    return v > 0
389
390
391is_greater_zero = is_positive
392
393
394def is_valid_bitmask(mask: int):
395    def _validator(value: int) -> bool:
396        return not bool(~mask & value)
397
398    return _validator
399
400
401def fix_bitmask(mask: int):
402    def _fixer(value: int) -> int:
403        return mask & value
404
405    return _fixer
406
407
408def is_integer_bool(v) -> bool:
409    return v in (0, 1)
410
411
412def fix_integer_bool(v) -> int:
413    return 1 if v else 0
414
415
416def is_one_of(values: Set):
417    def _validator(v) -> bool:
418        return v in values
419
420    return _validator
421
422
423def is_valid_one_line_text(text: str) -> bool:
424    has_line_breaks = bool(set(text).intersection({'\n', '\r'}))
425    return not has_line_breaks and not text.endswith('^')
426
427
428def fix_one_line_text(text: str) -> str:
429    return text.replace('\n', '').replace('\r', '').rstrip('^')
430
431
432def is_valid_attrib_tag(tag: str) -> bool:
433    return is_valid_one_line_text(tag)
434
435
436def fix_attrib_tag(tag: str) -> str:
437    return fix_one_line_text(tag)
438