diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b67014a8f30..0258be6f0ff8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -147,7 +147,7 @@ endfunction() # Define the fmt library, its includes and the needed defines. add_headers(FMT_HEADERS chrono.h color.h core.h format.h format-inl.h locale.h - ostream.h prepare.h printf.h time.h ranges.h) + ostream.h prepare.h printf.h time.h ranges.h safe_duration_cast.h) set(FMT_SOURCES src/format.cc) if (HAVE_OPEN) add_headers(FMT_HEADERS posix.h) diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h index 060ce31e0485..99a1bdda4edb 100644 --- a/include/fmt/chrono.h +++ b/include/fmt/chrono.h @@ -17,7 +17,7 @@ #include #ifdef FMT_SAFE_DURATION_CAST -# include +# include "safe_duration_cast.h" #endif FMT_BEGIN_NAMESPACE diff --git a/include/fmt/safe_duration_cast.h b/include/fmt/safe_duration_cast.h new file mode 100644 index 000000000000..e40c4760d201 --- /dev/null +++ b/include/fmt/safe_duration_cast.h @@ -0,0 +1,311 @@ +/* + * For conversion between std::chrono::durations without undefined + * behaviour or erroneous results. + * This is a stripped down version of duration_cast, for inclusion in libfmt. + * See https://github.com/pauldreik/safe_duration_cast + * + * Copyright Paul Dreik 2019 + * + * This file is licensed under the fmt license, see format.h + */ + +#include +#include +#include +#include + +#include "format.h" + +// see +// https://en.cppreference.com/w/User:D41D8CD98F/feature_testing_macros#C.2B.2B17 + +#if __cpp_constexpr >= 201304 +#define SDC_RELAXED_CONSTEXPR constexpr +#else +#define SDC_RELAXED_CONSTEXPR +#endif + +FMT_BEGIN_NAMESPACE + +namespace safe_duration_cast { + +/** + * converts From to To, without loss. If the dynamic value of from + * can't be converted to To without loss, ec is set. + */ +template::value) > +SDC_RELAXED_CONSTEXPR To +lossless_integral_conversion(const From from, int& ec) +{ + ec = 0; + using F = std::numeric_limits; + using T = std::numeric_limits; + static_assert(F::is_integer, "From must be integral"); + static_assert(T::is_integer, "To must be integral"); + + if + (F::is_signed == T::is_signed) + { + // A and B are both signed, or both unsigned. + if + (F::digits <= T::digits) + { + // From fits in To without any problem + } + else { + // From does not always fit in To, resort to a dynamic check. + if (from < T::min() || from > T::max()) { + // outside range. + ec = 1; + return {}; + } + } + } + + if + (F::is_signed && !T::is_signed) + { + // From may be negative, not allowed! + if (from < 0) { + ec = 1; + return {}; + } + + // From is positive. Can it always fit in To? + if + (F::digits <= T::digits) + { + // yes, From always fits in To. + } + else { + // from may not fit in To, we have to do a dynamic check + if (from > T::max()) { + ec = 1; + return {}; + } + } + } + + if + (!F::is_signed && T::is_signed) + { + // can from be held in To? + if + (F::digits < T::digits) + { + // yes, From always fits in To. + } + else { + // from may not fit in To, we have to do a dynamic check + if (from > T::max()) { + // outside range. + ec = 1; + return {}; + } + } + } + + // reaching here means all is ok for lossless conversion. + return static_cast(from); + +} // function + +template::value) > +SDC_RELAXED_CONSTEXPR To + lossless_integral_conversion(const From from, int& ec) { + ec=0; + return from; +} // function + +/** + * converts From to To if possible, otherwise ec is set. + * + * input | output + * ---------------------------------|--------------- + * NaN | NaN + * Inf | Inf + * normal, fits in output | converted (possibly with loss of precision) + * normal, does not fit in output | ec is set + * subnormal | best effort + * -Inf | -Inf + */ +template::value) > +SDC_RELAXED_CONSTEXPR To +safe_float_conversion(const From from, int& ec) +{ + ec = 0; + using T = std::numeric_limits; + static_assert(std::is_floating_point::value, "From must be floating"); + static_assert(std::is_floating_point::value, "To must be floating"); + + // catch the only happy case + if (std::isfinite(from)) { + if (from >= T::lowest() && from <= T::max()) { + return static_cast(from); + } + // not within range. + ec = 1; + return {}; + } + + // nan and inf will be preserved + return static_cast(from); +} // function + +template::value)> +SDC_RELAXED_CONSTEXPR To + safe_float_conversion(const From from, int& ec) { + ec=0; + static_assert(std::is_floating_point::value, "From must be floating"); + return from; +} + +/** + * safe duration cast between integral durations + */ +template::value), + FMT_ENABLE_IF(std::is_integral::value)> +To safe_duration_cast(std::chrono::duration from, int& ec) +{ + using From = std::chrono::duration; + ec = 0; + // the basic idea is that we need to convert from count() in the from type + // to count() in the To type, by multiplying it with this: + using Factor = std::ratio_divide; + + static_assert(Factor::num > 0, "num must be positive"); + static_assert(Factor::den > 0, "den must be positive"); + + // the conversion is like this: multiply from.count() with Factor::num + // /Factor::den and convert it to To::rep, all this without + // overflow/underflow. let's start by finding a suitable type that can hold + // both To, From and Factor::num + using IntermediateRep = + typename std::common_type::type; + + // safe conversion to IntermediateRep + IntermediateRep count = + lossless_integral_conversion(from.count(), ec); + if (ec) { + return {}; + } + // multiply with Factor::num without overflow or underflow + if + (Factor::num != 1) + { + constexpr auto max1 = + std::numeric_limits::max() / Factor::num; + if (count > max1) { + ec = 1; + return {}; + } + constexpr auto min1 = + std::numeric_limits::min() / Factor::num; + if (count < min1) { + ec = 1; + return {}; + } + count *= Factor::num; + } + + // this can't go wrong, right? den>0 is checked earlier. + if + (Factor::den != 1) { count /= Factor::den; } + // convert to the to type, safely + using ToRep = typename To::rep; + const ToRep tocount = lossless_integral_conversion(count, ec); + if (ec) { + return {}; + } + return To{ tocount }; +} + + +/** + * safe duration_cast between floating point durations + */ +template::value), + FMT_ENABLE_IF(std::is_floating_point::value)> +To safe_duration_cast(std::chrono::duration from, int& ec) +{ + using From = std::chrono::duration; + ec = 0; + if (std::isnan(from.count())) { + // nan in, gives nan out. easy. + return To{ std::numeric_limits::quiet_NaN() }; + } + // maybe we should also check if from is denormal, and decide what to do about + // it. + + // +-inf should be preserved. + if (std::isinf(from.count())) { + return To{ from.count() }; + } + + // the basic idea is that we need to convert from count() in the from type + // to count() in the To type, by multiplying it with this: + using Factor = std::ratio_divide; + + static_assert(Factor::num > 0, "num must be positive"); + static_assert(Factor::den > 0, "den must be positive"); + + // the conversion is like this: multiply from.count() with Factor::num + // /Factor::den and convert it to To::rep, all this without + // overflow/underflow. let's start by finding a suitable type that can hold + // both To, From and Factor::num + using IntermediateRep = + typename std::common_type::type; + + // force conversion of From::rep -> IntermediateRep to be safe, + // even if it will never happen be narrowing in this context. + IntermediateRep count = safe_float_conversion(from.count(), ec); + if (ec) { + return {}; + } + + // multiply with Factor::num without overflow or underflow + if + (Factor::num != 1) + { + constexpr auto max1 = + std::numeric_limits::max() / Factor::num; + if (count > max1) { + ec = 1; + return {}; + } + constexpr auto min1 = + std::numeric_limits::lowest() / Factor::num; + if (count < min1) { + ec = 1; + return {}; + } + count *= Factor::num; + } + + // this can't go wrong, right? den>0 is checked earlier. + if + (Factor::den != 1) { count /= Factor::den; } + + // convert to the to type, safely + using ToRep = typename To::rep; + + const ToRep tocount = safe_float_conversion(count, ec); + if (ec) { + return {}; + } + return To{ tocount }; +} + +} // namespace safe_duration_cast + +FMT_END_NAMESPACE