jcl-parser 2.0.1__tar.gz → 2.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/PKG-INFO +1 -1
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jcl_parser.egg-info/PKG-INFO +1 -1
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/__init__.py +1 -1
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/_job_builder.py +12 -2
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/_model_factories.py +56 -6
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/_post_parse.py +67 -20
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/_source_scanner.py +6 -3
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/explosion.py +69 -23
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/parser.py +12 -6
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/utils.py +71 -16
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/pyproject.toml +1 -1
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/LICENSE +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/README.md +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jcl_parser.egg-info/SOURCES.txt +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jcl_parser.egg-info/dependency_links.txt +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jcl_parser.egg-info/entry_points.txt +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jcl_parser.egg-info/requires.txt +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jcl_parser.egg-info/top_level.txt +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/__main__.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/_statement_parser.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/builtin_procs.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/cli.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/condition_parser.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/continuation_handler.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/dataset_lifecycle.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/errors.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/include_loader.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/jcl_file_type.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/jes_statements.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/lint/__init__.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/lint/base.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/lint/config.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/lint/rules.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/lint/runner.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/metrics.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/model.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/parameter_ast.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/parameter_validators.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/plugins/__init__.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/plugins/base.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/proc_loader.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/profiling.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/__init__.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/_control.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/_invoked.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/adrdssu.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/base.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/binder.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/bpxbatch.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/compilers.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/db2_batch.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/dfsrrc00.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/dsnutilb.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/ftp.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/icetool.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/idcams.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/iebcompr.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/iebdg.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/iebgener.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/iebptpch.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/iebupdte.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/iefbr14.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/iehinitt.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/iehlist.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/iehprogm.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/ikjeft01.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/irxjcl.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/registry.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/program_profiles/sort.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/py.typed +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/rc_analysis.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/sql_detector.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/system_symbols.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/jclparser/validators.py +0 -0
- {jcl_parser-2.0.1 → jcl_parser-2.0.2}/setup.cfg +0 -0
|
@@ -20,7 +20,7 @@ application, for example::
|
|
|
20
20
|
import logging as _logging
|
|
21
21
|
|
|
22
22
|
# Package metadata
|
|
23
|
-
__version__ = "2.0.
|
|
23
|
+
__version__ = "2.0.2" # x-release-please-version
|
|
24
24
|
__author__ = "JCL Parser Team"
|
|
25
25
|
__description__ = "Parse IBM JCL scripts into Python objects"
|
|
26
26
|
|
|
@@ -131,10 +131,13 @@ class JobBuilder:
|
|
|
131
131
|
"XMIT": "_on_xmit",
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
def __init__(self, dialect: str = "standard") -> None:
|
|
134
|
+
def __init__(self, dialect: str = "standard", strict: bool = False) -> None:
|
|
135
135
|
#: ``ParserConfig.dialect``: decides which JES control statements are
|
|
136
136
|
#: interpreted (see ``jclparser.plugins.SUPPORTED_DIALECTS``).
|
|
137
137
|
self.dialect = dialect
|
|
138
|
+
#: Strict parsing: a SET value over ``MAX_SYMBOL_VALUE_LENGTH`` raises
|
|
139
|
+
#: instead of being left unresolved with a warning.
|
|
140
|
+
self.strict = strict
|
|
138
141
|
self.job: Job | None = None
|
|
139
142
|
#: tuples: (ConditionalBlock, active_branch)
|
|
140
143
|
self.conditional_stack: list[tuple[ConditionalBlock, str]] = []
|
|
@@ -437,7 +440,14 @@ class JobBuilder:
|
|
|
437
440
|
symbols = self.job.set_statements if self.job else self.pending_sets
|
|
438
441
|
|
|
439
442
|
def _record(symbol: str, value: str) -> None:
|
|
440
|
-
resolved = _resolve_set_value(
|
|
443
|
+
resolved = _resolve_set_value(
|
|
444
|
+
value,
|
|
445
|
+
symbols,
|
|
446
|
+
symbol=symbol,
|
|
447
|
+
line_number=stmt.line_number,
|
|
448
|
+
strict=self.strict,
|
|
449
|
+
warnings=self._warnings_sink(),
|
|
450
|
+
)
|
|
441
451
|
if procedure is not None:
|
|
442
452
|
procedure.set_statements[symbol] = resolved
|
|
443
453
|
elif self.job:
|
|
@@ -21,13 +21,22 @@ import re
|
|
|
21
21
|
from collections.abc import Callable
|
|
22
22
|
from typing import Any
|
|
23
23
|
|
|
24
|
+
from .errors import JCLSyntaxError
|
|
24
25
|
from .model import DDStatement, Job, Statement, Step
|
|
25
|
-
from .utils import POSITIONAL_PARAM_PREFIX, is_positional_key
|
|
26
|
+
from .utils import MAX_SYMBOL_VALUE_LENGTH, POSITIONAL_PARAM_PREFIX, is_positional_key
|
|
26
27
|
|
|
27
28
|
_SET_SYMBOL_PATTERN = re.compile(r"&&|&([A-Z0-9@#$]+)\.?")
|
|
28
29
|
|
|
29
30
|
|
|
30
|
-
def _resolve_set_value(
|
|
31
|
+
def _resolve_set_value(
|
|
32
|
+
value: str,
|
|
33
|
+
known_symbols: dict[str, str],
|
|
34
|
+
*,
|
|
35
|
+
symbol: str = "",
|
|
36
|
+
line_number: int = 0,
|
|
37
|
+
strict: bool = False,
|
|
38
|
+
warnings: list[str] | None = None,
|
|
39
|
+
) -> str:
|
|
31
40
|
"""Resolve &SYMBOL references in a SET value using already-accumulated SET symbols.
|
|
32
41
|
|
|
33
42
|
Mirrors the mainframe rule: SET values are evaluated left-to-right so a
|
|
@@ -36,15 +45,56 @@ def _resolve_set_value(value: str, known_symbols: dict[str, str]) -> str:
|
|
|
36
45
|
itself holds a "&&NAME" temporary-dataset marker (e.g. ``SET
|
|
37
46
|
MYTEMP=&&WORK``) before it ever reaches a DD statement. That collapse is
|
|
38
47
|
explode_job()'s job (Tier 2), not this Tier-1 SET-value resolution.
|
|
48
|
+
|
|
49
|
+
A resolved value longer than :data:`~jclparser.utils.MAX_SYMBOL_VALUE_LENGTH`
|
|
50
|
+
is never built (#70): its length is computed first, and the value is
|
|
51
|
+
returned unresolved with a message on *warnings* -- or, when *strict*,
|
|
52
|
+
:class:`~jclparser.errors.JCLSyntaxError` is raised. *symbol* and
|
|
53
|
+
*line_number* only label that message.
|
|
39
54
|
"""
|
|
55
|
+
matches = list(_SET_SYMBOL_PATTERN.finditer(value))
|
|
56
|
+
if not matches:
|
|
57
|
+
return value
|
|
40
58
|
|
|
41
|
-
def
|
|
59
|
+
def _replacement(m: re.Match[str]) -> str:
|
|
42
60
|
if m.group(0) == "&&":
|
|
43
61
|
return "&&"
|
|
44
|
-
|
|
45
|
-
return known_symbols.get(sym, m.group(0))
|
|
62
|
+
return known_symbols.get(m.group(1).upper(), m.group(0))
|
|
46
63
|
|
|
47
|
-
|
|
64
|
+
replacements = [_replacement(m) for m in matches]
|
|
65
|
+
length = len(value) + sum(
|
|
66
|
+
len(r) - len(m.group(0)) for m, r in zip(matches, replacements)
|
|
67
|
+
)
|
|
68
|
+
if length > MAX_SYMBOL_VALUE_LENGTH:
|
|
69
|
+
_report_set_overflow(symbol, length, line_number, strict, warnings)
|
|
70
|
+
return value
|
|
71
|
+
parts: list[str] = []
|
|
72
|
+
pos = 0
|
|
73
|
+
for m, r in zip(matches, replacements):
|
|
74
|
+
parts.append(value[pos : m.start()])
|
|
75
|
+
parts.append(r)
|
|
76
|
+
pos = m.end()
|
|
77
|
+
parts.append(value[pos:])
|
|
78
|
+
return "".join(parts)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _report_set_overflow(
|
|
82
|
+
symbol: str,
|
|
83
|
+
length: int,
|
|
84
|
+
line_number: int,
|
|
85
|
+
strict: bool,
|
|
86
|
+
warnings: list[str] | None,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Raise (strict) or warn that SET *symbol* would resolve to *length* chars."""
|
|
89
|
+
message = (
|
|
90
|
+
f"SET symbol &{symbol or '?'} would resolve to {length:,} characters, "
|
|
91
|
+
f"more than the {MAX_SYMBOL_VALUE_LENGTH:,}-character limit"
|
|
92
|
+
)
|
|
93
|
+
if strict:
|
|
94
|
+
raise JCLSyntaxError(message, line_number)
|
|
95
|
+
if warnings is not None:
|
|
96
|
+
prefix = f"Line {line_number}: " if line_number else ""
|
|
97
|
+
warnings.append(f"{prefix}{message}; value left unresolved")
|
|
48
98
|
|
|
49
99
|
|
|
50
100
|
def _has_unresolved_symbol_ref(
|
|
@@ -246,7 +246,7 @@ def enforce_strict_dd_structure(content: str) -> None:
|
|
|
246
246
|
)
|
|
247
247
|
|
|
248
248
|
|
|
249
|
-
def resolve_set_symbols(job: Job) -> None:
|
|
249
|
+
def resolve_set_symbols(job: Job, strict: bool = False) -> None:
|
|
250
250
|
"""Substitute fully-resolved SET symbols into DD and EXEC-step parameters.
|
|
251
251
|
|
|
252
252
|
SET statements may appear before or after the DD statements that reference
|
|
@@ -274,11 +274,17 @@ def resolve_set_symbols(job: Job) -> None:
|
|
|
274
274
|
pass. It also covers EXEC step parameters (e.g. ``PARM=&SYM``), not just
|
|
275
275
|
DD dataset names/dispositions, so a job-level SET symbol resolves the same
|
|
276
276
|
way regardless of which kind of statement references it.
|
|
277
|
+
|
|
278
|
+
A SET value whose expansion would exceed ``utils.MAX_SYMBOL_VALUE_LENGTH``
|
|
279
|
+
characters is left unresolved with one warning on ``job.warnings`` (#70);
|
|
280
|
+
a statement referencing it keeps the reference as written (again with a
|
|
281
|
+
warning). With *strict*, :class:`~jclparser.errors.JCLSyntaxError` is
|
|
282
|
+
raised instead.
|
|
277
283
|
"""
|
|
278
284
|
from .explosion import substitute_dd_statement
|
|
279
285
|
from .system_symbols import is_system_symbol_name
|
|
280
286
|
|
|
281
|
-
_set_scope = _scope_for_table(job.set_statements)
|
|
287
|
+
_set_scope = _scope_for_table(job.set_statements, job.warnings, strict)
|
|
282
288
|
# A step read before a SET redefined a symbol sees the value in effect at
|
|
283
289
|
# that point (Step.set_symbols); symbols it has no snapshot for (forward
|
|
284
290
|
# references) still fall back to the job's final table.
|
|
@@ -290,7 +296,7 @@ def resolve_set_symbols(job: Job) -> None:
|
|
|
290
296
|
for _owner, _dd in _dds_with_owners(_flat_steps):
|
|
291
297
|
substitute_dd_statement(
|
|
292
298
|
_dd,
|
|
293
|
-
_scope_for_step(_owner, job, _set_scope, _step_scopes),
|
|
299
|
+
_scope_for_step(_owner, job, _set_scope, _step_scopes, strict),
|
|
294
300
|
strict=False,
|
|
295
301
|
warnings=job.warnings,
|
|
296
302
|
collapse_double_amp=False,
|
|
@@ -298,14 +304,18 @@ def resolve_set_symbols(job: Job) -> None:
|
|
|
298
304
|
)
|
|
299
305
|
|
|
300
306
|
for _step in _flat_steps:
|
|
301
|
-
_step_scope = _scope_for_step(_step, job, _set_scope, _step_scopes)
|
|
307
|
+
_step_scope = _scope_for_step(_step, job, _set_scope, _step_scopes, strict)
|
|
302
308
|
_substitute_step_parameters(_step, _step_scope, job.warnings)
|
|
303
309
|
_substitute_step_names(_step, _step_scope)
|
|
304
310
|
|
|
305
311
|
|
|
306
|
-
def _preresolve_set_table(table: dict[str, str]) -> dict[str, str]:
|
|
307
|
-
"""Return a copy of SET *table* with symbol references between values expanded.
|
|
308
|
-
|
|
312
|
+
def _preresolve_set_table(table: dict[str, str]) -> tuple[dict[str, str], list[str]]:
|
|
313
|
+
"""Return a copy of SET *table* with symbol references between values expanded.
|
|
314
|
+
|
|
315
|
+
Also returns the names of the symbols whose expansion would exceed
|
|
316
|
+
``utils.MAX_SYMBOL_VALUE_LENGTH``; those keep their unexpanded value.
|
|
317
|
+
"""
|
|
318
|
+
from .explosion import SymbolScope, _substitute_symbols_bounded, _SymbolOverflow
|
|
309
319
|
|
|
310
320
|
# Pre-resolve the symbol table so every value is fully expanded before
|
|
311
321
|
# we apply it to DD dataset names. On each pass we substitute symbols
|
|
@@ -313,36 +323,67 @@ def _preresolve_set_table(table: dict[str, str]) -> dict[str, str]:
|
|
|
313
323
|
# (excluding the symbol itself prevents direct self-reference loops).
|
|
314
324
|
# We repeat until no value changes or we reach the pass limit.
|
|
315
325
|
_raw: dict[str, str] = dict(table)
|
|
326
|
+
_overflowed: list[str] = []
|
|
316
327
|
for _pass in range(10):
|
|
317
328
|
_changed = False
|
|
318
329
|
for _name, _val in list(_raw.items()):
|
|
319
|
-
if "&" not in _val:
|
|
330
|
+
if "&" not in _val or _name in _overflowed:
|
|
320
331
|
continue
|
|
321
332
|
# Build a temporary scope that excludes this symbol's own name
|
|
322
333
|
# so "&A." inside SET A's value does not expand to itself.
|
|
323
334
|
_scope_for_val = SymbolScope(
|
|
324
335
|
set_symbols={k: v for k, v in _raw.items() if k != _name}
|
|
325
336
|
)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
337
|
+
try:
|
|
338
|
+
_new_val = _substitute_symbols_bounded(
|
|
339
|
+
_val,
|
|
340
|
+
_scope_for_val,
|
|
341
|
+
allow_undefined=True,
|
|
342
|
+
collapse_double_amp=False,
|
|
343
|
+
)
|
|
344
|
+
except _SymbolOverflow:
|
|
345
|
+
# Values only grow from pass to pass: never retry it.
|
|
346
|
+
_overflowed.append(_name)
|
|
347
|
+
continue
|
|
332
348
|
if _new_val != _val:
|
|
333
349
|
_raw[_name] = _new_val
|
|
334
350
|
_changed = True
|
|
335
351
|
if not _changed:
|
|
336
352
|
break # table has stabilised – no need for more passes
|
|
337
|
-
return _raw
|
|
353
|
+
return _raw, _overflowed
|
|
338
354
|
|
|
339
355
|
|
|
340
|
-
def
|
|
341
|
-
|
|
356
|
+
def _report_table_overflow(
|
|
357
|
+
overflowed: list[str], warnings: list[str], strict: bool
|
|
358
|
+
) -> None:
|
|
359
|
+
"""Raise (strict) or warn once that the *overflowed* SET symbols stay unresolved."""
|
|
360
|
+
from .utils import MAX_SYMBOL_VALUE_LENGTH
|
|
361
|
+
|
|
362
|
+
names = ", ".join(f"&{n}" for n in overflowed)
|
|
363
|
+
message = (
|
|
364
|
+
f"SET symbol(s) {names} expand to more than "
|
|
365
|
+
f"{MAX_SYMBOL_VALUE_LENGTH:,} characters"
|
|
366
|
+
)
|
|
367
|
+
if strict:
|
|
368
|
+
raise JCLSyntaxError(message)
|
|
369
|
+
message += "; left unresolved"
|
|
370
|
+
if message not in warnings:
|
|
371
|
+
warnings.append(message)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _scope_for_table(
|
|
375
|
+
table: dict[str, str], warnings: list[str], strict: bool = False
|
|
376
|
+
) -> SymbolScope:
|
|
377
|
+
"""Build the substitution scope for SET symbol *table* (see resolve_set_symbols).
|
|
378
|
+
|
|
379
|
+
Overflow of a SET value is reported on *warnings* (or raised, *strict*).
|
|
380
|
+
"""
|
|
342
381
|
from .explosion import SymbolScope
|
|
343
382
|
from .system_symbols import is_system_symbol_name
|
|
344
383
|
|
|
345
|
-
_raw = _preresolve_set_table(table)
|
|
384
|
+
_raw, _overflowed = _preresolve_set_table(table)
|
|
385
|
+
if _overflowed:
|
|
386
|
+
_report_table_overflow(_overflowed, warnings, strict)
|
|
346
387
|
|
|
347
388
|
# Symbols whose values still contain a genuinely unresolved
|
|
348
389
|
# &-reference after all passes are excluded from the final scope.
|
|
@@ -355,10 +396,15 @@ def _scope_for_table(table: dict[str, str]) -> SymbolScope:
|
|
|
355
396
|
# ``DSN=&SYSUID..WORK`` before that pass runs (keep_undefined below). A value that legitimately contains "&&" (e.g.
|
|
356
397
|
# SET MYTEMP=&&WORK) is NOT excluded -- it has nothing left to
|
|
357
398
|
# resolve, it just happens to contain an escaped literal ampersand.
|
|
399
|
+
#
|
|
400
|
+
# An overflowed symbol keeps its unexpanded value in the scope, so a
|
|
401
|
+
# statement referencing it overflows the substitution budget in turn and
|
|
402
|
+
# keeps the reference as written instead of having it blanked (#70).
|
|
358
403
|
_resolved_symbols = {
|
|
359
404
|
k: v
|
|
360
405
|
for k, v in _raw.items()
|
|
361
|
-
if
|
|
406
|
+
if k in _overflowed
|
|
407
|
+
or not _has_unresolved_symbol_ref(v, ignore=is_system_symbol_name)
|
|
362
408
|
}
|
|
363
409
|
return SymbolScope(set_symbols=_resolved_symbols)
|
|
364
410
|
|
|
@@ -368,6 +414,7 @@ def _scope_for_step(
|
|
|
368
414
|
job: Job,
|
|
369
415
|
set_scope: SymbolScope,
|
|
370
416
|
step_scopes: dict[int, SymbolScope],
|
|
417
|
+
strict: bool = False,
|
|
371
418
|
) -> SymbolScope:
|
|
372
419
|
"""Return the SET scope in effect for *step*, memoised in *step_scopes*.
|
|
373
420
|
|
|
@@ -378,7 +425,7 @@ def _scope_for_step(
|
|
|
378
425
|
return set_scope
|
|
379
426
|
if id(step) not in step_scopes:
|
|
380
427
|
step_scopes[id(step)] = _scope_for_table(
|
|
381
|
-
{**job.set_statements, **step.set_symbols}
|
|
428
|
+
{**job.set_statements, **step.set_symbols}, job.warnings, strict
|
|
382
429
|
)
|
|
383
430
|
return step_scopes[id(step)]
|
|
384
431
|
|
|
@@ -31,7 +31,7 @@ from .errors import JCLSyntaxError
|
|
|
31
31
|
from .utils import (
|
|
32
32
|
instream_delimiter,
|
|
33
33
|
is_continued_card_image,
|
|
34
|
-
|
|
34
|
+
normalize_jcl_lines,
|
|
35
35
|
split_source_lines,
|
|
36
36
|
)
|
|
37
37
|
|
|
@@ -60,7 +60,7 @@ def _merge_source_lines(content: str) -> ScannedSource:
|
|
|
60
60
|
or garbled records).
|
|
61
61
|
"""
|
|
62
62
|
raw_lines = split_source_lines(content)
|
|
63
|
-
lines =
|
|
63
|
+
lines = normalize_jcl_lines(raw_lines)
|
|
64
64
|
|
|
65
65
|
classifier = _RecordClassifier(lines)
|
|
66
66
|
jcl_indices = classifier.run()
|
|
@@ -116,7 +116,10 @@ def merge_continuations(
|
|
|
116
116
|
card_images={
|
|
117
117
|
n
|
|
118
118
|
for n, orig in enumerate(indices)
|
|
119
|
-
if is_continued_card_image(
|
|
119
|
+
if is_continued_card_image(
|
|
120
|
+
raw_lines[orig],
|
|
121
|
+
raw_lines[orig + 1] if orig + 1 < len(raw_lines) else None,
|
|
122
|
+
)
|
|
120
123
|
},
|
|
121
124
|
)
|
|
122
125
|
|
|
@@ -36,7 +36,7 @@ from .proc_loader import (
|
|
|
36
36
|
create_default_loader,
|
|
37
37
|
)
|
|
38
38
|
from .system_symbols import SystemSymbolProvider, get_default_provider
|
|
39
|
-
from .utils import jcl_value_text
|
|
39
|
+
from .utils import MAX_SYMBOL_VALUE_LENGTH, jcl_value_text
|
|
40
40
|
|
|
41
41
|
logger = logging.getLogger(__name__)
|
|
42
42
|
|
|
@@ -53,10 +53,23 @@ _SYMBOL_SUB_PATTERN = re.compile(r"&&|&([A-Z0-9@#$]+)\.?")
|
|
|
53
53
|
# how deep those nested references are followed.
|
|
54
54
|
_MAX_NESTED_SUBSTITUTION = 8
|
|
55
55
|
|
|
56
|
-
# Total characters
|
|
57
|
-
# Fan-out over distinct symbols
|
|
58
|
-
#
|
|
59
|
-
|
|
56
|
+
# Total characters the symbol values spliced in by one substitution may add
|
|
57
|
+
# up to (``utils.MAX_SYMBOL_VALUE_LENGTH``). Fan-out over distinct symbols
|
|
58
|
+
# (``A='&B&B&B'``, ``B='&C&C&C'`` ...) grows exponentially with depth even
|
|
59
|
+
# without a cycle, so depth alone is no bound (#70).
|
|
60
|
+
_MAX_NESTED_EXPANSION_CHARS = MAX_SYMBOL_VALUE_LENGTH
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class _SymbolOverflow(Exception):
|
|
64
|
+
"""Internal: a substitution would splice in more than the character budget."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _overflow_message(source_text: str) -> str:
|
|
68
|
+
return (
|
|
69
|
+
f"Symbols in '{source_text[:80]}' expand to more than "
|
|
70
|
+
f"{_MAX_NESTED_EXPANSION_CHARS:,} characters"
|
|
71
|
+
)
|
|
72
|
+
|
|
60
73
|
|
|
61
74
|
# A value ending in a bare ``&NAME`` reference (not the ``&&NAME`` literal).
|
|
62
75
|
_TRAILING_SYMBOL_REF = re.compile(r"(?<!&)&[A-Z0-9@#$]+$")
|
|
@@ -319,10 +332,13 @@ def _make_replacer(
|
|
|
319
332
|
*_resolving* holds the symbols whose values are being expanded above this
|
|
320
333
|
level; a reference back to one of them is a cycle and is left as written.
|
|
321
334
|
*_budget* is a one-element list shared by every nested level, counting
|
|
322
|
-
down the characters
|
|
335
|
+
down the characters the substitution may still splice in; running out
|
|
336
|
+
raises :class:`_SymbolOverflow`, so no level builds a string much longer
|
|
337
|
+
than the budget.
|
|
323
338
|
"""
|
|
324
339
|
if _budget is None:
|
|
325
340
|
_budget = [_MAX_NESTED_EXPANSION_CHARS]
|
|
341
|
+
budget = _budget
|
|
326
342
|
|
|
327
343
|
def _replacer(m: re.Match[str]) -> str:
|
|
328
344
|
if m.group(0) == "&&":
|
|
@@ -361,17 +377,13 @@ def _make_replacer(
|
|
|
361
377
|
collapse_double_amp=False,
|
|
362
378
|
_depth=_depth + 1,
|
|
363
379
|
_resolving=_resolving | {symbol_name},
|
|
364
|
-
_budget=
|
|
380
|
+
_budget=budget,
|
|
365
381
|
),
|
|
366
382
|
value,
|
|
367
383
|
)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
f"Symbol &{symbol_name} expands to more than "
|
|
372
|
-
f"{_MAX_NESTED_EXPANSION_CHARS:,} characters in "
|
|
373
|
-
f"'{source_text[:80]}'"
|
|
374
|
-
)
|
|
384
|
+
budget[0] -= len(value)
|
|
385
|
+
if budget[0] < 0:
|
|
386
|
+
raise _SymbolOverflow
|
|
375
387
|
if _TRAILING_SYMBOL_REF.search(value):
|
|
376
388
|
# Splicing a bare ``&SYSUID`` in front of ``.DATA`` would make that
|
|
377
389
|
# period the reference's delimiter; give it its own.
|
|
@@ -381,6 +393,30 @@ def _make_replacer(
|
|
|
381
393
|
return _replacer
|
|
382
394
|
|
|
383
395
|
|
|
396
|
+
def _substitute_symbols_bounded(
|
|
397
|
+
text: str,
|
|
398
|
+
scope: "SymbolScope",
|
|
399
|
+
strict: bool = False,
|
|
400
|
+
allow_undefined: bool = False,
|
|
401
|
+
warnings: list[str] | None = None,
|
|
402
|
+
collapse_double_amp: bool = True,
|
|
403
|
+
keep_undefined: Callable[[str], bool] | None = None,
|
|
404
|
+
) -> str:
|
|
405
|
+
""":func:`substitute_symbols` that raises :class:`_SymbolOverflow` on overflow."""
|
|
406
|
+
return _SYMBOL_SUB_PATTERN.sub(
|
|
407
|
+
_make_replacer(
|
|
408
|
+
scope,
|
|
409
|
+
strict,
|
|
410
|
+
allow_undefined,
|
|
411
|
+
source_text=text,
|
|
412
|
+
warnings=warnings,
|
|
413
|
+
collapse_double_amp=collapse_double_amp,
|
|
414
|
+
keep_undefined=keep_undefined,
|
|
415
|
+
),
|
|
416
|
+
text,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
384
420
|
def substitute_symbols(
|
|
385
421
|
text: str,
|
|
386
422
|
scope: "SymbolScope",
|
|
@@ -415,25 +451,35 @@ def substitute_symbols(
|
|
|
415
451
|
symbols) can still resolve it.
|
|
416
452
|
|
|
417
453
|
Returns:
|
|
418
|
-
Text with symbols substituted
|
|
454
|
+
Text with symbols substituted. When the substituted values would add
|
|
455
|
+
up to more than ``utils.MAX_SYMBOL_VALUE_LENGTH`` characters (chained
|
|
456
|
+
symbols that double at each step), *text* is returned unchanged and a
|
|
457
|
+
message goes to *warnings* (#70).
|
|
419
458
|
|
|
420
459
|
Raises:
|
|
421
460
|
UndefinedSymbolError: If strict=True and symbol undefined
|
|
461
|
+
JCLStructureError: If strict=True and the substitution would exceed
|
|
462
|
+
``utils.MAX_SYMBOL_VALUE_LENGTH`` characters
|
|
422
463
|
"""
|
|
423
464
|
if not text or not isinstance(text, str):
|
|
424
465
|
return text
|
|
425
|
-
|
|
426
|
-
|
|
466
|
+
try:
|
|
467
|
+
return _substitute_symbols_bounded(
|
|
468
|
+
text,
|
|
427
469
|
scope,
|
|
428
|
-
strict,
|
|
429
|
-
allow_undefined,
|
|
430
|
-
source_text=text,
|
|
470
|
+
strict=strict,
|
|
471
|
+
allow_undefined=allow_undefined,
|
|
431
472
|
warnings=warnings,
|
|
432
473
|
collapse_double_amp=collapse_double_amp,
|
|
433
474
|
keep_undefined=keep_undefined,
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
|
|
475
|
+
)
|
|
476
|
+
except _SymbolOverflow:
|
|
477
|
+
message = _overflow_message(text)
|
|
478
|
+
if strict:
|
|
479
|
+
raise JCLStructureError(message) from None
|
|
480
|
+
if warnings is not None:
|
|
481
|
+
warnings.append(f"{message}; left unresolved")
|
|
482
|
+
return text
|
|
437
483
|
|
|
438
484
|
|
|
439
485
|
def detect_circular_refs(
|
|
@@ -137,7 +137,7 @@ def _parse_jcl_core(
|
|
|
137
137
|
)
|
|
138
138
|
|
|
139
139
|
scanned = _merge_source_lines(content)
|
|
140
|
-
job = _build_job(scanned, config)
|
|
140
|
+
job = _build_job(scanned, config, strict=strict_mode)
|
|
141
141
|
|
|
142
142
|
# Anything the line-classification pass discarded or repaired (a comment
|
|
143
143
|
# field that looked like operands, an unterminated CNTL block) is reported
|
|
@@ -156,7 +156,7 @@ def _parse_jcl_core(
|
|
|
156
156
|
if strict_mode and not job.steps:
|
|
157
157
|
enforce_strict_dd_structure(content)
|
|
158
158
|
|
|
159
|
-
_resolve_references(job, expanded_includes)
|
|
159
|
+
_resolve_references(job, expanded_includes, strict=strict_mode)
|
|
160
160
|
|
|
161
161
|
if strict_mode and validate_job:
|
|
162
162
|
_raise_on_fatal_findings(job)
|
|
@@ -208,13 +208,17 @@ def _expand_includes(
|
|
|
208
208
|
return content, expanded
|
|
209
209
|
|
|
210
210
|
|
|
211
|
-
def _build_job(
|
|
211
|
+
def _build_job(
|
|
212
|
+
scanned: ScannedSource, config: ParserConfig | None, *, strict: bool = False
|
|
213
|
+
) -> Job:
|
|
212
214
|
"""Run the parse loop over the scanned source lines.
|
|
213
215
|
|
|
214
216
|
The builder holds all loop state and has one handler per statement type
|
|
215
217
|
(see jclparser/_job_builder.py).
|
|
216
218
|
"""
|
|
217
|
-
builder = JobBuilder(
|
|
219
|
+
builder = JobBuilder(
|
|
220
|
+
dialect=config.dialect if config is not None else "standard", strict=strict
|
|
221
|
+
)
|
|
218
222
|
for line, line_num in scanned.merged_lines:
|
|
219
223
|
# Handle inline data continuation
|
|
220
224
|
if builder.consume_inline_data(line):
|
|
@@ -250,7 +254,9 @@ def _build_job(scanned: ScannedSource, config: ParserConfig | None) -> Job:
|
|
|
250
254
|
return builder.finish()
|
|
251
255
|
|
|
252
256
|
|
|
253
|
-
def _resolve_references(
|
|
257
|
+
def _resolve_references(
|
|
258
|
+
job: Job, expanded_includes: list[str], *, strict: bool = False
|
|
259
|
+
) -> None:
|
|
254
260
|
"""Post-parse passes that need the whole job: INCLUDEs, OUTPUT=, SET."""
|
|
255
261
|
# Record any INCLUDE members that were expanded during pre-processing.
|
|
256
262
|
# Members resolved before the main parse loop never reach the INCLUDE
|
|
@@ -271,7 +277,7 @@ def _resolve_references(job: Job, expanded_includes: list[str]) -> None:
|
|
|
271
277
|
"resolving %d SET symbol(s) across DD statements",
|
|
272
278
|
len(job.set_statements),
|
|
273
279
|
)
|
|
274
|
-
resolve_set_symbols(job)
|
|
280
|
+
resolve_set_symbols(job, strict=strict)
|
|
275
281
|
|
|
276
282
|
|
|
277
283
|
def _raise_on_fatal_findings(job: Job) -> None:
|
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
import builtins
|
|
11
11
|
import errno
|
|
12
12
|
import re
|
|
13
|
+
from collections.abc import Sequence
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Any
|
|
15
16
|
|
|
@@ -94,21 +95,56 @@ def jcl_value_text(value: Any) -> str:
|
|
|
94
95
|
return str(value)
|
|
95
96
|
|
|
96
97
|
|
|
97
|
-
def
|
|
98
|
+
def _continues_on_next_record(line: str) -> bool:
|
|
99
|
+
"""True if a 72-character ``//`` statement record leaves its operand open
|
|
100
|
+
at column 71: a trailing comma, or an unclosed quoted string."""
|
|
101
|
+
if not line.startswith("//") or line.startswith("//*"):
|
|
102
|
+
return False
|
|
103
|
+
return line[70] == "," or line[:71].count("'") % 2 == 1
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _is_continuation_record(line: str) -> bool:
|
|
107
|
+
"""True if *line* is a ``//`` record with a blank name field and operands."""
|
|
108
|
+
return line.startswith("// ") and bool(line[3:].strip())
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _has_column_72_indicator(line: str, next_line: str | None) -> bool:
|
|
112
|
+
"""True if column 72 of a 72-character record is a continuation indicator.
|
|
113
|
+
|
|
114
|
+
Such a record is a fixed-format card whose sequence field was trimmed.
|
|
115
|
+
A blank column 71 says so on its own. With column 71 non-blank, the
|
|
116
|
+
record could equally be an editor line whose last character is content,
|
|
117
|
+
so column 72 counts as the indicator only when the operand is left open
|
|
118
|
+
at column 71 (trailing comma or unclosed quote) *and* the next record,
|
|
119
|
+
*next_line*, is a continuation (issue #80).
|
|
120
|
+
"""
|
|
121
|
+
if len(line) != 72 or line[71].isspace():
|
|
122
|
+
return False
|
|
123
|
+
if line[70] == " ":
|
|
124
|
+
return True
|
|
125
|
+
return (
|
|
126
|
+
next_line is not None
|
|
127
|
+
and _continues_on_next_record(line)
|
|
128
|
+
and _is_continuation_record(next_line)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def is_continued_card_image(line: str, next_line: str | None = None) -> bool:
|
|
98
133
|
"""Return ``True`` if raw record *line* is a card image continued in column 72.
|
|
99
134
|
|
|
100
135
|
Exactly the records :func:`normalize_jcl_line` takes a non-blank column-72
|
|
101
|
-
continuation indicator off
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
136
|
+
continuation indicator off (*next_line* is the raw record after *line*,
|
|
137
|
+
as there). For such a record the statement text fills columns 1-71, so
|
|
138
|
+
blanks up to column 71 are known to be coded -- which matters when a
|
|
139
|
+
quoted string is continued: it runs through column 71 and its trailing
|
|
140
|
+
blanks are part of the value (issue #65).
|
|
105
141
|
"""
|
|
106
|
-
if len(line) >= 80
|
|
142
|
+
if len(line) >= 80:
|
|
107
143
|
return not line[71].isspace()
|
|
108
|
-
return
|
|
144
|
+
return _has_column_72_indicator(line, next_line)
|
|
109
145
|
|
|
110
146
|
|
|
111
|
-
def normalize_jcl_line(line: str) -> str:
|
|
147
|
+
def normalize_jcl_line(line: str, next_line: str | None = None) -> str:
|
|
112
148
|
"""
|
|
113
149
|
Normalize JCL line by removing extra whitespace and handling continuations.
|
|
114
150
|
|
|
@@ -123,13 +159,18 @@ def normalize_jcl_line(line: str) -> str:
|
|
|
123
159
|
indicating a true mainframe fixed-width record. Lines shorter than 80
|
|
124
160
|
characters are assumed to come from a modern text editor where all
|
|
125
161
|
characters are actual JCL content and must not be truncated — with one
|
|
126
|
-
exception: a record of exactly 72 characters whose column
|
|
127
|
-
|
|
128
|
-
sequence field trimmed
|
|
129
|
-
|
|
162
|
+
exception: a record of exactly 72 characters whose column 72 is a
|
|
163
|
+
continuation indicator is a fixed-format record that had its trailing
|
|
164
|
+
sequence field trimmed. That is the case when column 71 is blank, or when
|
|
165
|
+
column 71 leaves the operand open (a trailing comma or an unclosed quoted
|
|
166
|
+
string) and *next_line*, the raw record after *line*, is a continuation
|
|
167
|
+
record (``//`` and a blank name field). Without *next_line* only the
|
|
168
|
+
blank-column-71 case applies. Use :func:`normalize_jcl_lines` for a whole
|
|
169
|
+
source.
|
|
130
170
|
|
|
131
171
|
Args:
|
|
132
172
|
line: Raw JCL line
|
|
173
|
+
next_line: The raw record after *line*, if any
|
|
133
174
|
|
|
134
175
|
Returns:
|
|
135
176
|
Normalized line
|
|
@@ -138,10 +179,7 @@ def normalize_jcl_line(line: str) -> str:
|
|
|
138
179
|
# column-72 continuation indicator (0-indexed 71). Only do this for full
|
|
139
180
|
# 80-column mainframe records; shorter lines are modern text-editor JCL
|
|
140
181
|
# where every character is content.
|
|
141
|
-
if len(line) >= 80:
|
|
142
|
-
line = line[:71]
|
|
143
|
-
elif len(line) == 72 and line[70] == " " and not line[71].isspace():
|
|
144
|
-
# Sequence field trimmed away, continuation indicator left behind.
|
|
182
|
+
if len(line) >= 80 or _has_column_72_indicator(line, next_line):
|
|
145
183
|
line = line[:71]
|
|
146
184
|
|
|
147
185
|
# Remove trailing whitespace but preserve leading spaces for continuation detection
|
|
@@ -153,6 +191,23 @@ def normalize_jcl_line(line: str) -> str:
|
|
|
153
191
|
return line
|
|
154
192
|
|
|
155
193
|
|
|
194
|
+
def normalize_jcl_lines(raw_lines: Sequence[str]) -> list[str]:
|
|
195
|
+
""":func:`normalize_jcl_line` every record of *raw_lines*, with lookahead."""
|
|
196
|
+
return [
|
|
197
|
+
normalize_jcl_line(line, raw_lines[i + 1] if i + 1 < len(raw_lines) else None)
|
|
198
|
+
for i, line in enumerate(raw_lines)
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
#: Longest value a symbol substitution may produce: a resolved SET value, a
|
|
203
|
+
#: pre-resolved SET table entry, or the characters one ``substitute_symbols()``
|
|
204
|
+
#: call splices in. A safety limit against chained symbols that double at each
|
|
205
|
+
#: step (``SET A2='&A1&A1'`` ...), deliberately far above the z/OS 255-character
|
|
206
|
+
#: symbol limit so every currently accepted job still parses. Past it the text
|
|
207
|
+
#: is left unresolved with a warning, or the strict paths raise.
|
|
208
|
+
MAX_SYMBOL_VALUE_LENGTH: int = 65536
|
|
209
|
+
|
|
210
|
+
|
|
156
211
|
#: Largest PROC / INCLUDE library member the loaders will read. Members are
|
|
157
212
|
#: named by the JCL being parsed, so an oversized file is refused (read as
|
|
158
213
|
#: missing, with a warning) rather than pulled into memory whole.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "jcl-parser"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.2"
|
|
8
8
|
description = "Parse IBM JCL (Job Control Language) scripts into Python objects"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "JCL Parser Team", email = "team@jclparser.dev"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|