Redo uncompyel6 options ...

Use click now and make more like decompyle3
This commit is contained in:
rocky
2024-02-10 13:19:32 -05:00
parent d7a1d5bbad
commit dd8ee1466d
4 changed files with 284 additions and 172 deletions

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2018, 2020-2021 Rocky Bernstein <rocky@gnu.org> # Copyright (C) 2018, 2020-2021 2024 Rocky Bernstein <rocky@gnu.org>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@@ -32,9 +32,11 @@
# 3.3 | pip | 10.0.1 | # 3.3 | pip | 10.0.1 |
# 3.4 | pip | 19.1.1 | # 3.4 | pip | 19.1.1 |
import os.path as osp
# Things that change more often go here. # Things that change more often go here.
copyright = """ copyright = """
Copyright (C) 2015-2021 Rocky Bernstein <rb@dustyfeet.com>. Copyright (C) 2015-2021, 2024 Rocky Bernstein <rb@dustyfeet.com>.
""" """
classifiers = [ classifiers = [
@@ -75,7 +77,7 @@ entry_points = {
] ]
} }
ftp_url = None ftp_url = None
install_requires = ["spark-parser >= 1.8.9, < 1.9.0", "xdis >= 6.0.8, < 6.2.0"] install_requires = ["click", "spark-parser >= 1.8.9, < 1.9.0", "xdis >= 6.0.8, < 6.2.0"]
license = "GPL3" license = "GPL3"
mailing_list = "python-debugger@googlegroups.com" mailing_list = "python-debugger@googlegroups.com"
@@ -88,21 +90,18 @@ web = "https://github.com/rocky/python-uncompyle6/"
zip_safe = True zip_safe = True
import os.path
def get_srcdir(): def get_srcdir():
filename = os.path.normcase(os.path.dirname(os.path.abspath(__file__))) filename = osp.normcase(osp.dirname(osp.abspath(__file__)))
return os.path.realpath(filename) return osp.realpath(filename)
srcdir = get_srcdir() srcdir = get_srcdir()
def read(*rnames): def read(*rnames):
return open(os.path.join(srcdir, *rnames)).read() return open(osp.join(srcdir, *rnames)).read()
# Get info from files; set: long_description and __version__ # Get info from files; set: long_description and VERSION
long_description = read("README.rst") + "\n" long_description = read("README.rst") + "\n"
exec(read("uncompyle6/version.py")) exec(read("uncompyle6/version.py"))

View File

