diff --git a/doc/changes.qbk b/doc/changes.qbk index fc10bcd..e314558 100644 --- a/doc/changes.qbk +++ b/doc/changes.qbk @@ -1,11 +1,19 @@ [/ Copyright 2021 Peter Dimov + Copyright 2022 Andrey Semashev Distributed under the Boost Software License, Version 1.0. https://boost.org/LICENSE_1_0.txt) ] [section Revision History] +[section Changes in 1.82.0] + +* Added [link core.snprintf `boost/core/snprintf.hpp`] header with portable definitions of `snprintf`, `vsnprintf` and + their `wchar_t` counterparts. + +[endsect] + [section Changes in 1.81.0] * [link core.empty_value `empty_value`] members are now marked as `constexpr`. diff --git a/doc/core.qbk b/doc/core.qbk index 8e2d1a3..a7943f7 100644 --- a/doc/core.qbk +++ b/doc/core.qbk @@ -70,6 +70,7 @@ criteria for inclusion is that the utility component be: [include swap.qbk] [include typeinfo.qbk] [include type_name.qbk] +[include snprintf.qbk] [include uncaught_exceptions.qbk] [include use_default.qbk] [include verbose_terminate_handler.qbk] diff --git a/doc/snprintf.qbk b/doc/snprintf.qbk new file mode 100644 index 0000000..814019a --- /dev/null +++ b/doc/snprintf.qbk @@ -0,0 +1,47 @@ +[/ + / Copyright (c) 2022 Andrey Semashev + / + / Distributed under the Boost Software License, Version 1.0. (See accompanying + / file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + /] + +[section:snprintf snprintf] + +[simplesect Authors] + +* Andrey Semashev + +[endsimplesect] + +[section Header ] + +The header `` provides portable definition of [@https://en.cppreference.com/w/c/io/fprintf `snprintf`], +`vsnprintf` and their corresponding `wchar_t` counterparts. On a platform that supports these functions in the standard library, +these definitions are equivalent to the standard functions. On other platforms (mainly, older MSVC versions) these functions +are emulated through non-standard functions that have similar behavior. + +Depending on the standard library, certain implementation differences are exposed to the user: + +* Any non-standard behavior with respect to string format description are not hidden by the emulation. +* Returned value of `boost::core::snprintf` in case if the output buffer is too small may not be equal to the number of characters + that would have been written if the buffer was large enough. It is, however, equal or larger than the buffer size, + which still allows the caller to detect the buffer overflow condition. The formatted output is still properly null-terminated + in this case. + +[note Unlike `snprintf`, `swprintf` does not return the number of characters to be written if the output buffer is too small +but returns -1 instead. Furthermore, `swprintf` may or may not produce characters in the output buffer in this case.] + +[section Example] +`` +char buf[10]; +int n = boost::core::snprintf(buf, sizeof(buf), "%d", i); +if (n < 0) + throw std::runtime_error("Formatting error"); +if (n >= sizeof(buf)) + throw std::runtime_error("Formatting buffer overflow"); +`` +[endsect] + +[endsect] + +[endsect] diff --git a/include/boost/core/snprintf.hpp b/include/boost/core/snprintf.hpp new file mode 100644 index 0000000..91e252b --- /dev/null +++ b/include/boost/core/snprintf.hpp @@ -0,0 +1,173 @@ +/* + * Copyright Andrey Semashev 2022. + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at + * http://www.boost.org/LICENSE_1_0.txt) + */ +/*! + * \file snprintf.hpp + * \author Andrey Semashev + * \date 06.12.2022 + * + * \brief The header provides more portable definition of snprintf and vsnprintf, + * as well as \c wchar_t counterparts. + */ + +#ifndef BOOST_CORE_SNPRINTF_HPP_INCLUDED_ +#define BOOST_CORE_SNPRINTF_HPP_INCLUDED_ + +#include +#include +#include + +#ifdef BOOST_HAS_PRAGMA_ONCE +#pragma once +#endif + +#if defined(__MINGW32__) + +#include +#include +#if !defined(__MINGW64_VERSION_MAJOR) +#include +#endif + +// MinGW32 and MinGW-w64 provide their own snprintf implementations that are compliant with the C standard. +#define BOOST_CORE_DETAIL_MINGW_SNPRINTF + +#elif (defined(BOOST_MSSTL_VERSION) && BOOST_MSSTL_VERSION < 140) + +#include +#include +#include + +// MSVC snprintfs are not conforming but they are good enough for typical use cases. +#define BOOST_CORE_DETAIL_MSVC_LEGACY_SNPRINTF + +#endif + +namespace boost { + +namespace core { + +#if defined(BOOST_CORE_DETAIL_MINGW_SNPRINTF) || defined(BOOST_CORE_DETAIL_MSVC_LEGACY_SNPRINTF) + +#if defined(BOOST_CORE_DETAIL_MINGW_SNPRINTF) + +inline int vsnprintf(char* buf, std::size_t size, const char* format, std::va_list args) +{ + return __mingw_vsnprintf(buf, size, format, args); +} + +inline int vswprintf(wchar_t* buf, std::size_t size, const wchar_t* format, std::va_list args) +{ +#if defined(__MINGW64_VERSION_MAJOR) + int res = __mingw_vsnwprintf(buf, size, format, args); + // __mingw_vsnwprintf returns the number of characters to be printed, but (v)swprintf is expected to return -1 on truncation + if (static_cast< unsigned int >(res) >= size) + res = -1; + return res; +#else + // Legacy MinGW32 does not provide __mingw_vsnwprintf, so use _vsnwprintf from MSVC CRT + if (BOOST_UNLIKELY(size == 0u || size > static_cast< std::size_t >(INT_MAX))) + return -1; + + int res = _vsnwprintf(buf, size, format, args); + // (v)swprintf is expected to return -1 on truncation, so we only need to ensure the output is null-terminated + if (static_cast< unsigned int >(res) >= size) + { + buf[size - 1u] = L'\0'; + res = -1; + } + + return res; +#endif +} + +#elif defined(BOOST_CORE_DETAIL_MSVC_LEGACY_SNPRINTF) + +#if defined(_MSC_VER) +#pragma warning(push) +// '_vsnprintf': This function or variable may be unsafe. Consider using _vsnprintf_s instead. +#pragma warning(disable: 4996) +#endif + +inline int vsnprintf(char* buf, std::size_t size, const char* format, std::va_list args) +{ + if (BOOST_UNLIKELY(size == 0u)) + return 0; + if (BOOST_UNLIKELY(size > static_cast< std::size_t >(INT_MAX))) + return -1; + + buf[size - 1u] = '\0'; + int res = _vsnprintf(buf, size, format, args); + if (static_cast< unsigned int >(res) >= size) + { + // _vsnprintf returns -1 if the output was truncated and in case of other errors. + // Detect truncation by checking whether the output buffer was written over entirely. + if (buf[size - 1u] != '\0') + { + buf[size - 1u] = '\0'; + res = static_cast< int >(size); + } + } + + return res; +} + +inline int vswprintf(wchar_t* buf, std::size_t size, const wchar_t* format, std::va_list args) +{ + if (BOOST_UNLIKELY(size == 0u || size > static_cast< std::size_t >(INT_MAX))) + return -1; + + int res = _vsnwprintf(buf, size, format, args); + // (v)swprintf is expected to return -1 on truncation, so we only need to ensure the output is null-terminated + if (static_cast< unsigned int >(res) >= size) + { + buf[size - 1u] = L'\0'; + res = -1; + } + + return res; +} + +#if defined(_MSC_VER) +#pragma warning(pop) +#endif + +#endif + +inline int snprintf(char* buf, std::size_t size, const char* format, ...) +{ + std::va_list args; + va_start(args, format); + int res = vsnprintf(buf, size, format, args); + va_end(args); + return res; +} + +inline int swprintf(wchar_t* buf, std::size_t size, const wchar_t* format, ...) +{ + std::va_list args; + va_start(args, format); + int res = vswprintf(buf, size, format, args); + va_end(args); + return res; +} + +#else // defined(BOOST_CORE_DETAIL_MINGW_SNPRINTF) || defined(BOOST_CORE_DETAIL_MSVC_LEGACY_SNPRINTF) + +// Standard-conforming compilers already have the correct snprintfs +using ::snprintf; +using ::vsnprintf; + +using ::swprintf; +using ::vswprintf; + +#endif // defined(BOOST_CORE_DETAIL_MINGW_SNPRINTF) || defined(BOOST_CORE_DETAIL_MSVC_LEGACY_SNPRINTF) + +} // namespace core + +} // namespace boost + +#endif // BOOST_CORE_SNPRINTF_HPP_INCLUDED_ diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index 63c3b9e..d51c098 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -303,6 +303,8 @@ compile-fail bit_width_fail.cpp run type_name_test.cpp ; +run snprintf_test.cpp ; + run sv_types_test.cpp ; run sv_construct_test.cpp ; run sv_iteration_test.cpp ; diff --git a/test/snprintf_test.cpp b/test/snprintf_test.cpp new file mode 100644 index 0000000..48483ba --- /dev/null +++ b/test/snprintf_test.cpp @@ -0,0 +1,86 @@ +/* + * Copyright Andrey Semashev 2022. + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at + * http://www.boost.org/LICENSE_1_0.txt) + */ +/*! + * \file snprintf_test.cpp + * \author Andrey Semashev + * \date 06.12.2022 + * + * This file contains tests for \c boost::core::snprintf. + */ + +#include +#include +#include +#include + +void test_snprintf() +{ + char buf[11]; + std::memset(buf, 0xFF, sizeof(buf)); + + std::size_t buf_size = sizeof(buf) - 1u; + + int res = boost::core::snprintf(buf, buf_size, "%s", "0123"); + BOOST_TEST_EQ(res, 4); + int cmp_res = std::memcmp(buf, "0123", sizeof("0123")); + BOOST_TEST_EQ(cmp_res, 0); + + std::memset(buf, 0xFF, sizeof(buf)); + + // Suppress compiler checks for buffer overflow + const char* volatile str = "0123456789"; + + res = boost::core::snprintf(buf, buf_size, "%s", str); + BOOST_TEST_GE(res, 10); + cmp_res = std::memcmp(buf, "012345678", sizeof("012345678")); + BOOST_TEST_EQ(cmp_res, 0); + BOOST_TEST_EQ(buf[10], static_cast< char >(~static_cast< char >(0))); + + std::memset(buf, 0xFF, sizeof(buf)); + + res = boost::core::snprintf(buf, 0, "%s", str); + BOOST_TEST_GE(res, 0); + BOOST_TEST_EQ(buf[0], static_cast< char >(~static_cast< char >(0))); +} + +void test_swprintf() +{ + wchar_t buf[11]; + std::memset(buf, 0xFF, sizeof(buf)); + + std::size_t buf_size = sizeof(buf) / sizeof(*buf) - 1u; + + int res = boost::core::swprintf(buf, buf_size, L"%ls", L"0123"); + BOOST_TEST_EQ(res, 4); + int cmp_res = std::memcmp(buf, L"0123", sizeof(L"0123")); + BOOST_TEST_EQ(cmp_res, 0); + + std::memset(buf, 0xFF, sizeof(buf)); + + // Suppress compiler checks for buffer overflow + const wchar_t* volatile str = L"0123456789"; + + res = boost::core::swprintf(buf, buf_size, L"%ls", str); + BOOST_TEST_LT(res, 0); + // swprintf may or may not write to the buffer in case of overflow. + // E.g. glibc 2.35 doesn't and libc on MacOS 11 does. + BOOST_TEST_EQ(buf[10], static_cast< wchar_t >(~static_cast< wchar_t >(0))); + + std::memset(buf, 0xFF, sizeof(buf)); + + res = boost::core::swprintf(buf, 0, L"%ls", str); + BOOST_TEST_LT(res, 0); + BOOST_TEST_EQ(buf[0], static_cast< wchar_t >(~static_cast< wchar_t >(0))); +} + +int main() +{ + test_snprintf(); + test_swprintf(); + + return boost::report_errors(); +}