[PATCH v11 0/2] MR10460: msvcrt: Add support for %Z printf specifier.
This specifier allows printing the `UNICODE_STRING` and `ANSI_STRING` structures. It is useful for running unit tests for kernel mode or shared components on WINE. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=59563 -- v11: msvcrt/tests: Add tests for %Z printf specifier. msvcrt: Add support for %Z printf specifier. https://gitlab.winehq.org/wine/wine/-/merge_requests/10460
From: Trung Nguyen <me@trungnt2910.com> This specifier allows printing the `UNICODE_STRING` and `ANSI_STRING` structures. It is useful for running unit tests for kernel mode or shared components on WINE. Co-authored-by: Piotr Caban <piotr@codeweavers.com> Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=59563 --- dlls/msvcrt/printf.h | 45 ++++++++++++++++++++++++++++++++++---------- dlls/msvcrt/wcs.c | 1 + 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/dlls/msvcrt/printf.h b/dlls/msvcrt/printf.h index cfcfd5be771..c47b3aadd9c 100644 --- a/dlls/msvcrt/printf.h +++ b/dlls/msvcrt/printf.h @@ -255,13 +255,28 @@ static inline int FUNC_NAME(pf_output_format_str)(FUNC_NAME(puts_clbk) pf_puts, return r>=0 ? ret : r; } +static inline BOOL FUNC_NAME(pf_is_str_wide)(pf_flags *flags, BOOL legacy_wide) +{ + BOOL api_is_wide = sizeof(APICHAR) == sizeof(wchar_t); + BOOL complement_is_narrow = legacy_wide && api_is_wide; + + if((flags->NaturalString && api_is_wide) || flags->WideString || flags->IntegerLength == LEN_LONG) + return TRUE; + if((flags->NaturalString && !api_is_wide) || flags->IntegerLength == LEN_SHORT) + return FALSE; + +#if _MSVCR_VER < 140 + if (flags->Format == 'Z') + return FALSE; +#endif + + return (flags->Format=='s' || flags->Format=='c') == complement_is_narrow; +} + static inline int FUNC_NAME(pf_handle_string)(FUNC_NAME(puts_clbk) pf_puts, void *puts_ctx, const void *str, int len, pf_flags *flags, _locale_t locale, BOOL legacy_wide) { - BOOL api_is_wide = sizeof(APICHAR) == sizeof(wchar_t); - BOOL complement_is_narrow = legacy_wide ? api_is_wide : FALSE; #ifdef PRINTF_WIDE - if(!str) return FUNC_NAME(pf_output_format_wstr)(pf_puts, puts_ctx, L"(null)", 6, flags, locale); #else @@ -269,15 +284,10 @@ static inline int FUNC_NAME(pf_handle_string)(FUNC_NAME(puts_clbk) pf_puts, void return FUNC_NAME(pf_output_format_str)(pf_puts, puts_ctx, "(null)", 6, flags, locale); #endif - if((flags->NaturalString && api_is_wide) || flags->WideString || flags->IntegerLength == LEN_LONG) + if(FUNC_NAME(pf_is_str_wide)(flags, legacy_wide)) return FUNC_NAME(pf_output_format_wstr)(pf_puts, puts_ctx, str, len, flags, locale); - if((flags->NaturalString && !api_is_wide) || flags->IntegerLength == LEN_SHORT) - return FUNC_NAME(pf_output_format_str)(pf_puts, puts_ctx, str, len, flags, locale); - - if((flags->Format=='S' || flags->Format=='C') == complement_is_narrow) - return FUNC_NAME(pf_output_format_str)(pf_puts, puts_ctx, str, len, flags, locale); else - return FUNC_NAME(pf_output_format_wstr)(pf_puts, puts_ctx, str, len, flags, locale); + return FUNC_NAME(pf_output_format_str)(pf_puts, puts_ctx, str, len, flags, locale); } static inline int FUNC_NAME(pf_output_special_fp)(FUNC_NAME(puts_clbk) pf_puts, void *puts_ctx, @@ -1142,6 +1152,21 @@ int FUNC_NAME(pf_printf)(FUNC_NAME(puts_clbk) pf_puts, void *puts_ctx, const API i = FUNC_NAME(pf_handle_string)(pf_puts, puts_ctx, &ch, 1, &flags, locale, legacy_wide); if(i < 0) i = 0; /* ignore conversion error */ + } else if(flags.Format == 'Z') { + /* UNICODE_STRING and ANSI_STRING have the same layout. */ + UNICODE_STRING *ustr = pf_args(args_ctx, pos, VT_PTR, valist).get_ptr; + void *str = ustr ? ustr->Buffer : NULL; + int len = ustr ? ustr->Length : 0; + + if(FUNC_NAME(pf_is_str_wide)(&flags, legacy_wide)) { + if(len & 1) { + *_errno() = EINVAL; + return -1; + } + len /= sizeof(wchar_t); + } + i = FUNC_NAME(pf_handle_string)(pf_puts, puts_ctx, str, len, + &flags, locale, legacy_wide); } else if(flags.Format == 'p') { flags.Format = 'X'; flags.PadZero = TRUE; diff --git a/dlls/msvcrt/wcs.c b/dlls/msvcrt/wcs.c index 1b7bc3e54e0..9ad7181311a 100644 --- a/dlls/msvcrt/wcs.c +++ b/dlls/msvcrt/wcs.c @@ -28,6 +28,7 @@ #include <wctype.h> #include "msvcrt.h" #include "winnls.h" +#include "winternl.h" #include "wtypes.h" #include "wine/debug.h" -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10460
From: Trung Nguyen <me@trungnt2910.com> Co-authored-by: Piotr Caban <piotr@codeweavers.com> Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=59563 --- dlls/msvcrt/tests/printf.c | 19 +++++++++++++++++++ dlls/ucrtbase/tests/printf.c | 23 ++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/dlls/msvcrt/tests/printf.c b/dlls/msvcrt/tests/printf.c index c21b1285275..d401f449f84 100644 --- a/dlls/msvcrt/tests/printf.c +++ b/dlls/msvcrt/tests/printf.c @@ -33,6 +33,7 @@ #include "windef.h" #include "winbase.h" #include "winnls.h" +#include "winternl.h" #include "wine/test.h" @@ -170,6 +171,7 @@ static void test_sprintf( void ) { "% d", " 1", 0, INT_ARG, 1 }, { "%+ d", "+1", 0, INT_ARG, 1 }, { "%S", "wide", 0, PTR_ARG, 0, 0, 0, L"wide" }, + { "%Z", "ansi", 0, PTR_ARG, 0, 0, 0, &(ANSI_STRING){ 4, 4, (char *)"ansi" } }, { "%04c", "0001", 0, INT_ARG, '1' }, { "%-04c", "1 ", 0, INT_ARG, '1' }, { "%#012x", "0x0000000001", 0, INT_ARG, 1 }, @@ -194,6 +196,9 @@ static void test_sprintf( void ) { "%w-s", "-s", 0, PTR_ARG, 0, 0, 0, L"wide" }, { "%ls", "wide", 0, PTR_ARG, 0, 0, 0, L"wide" }, { "%Ls", "not wide", 0, PTR_ARG, 0, 0, 0, "not wide" }, + { "%wZ", "wide", 0, PTR_ARG, 0, 0, 0, &(UNICODE_STRING){ 8, 8, (wchar_t *)L"wide" } }, + { "%wZ", "wi", 0, PTR_ARG, 0, 0, 0, &(UNICODE_STRING){ 4, 8, (wchar_t *)L"wide" } }, + { "%hZ", "not wide", 0, PTR_ARG, 0, 0, 0, &(ANSI_STRING){ 8, 8, (char *)"not wide" } }, { "%b", "b", 0, NO_ARG }, { "%3c", " a", 0, INT_ARG, 'a' }, { "%3d", "1234", 0, INT_ARG, 1234 }, @@ -215,6 +220,10 @@ static void test_sprintf( void ) { "%o", "12", 0, INT_ARG, 10 }, { "%s", "(null)", 0, PTR_ARG, 0, 0, 0, NULL }, { "%s", "%%%%", 0, PTR_ARG, 0, 0, 0, "%%%%" }, + { "%Z", "(null)", 0, PTR_ARG, 0, 0, 0, NULL }, + { "%Z", "(null)", 0, PTR_ARG, 0, 0, 0, &(ANSI_STRING){ 0, 0, NULL } }, + { "%Z", "(null)", 0, PTR_ARG, 0, 0, 0, &(ANSI_STRING){ 1, 1, NULL } }, + { "%Z", "no", 0, PTR_ARG, 0, 0, 0, &(ANSI_STRING){ 2, 8, (char *)"not wide" } }, { "%u", "4294967295", 0, INT_ARG, -1 }, { "%w", "", 0, INT_ARG, -1 }, { "%h", "", 0, INT_ARG, -1 }, @@ -434,6 +443,11 @@ static void test_sprintf( void ) r = p_sprintf(buffer, "% *2d", 0, 7); ok(r==2, "r = %d\n", r); ok(!strcmp(buffer, " 7"), "failed: \"%s\"\n", buffer); + + errno = 0; + r = p_sprintf(buffer, "%wZ", &(UNICODE_STRING){ 5, 8, (wchar_t *)L"wide" }); + ok(r==-1, "r = %d\n", r); + ok(errno == EINVAL, "errno = %d\n", errno); } static void test_swprintf( void ) @@ -441,6 +455,7 @@ static void test_swprintf( void ) wchar_t buffer[100]; double pnumber = 789456123; const char string[] = "string"; + ANSI_STRING ansi_string = { 11, 11, (char *)"ansi string" }; swprintf(buffer, L"%+#23.15e", pnumber); ok(wcsstr(buffer, L"e+008") != 0, "Sprintf different\n"); @@ -450,6 +465,10 @@ static void test_swprintf( void ) ok(wcslen(buffer) == 6, "Problem with \"%%S\" interpretation\n"); swprintf(buffer, L"%hs", string); ok(!wcscmp(L"string", buffer), "swprintf failed with %%hs\n"); + swprintf(buffer, L"%Z", &ansi_string); + ok(!wcscmp(L"ansi string", buffer), "swprintf failed with %%Z\n"); + swprintf(buffer, L"%hZ", &ansi_string); + ok(!wcscmp(L"ansi string", buffer), "swprintf failed with %%hZ\n"); } static void test_snprintf (void) diff --git a/dlls/ucrtbase/tests/printf.c b/dlls/ucrtbase/tests/printf.c index 16dd4f3727b..5b841047f0b 100644 --- a/dlls/ucrtbase/tests/printf.c +++ b/dlls/ucrtbase/tests/printf.c @@ -29,6 +29,7 @@ #include "windef.h" #include "winbase.h" #include "winnls.h" +#include "winternl.h" #include "wine/test.h" @@ -533,14 +534,19 @@ static void test_printf_legacy_wide(void) { const wchar_t wide[] = {'A','B','C','D',0}; const char narrow[] = "abcd"; + const UNICODE_STRING wide_str = { 8, sizeof(wide), (wchar_t *)wide }; + const ANSI_STRING narrow_str = { 4, sizeof(narrow), (char *)narrow }; const char out[] = "abcd ABCD"; /* The legacy wide flag doesn't affect narrow printfs, so the same * format should behave the same both with and without the flag. */ const char narrow_fmt[] = "%s %ls"; + const char narrow_str_fmt[] = "%hZ %Z"; /* The standard behaviour is to use the same format as for the narrow * case, while the legacy case has got a different meaning for %s. */ const wchar_t std_wide_fmt[] = {'%','s',' ','%','l','s',0}; const wchar_t legacy_wide_fmt[] = {'%','h','s',' ','%','s',0}; + const wchar_t std_wide_str_fmt[] = {'%','h','Z',' ','%','Z',0}; + const wchar_t legacy_wide_str_fmt[] = {'%','Z',' ','%','l','Z',0}; char buffer[20]; wchar_t wbuffer[20]; @@ -549,10 +555,25 @@ static void test_printf_legacy_wide(void) vsprintf_wrapper(_CRT_INTERNAL_PRINTF_LEGACY_WIDE_SPECIFIERS, buffer, sizeof(buffer), narrow_fmt, narrow, wide); ok(!strcmp(buffer, out), "buffer wrong, got=%s\n", buffer); + vsprintf_wrapper(0, buffer, sizeof(buffer), narrow_str_fmt, &narrow_str, &wide_str); + ok(!strcmp(buffer, out), "buffer wrong, got=%s\n", buffer); + vsprintf_wrapper(_CRT_INTERNAL_PRINTF_LEGACY_WIDE_SPECIFIERS, buffer, + sizeof(buffer), narrow_str_fmt, &narrow_str, &wide_str); + ok(!strcmp(buffer, out), "buffer wrong, got=%s\n", buffer); + vswprintf_wrapper(0, wbuffer, sizeof(wbuffer), std_wide_fmt, narrow, wide); WideCharToMultiByte(CP_ACP, 0, wbuffer, -1, buffer, sizeof(buffer), NULL, NULL); ok(!strcmp(buffer, out), "buffer wrong, got=%s\n", buffer); - vswprintf_wrapper(_CRT_INTERNAL_PRINTF_LEGACY_WIDE_SPECIFIERS, wbuffer, sizeof(wbuffer), legacy_wide_fmt, narrow, wide); + vswprintf_wrapper(_CRT_INTERNAL_PRINTF_LEGACY_WIDE_SPECIFIERS, wbuffer, + sizeof(wbuffer), legacy_wide_fmt, narrow, wide); + WideCharToMultiByte(CP_ACP, 0, wbuffer, -1, buffer, sizeof(buffer), NULL, NULL); + ok(!strcmp(buffer, out), "buffer wrong, got=%s\n", buffer); + + vswprintf_wrapper(0, wbuffer, sizeof(wbuffer), std_wide_str_fmt, &narrow_str, &wide_str); + WideCharToMultiByte(CP_ACP, 0, wbuffer, -1, buffer, sizeof(buffer), NULL, NULL); + ok(!strcmp(buffer, out), "buffer wrong, got=%s\n", buffer); + vswprintf_wrapper(_CRT_INTERNAL_PRINTF_LEGACY_WIDE_SPECIFIERS, wbuffer, + sizeof(wbuffer), legacy_wide_str_fmt, &narrow_str, &wide_str); WideCharToMultiByte(CP_ACP, 0, wbuffer, -1, buffer, sizeof(buffer), NULL, NULL); ok(!strcmp(buffer, out), "buffer wrong, got=%s\n", buffer); } -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10460
On Wed Apr 1 15:50:19 2026 +0000, Piotr Caban wrote:
Please ignore the comment about portability - I was wrong. Sorry about that. The results for ucrtbase are strange. The documentation states that ucrtbase has a bug that is preserved for backward compatibility. Unfortunately this means that ucrtbase needs to be special-cased. I was hoping that it can be avoided by using proper flags. I have added some minor changes to your patches: [aefe9521b1e20674f059fa52be45d45a47c44d88](https://gitlab.winehq.org/piotr/wine/-/commit/aefe9521b1e20674f059fa52be45d4...), [c84e147eceeb2a48e1053bf9e4ab7cb6ea243f9e](https://gitlab.winehq.org/piotr/wine/-/commit/c84e147eceeb2a48e1053bf9e4ab7c...). If it looks good please push the patches to this MR. Here's a short list of changes: * added more tests * fixed handling of UNICODE_STRING with broken length * pass length in characters to pf_handle_string function * simplify pf_is_str_wide helper Thanks, I've reviewed and pushed the patches. I made some changes on top of that:
* Whitespace changes in the block that calls `pf_is_str_wide` in `pf_printf` to be more consistent with surrounding code. * The `%Z` test with a truncated string was changed from `"wide"` to `"not wide"` because its parameter is a `ANSI_STRING` (this one was an issue I left behind in my previous patch, not related to your changes). -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10460#note_134711
Trung Nguyen (@trungnt2910) commented about dlls/msvcrt/printf.h:
i = FUNC_NAME(pf_handle_string)(pf_puts, puts_ctx, &ch, 1, &flags, locale, legacy_wide); if(i < 0) i = 0; /* ignore conversion error */ + } else if(flags.Format == 'Z') { + /* UNICODE_STRING and ANSI_STRING have the same layout. */ + UNICODE_STRING *ustr = pf_args(args_ctx, pos, VT_PTR, valist).get_ptr; + void *str = ustr ? ustr->Buffer : NULL; + int len = ustr ? ustr->Length : 0; + + if(FUNC_NAME(pf_is_str_wide)(&flags, legacy_wide)) { + if(len & 1) {
```suggestion:-0+0 if(len % sizeof(wchar_t)) { ``` IMO this would make more logical sense and, with optimizations on, be compiled to the same thing. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10460#note_134739
On Thu Apr 2 03:28:56 2026 +0000, Trung Nguyen wrote:
```suggestion:-0+0 if(len % sizeof(wchar_t)) { ``` IMO this would make more logical sense and, with optimizations on, be compiled to the same thing.
Sure, please change it as preferred. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10460#note_134741
participants (3)
-
Piotr Caban (@piotr) -
Trung Nguyen -
Trung Nguyen (@trungnt2910)