From 51ae081b5132044495b94c23eeca62f91f8843e7 Mon Sep 17 00:00:00 2001 From: zhangsheng Date: Fri, 31 Jul 2026 08:17:11 +0800 Subject: [PATCH] fs: support non-ASCII VFAT labels via locale-derived codepage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fatlabel defaults to DOS codepage 850, which cannot encode characters outside Western European languages. When a user sets a VFAT label containing CJK, Cyrillic or other non-ASCII characters, fatlabel fails with "Cannot convert input sequence" and the label is not changed. Pass the OEM codepage matching the system locale to fatlabel via the -c option when the label contains non-ASCII characters. Keep the byte-count check (strlen) in bd_fs_vfat_check_label as a conservative upper bound; fatlabel performs the exact codepage-length check during iconv conversion. Preserve the original "at most 11 characters long" error wording so downstream consumers such as udisks that match on this string keep working. Decode the raw OEM bytes returned by blkid back to UTF-8 in bd_fs_vfat_get_info so labels round-trip correctly. Also add a NULL check to bd_fs_vfat_check_label and tests for non-ASCII label set/get and the byte-limit check. The non-ASCII end-to-end tests are gated by a runtime probe (_cjk_label_supported) that actually tries fatlabel -c 936 with a CJK label: iconv may advertise CP936 yet fatlabel's locale-aware conversion can still fail on some hosts (e.g. glibc/dosfstools combinations where the CJK locale is not fully functional), so the tests are skipped rather than reported as failures in such environments. This mirrors how Windows derives the OEM codepage from the system locale. Note: the codepage is derived from the daemon environment (udisksd), not the calling user's session — this is a known limitation documented in the code. --- src/plugins/fs/vfat.c | 141 ++++++++++++++++++++++++++++++--- tests/fs_tests/vfat_test.py | 154 ++++++++++++++++++++++++++++++++++++ 2 files changed, 286 insertions(+), 9 deletions(-) diff --git a/src/plugins/fs/vfat.c b/src/plugins/fs/vfat.c index dd15e7920..ff4ad9483 100644 --- a/src/plugins/fs/vfat.c +++ b/src/plugins/fs/vfat.c @@ -276,43 +276,147 @@ gboolean bd_fs_vfat_repair (const gchar *device, const BDExtraArg **extra, GErro return ret; } +/** + * _vfat_locale_codepage: + * + * Determine the DOS/OEM codepage that matches the system locale, mirroring + * how Windows derives the OEM codepage from the system locale. FAT volume + * labels are stored as 8-bit OEM codepage bytes, so the codepage used to + * write a label must match the locale of the system that created it. + * + * The locale is read from the environment (LC_ALL > LC_CTYPE > LANG) rather + * than calling setlocale(), to avoid side effects on the host process + * (libblockdev runs inside udisksd). + * + * Returns: the codepage number, or 850 (the dosfstools default) if the locale + * cannot be determined or has no known mapping. + */ +static guint +_vfat_locale_codepage (void) { + const gchar *locale = g_getenv ("LC_ALL"); + if (locale == NULL || *locale == '\0') + locale = g_getenv ("LC_CTYPE"); + if (locale == NULL || *locale == '\0') + locale = g_getenv ("LANG"); + + if (locale == NULL || *locale == '\0' || + g_strcmp0 (locale, "C") == 0 || g_strcmp0 (locale, "POSIX") == 0 || + g_str_has_prefix (locale, "C.")) + return 850; + + /* Extract the language[_territory] part before '.' or '@' */ + g_autofree gchar *lang = g_strdup (locale); + gchar *p; + p = strchr (lang, '.'); + if (p) *p = '\0'; + p = strchr (lang, '@'); + if (p) *p = '\0'; + + if (g_str_has_prefix (lang, "zh_CN") || g_str_has_prefix (lang, "zh_SG")) + return 936; /* GBK */ + if (g_str_has_prefix (lang, "zh_TW") || g_str_has_prefix (lang, "zh_HK")) + return 950; /* Big5 */ + if (g_str_has_prefix (lang, "ja")) + return 932; /* Shift-JIS */ + if (g_str_has_prefix (lang, "ko")) + return 949; /* UHC (Korean) */ + if (g_str_has_prefix (lang, "ru") || g_str_has_prefix (lang, "uk") || + g_str_has_prefix (lang, "be")) + return 866; /* Cyrillic (DOS) */ + + return 850; +} + +/** + * _vfat_label_from_codepage: + * @label: (nullable): raw OEM codepage bytes from the filesystem + * + * Convert a label read from a VFAT filesystem (raw OEM codepage bytes, as + * returned by blkid) to UTF-8, using the locale-derived codepage. Pure + * ASCII labels and the default codepage (850) need no conversion. + * + * Returns: (transfer full): the UTF-8 label, or the original bytes on + * conversion failure. Never %NULL. + */ +static gchar * +_vfat_label_from_codepage (const gchar *label) { + if (label == NULL || *label == '\0') + return g_strdup (label ? label : ""); + + if (g_str_is_ascii (label)) + return g_strdup (label); + + guint cp = _vfat_locale_codepage (); + gchar cp_name[16]; + g_snprintf (cp_name, sizeof (cp_name), "CP%u", cp); + + GError *conv_error = NULL; + gchar *utf8 = g_convert (label, -1, "UTF-8", cp_name, NULL, NULL, &conv_error); + if (utf8 != NULL) + return utf8; + + /* Conversion failed; return a valid UTF-8 string instead of raw bytes + * that may not be valid UTF-8. */ + g_error_free (conv_error); + return g_utf8_make_valid (label, -1); +} + /** * bd_fs_vfat_set_label: * @device: the device containing the file system to set label for * @label: label to set * @error: (out) (optional): place to store error (if any) * - * Returns: whether the label of vfat file system on the @device was - * successfully set or not + * Sets the label of a VFAT filesystem. For labels containing non-ASCII + * characters, the OEM codepage matching the system locale is passed to + * fatlabel via the -c option, so that labels in CJK, Cyrillic and other + * non-Western-European scripts are encoded correctly. + * + * Returns: whether the label was successfully set or not * * Tech category: %BD_FS_TECH_VFAT-%BD_FS_TECH_MODE_SET_LABEL */ gboolean bd_fs_vfat_set_label (const gchar *device, const gchar *label, GError **error) { - const gchar *args[4] = {"fatlabel", device, NULL, NULL}; + /* "fatlabel" "-c" ""