summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoseph Brill <48932340+jcbrill@users.noreply.github.com>2024-09-26 18:34:35 (GMT)
committerJoseph Brill <48932340+jcbrill@users.noreply.github.com>2024-09-26 18:34:35 (GMT)
commitc6987ca8568e2082a56063837d539b8045b85917 (patch)
treed0d08aad6d174884df92f37450ebccf091ce32b5
parent4af253800fbb03751e1b4537f1362a09dcfeef8a (diff)
downloadSCons-c6987ca8568e2082a56063837d539b8045b85917.zip
SCons-c6987ca8568e2082a56063837d539b8045b85917.tar.gz
SCons-c6987ca8568e2082a56063837d539b8045b85917.tar.bz2
Validate the SCONS_MSCOMMON_DEBUG file name.
Changes: * Issue warning and remove leading and trailing double quotes from SCONS_MSCOMMON_DEBUG file name when present. * Issue warning when SCONS_MSCOMMON_DEBUG file name is likely invalid. Known issues: * A false positive warning may be issued when a colon is detected in the file name. An output file and hidden alternate data stream file may be created.
-rw-r--r--SCons/Tool/MSCommon/common.py177
1 files changed, 175 insertions, 2 deletions
diff --git a/SCons/Tool/MSCommon/common.py b/SCons/Tool/MSCommon/common.py
index af3afb5..ba43f8a 100644
--- a/SCons/Tool/MSCommon/common.py
+++ b/SCons/Tool/MSCommon/common.py
@@ -33,6 +33,7 @@ import sys
from contextlib import suppress
from subprocess import DEVNULL, PIPE
from pathlib import Path
+from typing import Optional, Tuple
import SCons.Util
import SCons.Warnings
@@ -40,9 +41,175 @@ import SCons.Warnings
class MSVCCacheInvalidWarning(SCons.Warnings.WarningOnByDefault):
pass
+class WindowsFileNameWarning(SCons.Warnings.WarningOnByDefault):
+ pass
+
+class _WindowsFileName:
+
+ # Known Issues:
+ # * NTFS filenames with a colon are reported as invalid (false positive)
+ # given a file named "colonbeg:colonend.txt":
+ # * file "colonbeg" is created and appears empty
+ # * hidden alternate data stream file "colonbeg:colonend.txt:$DATA" is created
+ # * use "dir /R colonbeg" to display alternate data streams of files
+
+ drive_prefix = r"([a-zA-Z][:])[\\]?(?P<suffix>.*)$"
+ re_drive_spec = re.compile(drive_prefix)
+
+ unc_prefix = r"(\\\\[^\\]+)[\\](?P<suffix>.*)$"
+ re_unc_spec = re.compile(unc_prefix)
+
+ re_illegal_chars = re.compile(
+ r'[<>:"/\\|?*\r\t\n]',
+ re.IGNORECASE
+ )
+
+ re_reserved_names = re.compile(
+ r"^(?P<reserved>CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(\.|$)",
+ re.IGNORECASE,
+ )
+
+ @classmethod
+ def is_filename_invalid(cls, fileorig: str) -> Tuple[str, str]:
+
+ filename = fileorig
+ if not filename:
+ reason = "file name is empty"
+ return reason, filename
+
+ filename = os.fspath(filename)
+ filename = os.path.abspath(filename)
+
+ pathspec, filespec = os.path.split(filename)
+
+ if filespec and not filespec.strip():
+ reason = f"file name is whitespace only: {filespec!r}"
+ return reason, filename
+
+ suffix = ""
+
+ do_once = True
+ while do_once:
+ do_once = False
+
+ m = cls.re_drive_spec.match(filename)
+ if m:
+ # filename: r"c:\dir\filename.txt"
+ # prefix: r"c:"
+ # suffix: r"dir\filename.txt"
+ suffix = m.group("suffix")
+ break
+
+ m = cls.re_unc_spec.match(filename)
+ if m:
+ # filename: r"\\server\share\filename.txt"
+ # prefix: r"\\server"
+ # suffix: r"share\filename.txt"
+ suffix = m.group("suffix")
+ break
+
+ reason = "unrecognized path prefix"
+ return reason, filename
+
+ if suffix:
+
+ comps = suffix.split(os.path.sep)
+ for name in comps:
+
+ if not name:
+ reason = "file name component is empty"
+ return reason, filename
+
+ if name and not name.strip():
+ reason = f"file name component is whitespace-only ({name!r})"
+ return reason, filename
+
+ illegal_chars = cls.re_illegal_chars.findall(name)
+ if illegal_chars:
+ seen_chars = ', '.join(list(
+ {repr(s): s for s in illegal_chars}.keys()
+ ))
+ reason = f"file name contains illegal characters ({seen_chars})"
+ return reason, filename
+
+ match = cls.re_reserved_names.match(name)
+ if match:
+ reserved = match.group("reserved")
+ reason = f"file name contains a reserved name ({reserved!r})"
+ return reason, filename
+
+ if not os.path.exists(pathspec):
+ reason = "path to file name does not exist"
+ return reason, filename
+
+ if os.path.exists(filename) and os.path.isdir(filename):
+ reason = "file name is a directory"
+ return reason, filename
+
+ return "", filename
+
+ @classmethod
+ def check_filename_invalid(
+ cls,
+ filename: str,
+ description: Optional[str] = None
+ ) -> bool:
+ reason, abspath = cls.is_filename_invalid(filename)
+ if reason:
+ if description:
+ msg_description = description + " "
+ else:
+ msg_description = ""
+ msg = (
+ f"{msg_description}file name is invalid:\n"
+ f" filename: {filename!r}\n"
+ f" abspath: {abspath!r}\n"
+ f" reason: {reason}"
+ )
+ SCons.Warnings.warn(WindowsFileNameWarning, msg)
+ return bool(reason)
+
+ @classmethod
+ def process_filename(
+ cls,
+ filename: Optional[str],
+ description: Optional[str] = None
+ ) -> Optional[str]:
+ if filename is None:
+ return filename
+ if len(filename) < 2:
+ return filename
+ if filename[0] == '"' and filename[-1] == '"':
+ fileorig = filename
+ filename = filename[1:-1]
+ if description:
+ msg_description = description + " "
+ else:
+ msg_description = ""
+ msg = (
+ f"{msg_description}file name is invalid:\n"
+ f" filename: {fileorig!r}\n"
+ f" modified: {filename!r}\n"
+ f" action: leading and trailing double quotes removed"
+ )
+ SCons.Warnings.warn(WindowsFileNameWarning, msg)
+ return filename
+
+ @classmethod
+ def get_filename_environ(
+ cls,
+ evar: str,
+ ) -> Optional[str]:
+ filename = cls.process_filename(os.environ.get(evar), description=evar)
+ return filename
+
+check_filename_invalid = _WindowsFileName.check_filename_invalid
+get_filename_environ = _WindowsFileName.get_filename_environ
+
+_LOGFILE_EVAR = "SCONS_MSCOMMON_DEBUG"
# SCONS_MSCOMMON_DEBUG is internal-use so undocumented:
# set to '-' to print to console, else set to filename to log to
-LOGFILE = os.environ.get('SCONS_MSCOMMON_DEBUG')
+LOGFILE = get_filename_environ(_LOGFILE_EVAR)
if LOGFILE:
import logging
@@ -128,8 +295,14 @@ if LOGFILE:
log_prefix = 'debug: '
log_handler = logging.StreamHandler(sys.stdout)
else:
+ isinvalid = check_filename_invalid(LOGFILE, description=_LOGFILE_EVAR)
log_prefix = ''
- log_handler = logging.FileHandler(filename=LOGFILE)
+ try:
+ log_handler = logging.FileHandler(filename=LOGFILE)
+ except Exception as e:
+ if isinvalid:
+ raise e.with_traceback(None)
+ raise
log_formatter = _CustomFormatter(log_prefix)
log_handler.setFormatter(log_formatter)
logger = logging.getLogger(name=__name__)