@@ -1,15 +1,19 @@
#!/usr/bin/env python #!/usr/bin/env python
# Mode: -*- python -*- # Mode: -*- python -*-
# #
# Copyright (c) 2015-2017, 2019-2020, 2023 by Rocky Bernstein # Copyright (c) 2015-2017, 2019-2020, 2023-2024
# by Rocky Bernstein
# Copyright (c) 2000-2002 by hartmut Goebel <h.goebel@crazy-compilers.com> # Copyright (c) 2000-2002 by hartmut Goebel <h.goebel@crazy-compilers.com>
# #
from __future__ import print_function from __future__ import print_function
import getopt
import os import os
import sys import sys
import time import time
from typing import List
import click
from xdis.version_info import version_tuple_to_str
from uncompyle6 import verify from uncompyle6 import verify
from uncompyle6.main import main, status_msg from uncompyle6.main import main, status_msg
@@ -17,150 +21,162 @@ from uncompyle6.version import __version__
program = "uncompyle6" program = "uncompyle6"
__doc__ = """
Usage:
%s [OPTIONS]... [ FILE | DIR]...
%s [--help | -h | --V | --version]
Examples:
%s foo.pyc bar.pyc # decompile foo.pyc, bar.pyc to stdout
%s -o . foo.pyc bar.pyc # decompile to ./foo.pyc_dis and ./bar.pyc_dis
%s -o /tmp /usr/lib/python1.5 # decompile whole library
Options:
-o <path> output decompiled files to this path:
if multiple input files are decompiled, the common prefix
is stripped from these names and the remainder appended to
<path>
uncompyle6 -o /tmp bla/fasel.pyc bla/foo.pyc
-> /tmp/fasel.pyc_dis, /tmp/foo.pyc_dis
uncompyle6 -o /tmp bla/fasel.pyc bar/foo.pyc
-> /tmp/bla/fasel.pyc_dis, /tmp/bar/foo.pyc_dis
uncompyle6 -o /tmp /usr/lib/python1.5
-> /tmp/smtplib.pyc_dis ... /tmp/lib-tk/FixTk.pyc_dis
--compile | -c <python-file>
attempts a decompilation after compiling <python-file>
-d print timestamps
-p <integer> use <integer> number of processes
-r recurse directories looking for .pyc and .pyo files
--fragments use fragments deparser
--verify compare generated source with input byte-code
--verify-run compile generated source, run it and check exit code
--syntax-verify compile generated source
--linemaps generated line number correspondencies between byte-code
and generated source output
--encoding <encoding>
use <encoding> in generated source according to pep-0263
--help show this message
Debugging Options:
--asm | -a include byte-code (disables --verify)
--grammar | -g show matching grammar
--tree={before|after}
-t {before|after} include syntax before (or after) tree transformation
(disables --verify)
--tree++ | -T add template rules to --tree=before when possible
Extensions of generated files:
'.pyc_dis' '.pyo_dis' successfully decompiled (and verified if --verify)
+ '_unverified' successfully decompile but --verify failed
+ '_failed' decompile failed (contact author for enhancement)
""" % (
(program,) * 5
)
program = "uncompyle6"
def usage(): def usage():
print(__doc__) print(__doc__)
sys.exit(1) sys.exit(1)
def main_bin(): # __doc__ = """
recurse_dirs = False # Usage:
numproc = 0 # %s [OPTIONS]... [ FILE | DIR]...
outfile = "-" # %s [--help | -h | --V | --version]
out_base = None
source_paths = []
timestamp = False
timestampfmt = "# %Y.%m.%d %H:%M:%S %Z"
try: # Examples:
opts, pyc_paths = getopt.getopt( # %s foo.pyc bar.pyc # decompile foo.pyc, bar.pyc to stdout
sys.argv[1:], # %s -o . foo.pyc bar.pyc # decompile to ./foo.pyc_dis and ./bar.pyc_dis
"hac:gtTdrVo:p:", # %s -o /tmp /usr/lib/python1.5 # decompile whole library
"help asm compile= grammar linemaps recurse "
"timestamp tree= tree+ " # Options:
"fragments verify verify-run version " # -o <path> output decompiled files to this path:
"syntax-verify " # if multiple input files are decompiled, the common prefix
"showgrammar encoding=".split(" "), # is stripped from these names and the remainder appended to
# <path>
# uncompyle6 -o /tmp bla/fasel.pyc bla/foo.pyc
# -> /tmp/fasel.pyc_dis, /tmp/foo.pyc_dis
# uncompyle6 -o /tmp bla/fasel.pyc bar/foo.pyc
# -> /tmp/bla/fasel.pyc_dis, /tmp/bar/foo.pyc_dis
# uncompyle6 -o /tmp /usr/lib/python1.5
# -> /tmp/smtplib.pyc_dis ... /tmp/lib-tk/FixTk.pyc_dis
# --compile | -c <python-file>
# attempts a decompilation after compiling <python-file>
# -d print timestamps
# -p <integer> use <integer> number of processes
# -r recurse directories looking for .pyc and .pyo files
# --fragments use fragments deparser
# --verify compare generated source with input byte-code
# --verify-run compile generated source, run it and check exit code
# --syntax-verify compile generated source
# --linemaps generated line number correspondencies between byte-code
# and generated source output
# --encoding <encoding>
# use <encoding> in generated source according to pep-0263
# --help show this message
# Debugging Options:
# --asm | -a include byte-code (disables --verify)
# --grammar | -g show matching grammar
# --tree={before|after}
# -t {before|after} include syntax before (or after) tree transformation
# (disables --verify)
# --tree++ | -T add template rules to --tree=before when possible
# Extensions of generated files:
# '.pyc_dis' '.pyo_dis' successfully decompiled (and verified if --verify)
# + '_unverified' successfully decompile but --verify failed
# + '_failed' decompile failed (contact author for enhancement)
# """ % (
# (program,) * 5
# )
@click.command()
@click.option(
"--asm++/--no-asm++",
"-A",
"asm_plus",
default=False,
help="show xdis assembler and tokenized assembler",
)
@click.option("--asm/--no-asm", "-a", default=False)
@click.option("--grammar/--no-grammar", "-g", "show_grammar", default=False)
@click.option("--tree/--no-tree", "-t", default=False)
@click.option(
"--tree++/--no-tree++",
"-T",
"tree_plus",
default=False,
help="show parse tree and Abstract Syntax Tree",
)
@click.option(
"--linemaps/--no-linemaps",
default=False,
help="show line number correspondencies between byte-code "
"and generated source output",
)
@click.option(
"--verify",
type=click.Choice(["run", "syntax"]),
default=None,
)
@click.option(
"--recurse/--no-recurse",
"-r",
"recurse_dirs",
default=False,
)
@click.option(
"--output",
"-o",
"outfile",
type=click.Path(
exists=True, file_okay=True, dir_okay=True, writable=True, resolve_path=True
),
required=False,
)
@click.version_option(version=__version__)
@click.option(
"--start-offset",
"start_offset",
default=0,
help="start decomplation at offset; default is 0 or the starting offset.",
)
@click.version_option(version=__version__)
@click.option(
"--stop-offset",
"stop_offset",
default=-1,
help="stop decomplation when seeing an offset greater or equal to this; default is "
"-1 which indicates no stopping point.",
)
@click.argument("files", nargs=-1, type=click.Path(readable=True), required=True)
def main_bin(
asm: bool,
asm_plus: bool,
show_grammar,
tree: bool,
tree_plus: bool,
linemaps: bool,
verify,
recurse_dirs: bool,
outfile,
start_offset: int,
stop_offset: int,
files,
):
"""
Cross Python bytecode decompiler for Python bytecode up to Python 3.8.
"""
version_tuple = sys.version_info[0:2]
if version_tuple < (3, 7):
print(
f"Error: This version of the {program} runs from Python 3.7 or greater."
f"You need another branch of this code for Python before 3.7."
f""" \n\tYou have version: {version_tuple_to_str()}."""
) )
except getopt.GetoptError as e:
print("%s: %s" % (os.path.basename(sys.argv[0]), e), file=sys.stderr)
sys.exit(-1) sys.exit(-1)
options = { numproc = 0
"showasm": None out_base = None
}
for opt, val in opts:
if opt in ("-h", "--help"):
print(__doc__)
sys.exit(0)
elif opt in ("-V", "--version"):
print("%s %s" % (program, __version__))
sys.exit(0)
elif opt == "--verify":
options["do_verify"] = "strong"
elif opt == "--syntax-verify":
options["do_verify"] = "weak"
elif opt == "--fragments":
options["do_fragments"] = True
elif opt == "--verify-run":
options["do_verify"] = "verify-run"
elif opt == "--linemaps":
options["do_linemaps"] = True
elif opt in ("--asm", "-a"):
if options["showasm"] == None:
options["showasm"] = "after"
else:
options["showasm"] = "both"
options["do_verify"] = None
elif opt in ("--tree", "-t"):
if "showast" not in options:
options["showast"] = {}
if val == "before":
options["showast"][val] = True
elif val == "after":
options["showast"][val] = True
else:
options["showast"]["before"] = True
options["do_verify"] = None
elif opt in ("--tree+", "-T"):
if "showast" not in options:
options["showast"] = {}
options["showast"]["after"] = True
options["showast"]["before"] = True
options["do_verify"] = None
elif opt in ("--grammar", "-g"):
options["showgrammar"] = True
elif opt == "-o":
outfile = val
elif opt in ("--timestamp", "-d"):
timestamp = True
elif opt in ("--compile", "-c"):
source_paths.append(val)
elif opt == "-p":
numproc = int(val)
elif opt in ("--recurse", "-r"):
recurse_dirs = True
elif opt == "--encoding":
options["source_encoding"] = val
else:
print(opt, file=sys.stderr)
usage()
# expand directory if specified out_base = None
source_paths: List[str] = []
timestamp = False
timestampfmt = "# %Y.%m.%d %H:%M:%S %Z"
pyc_paths = files
# Expand directory if "recurse" was specified.
if recurse_dirs: if recurse_dirs:
expanded_files = [] expanded_files = []
for f in pyc_paths: for f in pyc_paths:
@@ -194,15 +210,32 @@ def main_bin():
out_base = outfile out_base = outfile
outfile = None outfile = None
# A second -a turns show_asm="after" into show_asm="before"
if asm_plus or asm:
asm_opt = "both" if asm_plus else "after"
else:
asm_opt = None
if timestamp: if timestamp:
print(time.strftime(timestampfmt)) print(time.strftime(timestampfmt))
if numproc <= 1: if numproc <= 1:
show_ast = {"before": tree or tree_plus, "after": tree_plus}
try: try:
result = main( result = main(
src_base, out_base, pyc_paths, source_paths, outfile, **options src_base,
out_base,
pyc_paths,
source_paths,
outfile,
showasm=asm_opt,
showgrammar=show_grammar,
showast=show_ast,
do_verify=verify,
do_linemaps=linemaps,
start_offset=start_offset,
stop_offset=stop_offset,
) )
result = [options.get("do_verify", None)] + list(result)
if len(pyc_paths) > 1: if len(pyc_paths) > 1:
mess = status_msg(*result) mess = status_msg(*result)
print("# " + mess) print("# " + mess)

