diff options
author | Treehugger Robot <treehugger-gerrit@google.com> | 2018-01-20 01:17:25 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2018-01-20 01:17:25 +0000 |
commit | 2b66844a0beffff0d2fcf20938e64cdd387131ac (patch) | |
tree | c3882636c464bd981f06ac7f99ff136e68c636b4 | |
parent | df4c92888e6283c8f6f1731391a6bccbc30d64db (diff) | |
parent | 38e4aefe6c7804d809cfe9e05619ac1a6935e565 (diff) | |
download | bionic-2b66844a0beffff0d2fcf20938e64cdd387131ac.tar.gz |
Merge "Support %mc/%ms/%m[ in sscanf."
-rw-r--r-- | libc/Android.bp | 2 | ||||
-rw-r--r-- | libc/stdio/vfscanf.cpp (renamed from libc/stdio/vfscanf.c) | 290 | ||||
-rw-r--r-- | tests/stdio_test.cpp | 132 |
3 files changed, 309 insertions, 115 deletions
diff --git a/libc/Android.bp b/libc/Android.bp index 5f04e2878..78fcb126b 100644 --- a/libc/Android.bp +++ b/libc/Android.bp @@ -18,7 +18,7 @@ libc_common_src_files = [ "stdio/refill.c", "stdio/stdio.cpp", "stdio/stdio_ext.cpp", - "stdio/vfscanf.c", + "stdio/vfscanf.cpp", "stdio/vfwscanf.c", "stdlib/exit.c", ] diff --git a/libc/stdio/vfscanf.c b/libc/stdio/vfscanf.cpp index f0ed4ae71..f2e136c68 100644 --- a/libc/stdio/vfscanf.c +++ b/libc/stdio/vfscanf.cpp @@ -37,74 +37,68 @@ #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <sys/param.h> #include <wctype.h> #include "local.h" #include <private/bionic_ctype.h> +#include <private/bionic_fortify.h> +#include <private/bionic_mbstate.h> #define BUF 513 /* Maximum length of numeric string. */ -/* - * Flags used during conversion. - */ -#define LONG 0x00001 /* l: long or double */ -#define LONGDBL 0x00002 /* L: long double */ -#define SHORT 0x00004 /* h: short */ -#define SHORTSHORT 0x00008 /* hh: 8 bit integer */ -#define LLONG 0x00010 /* ll: long long (+ deprecated q: quad) */ -#define POINTER 0x00020 /* p: void * (as hex) */ -#define SIZEINT 0x00040 /* z: (signed) size_t */ -#define MAXINT 0x00080 /* j: intmax_t */ -#define PTRINT 0x00100 /* t: ptrdiff_t */ -#define NOSKIP 0x00200 /* [ or c: do not skip blanks */ -#define SUPPRESS 0x00400 /* *: suppress assignment */ -#define UNSIGNED 0x00800 /* %[oupxX] conversions */ - -/* - * The following are used in numeric conversions only: - * SIGNOK, HAVESIGN, NDIGITS, DPTOK, and EXPOK are for floating point; - * SIGNOK, HAVESIGN, NDIGITS, PFXOK, and NZDIGITS are for integral. - */ -#define SIGNOK 0x01000 /* +/- is (still) legal */ -#define HAVESIGN 0x02000 /* sign detected */ -#define NDIGITS 0x04000 /* no digits detected */ - -#define DPTOK 0x08000 /* (float) decimal point is still legal */ -#define EXPOK 0x10000 /* (float) exponent (e+3, etc) still legal */ - -#define PFXOK 0x08000 /* 0x prefix is (still) legal */ -#define NZDIGITS 0x10000 /* no zero digits detected */ - -/* - * Conversion types. - */ -#define CT_CHAR 0 /* %c conversion */ -#define CT_CCL 1 /* %[...] conversion */ -#define CT_STRING 2 /* %s conversion */ -#define CT_INT 3 /* integer, i.e., strtoimax or strtoumax */ -#define CT_FLOAT 4 /* floating, i.e., strtod */ - -static u_char* __sccl(char*, u_char*); +// Flags used during conversion. +// Size/type: +#define LONG 0x00001 // l: long or double +#define LONGDBL 0x00002 // L: long double +#define SHORT 0x00004 // h: short +#define SHORTSHORT 0x00008 // hh: 8 bit integer +#define LLONG 0x00010 // ll: long long (+ deprecated q: quad) +#define POINTER 0x00020 // p: void* (as hex) +#define SIZEINT 0x00040 // z: (signed) size_t +#define MAXINT 0x00080 // j: intmax_t +#define PTRINT 0x00100 // t: ptrdiff_t +#define NOSKIP 0x00200 // [ or c: do not skip blanks +// Modifiers: +#define SUPPRESS 0x00400 // *: suppress assignment +#define UNSIGNED 0x00800 // %[oupxX] conversions +#define ALLOCATE 0x01000 // m: allocate a char* +// Internal use during integer parsing: +#define SIGNOK 0x02000 // +/- is (still) legal +#define HAVESIGN 0x04000 // Sign detected +#define NDIGITS 0x08000 // No digits detected +#define PFXOK 0x10000 // "0x" prefix is (still) legal +#define NZDIGITS 0x20000 // No zero digits detected + +// Conversion types. +#define CT_CHAR 0 // %c conversion +#define CT_CCL 1 // %[...] conversion +#define CT_STRING 2 // %s conversion +#define CT_INT 3 // Integer: strtoimax/strtoumax +#define CT_FLOAT 4 // Float: strtod + +static const unsigned char* __sccl(char*, const unsigned char*); /* * Internal, unlocked version of vfscanf */ -int __svfscanf(FILE* fp, const char* fmt0, __va_list ap) { - u_char* fmt = (u_char*)fmt0; +int __svfscanf(FILE* fp, const char* fmt0, va_list ap) { + const unsigned char* fmt = reinterpret_cast<const unsigned char*>(fmt0); int c; /* character from format, or conversion */ size_t width; /* field width, or 0 */ - char* p; /* points into all kinds of strings */ - int n; /* handy integer */ + char* p; + wchar_t* wcp; + size_t n; int flags; /* flags as defined above */ - char* p0; /* saves original value of p when necessary */ int nassigned; /* number of fields assigned */ int nread; /* number of characters consumed from fp */ int base; /* base argument to strtoimax/strtouimax */ char ccltab[256]; /* character class table for %[...] */ char buf[BUF]; /* buffer for numeric conversions */ - wchar_t* wcp; /* handy wide character pointer */ size_t nconv; /* length of multibyte sequence converted */ mbstate_t mbs; + void* allocation = NULL; // Allocated but unassigned result for %mc/%ms/%m[. + size_t capacity = 0; // Number of char/wchar_t units allocated in `allocation`. /* `basefix' is used to avoid `if' tests in the integer scanner */ static short basefix[17] = { 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; @@ -113,10 +107,9 @@ int __svfscanf(FILE* fp, const char* fmt0, __va_list ap) { nassigned = 0; nread = 0; - base = 0; /* XXX just to keep gcc happy */ for (;;) { c = *fmt++; - if (c == 0) return (nassigned); + if (c == 0) return nassigned; if (IsSpace(c)) { while ((fp->_r > 0 || __srefill(fp) == 0) && IsSpace(*fp->_p)) nread++, fp->_r--, fp->_p++; continue; @@ -164,6 +157,9 @@ literal: flags |= LONG; } goto again; + case 'm': + flags |= ALLOCATE; + goto again; case 'q': flags |= LLONG; /* deprecated */ goto again; @@ -262,29 +258,30 @@ literal: case 'n': if (flags & SUPPRESS) continue; - if (flags & SHORTSHORT) + if (flags & SHORTSHORT) { *va_arg(ap, signed char*) = nread; - else if (flags & SHORT) + } else if (flags & SHORT) { *va_arg(ap, short*) = nread; - else if (flags & LONG) + } else if (flags & LONG) { *va_arg(ap, long*) = nread; - else if (flags & SIZEINT) + } else if (flags & SIZEINT) { *va_arg(ap, ssize_t*) = nread; - else if (flags & PTRINT) + } else if (flags & PTRINT) { *va_arg(ap, ptrdiff_t*) = nread; - else if (flags & LLONG) + } else if (flags & LLONG) { *va_arg(ap, long long*) = nread; - else if (flags & MAXINT) + } else if (flags & MAXINT) { *va_arg(ap, intmax_t*) = nread; - else + } else { *va_arg(ap, int*) = nread; + } continue; /* * Disgusting backwards compatibility hacks. XXX */ case '\0': /* compat */ - return (EOF); + return EOF; default: /* compat */ if (IsUpper(c)) flags |= LONG; @@ -293,6 +290,13 @@ literal: break; } + if ((flags & ALLOCATE) != 0 && c > CT_STRING) { + __fortify_fatal("scanf 'm' only works with %%c/%%s/%%["); + } + if ((flags & (ALLOCATE|SUPPRESS)) == (ALLOCATE|SUPPRESS)) { + __fortify_fatal("scanf 'm' makes no sense with '*'"); + } + /* * We have a conversion that requires input. */ @@ -326,42 +330,53 @@ literal: /* scan arbitrary characters (sets NOSKIP) */ if (width == 0) width = 1; if (flags & LONG) { - wcp = ((flags & SUPPRESS) == 0) ? va_arg(ap, wchar_t*) : NULL; - n = 0; + if (flags & ALLOCATE) { + allocation = wcp = reinterpret_cast<wchar_t*>(malloc(width * sizeof(wchar_t))); + if (allocation == NULL) goto allocation_failure; + } else if (flags & SUPPRESS) { + wcp = NULL; + } else { + wcp = va_arg(ap, wchar_t*); + } + size_t bytes = 0; while (width != 0) { - if (n == (int)MB_CUR_MAX) { + if (bytes == MB_CUR_MAX) { fp->_flags |= __SERR; goto input_failure; } - buf[n++] = *fp->_p; + buf[bytes++] = *fp->_p; fp->_p++; fp->_r--; memset(&mbs, 0, sizeof(mbs)); - nconv = mbrtowc(wcp, buf, n, &mbs); - if (nconv == (size_t)-1) { + nconv = mbrtowc(wcp, buf, bytes, &mbs); + if (nconv == __MB_ERR_ILLEGAL_SEQUENCE) { fp->_flags |= __SERR; goto input_failure; } if (nconv == 0 && !(flags & SUPPRESS)) *wcp = L'\0'; - if (nconv != (size_t)-2) { - nread += n; + if (nconv != __MB_ERR_INCOMPLETE_SEQUENCE) { + nread += bytes; width--; if (!(flags & SUPPRESS)) wcp++; - n = 0; + bytes = 0; } if (fp->_r <= 0 && __srefill(fp)) { - if (n != 0) { + if (bytes != 0) { fp->_flags |= __SERR; goto input_failure; } break; } } + if (allocation != NULL) { + *va_arg(ap, wchar_t**) = reinterpret_cast<wchar_t*>(allocation); + allocation = NULL; + } if (!(flags & SUPPRESS)) nassigned++; } else if (flags & SUPPRESS) { size_t sum = 0; for (;;) { - if ((n = fp->_r) < (int)width) { + if ((n = fp->_r) < width) { sum += n; width -= n; fp->_p += n; @@ -378,9 +393,18 @@ literal: } nread += sum; } else { - size_t r = fread((void*)va_arg(ap, char*), 1, width, fp); - + if (flags & ALLOCATE) { + allocation = p = reinterpret_cast<char*>(malloc(width)); + if (allocation == NULL) goto allocation_failure; + } else { + p = va_arg(ap, char*); + } + size_t r = fread(p, 1, width, fp); if (r == 0) goto input_failure; + if (allocation != NULL) { + *va_arg(ap, char**) = reinterpret_cast<char*>(allocation); + allocation = NULL; + } nread += r; nassigned++; } @@ -390,55 +414,72 @@ literal: case CT_STRING: // CT_CCL: scan a (nonempty) character class (sets NOSKIP). // CT_STRING: like CCL, but zero-length string OK, & no NOSKIP. - if (width == 0) width = (size_t)~0; // 'infinity'. + if (width == 0) width = SIZE_MAX; if (flags & LONG) { - wchar_t twc; - int nchars = 0; - - wcp = (flags & SUPPRESS) == 0 ? va_arg(ap, wchar_t*) : &twc; + // TODO: since no-one cares, replace this with a simple fgetwc loop? n = 0; + if (flags & ALLOCATE) { + capacity = MIN(width, 32); + allocation = wcp = reinterpret_cast<wchar_t*>(malloc(sizeof(wchar_t) * capacity)); + if (allocation == NULL) goto allocation_failure; + } else if (flags & SUPPRESS) { + wcp = NULL; + } else { + wcp = va_arg(ap, wchar_t*); + } + size_t bytes = 0; while ((c == CT_CCL || !IsSpace(*fp->_p)) && width != 0) { - if (n == (int)MB_CUR_MAX) { + if (bytes == MB_CUR_MAX) { fp->_flags |= __SERR; goto input_failure; } - buf[n++] = *fp->_p; + buf[bytes++] = *fp->_p; fp->_p++; fp->_r--; + wchar_t wc = L'\0'; memset(&mbs, 0, sizeof(mbs)); - nconv = mbrtowc(wcp, buf, n, &mbs); - if (nconv == (size_t)-1) { + nconv = mbrtowc(&wc, buf, bytes, &mbs); + if (nconv == __MB_ERR_ILLEGAL_SEQUENCE) { fp->_flags |= __SERR; goto input_failure; } - if (nconv == 0) *wcp = L'\0'; - if (nconv != (size_t)-2) { - if ((c == CT_CCL && wctob(*wcp) != EOF && !ccltab[wctob(*wcp)]) || (c == CT_STRING && iswspace(*wcp))) { - while (n != 0) { - n--; - ungetc(buf[n], fp); + if (nconv != __MB_ERR_INCOMPLETE_SEQUENCE) { + if ((c == CT_CCL && wctob(wc) != EOF && !ccltab[wctob(wc)]) || (c == CT_STRING && iswspace(wc))) { + while (bytes != 0) { + bytes--; + ungetc(buf[bytes], fp); } break; } - nread += n; + if (wcp) wcp[n] = wc; + n++; + if (allocation != NULL && n == capacity) { + capacity *= 2; + wchar_t* new_allocation = + reinterpret_cast<wchar_t*>(realloc(allocation, sizeof(wchar_t) * capacity)); + if (new_allocation == NULL) goto allocation_failure; + allocation = wcp = new_allocation; + } + nread += bytes; width--; - if (!(flags & SUPPRESS)) wcp++; - nchars++; - n = 0; + bytes = 0; } if (fp->_r <= 0 && __srefill(fp)) { - if (n != 0) { + if (bytes != 0) { fp->_flags |= __SERR; goto input_failure; } break; } } - if (c == CT_CCL && n != 0) { + if (c == CT_CCL && bytes != 0) { fp->_flags |= __SERR; goto input_failure; } - n = nchars; + if (allocation != NULL) { + *va_arg(ap, wchar_t**) = reinterpret_cast<wchar_t*>(allocation); + allocation = NULL; + } } else if (flags & SUPPRESS) { n = 0; while ((c == CT_CCL && ccltab[*fp->_p]) || (c == CT_STRING && !IsSpace(*fp->_p))) { @@ -449,29 +490,46 @@ literal: break; } } + nread += n; } else { - p0 = p = va_arg(ap, char*); + if (flags & ALLOCATE) { + capacity = MIN(width, 32); + allocation = p = reinterpret_cast<char*>(malloc(capacity)); + if (allocation == NULL) goto allocation_failure; + } else { + p = va_arg(ap, char*); + } + n = 0; while ((c == CT_CCL && ccltab[*fp->_p]) || (c == CT_STRING && !IsSpace(*fp->_p))) { fp->_r--; - *p++ = *fp->_p++; + p[n++] = *fp->_p++; + if (allocation != NULL && n == capacity) { + capacity *= 2; + char* new_allocation = reinterpret_cast<char*>(realloc(allocation, capacity)); + if (new_allocation == NULL) goto allocation_failure; + allocation = p = new_allocation; + } if (--width == 0) break; if (fp->_r <= 0 && __srefill(fp)) { - if (c == CT_CCL && p == p0) goto input_failure; + if (c == CT_CCL && n == 0) goto input_failure; break; } } - n = p - p0; + nread += n; + if (allocation != NULL) { + *va_arg(ap, char**) = reinterpret_cast<char*>(allocation); + allocation = NULL; + } } if (c == CT_CCL && n == 0) goto match_failure; if (!(flags & SUPPRESS)) { if (flags & LONG) { - *wcp = L'\0'; + wcp[n] = L'\0'; } else { - *p = '\0'; + p[n] = '\0'; } ++nassigned; } - nread += n; break; case CT_INT: @@ -610,28 +668,30 @@ literal: uintmax_t res; *p = '\0'; - if (flags & UNSIGNED) + if (flags & UNSIGNED) { res = strtoumax(buf, NULL, base); - else + } else { res = strtoimax(buf, NULL, base); - if (flags & POINTER) + } + if (flags & POINTER) { *va_arg(ap, void**) = (void*)(uintptr_t)res; - else if (flags & MAXINT) + } else if (flags & MAXINT) { *va_arg(ap, intmax_t*) = res; - else if (flags & LLONG) + } else if (flags & LLONG) { *va_arg(ap, long long*) = res; - else if (flags & SIZEINT) + } else if (flags & SIZEINT) { *va_arg(ap, ssize_t*) = res; - else if (flags & PTRINT) + } else if (flags & PTRINT) { *va_arg(ap, ptrdiff_t*) = res; - else if (flags & LONG) + } else if (flags & LONG) { *va_arg(ap, long*) = res; - else if (flags & SHORT) + } else if (flags & SHORT) { *va_arg(ap, short*) = res; - else if (flags & SHORTSHORT) + } else if (flags & SHORTSHORT) { *va_arg(ap, signed char*) = res; - else + } else { *va_arg(ap, int*) = res; + } nassigned++; } nread += p - buf; @@ -659,10 +719,12 @@ literal: break; } } +allocation_failure: input_failure: + free(allocation); if (nassigned == 0) nassigned = -1; match_failure: - return (nassigned); + return nassigned; } /* @@ -671,7 +733,7 @@ match_failure: * closing `]'. The table has a 1 wherever characters should be * considered part of the scanset. */ -static u_char* __sccl(char* tab, u_char* fmt) { +static const unsigned char* __sccl(char* tab, const unsigned char* fmt) { int c, n, v; /* first `clear' the whole table */ @@ -744,7 +806,7 @@ static u_char* __sccl(char* tab, u_char* fmt) { break; case ']': /* end of scanset */ - return (fmt); + return fmt; default: /* just another character */ c = n; diff --git a/tests/stdio_test.cpp b/tests/stdio_test.cpp index d0d91309e..c1a51a808 100644 --- a/tests/stdio_test.cpp +++ b/tests/stdio_test.cpp @@ -1016,6 +1016,138 @@ TEST(STDIO_TEST, swscanf_ccl) { CheckScanf(swscanf, L"+,-/.", L"%[+--/]", 1, "+,-/"); } +template <typename T1, typename T2> +static void CheckScanfM(int sscanf_fn(const T1*, const T1*, ...), + const T1* input, const T1* fmt, + int expected_count, const T2* expected_string) { + T2* result = nullptr; + ASSERT_EQ(expected_count, sscanf_fn(input, fmt, &result)) << fmt; + if (expected_string == nullptr) { + ASSERT_EQ(nullptr, result); + } else { + ASSERT_STREQ(expected_string, result) << fmt; + } + free(result); +} + +TEST(STDIO_TEST, sscanf_mc) { + char* p1 = nullptr; + char* p2 = nullptr; + ASSERT_EQ(2, sscanf("hello", "%mc%mc", &p1, &p2)); + ASSERT_EQ('h', *p1); + ASSERT_EQ('e', *p2); + free(p1); + free(p2); + + p1 = nullptr; + ASSERT_EQ(1, sscanf("hello", "%4mc", &p1)); + ASSERT_EQ('h', p1[0]); + ASSERT_EQ('e', p1[1]); + ASSERT_EQ('l', p1[2]); + ASSERT_EQ('l', p1[3]); + free(p1); + + p1 = nullptr; + ASSERT_EQ(1, sscanf("hello world", "%30mc", &p1)); + ASSERT_EQ('h', p1[0]); + ASSERT_EQ('e', p1[1]); + ASSERT_EQ('l', p1[2]); + ASSERT_EQ('l', p1[3]); + ASSERT_EQ('o', p1[4]); + free(p1); +} + + +TEST(STDIO_TEST, sscanf_mlc) { + // This is so useless that clang doesn't even believe it exists... +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wformat-invalid-specifier" +#pragma clang diagnostic ignored "-Wformat-extra-args" + + wchar_t* p1 = nullptr; + wchar_t* p2 = nullptr; + ASSERT_EQ(2, sscanf("hello", "%mlc%mlc", &p1, &p2)); + ASSERT_EQ(L'h', *p1); + ASSERT_EQ(L'e', *p2); + free(p1); + free(p2); + + p1 = nullptr; + ASSERT_EQ(1, sscanf("hello", "%4mlc", &p1)); + ASSERT_EQ(L'h', p1[0]); + ASSERT_EQ(L'e', p1[1]); + ASSERT_EQ(L'l', p1[2]); + ASSERT_EQ(L'l', p1[3]); + free(p1); + + p1 = nullptr; + ASSERT_EQ(1, sscanf("hello world", "%30mlc", &p1)); + ASSERT_EQ(L'h', p1[0]); + ASSERT_EQ(L'e', p1[1]); + ASSERT_EQ(L'l', p1[2]); + ASSERT_EQ(L'l', p1[3]); + ASSERT_EQ(L'o', p1[4]); + free(p1); +#pragma clang diagnostic pop +} + + +TEST(STDIO_TEST, sscanf_ms) { + CheckScanfM(sscanf, "hello", "%ms", 1, "hello"); + CheckScanfM(sscanf, "hello", "%4ms", 1, "hell"); + CheckScanfM(sscanf, "hello world", "%30ms", 1, "hello"); +} + +TEST(STDIO_TEST, sscanf_mls) { + CheckScanfM(sscanf, "hello", "%mls", 1, L"hello"); + CheckScanfM(sscanf, "hello", "%4mls", 1, L"hell"); + CheckScanfM(sscanf, "hello world", "%30mls", 1, L"hello"); +} + +TEST(STDIO_TEST, sscanf_m_ccl) { + CheckScanfM(sscanf, "hello", "%m[a-z]", 1, "hello"); + CheckScanfM(sscanf, "hello", "%4m[a-z]", 1, "hell"); + CheckScanfM(sscanf, "hello world", "%30m[a-z]", 1, "hello"); +} + +TEST(STDIO_TEST, sscanf_ml_ccl) { + CheckScanfM(sscanf, "hello", "%ml[a-z]", 1, L"hello"); + CheckScanfM(sscanf, "hello", "%4ml[a-z]", 1, L"hell"); + CheckScanfM(sscanf, "hello world", "%30ml[a-z]", 1, L"hello"); +} + +TEST(STDIO_TEST, sscanf_ls) { + wchar_t w[32] = {}; + ASSERT_EQ(1, sscanf("hello world", "%ls", w)); + ASSERT_EQ(L"hello", std::wstring(w)); +} + +TEST(STDIO_TEST, sscanf_ls_suppress) { + ASSERT_EQ(0, sscanf("hello world", "%*ls %*ls")); +} + +TEST(STDIO_TEST, sscanf_ls_n) { + setlocale(LC_ALL, "C.UTF-8"); + wchar_t w[32] = {}; + int pos = 0; + ASSERT_EQ(1, sscanf("\xc4\x80", "%ls%n", w, &pos)); + ASSERT_EQ(static_cast<wchar_t>(256), w[0]); + ASSERT_EQ(2, pos); +} + +TEST(STDIO_TEST, sscanf_ls_realloc) { + // This is so useless that clang doesn't even believe it exists... +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wformat-invalid-specifier" +#pragma clang diagnostic ignored "-Wformat-extra-args" + wchar_t* p1 = nullptr; + wchar_t* p2 = nullptr; + ASSERT_EQ(2, sscanf("1234567890123456789012345678901234567890 world", "%mls %mls", &p1, &p2)); + ASSERT_EQ(L"1234567890123456789012345678901234567890", std::wstring(p1)); + ASSERT_EQ(L"world", std::wstring(p2)); +#pragma clang diagnostic pop +} + // https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=202240 TEST(STDIO_TEST, scanf_wscanf_EOF) { EXPECT_EQ(0, sscanf("b", "ab")); |