-
Notifications
You must be signed in to change notification settings - Fork 62
fs: support non-ASCII VFAT labels via locale-derived codepage #1202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,8 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import re | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import subprocess | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import tempfile | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import unittest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from packaging.version import Version | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -23,6 +26,57 @@ def _get_dosfstools_version(): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DOSFSTOOLS_VERSION = _get_dosfstools_version() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _has_codepage(cp): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Check whether the given DOS codepage is available via iconv.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p = subprocess.run(["iconv", "-l"], capture_output=True, text=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "CP%d" % cp in p.stdout or "CP%d//" % cp in p.stdout | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _cjk_label_supported(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Check whether fatlabel can actually write a CJK label using the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| locale-derived codepage in the current environment. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| This goes beyond _has_codepage(): iconv may advertise CP936 yet | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fatlabel's locale-aware conversion can still fail on some hosts (e.g. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| when the CJK locale is not fully functional for the running glibc/dosfstools | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| combination), so probe the real fatlabel behaviour before running the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end-to-end CJK tests and skip them if the environment cannot handle it. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not _has_codepage(936): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| img = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fd = -1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fd, img = tempfile.mkstemp(prefix="libblockdev.cjk.", suffix=".vfat") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.close(fd) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fd = -1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Create a small image (mkfs.vfat needs a real file with a size) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with open(img, "wb") as f: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f.truncate(8 * 1024 * 1024) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| subprocess.run(["mkfs.vfat", "-F", "32", img], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| capture_output=True, check=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| env = dict(os.environ, LC_ALL="zh_CN.UTF-8") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| r = subprocess.run(["fatlabel", "-c", "936", img, "测试"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| capture_output=True, env=env) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return r.returncode == 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| finally: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if fd >= 0: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.close(fd) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except OSError: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if img and os.path.exists(img): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.unlink(img) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except OSError: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class VfatNoDevTestCase(FSNoDevTestCase): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -281,6 +335,106 @@ def test_vfat_set_uuid(self): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BlockDev.fs_vfat_check_uuid(10 * "f") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class VfatCheckLabel(VfatNoDevTestCase): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Tests for bd_fs_vfat_check_label (no device needed).""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_vfat_check_label_byte_limit(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """The limit is 11 bytes (strlen), matching the upstream behaviour. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| The error message wording ("characters") is preserved for | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| compatibility with downstream consumers such as udisks that match | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| on this string. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| succ = BlockDev.fs_vfat_check_label("a" * 11) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.assertTrue(succ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with self.assertRaisesRegex(GLib.GError, "at most 11 characters long."): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BlockDev.fs_vfat_check_label("a" * 12) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_vfat_check_label_non_ascii_accepted(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Non-ASCII labels within the byte limit are accepted. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "测试U盘" is 10 UTF-8 bytes (3 CJK chars + 1 ASCII), which fits | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| within the 11-byte limit regardless of locale. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| succ = BlockDev.fs_vfat_check_label("测试U盘") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.assertTrue(succ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_vfat_check_label_forbidden_chars(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Forbidden characters must still be rejected.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for ch in '"*/:<>?\\|': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with self.assertRaisesRegex(GLib.GError, "not supported in VFAT labels"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BlockDev.fs_vfat_check_label("A" + ch + "B") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @unittest.skipUnless(_cjk_label_supported(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "fatlabel cannot write CJK labels with codepage 936 in this environment") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class VfatSetLabelNonAscii(VfatTestCase): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Tests for non-ASCII (CJK) VFAT labels using locale-derived codepage.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @classmethod | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def setUpClass(cls): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super(VfatSetLabelNonAscii, cls).setUpClass() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cls._saved_env = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for var in ("LC_ALL", "LC_CTYPE", "LANG"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cls._saved_env[var] = os.environ.get(var) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @classmethod | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def tearDownClass(cls): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for var, val in cls._saved_env.items(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if val is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.environ.pop(var, None) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.environ[var] = val | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super(VfatSetLabelNonAscii, cls).tearDownClass() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def setUp(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super(VfatSetLabelNonAscii, self).setUp() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # _vfat_locale_codepage reads LC_ALL > LC_CTYPE > LANG from the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # environment; set a CJK locale so it maps to codepage 936 (GBK). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.environ["LC_ALL"] = "zh_CN.UTF-8" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_vfat_set_non_ascii_label_roundtrip(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Set a CJK label and verify get_info reads it back as UTF-8.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| succ = BlockDev.fs_vfat_mkfs(self.loop_devs[0], self._mkfs_options) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.assertTrue(succ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| label = "测试U盘" # 4 CJK/Latin chars, 7 GBK bytes, 10 UTF-8 bytes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| succ = BlockDev.fs_vfat_set_label(self.loop_devs[0], label) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.assertTrue(succ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi = BlockDev.fs_vfat_get_info(self.loop_devs[0]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.assertTrue(fi) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.assertEqual(fi.label, label) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_vfat_set_ascii_label_unchanged(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Pure-ASCII labels must not be affected by the codepage change.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| succ = BlockDev.fs_vfat_mkfs(self.loop_devs[0], self._mkfs_options) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.assertTrue(succ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| succ = BlockDev.fs_vfat_set_label(self.loop_devs[0], "HELLO") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.assertTrue(succ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi = BlockDev.fs_vfat_get_info(self.loop_devs[0]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.assertTrue(fi) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.assertEqual(fi.label, "HELLO") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_vfat_set_label_max_cjk(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """5 CJK characters (10 GBK bytes) fit in 11 DOS bytes; 6 do not.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| succ = BlockDev.fs_vfat_mkfs(self.loop_devs[0], self._mkfs_options) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.assertTrue(succ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 5 CJK chars = 10 GBK bytes <= 11 -- should succeed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| five = "测" * 5 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| succ = BlockDev.fs_vfat_set_label(self.loop_devs[0], five) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.assertTrue(succ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi = BlockDev.fs_vfat_get_info(self.loop_devs[0]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self.assertEqual(fi.label, five) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Johnson-zs marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+424
to
+436
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Add the missing boundary assertion for 6 CJK characters. The docstring states 6 CJK characters (12 GBK bytes) "do not" fit, but the test only exercises the 5-character success case. Add the rejection case to verify the real OEM byte limit end-to-end. ✅ Proposed fix five = "测" * 5
succ = BlockDev.fs_vfat_set_label(self.loop_devs[0], five)
self.assertTrue(succ)
fi = BlockDev.fs_vfat_get_info(self.loop_devs[0])
self.assertEqual(fi.label, five)
+
+ # 6 CJK chars = 12 GBK bytes > 11 -- should fail
+ six = "测" * 6
+ with self.assertRaises(GLib.GError):
+ BlockDev.fs_vfat_set_label(self.loop_devs[0], six)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @utils.required_plugins(("tools",)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class VfatResize(VfatTestCase): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_vfat_resize(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.