View File

@@ -15,9 +15,11 @@
import datetime import datetime
import os import os
import os.path as osp
import py_compile import py_compile
import sys import sys
from typing import Any, Optional, Tuple import tempfile
from typing import Any, Optional, TextIO, Tuple
from xdis import iscode from xdis import iscode
from xdis.load import load_module from xdis.load import load_module
@@ -38,9 +40,9 @@ def _get_outstream(outfile: str) -> Any:
""" """
Return an opened output file descriptor for ``outfile``. Return an opened output file descriptor for ``outfile``.
""" """
dir_name = os.path.dirname(outfile) dir_name = osp.dirname(outfile)
failed_file = outfile + "_failed" failed_file = outfile + "_failed"
if os.path.exists(failed_file): if osp.exists(failed_file):
os.remove(failed_file) os.remove(failed_file)
try: try:
os.makedirs(dir_name) os.makedirs(dir_name)
@@ -52,7 +54,7 @@ def _get_outstream(outfile: str) -> Any:
def decompile( def decompile(
co, co,
bytecode_version: Tuple[int] = PYTHON_VERSION_TRIPLE, bytecode_version: Tuple[int] = PYTHON_VERSION_TRIPLE,
out=sys.stdout, out: Optional[TextIO] = sys.stdout,
showasm: Optional[str] = None, showasm: Optional[str] = None,
showast={}, showast={},
timestamp=None, timestamp=None,
@@ -60,11 +62,13 @@ def decompile(
source_encoding=None, source_encoding=None,
code_objects={}, code_objects={},
source_size=None, source_size=None,
is_pypy=False, is_pypy: bool = False,
magic_int=None, magic_int=None,
mapstream=None, mapstream=None,
do_fragments=False, do_fragments=False,
compile_mode="exec", compile_mode="exec",
start_offset: int = 0,
stop_offset: int = -1,
) -> Any: ) -> Any:
""" """
ingests and deparses a given code block 'co' ingests and deparses a given code block 'co'
@@ -132,11 +136,12 @@ def decompile(
debug_opts=debug_opts, debug_opts=debug_opts,
) )
header_count = 3 + len(sys_version_lines) header_count = 3 + len(sys_version_lines)
linemap = [ if deparsed is not None:
(line_no, deparsed.source_linemap[line_no] + header_count) linemap = [
for line_no in sorted(deparsed.source_linemap.keys()) (line_no, deparsed.source_linemap[line_no] + header_count)
] for line_no in sorted(deparsed.source_linemap.keys())
mapstream.write(f"\n\n# {linemap}\n") ]
mapstream.write(f"\n\n# {linemap}\n")
else: else:
if do_fragments: if do_fragments:
deparse_fn = code_deparse_fragments deparse_fn = code_deparse_fragments
@@ -149,8 +154,11 @@ def decompile(
is_pypy=is_pypy, is_pypy=is_pypy,
debug_opts=debug_opts, debug_opts=debug_opts,
compile_mode=compile_mode, compile_mode=compile_mode,
start_offset=start_offset,
stop_offset=stop_offset,
) )
pass pass
real_out.write("\n")
return deparsed return deparsed
except pysource.SourceWalkerError as e: except pysource.SourceWalkerError as e:
# deparsing failed # deparsing failed
@@ -175,13 +183,15 @@ def compile_file(source_path: str) -> str:
def decompile_file( def decompile_file(
filename: str, filename: str,
outstream=None, outstream: Optional[TextIO] = None,
showasm=None, showasm: Optional[str] = None,
showast={}, showast={},
showgrammar=False, showgrammar=False,
source_encoding=None, source_encoding=None,
mapstream=None, mapstream=None,
do_fragments=False, do_fragments=False,
start_offset=0,
stop_offset=-1,
) -> Any: ) -> Any:
""" """
decompile Python byte-code file (.pyc). Return objects to decompile Python byte-code file (.pyc). Return objects to
@@ -211,6 +221,8 @@ def decompile_file(
is_pypy=is_pypy, is_pypy=is_pypy,
magic_int=magic_int, magic_int=magic_int,
mapstream=mapstream, mapstream=mapstream,
start_offset=start_offset,
stop_offset=stop_offset,
), ),
) )
else: else:
@@ -231,6 +243,8 @@ def decompile_file(
mapstream=mapstream, mapstream=mapstream,
do_fragments=do_fragments, do_fragments=do_fragments,
compile_mode="exec", compile_mode="exec",
start_offset=start_offset,
stop_offset=stop_offset,
) )
] ]
return deparsed return deparsed
@@ -242,13 +256,16 @@ def main(
out_base: Optional[str], out_base: Optional[str],
compiled_files: list, compiled_files: list,
source_files: list, source_files: list,
outfile=None, outfile: Optional[str] = None,
showasm: Optional[str] = None, showasm: Optional[str] = None,
showast={}, showast={},
showgrammar=False, do_verify: Optional[str] = None,
showgrammar: bool = False,
source_encoding=None, source_encoding=None,
do_linemaps=False, do_linemaps=False,
do_fragments=False, do_fragments=False,
start_offset: int = 0,
stop_offset: int = -1,
) -> Tuple[int, int, int, int]: ) -> Tuple[int, int, int, int]:
""" """
in_base base directory for input files in_base base directory for input files
@@ -261,7 +278,8 @@ def main(
- files below out_base out_base=... - files below out_base out_base=...
- stdout out_base=None, outfile=None - stdout out_base=None, outfile=None
""" """
tot_files = okay_files = failed_files = verify_failed_files = 0 tot_files = okay_files = failed_files = 0
verify_failed_files = 0 if do_verify else 0
current_outfile = outfile current_outfile = outfile
linemap_stream = None linemap_stream = None
@@ -269,9 +287,9 @@ def main(
compiled_files.append(compile_file(source_path)) compiled_files.append(compile_file(source_path))
for filename in compiled_files: for filename in compiled_files:
infile = os.path.join(in_base, filename) infile = osp.join(in_base, filename)
# print("XXX", infile) # print("XXX", infile)
if not os.path.exists(infile): if not osp.exists(infile):
sys.stderr.write(f"File '{infile}' doesn't exist. Skipped\n") sys.stderr.write(f"File '{infile}' doesn't exist. Skipped\n")
continue continue
@@ -284,14 +302,19 @@ def main(
if outfile: # outfile was given as parameter if outfile: # outfile was given as parameter
outstream = _get_outstream(outfile) outstream = _get_outstream(outfile)
elif out_base is None: elif out_base is None:
outstream = sys.stdout out_base = tempfile.mkdtemp(prefix="py-dis-")
if do_verify and filename.endswith(".pyc"):
current_outfile = osp.join(out_base, filename[0:-1])
outstream = open(current_outfile, "w")
else:
outstream = sys.stdout
if do_linemaps: if do_linemaps:
linemap_stream = sys.stdout linemap_stream = sys.stdout
else: else:
if filename.endswith(".pyc"): if filename.endswith(".pyc"):
current_outfile = os.path.join(out_base, filename[0:-1]) current_outfile = osp.join(out_base, filename[0:-1])
else: else:
current_outfile = os.path.join(out_base, filename) + "_dis" current_outfile = osp.join(out_base, filename) + "_dis"
pass pass
pass pass
@@ -299,9 +322,9 @@ def main(
# print(current_outfile, file=sys.stderr) # print(current_outfile, file=sys.stderr)
# Try to uncompile the input file # Try to decompile the input file.
try: try:
deparsed = decompile_file( deparsed_objects = decompile_file(
infile, infile,
outstream, outstream,
showasm, showasm,
@@ -310,11 +333,13 @@ def main(
source_encoding, source_encoding,
linemap_stream, linemap_stream,
do_fragments, do_fragments,
start_offset,
stop_offset,
) )
if do_fragments: if do_fragments:
for d in deparsed: for deparsed_object in deparsed_objects:
last_mod = None last_mod = None
offsets = d.offsets offsets = deparsed_object.offsets
for e in sorted( for e in sorted(
[k for k in offsets.keys() if isinstance(k[1], int)] [k for k in offsets.keys() if isinstance(k[1], int)]
): ):
@@ -323,11 +348,48 @@ def main(
outstream.write(f"{line}\n{e[0]}\n{line}\n") outstream.write(f"{line}\n{e[0]}\n{line}\n")
last_mod = e[0] last_mod = e[0]
info = offsets[e] info = offsets[e]
extract_info = d.extract_node_info(info) extract_info = deparse_object.extract_node_info(info)
outstream.write(f"{info.node.format().strip()}" + "\n") outstream.write(f"{info.node.format().strip()}" + "\n")
outstream.write(extract_info.selectedLine + "\n") outstream.write(extract_info.selectedLine + "\n")
outstream.write(extract_info.markerLine + "\n\n") outstream.write(extract_info.markerLine + "\n\n")
pass pass
if do_verify:
for deparsed_object in deparsed_objects:
deparsed_object.f.close()
if PYTHON_VERSION_TRIPLE[:2] != deparsed_object.version[:2]:
sys.stdout.write(
f"\n# skipping running {deparsed_object.f.name}; it is"
f"{version_tuple_to_str(deparsed_object.version, end=2)}, "
"and we are "
f"{version_tuple_to_str(PYTHON_VERSION_TRIPLE, end=2)}\n"
)
else:
check_type = "syntax check"
if do_verify == "run":
check_type = "run"
result = subprocess.run(
[sys.executable, deparsed_object.f.name],
capture_output=True,
)
valid = result.returncode == 0
output = result.stdout.decode()
if output:
print(output)
pass
if not valid:
print(result.stderr.decode())
else:
valid = syntax_check(deparsed_object.f.name)
if not valid:
verify_failed_files += 1
sys.stderr.write(
f"\n# {check_type} failed on file {deparsed_object.f.name}\n"
)
# sys.stderr.write(f"Ran {deparsed_object.f.name}\n")
pass pass
tot_files += 1 tot_files += 1
except (ValueError, SyntaxError, ParserError, pysource.SourceWalkerError) as e: except (ValueError, SyntaxError, ParserError, pysource.SourceWalkerError) as e:

View File

@@ -2036,6 +2036,8 @@ def code_deparse(
code_objects={}, code_objects={},
compile_mode="exec", compile_mode="exec",
walker=FragmentsWalker, walker=FragmentsWalker,
start_offset: int = 0,
stop_offset: int = -1,
): ):
""" """
Convert the code object co into a python source fragment. Convert the code object co into a python source fragment.
@@ -2070,6 +2072,22 @@ def code_deparse(
tokens, customize = scanner.ingest(co, code_objects=code_objects, show_asm=show_asm) tokens, customize = scanner.ingest(co, code_objects=code_objects, show_asm=show_asm)
tokens, customize = scanner.ingest(co) tokens, customize = scanner.ingest(co)
if start_offset > 0:
for i, t in enumerate(tokens):
# If t.offset is a string, we want to skip this.
if isinstance(t.offset, int) and t.offset >= start_offset:
tokens = tokens[i:]
break
if stop_offset > -1:
for i, t in enumerate(tokens):
# In contrast to the test for start_offset If t.offset is
# a string, we want to extract the integer offset value.
if t.off2int() >= stop_offset:
tokens = tokens[:i]
break
maybe_show_asm(show_asm, tokens) maybe_show_asm(show_asm, tokens)
debug_parser = dict(PARSER_DEFAULT_DEBUG) debug_parser = dict(PARSER_DEFAULT_DEBUG)