From 1dde40624e4ebc9b12c342a5351595cad733d42b Mon Sep 17 00:00:00 2001 From: Jason G <41053218+gianghungtien@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:46:24 +0700 Subject: [PATCH] gh-122143: Fix os.path.splitroot() and os.path.normpath() for undecodable bytes paths on Windows The native helpers nt._path_splitroot_ex() and nt._path_normpath() convert the path to a wide string first, so a bytes path which cannot be decoded with the filesystem encoding (UTF-8 with the "surrogatepass" error handler on Windows) failed with UnicodeDecodeError. os.path.splitroot() and os.path.splitdrive() regressed this way in 3.13, when the native helper was added; os.path.normpath() has behaved this way since the native helper was added in 3.12. Enable suppress_value_error for both helpers and delegate such paths to the pure Python implementations, which operate on bytes directly, as they already do on POSIX. path_converter() now keeps the original object when it suppresses a ValueError, so that the fallback can be given the path. This also fixes os.path.split(), os.path.abspath() and os.path.realpath(), which are built on top of the two helpers. --- Lib/ntpath.py | 171 ++++++++++-------- Lib/posixpath.py | 111 +++++++----- Lib/test/test_ntpath.py | 89 +++++---- ...-07-24-16-05-00.gh-issue-122143.Qw3Kz1.rst | 4 + Modules/clinic/posixmodule.c.h | 6 +- Modules/posixmodule.c | 49 ++++- 6 files changed, 254 insertions(+), 176 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-24-16-05-00.gh-issue-122143.Qw3Kz1.rst diff --git a/Lib/ntpath.py b/Lib/ntpath.py index b3c23f0abc2d889..7dbf89fcc0cb80e 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -168,52 +168,59 @@ def splitdrive(p, /): return drive, root + tail +def splitroot(p, /): + """Split a pathname into drive, root and tail. + + The tail contains anything after the root.""" + p = os.fspath(p) + if isinstance(p, bytes): + sep = b'\\' + altsep = b'/' + colon = b':' + unc_prefix = b'\\\\?\\UNC\\' + empty = b'' + else: + sep = '\\' + altsep = '/' + colon = ':' + unc_prefix = '\\\\?\\UNC\\' + empty = '' + normp = p.replace(altsep, sep) + if normp[:1] == sep: + if normp[1:2] == sep: + # UNC drives, e.g. \\server\share or \\?\UNC\server\share + # Device drives, e.g. \\.\device or \\?\device + start = 8 if normp[:8].upper() == unc_prefix else 2 + index = normp.find(sep, start) + if index == -1: + return p, empty, empty + index2 = normp.find(sep, index + 1) + if index2 == -1: + return p, empty, empty + return p[:index2], p[index2:index2 + 1], p[index2 + 1:] + else: + # Relative path with root, e.g. \Windows + return empty, p[:1], p[1:] + elif normp[1:2] == colon: + if normp[2:3] == sep: + # Absolute drive-letter path, e.g. X:\Windows + return p[:2], p[2:3], p[3:] + else: + # Relative path with drive, e.g. X:Windows + return p[:2], empty, p[2:] + else: + # Relative path, e.g. Windows + return empty, empty, p + + +# Kept under a private name so that nt._path_splitroot_ex() can delegate to it +# for bytes paths that cannot be decoded with the filesystem encoding. +_splitroot_fallback = splitroot + try: from nt import _path_splitroot_ex as splitroot except ImportError: - def splitroot(p, /): - """Split a pathname into drive, root and tail. - - The tail contains anything after the root.""" - p = os.fspath(p) - if isinstance(p, bytes): - sep = b'\\' - altsep = b'/' - colon = b':' - unc_prefix = b'\\\\?\\UNC\\' - empty = b'' - else: - sep = '\\' - altsep = '/' - colon = ':' - unc_prefix = '\\\\?\\UNC\\' - empty = '' - normp = p.replace(altsep, sep) - if normp[:1] == sep: - if normp[1:2] == sep: - # UNC drives, e.g. \\server\share or \\?\UNC\server\share - # Device drives, e.g. \\.\device or \\?\device - start = 8 if normp[:8].upper() == unc_prefix else 2 - index = normp.find(sep, start) - if index == -1: - return p, empty, empty - index2 = normp.find(sep, index + 1) - if index2 == -1: - return p, empty, empty - return p[:index2], p[index2:index2 + 1], p[index2 + 1:] - else: - # Relative path with root, e.g. \Windows - return empty, p[:1], p[1:] - elif normp[1:2] == colon: - if normp[2:3] == sep: - # Absolute drive-letter path, e.g. X:\Windows - return p[:2], p[2:3], p[3:] - else: - # Relative path with drive, e.g. X:Windows - return p[:2], empty, p[2:] - else: - # Relative path, e.g. Windows - return empty, empty, p + pass # Split a path in head (everything up to the last '/') and tail (the @@ -470,45 +477,51 @@ def repl(m): # Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B. # Previously, this function also truncated pathnames to 8+3 format, # but as this module is called "ntpath", that's obviously wrong! -try: - from nt import _path_normpath as normpath - -except ImportError: - def normpath(path): - """Normalize path, eliminating double slashes, etc.""" - path = os.fspath(path) - if isinstance(path, bytes): - sep = b'\\' - altsep = b'/' - curdir = b'.' - pardir = b'..' - else: - sep = '\\' - altsep = '/' - curdir = '.' - pardir = '..' - path = path.replace(altsep, sep) - drive, root, path = splitroot(path) - prefix = drive + root - comps = path.split(sep) - i = 0 - while i < len(comps): - if not comps[i] or comps[i] == curdir: +def normpath(path): + """Normalize path, eliminating double slashes, etc.""" + path = os.fspath(path) + if isinstance(path, bytes): + sep = b'\\' + altsep = b'/' + curdir = b'.' + pardir = b'..' + else: + sep = '\\' + altsep = '/' + curdir = '.' + pardir = '..' + path = path.replace(altsep, sep) + drive, root, path = _splitroot_fallback(path) + prefix = drive + root + comps = path.split(sep) + i = 0 + while i < len(comps): + if not comps[i] or comps[i] == curdir: + del comps[i] + elif comps[i] == pardir: + if i > 0 and comps[i-1] != pardir: + del comps[i-1:i+1] + i -= 1 + elif i == 0 and root: del comps[i] - elif comps[i] == pardir: - if i > 0 and comps[i-1] != pardir: - del comps[i-1:i+1] - i -= 1 - elif i == 0 and root: - del comps[i] - else: - i += 1 else: i += 1 - # If the path is now empty, substitute '.' - if not prefix and not comps: - comps.append(curdir) - return prefix + sep.join(comps) + else: + i += 1 + # If the path is now empty, substitute '.' + if not prefix and not comps: + comps.append(curdir) + return prefix + sep.join(comps) + + +# Kept under a private name so that nt._path_normpath() can delegate to it for +# bytes paths that cannot be decoded with the filesystem encoding. +_normpath_fallback = normpath + +try: + from nt import _path_normpath as normpath +except ImportError: + pass # Return an absolute path. diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 8025b063397a038..e86963d0e9b134d 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -136,30 +136,37 @@ def splitdrive(p, /): return p[:0], p +def splitroot(p, /): + """Split a pathname into drive, root and tail. + + The tail contains anything after the root.""" + p = os.fspath(p) + if isinstance(p, bytes): + sep = b'/' + empty = b'' + else: + sep = '/' + empty = '' + if p[:1] != sep: + # Relative path, e.g.: 'foo' + return empty, empty, p + elif p[1:2] != sep or p[2:3] == sep: + # Absolute path, e.g.: '/foo', '///foo', '////foo', etc. + return empty, sep, p[1:] + else: + # Precisely two leading slashes, e.g.: '//foo'. Implementation defined per POSIX, see + # https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13 + return empty, p[:2], p[2:] + + +# Kept under a private name so that posix._path_splitroot_ex() can delegate to +# it for bytes paths that cannot be decoded with the filesystem encoding. +_splitroot_fallback = splitroot + try: from posix import _path_splitroot_ex as splitroot except ImportError: - def splitroot(p, /): - """Split a pathname into drive, root and tail. - - The tail contains anything after the root.""" - p = os.fspath(p) - if isinstance(p, bytes): - sep = b'/' - empty = b'' - else: - sep = '/' - empty = '' - if p[:1] != sep: - # Relative path, e.g.: 'foo' - return empty, empty, p - elif p[1:2] != sep or p[2:3] == sep: - # Absolute path, e.g.: '/foo', '///foo', '////foo', etc. - return empty, sep, p[1:] - else: - # Precisely two leading slashes, e.g.: '//foo'. Implementation defined per POSIX, see - # https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13 - return empty, p[:2], p[2:] + pass # Return the tail (basename) part of a path, same as split(path)[1]. @@ -338,37 +345,43 @@ def repl(m): # It should be understood that this may change the meaning of the path # if it contains symbolic links! +def normpath(path): + """Normalize path, eliminating double slashes, etc.""" + path = os.fspath(path) + if isinstance(path, bytes): + sep = b'/' + dot = b'.' + dotdot = b'..' + else: + sep = '/' + dot = '.' + dotdot = '..' + if not path: + return dot + _, initial_slashes, path = _splitroot_fallback(path) + comps = path.split(sep) + new_comps = [] + for comp in comps: + if not comp or comp == dot: + continue + if (comp != dotdot or (not initial_slashes and not new_comps) or + (new_comps and new_comps[-1] == dotdot)): + new_comps.append(comp) + elif new_comps: + new_comps.pop() + comps = new_comps + path = initial_slashes + sep.join(comps) + return path or dot + + +# Kept under a private name so that posix._path_normpath() can delegate to it +# for bytes paths that cannot be decoded with the filesystem encoding. +_normpath_fallback = normpath + try: from posix import _path_normpath as normpath - except ImportError: - def normpath(path): - """Normalize path, eliminating double slashes, etc.""" - path = os.fspath(path) - if isinstance(path, bytes): - sep = b'/' - dot = b'.' - dotdot = b'..' - else: - sep = '/' - dot = '.' - dotdot = '..' - if not path: - return dot - _, initial_slashes, path = splitroot(path) - comps = path.split(sep) - new_comps = [] - for comp in comps: - if not comp or comp == dot: - continue - if (comp != dotdot or (not initial_slashes and not new_comps) or - (new_comps and new_comps[-1] == dotdot)): - new_comps.append(comp) - elif new_comps: - new_comps.pop() - comps = new_comps - path = initial_slashes + sep.join(comps) - return path or dot + pass def abspath(path): diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 936332bf94ffe77..1a2502eff6e3323 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -149,13 +149,15 @@ def test_splitdrive_invalid_paths(self): (b'\\\\ser\x00ver\\sha\x00re', b'\\di\x00r')) self.assertEqual(splitdrive("\\\\\udfff\\\udffe\\\udffd"), ('\\\\\udfff\\\udffe', '\\\udffd')) - if sys.platform == 'win32': - self.assertRaises(UnicodeDecodeError, splitdrive, b'\\\\\xff\\share\\dir') - self.assertRaises(UnicodeDecodeError, splitdrive, b'\\\\server\\\xff\\dir') - self.assertRaises(UnicodeDecodeError, splitdrive, b'\\\\server\\share\\\xff') - else: - self.assertEqual(splitdrive(b'\\\\\xff\\\xfe\\\xfd'), - (b'\\\\\xff\\\xfe', b'\\\xfd')) + # Undecodable bytes paths (gh-122143). + self.assertEqual(splitdrive(b'\\\\\xff\\share\\dir'), + (b'\\\\\xff\\share', b'\\dir')) + self.assertEqual(splitdrive(b'\\\\server\\\xff\\dir'), + (b'\\\\server\\\xff', b'\\dir')) + self.assertEqual(splitdrive(b'\\\\server\\share\\\xff'), + (b'\\\\server\\share', b'\\\xff')) + self.assertEqual(splitdrive(b'\\\\\xff\\\xfe\\\xfd'), + (b'\\\\\xff\\\xfe', b'\\\xfd')) def test_splitroot(self): tester("ntpath.splitroot('')", ('', '', '')) @@ -255,13 +257,17 @@ def test_splitroot_invalid_paths(self): (b'\\\\ser\x00ver\\sha\x00re', b'\\', b'di\x00r')) self.assertEqual(splitroot("\\\\\udfff\\\udffe\\\udffd"), ('\\\\\udfff\\\udffe', '\\', '\udffd')) - if sys.platform == 'win32': - self.assertRaises(UnicodeDecodeError, splitroot, b'\\\\\xff\\share\\dir') - self.assertRaises(UnicodeDecodeError, splitroot, b'\\\\server\\\xff\\dir') - self.assertRaises(UnicodeDecodeError, splitroot, b'\\\\server\\share\\\xff') - else: - self.assertEqual(splitroot(b'\\\\\xff\\\xfe\\\xfd'), - (b'\\\\\xff\\\xfe', b'\\', b'\xfd')) + # Undecodable bytes paths (gh-122143). + self.assertEqual(splitroot(b'\\\\\xff\\share\\dir'), + (b'\\\\\xff\\share', b'\\', b'dir')) + self.assertEqual(splitroot(b'\\\\server\\\xff\\dir'), + (b'\\\\server\\\xff', b'\\', b'dir')) + self.assertEqual(splitroot(b'\\\\server\\share\\\xff'), + (b'\\\\server\\share', b'\\', b'\xff')) + self.assertEqual(splitroot(b'\\\\\xff\\\xfe\\\xfd'), + (b'\\\\\xff\\\xfe', b'\\', b'\xfd')) + self.assertEqual(splitroot(FakePath(b'c:\\\xff')), + (b'c:', b'\\', b'\xff')) def test_split(self): tester('ntpath.split("c:\\foo\\bar")', ('c:\\foo', 'bar')) @@ -283,12 +289,10 @@ def test_split_invalid_paths(self): (b'c:\\fo\x00o', b'ba\x00r')) self.assertEqual(split('c:\\\udfff\\\udffe'), ('c:\\\udfff', '\udffe')) - if sys.platform == 'win32': - self.assertRaises(UnicodeDecodeError, split, b'c:\\\xff\\bar') - self.assertRaises(UnicodeDecodeError, split, b'c:\\foo\\\xff') - else: - self.assertEqual(split(b'c:\\\xff\\\xfe'), - (b'c:\\\xff', b'\xfe')) + # Undecodable bytes paths (gh-122143). + self.assertEqual(split(b'c:\\\xff\\bar'), (b'c:\\\xff', b'bar')) + self.assertEqual(split(b'c:\\foo\\\xff'), (b'c:\\foo', b'\xff')) + self.assertEqual(split(b'c:\\\xff\\\xfe'), (b'c:\\\xff', b'\xfe')) def test_isabs(self): tester('ntpath.isabs("foo\\bar")', 0) @@ -481,12 +485,10 @@ def test_normpath_invalid_paths(self): self.assertEqual(normpath(b'fo\x00o\\..\\bar'), b'bar') self.assertEqual(normpath('\udfff'), '\udfff') self.assertEqual(normpath('\udfff\\..\\foo'), 'foo') - if sys.platform == 'win32': - self.assertRaises(UnicodeDecodeError, normpath, b'\xff') - self.assertRaises(UnicodeDecodeError, normpath, b'\xff\\..\\foo') - else: - self.assertEqual(normpath(b'\xff'), b'\xff') - self.assertEqual(normpath(b'\xff\\..\\foo'), b'foo') + # Undecodable bytes paths (gh-122143). + self.assertEqual(normpath(b'\xff'), b'\xff') + self.assertEqual(normpath(b'\xff\\..\\foo'), b'foo') + self.assertEqual(normpath(b'C:\\\xff\\..\\foo'), b'C:\\foo') def test_realpath_curdir(self): expected = ntpath.normpath(os.getcwd()) @@ -643,19 +645,33 @@ def test_realpath_invalid_paths(self): self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFNb + b'\\nonexistent') @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') - @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING}) - def test_realpath_invalid_unicode_paths(self, kwargs): + def test_realpath_invalid_unicode_paths(self): + # gh-122143: Undecodable bytes paths are handled like other paths + # which cannot be passed to the OS, e.g. paths with embedded nulls. realpath = ntpath.realpath ABSTFN = ntpath.abspath(os_helper.TESTFN) ABSTFNb = os.fsencode(ABSTFN) path = ABSTFNb + b'\xff' - self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) + self.assertEqual(realpath(path, strict=False), path) + self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST) + self.assertRaises(OSError, realpath, path, strict=True) + self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) path = ABSTFNb + b'\\nonexistent\\\xff' - self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) + self.assertEqual(realpath(path, strict=False), path) + self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST) + self.assertRaises(OSError, realpath, path, strict=True) + self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) path = ABSTFNb + b'\xff\\..' - self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) + self.assertEqual(realpath(path, strict=False), os.getcwdb()) + self.assertEqual(realpath(path, strict=ALL_BUT_LAST), os.getcwdb()) + self.assertEqual(realpath(path, strict=True), os.getcwdb()) + self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwdb()) path = ABSTFNb + b'\\nonexistent\\\xff\\..' - self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) + self.assertEqual(realpath(path, strict=False), ABSTFNb + b'\\nonexistent') + self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST) + self.assertRaises(OSError, realpath, path, strict=True) + self.assertEqual(realpath(path, strict=ALLOW_MISSING), + ABSTFNb + b'\\nonexistent') @os_helper.skip_unless_symlink @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') @@ -1268,12 +1284,9 @@ def test_abspath_invalid_paths(self): self.assertEqual(abspath(b'c:\\fo\x00o\\..\\bar'), b'c:\\bar') self.assertEqual(abspath('c:\\\udfff'), 'c:\\\udfff') self.assertEqual(abspath('c:\\\udfff\\..\\foo'), 'c:\\foo') - if sys.platform == 'win32': - self.assertRaises(UnicodeDecodeError, abspath, b'c:\\\xff') - self.assertRaises(UnicodeDecodeError, abspath, b'c:\\\xff\\..\\foo') - else: - self.assertEqual(abspath(b'c:\\\xff'), b'c:\\\xff') - self.assertEqual(abspath(b'c:\\\xff\\..\\foo'), b'c:\\foo') + # Undecodable bytes paths (gh-122143). + self.assertEqual(abspath(b'c:\\\xff'), b'c:\\\xff') + self.assertEqual(abspath(b'c:\\\xff\\..\\foo'), b'c:\\foo') def test_relpath(self): tester('ntpath.relpath("a")', 'a') diff --git a/Misc/NEWS.d/next/Library/2026-07-24-16-05-00.gh-issue-122143.Qw3Kz1.rst b/Misc/NEWS.d/next/Library/2026-07-24-16-05-00.gh-issue-122143.Qw3Kz1.rst new file mode 100644 index 000000000000000..2424be76a91e7b5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-24-16-05-00.gh-issue-122143.Qw3Kz1.rst @@ -0,0 +1,4 @@ +Fix :func:`os.path.splitroot`, :func:`os.path.splitdrive` and +:func:`os.path.normpath` on Windows raising :exc:`UnicodeDecodeError` for +:class:`bytes` paths that cannot be decoded with the filesystem encoding. +Such paths are now split and normalized as bytes, like on other platforms. diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h index ac9b63dec9eb440..3dbd06fde42ed1c 100644 --- a/Modules/clinic/posixmodule.c.h +++ b/Modules/clinic/posixmodule.c.h @@ -2630,7 +2630,7 @@ static PyObject * os__path_splitroot_ex(PyObject *module, PyObject *arg) { PyObject *return_value = NULL; - path_t path = PATH_T_INITIALIZE("_path_splitroot_ex", "path", 0, 1, 1, 0, 0); + path_t path = PATH_T_INITIALIZE("_path_splitroot_ex", "path", 0, 1, 1, 1, 0); if (!path_converter(arg, &path)) { goto exit; @@ -2688,7 +2688,7 @@ os__path_normpath(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyO }; #undef KWTUPLE PyObject *argsbuf[1]; - path_t path = PATH_T_INITIALIZE("_path_normpath", "path", 0, 1, 1, 0, 0); + path_t path = PATH_T_INITIALIZE("_path_normpath", "path", 0, 1, 1, 1, 0); args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); @@ -13734,4 +13734,4 @@ os__emscripten_log(PyObject *module, PyObject *const *args, Py_ssize_t nargs, Py #ifndef OS__EMSCRIPTEN_LOG_METHODDEF #define OS__EMSCRIPTEN_LOG_METHODDEF #endif /* !defined(OS__EMSCRIPTEN_LOG_METHODDEF) */ -/*[clinic end generated code: output=d641f02a97057666 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=3bbd2768d5dbb722 input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index de9575781881563..bab63f3407d24df 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -1298,7 +1298,8 @@ get_posix_state(PyObject *module) * yourself! * path.value_error * If nonzero, then suppress_value_error was specified and a ValueError - * occurred. + * occurred. path.wide and path.narrow are NULL in this case, but + * path.object still refers to the original object. * path.length * The length of the path in characters, if specified as * a string. @@ -1560,12 +1561,12 @@ path_converter(PyObject *o, void *p) return Py_CLEANUP_SUPPORTED; error_exit: - Py_XDECREF(o); Py_XDECREF(bytes); PyMem_Free(wide); if (!path->suppress_value_error || !PyErr_ExceptionMatches(PyExc_ValueError)) { + Py_XDECREF(o); return 0; } PyErr_Clear(); @@ -1574,7 +1575,9 @@ path_converter(PyObject *o, void *p) path->fd = -1; path->value_error = 1; path->length = 0; - path->object = NULL; + /* Keep the original object so that the caller can fall back to another + implementation which does not need the converted path. */ + path->object = o; return Py_CLEANUP_SUPPORTED; } @@ -6059,10 +6062,34 @@ os__path_isjunction_impl(PyObject *module, path_t *path) #endif /* MS_WINDOWS */ +/* A bytes path which cannot be decoded with the filesystem encoding cannot be + handled by the wide character implementations below. This happens on + Windows, where the filesystem error handler is "surrogatepass" rather than + "surrogateescape", so bytes which are not valid UTF-8 fail to decode. Such + paths are handled by the pure Python implementation in os.path, which + operates on bytes directly. */ +static PyObject * +path_fallback(const char *attrname, PyObject *path) +{ +#ifdef MS_WINDOWS + const char *modname = "ntpath"; +#else + const char *modname = "posixpath"; +#endif + PyObject *func = PyImport_ImportModuleAttrString(modname, attrname); + if (func == NULL) { + return NULL; + } + PyObject *result = PyObject_CallOneArg(func, path); + Py_DECREF(func); + return result; +} + + /*[clinic input] os._path_splitroot_ex - path: path_t(make_wide=True, nonstrict=True) + path: path_t(make_wide=True, nonstrict=True, suppress_value_error=True) / Split a pathname into drive, root and tail. @@ -6072,11 +6099,15 @@ The tail contains anything after the root. static PyObject * os__path_splitroot_ex_impl(PyObject *module, path_t *path) -/*[clinic end generated code: output=4b0072b6cdf4b611 input=012fbfad14888b2b]*/ +/*[clinic end generated code: output=4b0072b6cdf4b611 input=b89b7f547b9ebe09]*/ { Py_ssize_t drvsize, rootsize; PyObject *drv = NULL, *root = NULL, *tail = NULL, *result = NULL; + if (path->value_error) { + return path_fallback("_splitroot_fallback", path->object); + } + const wchar_t *buffer = path->wide; _Py_skiproot(buffer, path->length, &drvsize, &rootsize); drv = PyUnicode_FromWideChar(buffer, drvsize); @@ -6118,15 +6149,19 @@ os__path_splitroot_ex_impl(PyObject *module, path_t *path) /*[clinic input] os._path_normpath - path: path_t(make_wide=True, nonstrict=True) + path: path_t(make_wide=True, nonstrict=True, suppress_value_error=True) Normalize path, eliminating double slashes, etc. [clinic start generated code]*/ static PyObject * os__path_normpath_impl(PyObject *module, path_t *path) -/*[clinic end generated code: output=d353e7ed9410c044 input=3d4ac23b06332dcb]*/ +/*[clinic end generated code: output=d353e7ed9410c044 input=721c48886c26c08b]*/ { + if (path->value_error) { + return path_fallback("_normpath_fallback", path->object); + } + PyObject *result; Py_ssize_t norm_len; wchar_t *norm_path = _Py_normpath_and_size((wchar_t *)path->wide,