diff --git a/pathvalidate/_common.py b/pathvalidate/_common.py index 440c81e..3d589ec 100644 --- a/pathvalidate/_common.py +++ b/pathvalidate/_common.py @@ -160,4 +160,8 @@ def truncate_str(text: str, encoding: str, max_bytes: int) -> str: str_bytes = text.encode(encoding) str_bytes = str_bytes[:max_bytes] # last char might be malformed, ignore it - return str_bytes.decode(encoding, "ignore") + truncated = str_bytes.decode(encoding, "ignore") + # Stateful encodings can add closing escape sequences when re-encoded. + while truncated and len(truncated.encode(encoding)) > max_bytes: + truncated = truncated[:-1] + return truncated diff --git a/test/test_filename.py b/test/test_filename.py index 285dc2b..c07023f 100644 --- a/test/test_filename.py +++ b/test/test_filename.py @@ -808,3 +808,25 @@ def test_max_len_fs_encoding(self, value, platform, fs_encoding, max_len, expect "fs_encoding": fs_encoding, } assert sanitize_filename(value, **kwargs) == expected + + @pytest.mark.parametrize( + ["value", "fs_encoding", "max_len", "expected"], + [ + ["日本語", "iso2022_jp", 8, "日"], + ["日本語", "iso2022_jp", 9, "日"], + ["日本語", "iso2022_jp", 10, "日本"], + ["日本語", "iso2022_jp", 12, "日本語"], + ["汉字测试", "hz", 8, "汉字"], + ["日本語", "utf-7", 7, "日"], + ["日本語", "utf-7", 8, "日本"], + ["日本語", "utf-8", 8, "日本"], + ["日本語", "utf-16", 7, "日本"], + ], + ) + def test_sanitized_name_fits_reencoded_byte_limit(self, value, fs_encoding, max_len, expected): + kwargs = {"platform": "linux", "fs_encoding": fs_encoding, "max_len": max_len} + sanitized = sanitize_filename(value, **kwargs) + assert sanitized == expected + assert len(sanitized.encode(fs_encoding)) <= max_len + validate_filename(sanitized, **kwargs) + assert sanitize_filename(value, validate_after_sanitize=True, **kwargs) == expected