From 40fbc8ee7e9941c5bd8487a24370f6b734dcf451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Mon, 10 Mar 2025 03:16:37 +0000 Subject: [PATCH] Fix and simplify path truncation --- beets/util/__init__.py | 32 ++++++++++++++++++-------------- test/test_util.py | 27 ++++++++++++--------------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index d8340a978..ff0a5d273 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -696,22 +696,26 @@ def sanitize_path(path: str, replacements: Replacements | None = None) -> str: return os.path.join(*comps) -def truncate_path(path: AnyStr) -> AnyStr: - """Given a bytestring path or a Unicode path fragment, truncate the - components to a legal length. In the last component, the extension - is preserved. +def truncate_str(s: str, length: int) -> str: + """Truncate the string to the given byte length. + + If we end up truncating a unicode character in the middle (rendering it invalid), + it is removed: + + >>> s = "🎹🎶" # 8 bytes + >>> truncate_str(s, 6) + '🎹' """ + return os.fsencode(s)[:length].decode(sys.getfilesystemencoding(), "ignore") + + +def truncate_path(str_path: str) -> str: + """Truncate each path part to a legal length preserving the extension.""" max_length = get_max_filename_length() - comps = components(path) - - out = [c[:length] for c in comps] - base, ext = os.path.splitext(comps[-1]) - if ext: - # Last component has an extension. - base = base[: max_length - len(ext)] - out[-1] = base + ext - - return os.path.join(*out) + path = Path(str_path) + parent_parts = [truncate_str(p, max_length) for p in path.parts[:-1]] + stem = truncate_str(path.stem, max_length - len(path.suffix)) + return str(Path(*parent_parts, stem).with_suffix(path.suffix)) def _legalize_stage( diff --git a/test/test_util.py b/test/test_util.py index f5b4fd102..a4b224ee3 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -175,18 +175,15 @@ class PathConversionTest(BeetsTestCase): assert outpath == "C:\\caf\xe9".encode() -class PathTruncationTest(BeetsTestCase): - def test_truncate_bytestring(self): - with _common.platform_posix(): - p = util.truncate_path(b"abcde/fgh", 4) - assert p == b"abcd/fgh" - - def test_truncate_unicode(self): - with _common.platform_posix(): - p = util.truncate_path("abcde/fgh", 4) - assert p == "abcd/fgh" - - def test_truncate_preserves_extension(self): - with _common.platform_posix(): - p = util.truncate_path("abcde/fgh.ext", 5) - assert p == "abcde/f.ext" +@patch("beets.util.get_max_filename_length", lambda: 5) +@pytest.mark.parametrize( + "path, expected", + [ + ("abcdeX/fgh", "abcde/fgh"), + ("abcde/fXX.ext", "abcde/f.ext"), + ("a🎹/a.ext", "a🎹/a.ext"), + ("ab🎹/a.ext", "ab/a.ext"), + ], +) +def test_truncate_path(path, expected): + assert util.truncate_path(path) == expected